thepopebot 1.1.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +1 -1
  2. package/api/index.js +72 -165
  3. package/bin/cli.js +36 -6
  4. package/bin/local.sh +31 -0
  5. package/bin/postinstall.js +6 -2
  6. package/config/index.js +2 -11
  7. package/config/instrumentation.js +17 -5
  8. package/lib/actions.js +7 -6
  9. package/lib/ai/agent.js +36 -0
  10. package/lib/ai/index.js +274 -0
  11. package/lib/ai/model.js +67 -0
  12. package/lib/ai/tools.js +49 -0
  13. package/lib/auth/actions.js +28 -0
  14. package/lib/auth/config.js +45 -0
  15. package/lib/auth/index.js +27 -0
  16. package/lib/auth/middleware.js +30 -0
  17. package/lib/channels/base.js +56 -0
  18. package/lib/channels/index.js +15 -0
  19. package/lib/channels/telegram.js +146 -0
  20. package/lib/chat/actions.js +239 -0
  21. package/lib/chat/api.js +103 -0
  22. package/lib/chat/components/app-sidebar.js +161 -0
  23. package/lib/chat/components/app-sidebar.jsx +214 -0
  24. package/lib/chat/components/chat-header.js +9 -0
  25. package/lib/chat/components/chat-header.jsx +14 -0
  26. package/lib/chat/components/chat-input.js +230 -0
  27. package/lib/chat/components/chat-input.jsx +232 -0
  28. package/lib/chat/components/chat-nav-context.js +11 -0
  29. package/lib/chat/components/chat-nav-context.jsx +11 -0
  30. package/lib/chat/components/chat-page.js +70 -0
  31. package/lib/chat/components/chat-page.jsx +89 -0
  32. package/lib/chat/components/chat.js +78 -0
  33. package/lib/chat/components/chat.jsx +91 -0
  34. package/lib/chat/components/chats-page.js +170 -0
  35. package/lib/chat/components/chats-page.jsx +203 -0
  36. package/lib/chat/components/crons-page.js +144 -0
  37. package/lib/chat/components/crons-page.jsx +204 -0
  38. package/lib/chat/components/greeting.js +11 -0
  39. package/lib/chat/components/greeting.jsx +14 -0
  40. package/lib/chat/components/icons.js +518 -0
  41. package/lib/chat/components/icons.jsx +482 -0
  42. package/lib/chat/components/index.js +19 -0
  43. package/lib/chat/components/message.js +66 -0
  44. package/lib/chat/components/message.jsx +92 -0
  45. package/lib/chat/components/messages.js +63 -0
  46. package/lib/chat/components/messages.jsx +72 -0
  47. package/lib/chat/components/notifications-page.js +54 -0
  48. package/lib/chat/components/notifications-page.jsx +83 -0
  49. package/lib/chat/components/page-layout.js +21 -0
  50. package/lib/chat/components/page-layout.jsx +28 -0
  51. package/lib/chat/components/settings-layout.js +37 -0
  52. package/lib/chat/components/settings-layout.jsx +51 -0
  53. package/lib/chat/components/settings-secrets-page.js +216 -0
  54. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  55. package/lib/chat/components/sidebar-history-item.js +54 -0
  56. package/lib/chat/components/sidebar-history-item.jsx +50 -0
  57. package/lib/chat/components/sidebar-history.js +92 -0
  58. package/lib/chat/components/sidebar-history.jsx +132 -0
  59. package/lib/chat/components/sidebar-user-nav.js +59 -0
  60. package/lib/chat/components/sidebar-user-nav.jsx +69 -0
  61. package/lib/chat/components/swarm-page.js +250 -0
  62. package/lib/chat/components/swarm-page.jsx +356 -0
  63. package/lib/chat/components/triggers-page.js +121 -0
  64. package/lib/chat/components/triggers-page.jsx +177 -0
  65. package/lib/chat/components/ui/dropdown-menu.js +98 -0
  66. package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
  67. package/lib/chat/components/ui/scroll-area.js +13 -0
  68. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  69. package/lib/chat/components/ui/separator.js +21 -0
  70. package/lib/chat/components/ui/separator.jsx +18 -0
  71. package/lib/chat/components/ui/sheet.js +75 -0
  72. package/lib/chat/components/ui/sheet.jsx +95 -0
  73. package/lib/chat/components/ui/sidebar.js +227 -0
  74. package/lib/chat/components/ui/sidebar.jsx +245 -0
  75. package/lib/chat/components/ui/tooltip.js +56 -0
  76. package/lib/chat/components/ui/tooltip.jsx +66 -0
  77. package/lib/chat/utils.js +11 -0
  78. package/lib/cron.js +7 -8
  79. package/lib/db/api-keys.js +160 -0
  80. package/lib/db/chats.js +129 -0
  81. package/lib/db/index.js +106 -0
  82. package/lib/db/notifications.js +99 -0
  83. package/lib/db/schema.js +51 -0
  84. package/lib/db/users.js +89 -0
  85. package/lib/paths.js +23 -17
  86. package/lib/tools/create-job.js +3 -3
  87. package/lib/tools/github.js +145 -1
  88. package/lib/tools/openai.js +1 -1
  89. package/lib/tools/telegram.js +4 -3
  90. package/lib/triggers.js +6 -7
  91. package/lib/utils/render-md.js +6 -6
  92. package/package.json +43 -6
  93. package/setup/lib/auth.mjs +22 -9
  94. package/setup/lib/prerequisites.mjs +10 -3
  95. package/setup/lib/telegram-verify.mjs +3 -16
  96. package/setup/setup-telegram.mjs +31 -62
  97. package/setup/setup.mjs +58 -98
  98. package/templates/.dockerignore +5 -0
  99. package/templates/.env.example +18 -2
  100. package/templates/.github/workflows/auto-merge.yml +1 -1
  101. package/templates/.github/workflows/build-image.yml +6 -4
  102. package/templates/.github/workflows/notify-job-failed.yml +2 -2
  103. package/templates/.github/workflows/notify-pr-complete.yml +2 -2
  104. package/templates/.github/workflows/run-job.yml +24 -10
  105. package/templates/CLAUDE.md +5 -3
  106. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  107. package/templates/app/api/chat/route.js +1 -0
  108. package/templates/app/chat/[chatId]/page.js +8 -0
  109. package/templates/app/chats/page.js +7 -0
  110. package/templates/app/components/ascii-logo.jsx +10 -0
  111. package/templates/app/components/login-form.jsx +81 -0
  112. package/templates/app/components/setup-form.jsx +82 -0
  113. package/templates/app/components/theme-provider.jsx +11 -0
  114. package/templates/app/components/theme-toggle.jsx +38 -0
  115. package/templates/app/components/ui/button.jsx +21 -0
  116. package/templates/app/components/ui/card.jsx +23 -0
  117. package/templates/app/components/ui/input.jsx +10 -0
  118. package/templates/app/components/ui/label.jsx +10 -0
  119. package/templates/app/crons/page.js +7 -0
  120. package/templates/app/globals.css +66 -0
  121. package/templates/app/layout.js +9 -2
  122. package/templates/app/login/page.js +15 -0
  123. package/templates/app/notifications/page.js +7 -0
  124. package/templates/app/page.js +6 -30
  125. package/templates/app/settings/layout.js +7 -0
  126. package/templates/app/settings/page.js +5 -0
  127. package/templates/app/settings/secrets/page.js +5 -0
  128. package/templates/app/swarm/page.js +7 -0
  129. package/templates/app/triggers/page.js +7 -0
  130. package/templates/config/CRONS.json +2 -2
  131. package/templates/config/TRIGGERS.json +2 -2
  132. package/templates/docker/event_handler/Dockerfile +19 -0
  133. package/templates/docker/{entrypoint.sh → job/entrypoint.sh} +4 -4
  134. package/templates/docker/runner/Dockerfile +38 -0
  135. package/templates/docker/runner/entrypoint.sh +41 -0
  136. package/templates/docker-compose.yml +52 -0
  137. package/templates/instrumentation.js +6 -1
  138. package/templates/middleware.js +1 -0
  139. package/templates/postcss.config.mjs +5 -0
  140. package/lib/claude/conversation.js +0 -76
  141. package/lib/claude/index.js +0 -142
  142. package/lib/claude/tools.js +0 -54
  143. /package/templates/docker/{Dockerfile → job/Dockerfile} +0 -0
@@ -0,0 +1,170 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { PageLayout } from "./page-layout.js";
5
+ import { MessageIcon, TrashIcon, SearchIcon, PlusIcon } from "./icons.js";
6
+ import { getChats, deleteChat } from "../actions.js";
7
+ import { cn } from "../utils.js";
8
+ function groupChatsByDate(chats) {
9
+ const now = /* @__PURE__ */ new Date();
10
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
11
+ const yesterday = new Date(today.getTime() - 864e5);
12
+ const last7Days = new Date(today.getTime() - 7 * 864e5);
13
+ const last30Days = new Date(today.getTime() - 30 * 864e5);
14
+ const groups = {
15
+ Today: [],
16
+ Yesterday: [],
17
+ "Last 7 Days": [],
18
+ "Last 30 Days": [],
19
+ Older: []
20
+ };
21
+ for (const chat of chats) {
22
+ const date = new Date(chat.updatedAt);
23
+ if (date >= today) {
24
+ groups.Today.push(chat);
25
+ } else if (date >= yesterday) {
26
+ groups.Yesterday.push(chat);
27
+ } else if (date >= last7Days) {
28
+ groups["Last 7 Days"].push(chat);
29
+ } else if (date >= last30Days) {
30
+ groups["Last 30 Days"].push(chat);
31
+ } else {
32
+ groups.Older.push(chat);
33
+ }
34
+ }
35
+ return groups;
36
+ }
37
+ function timeAgo(date) {
38
+ const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1e3);
39
+ if (seconds < 60) return "just now";
40
+ const minutes = Math.floor(seconds / 60);
41
+ if (minutes < 60) return `${minutes}m ago`;
42
+ const hours = Math.floor(minutes / 60);
43
+ if (hours < 24) return `${hours}h ago`;
44
+ const days = Math.floor(hours / 24);
45
+ if (days < 30) return `${days}d ago`;
46
+ const months = Math.floor(days / 30);
47
+ return `${months}mo ago`;
48
+ }
49
+ function ChatsPage({ session }) {
50
+ const [chats, setChats] = useState([]);
51
+ const [loading, setLoading] = useState(true);
52
+ const [query, setQuery] = useState("");
53
+ const navigateToChat = (id) => {
54
+ window.location.href = id ? `/chat/${id}` : "/";
55
+ };
56
+ const loadChats = async () => {
57
+ try {
58
+ const result = await getChats();
59
+ setChats(result);
60
+ } catch (err) {
61
+ console.error("Failed to load chats:", err);
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ };
66
+ useEffect(() => {
67
+ loadChats();
68
+ }, []);
69
+ useEffect(() => {
70
+ const handler = () => loadChats();
71
+ window.addEventListener("chatsupdated", handler);
72
+ return () => window.removeEventListener("chatsupdated", handler);
73
+ }, []);
74
+ const handleDelete = async (chatId) => {
75
+ const { success } = await deleteChat(chatId);
76
+ if (success) {
77
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
78
+ }
79
+ };
80
+ const filtered = query ? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase())) : chats;
81
+ const grouped = groupChatsByDate(filtered);
82
+ return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
83
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
84
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Chats" }),
85
+ /* @__PURE__ */ jsxs(
86
+ "button",
87
+ {
88
+ onClick: () => navigateToChat(null),
89
+ className: "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90",
90
+ children: [
91
+ /* @__PURE__ */ jsx(PlusIcon, { size: 14 }),
92
+ "New chat"
93
+ ]
94
+ }
95
+ )
96
+ ] }),
97
+ /* @__PURE__ */ jsxs("div", { className: "relative mb-4", children: [
98
+ /* @__PURE__ */ jsx(
99
+ "input",
100
+ {
101
+ type: "text",
102
+ placeholder: "Search your chats...",
103
+ value: query,
104
+ onChange: (e) => setQuery(e.target.value),
105
+ className: "w-full rounded-md border border-input bg-background px-9 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
106
+ }
107
+ ),
108
+ /* @__PURE__ */ jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none", children: /* @__PURE__ */ jsx(SearchIcon, { size: 16 }) })
109
+ ] }),
110
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mb-4", children: [
111
+ filtered.length,
112
+ " ",
113
+ filtered.length === 1 ? "chat" : "chats"
114
+ ] }),
115
+ loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) }) : filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground py-8 text-center", children: query ? "No chats match your search." : "No chats yet. Start a conversation!" }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: Object.entries(grouped).map(
116
+ ([label, groupChats]) => groupChats.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "mb-4", children: [
117
+ /* @__PURE__ */ jsx("h2", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2", children: label }),
118
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col divide-y divide-border", children: groupChats.map((chat) => /* @__PURE__ */ jsx(
119
+ ChatRow,
120
+ {
121
+ chat,
122
+ onNavigate: navigateToChat,
123
+ onDelete: handleDelete
124
+ },
125
+ chat.id
126
+ )) })
127
+ ] }, label) : null
128
+ ) })
129
+ ] });
130
+ }
131
+ function ChatRow({ chat, onNavigate, onDelete }) {
132
+ const [showDelete, setShowDelete] = useState(false);
133
+ return /* @__PURE__ */ jsxs(
134
+ "div",
135
+ {
136
+ className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
137
+ onMouseEnter: () => setShowDelete(true),
138
+ onMouseLeave: () => setShowDelete(false),
139
+ onClick: () => onNavigate(chat.id),
140
+ children: [
141
+ /* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
142
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
143
+ /* @__PURE__ */ jsx("span", { className: "text-sm truncate block", children: chat.title || "New Chat" }),
144
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
145
+ "Last message ",
146
+ timeAgo(chat.updatedAt)
147
+ ] })
148
+ ] }),
149
+ showDelete && /* @__PURE__ */ jsx(
150
+ "button",
151
+ {
152
+ className: cn(
153
+ "shrink-0 rounded-md p-1.5",
154
+ "text-muted-foreground hover:text-destructive hover:bg-muted"
155
+ ),
156
+ onClick: (e) => {
157
+ e.stopPropagation();
158
+ onDelete(chat.id);
159
+ },
160
+ "aria-label": "Delete chat",
161
+ children: /* @__PURE__ */ jsx(TrashIcon, { size: 14 })
162
+ }
163
+ )
164
+ ]
165
+ }
166
+ );
167
+ }
168
+ export {
169
+ ChatsPage
170
+ };
@@ -0,0 +1,203 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { PageLayout } from './page-layout.js';
5
+ import { MessageIcon, TrashIcon, SearchIcon, PlusIcon } from './icons.js';
6
+ import { getChats, deleteChat } from '../actions.js';
7
+ import { cn } from '../utils.js';
8
+
9
+ function groupChatsByDate(chats) {
10
+ const now = new Date();
11
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
12
+ const yesterday = new Date(today.getTime() - 86400000);
13
+ const last7Days = new Date(today.getTime() - 7 * 86400000);
14
+ const last30Days = new Date(today.getTime() - 30 * 86400000);
15
+
16
+ const groups = {
17
+ Today: [],
18
+ Yesterday: [],
19
+ 'Last 7 Days': [],
20
+ 'Last 30 Days': [],
21
+ Older: [],
22
+ };
23
+
24
+ for (const chat of chats) {
25
+ const date = new Date(chat.updatedAt);
26
+ if (date >= today) {
27
+ groups.Today.push(chat);
28
+ } else if (date >= yesterday) {
29
+ groups.Yesterday.push(chat);
30
+ } else if (date >= last7Days) {
31
+ groups['Last 7 Days'].push(chat);
32
+ } else if (date >= last30Days) {
33
+ groups['Last 30 Days'].push(chat);
34
+ } else {
35
+ groups.Older.push(chat);
36
+ }
37
+ }
38
+
39
+ return groups;
40
+ }
41
+
42
+ function timeAgo(date) {
43
+ const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
44
+ if (seconds < 60) return 'just now';
45
+ const minutes = Math.floor(seconds / 60);
46
+ if (minutes < 60) return `${minutes}m ago`;
47
+ const hours = Math.floor(minutes / 60);
48
+ if (hours < 24) return `${hours}h ago`;
49
+ const days = Math.floor(hours / 24);
50
+ if (days < 30) return `${days}d ago`;
51
+ const months = Math.floor(days / 30);
52
+ return `${months}mo ago`;
53
+ }
54
+
55
+ export function ChatsPage({ session }) {
56
+ const [chats, setChats] = useState([]);
57
+ const [loading, setLoading] = useState(true);
58
+ const [query, setQuery] = useState('');
59
+
60
+ const navigateToChat = (id) => {
61
+ window.location.href = id ? `/chat/${id}` : '/';
62
+ };
63
+
64
+ const loadChats = async () => {
65
+ try {
66
+ const result = await getChats();
67
+ setChats(result);
68
+ } catch (err) {
69
+ console.error('Failed to load chats:', err);
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ useEffect(() => {
76
+ loadChats();
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ const handler = () => loadChats();
81
+ window.addEventListener('chatsupdated', handler);
82
+ return () => window.removeEventListener('chatsupdated', handler);
83
+ }, []);
84
+
85
+ const handleDelete = async (chatId) => {
86
+ const { success } = await deleteChat(chatId);
87
+ if (success) {
88
+ setChats((prev) => prev.filter((c) => c.id !== chatId));
89
+ }
90
+ };
91
+
92
+ const filtered = query
93
+ ? chats.filter((c) => c.title?.toLowerCase().includes(query.toLowerCase()))
94
+ : chats;
95
+
96
+ const grouped = groupChatsByDate(filtered);
97
+
98
+ return (
99
+ <PageLayout session={session}>
100
+ {/* Header */}
101
+ <div className="flex items-center justify-between mb-6">
102
+ <h1 className="text-2xl font-semibold">Chats</h1>
103
+ <button
104
+ onClick={() => navigateToChat(null)}
105
+ className="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90"
106
+ >
107
+ <PlusIcon size={14} />
108
+ New chat
109
+ </button>
110
+ </div>
111
+
112
+ {/* Search */}
113
+ <div className="relative mb-4">
114
+ <input
115
+ type="text"
116
+ placeholder="Search your chats..."
117
+ value={query}
118
+ onChange={(e) => setQuery(e.target.value)}
119
+ className="w-full rounded-md border border-input bg-background px-9 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
120
+ />
121
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none">
122
+ <SearchIcon size={16} />
123
+ </div>
124
+ </div>
125
+
126
+ {/* Count */}
127
+ <p className="text-sm text-muted-foreground mb-4">
128
+ {filtered.length} {filtered.length === 1 ? 'chat' : 'chats'}
129
+ </p>
130
+
131
+ {/* Chat list */}
132
+ {loading ? (
133
+ <div className="flex flex-col gap-3">
134
+ {[...Array(5)].map((_, i) => (
135
+ <div key={i} className="h-14 animate-pulse rounded-md bg-border/50" />
136
+ ))}
137
+ </div>
138
+ ) : filtered.length === 0 ? (
139
+ <p className="text-sm text-muted-foreground py-8 text-center">
140
+ {query ? 'No chats match your search.' : 'No chats yet. Start a conversation!'}
141
+ </p>
142
+ ) : (
143
+ <div className="flex flex-col">
144
+ {Object.entries(grouped).map(([label, groupChats]) =>
145
+ groupChats.length > 0 ? (
146
+ <div key={label} className="mb-4">
147
+ <h2 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">
148
+ {label}
149
+ </h2>
150
+ <div className="flex flex-col divide-y divide-border">
151
+ {groupChats.map((chat) => (
152
+ <ChatRow
153
+ key={chat.id}
154
+ chat={chat}
155
+ onNavigate={navigateToChat}
156
+ onDelete={handleDelete}
157
+ />
158
+ ))}
159
+ </div>
160
+ </div>
161
+ ) : null
162
+ )}
163
+ </div>
164
+ )}
165
+ </PageLayout>
166
+ );
167
+ }
168
+
169
+ function ChatRow({ chat, onNavigate, onDelete }) {
170
+ const [showDelete, setShowDelete] = useState(false);
171
+
172
+ return (
173
+ <div
174
+ className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
175
+ onMouseEnter={() => setShowDelete(true)}
176
+ onMouseLeave={() => setShowDelete(false)}
177
+ onClick={() => onNavigate(chat.id)}
178
+ >
179
+ <MessageIcon size={16} />
180
+ <div className="flex-1 min-w-0">
181
+ <span className="text-sm truncate block">{chat.title || 'New Chat'}</span>
182
+ <span className="text-xs text-muted-foreground">
183
+ Last message {timeAgo(chat.updatedAt)}
184
+ </span>
185
+ </div>
186
+ {showDelete && (
187
+ <button
188
+ className={cn(
189
+ 'shrink-0 rounded-md p-1.5',
190
+ 'text-muted-foreground hover:text-destructive hover:bg-muted'
191
+ )}
192
+ onClick={(e) => {
193
+ e.stopPropagation();
194
+ onDelete(chat.id);
195
+ }}
196
+ aria-label="Delete chat"
197
+ >
198
+ <TrashIcon size={14} />
199
+ </button>
200
+ )}
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ import { PageLayout } from "./page-layout.js";
5
+ import { ClockIcon, SpinnerIcon, ChevronDownIcon } from "./icons.js";
6
+ import { getSwarmConfig } from "../actions.js";
7
+ function describeCron(schedule) {
8
+ const parts = schedule.trim().split(/\s+/);
9
+ if (parts.length !== 5) return schedule;
10
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
11
+ if (minute.startsWith("*/") && hour === "*" && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
12
+ const n = parseInt(minute.slice(2), 10);
13
+ if (n === 1) return "Every minute";
14
+ return `Every ${n} minutes`;
15
+ }
16
+ if (hour.startsWith("*/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
17
+ const n = parseInt(hour.slice(2), 10);
18
+ if (n === 1) return "Every hour";
19
+ return `Every ${n} hours`;
20
+ }
21
+ if (minute !== "*" && hour !== "*" && !hour.includes("/") && !minute.includes("/") && dayOfMonth === "*" && month === "*" && dayOfWeek === "*") {
22
+ const h = parseInt(hour, 10);
23
+ const m = parseInt(minute, 10);
24
+ const period = h >= 12 ? "PM" : "AM";
25
+ const displayH = h === 0 ? 12 : h > 12 ? h - 12 : h;
26
+ return `Daily at ${displayH}:${String(m).padStart(2, "0")} ${period}`;
27
+ }
28
+ if (minute !== "*" && hour !== "*" && dayOfMonth === "*" && month === "*" && dayOfWeek !== "*") {
29
+ const dayNames = { "0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed", "4": "Thu", "5": "Fri", "6": "Sat" };
30
+ const days = dayOfWeek.split(",").map((d) => dayNames[d] || d).join(", ");
31
+ const h = parseInt(hour, 10);
32
+ const m = parseInt(minute, 10);
33
+ const period = h >= 12 ? "PM" : "AM";
34
+ const displayH = h === 0 ? 12 : h > 12 ? h - 12 : h;
35
+ return `${days} at ${displayH}:${String(m).padStart(2, "0")} ${period}`;
36
+ }
37
+ return schedule;
38
+ }
39
+ const typeBadgeStyles = {
40
+ agent: "bg-purple-500/10 text-purple-500",
41
+ command: "bg-blue-500/10 text-blue-500",
42
+ webhook: "bg-orange-500/10 text-orange-500"
43
+ };
44
+ function CronCard({ cron }) {
45
+ const [expanded, setExpanded] = useState(false);
46
+ const type = cron.type || "agent";
47
+ const disabled = cron.enabled === false;
48
+ return /* @__PURE__ */ jsxs(
49
+ "div",
50
+ {
51
+ className: `rounded-lg border bg-card transition-opacity ${disabled ? "opacity-60" : ""}`,
52
+ children: [
53
+ /* @__PURE__ */ jsxs(
54
+ "button",
55
+ {
56
+ onClick: () => setExpanded(!expanded),
57
+ className: "flex items-center gap-3 w-full text-left p-4 hover:bg-accent/50 rounded-lg",
58
+ children: [
59
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 rounded-md bg-muted p-2", children: /* @__PURE__ */ jsx(ClockIcon, { size: 16 }) }),
60
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
61
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium truncate", children: cron.name }),
62
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-0.5", children: [
63
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: cron.schedule }),
64
+ /* @__PURE__ */ jsx("span", { className: "mx-1.5 text-border", children: "|" }),
65
+ describeCron(cron.schedule)
66
+ ] })
67
+ ] }),
68
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
69
+ /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${typeBadgeStyles[type] || typeBadgeStyles.agent}`, children: type }),
70
+ /* @__PURE__ */ jsx(
71
+ "span",
72
+ {
73
+ className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${disabled ? "bg-muted text-muted-foreground" : "bg-green-500/10 text-green-500"}`,
74
+ children: disabled ? "disabled" : "enabled"
75
+ }
76
+ ),
77
+ /* @__PURE__ */ jsx("span", { className: `transition-transform ${expanded ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: 14 }) })
78
+ ] })
79
+ ]
80
+ }
81
+ ),
82
+ expanded && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
83
+ type === "agent" && cron.job && /* @__PURE__ */ jsxs("div", { children: [
84
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Job prompt" }),
85
+ /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: cron.job })
86
+ ] }),
87
+ type === "command" && cron.command && /* @__PURE__ */ jsxs("div", { children: [
88
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Command" }),
89
+ /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: cron.command })
90
+ ] }),
91
+ type === "webhook" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
92
+ /* @__PURE__ */ jsxs("div", { children: [
93
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "URL" }),
94
+ /* @__PURE__ */ jsxs("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto", children: [
95
+ cron.method && cron.method !== "POST" ? `${cron.method} ` : "",
96
+ cron.url
97
+ ] })
98
+ ] }),
99
+ cron.vars && Object.keys(cron.vars).length > 0 && /* @__PURE__ */ jsxs("div", { children: [
100
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Variables" }),
101
+ /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: JSON.stringify(cron.vars, null, 2) })
102
+ ] })
103
+ ] })
104
+ ] })
105
+ ]
106
+ }
107
+ );
108
+ }
109
+ function CronsPage({ session }) {
110
+ const [crons, setCrons] = useState([]);
111
+ const [loading, setLoading] = useState(true);
112
+ useEffect(() => {
113
+ getSwarmConfig().then((data) => {
114
+ if (data?.crons) setCrons(data.crons);
115
+ }).catch(() => {
116
+ }).finally(() => setLoading(false));
117
+ }, []);
118
+ const enabledCount = crons.filter((c) => c.enabled !== false).length;
119
+ return /* @__PURE__ */ jsxs(PageLayout, { session, children: [
120
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
121
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Cron Jobs" }),
122
+ !loading && /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground mt-1", children: [
123
+ crons.length,
124
+ " job",
125
+ crons.length !== 1 ? "s" : "",
126
+ " configured, ",
127
+ enabledCount,
128
+ " enabled"
129
+ ] })
130
+ ] }),
131
+ loading ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-20 animate-pulse rounded-lg bg-border/50" }, i)) }) : crons.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center", children: [
132
+ /* @__PURE__ */ jsx("div", { className: "rounded-full bg-muted p-4 mb-4", children: /* @__PURE__ */ jsx(ClockIcon, { size: 24 }) }),
133
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-medium mb-1", children: "No cron jobs configured" }),
134
+ /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground max-w-sm", children: [
135
+ "Add scheduled jobs by editing ",
136
+ /* @__PURE__ */ jsx("span", { className: "font-mono", children: "config/CRONS.json" }),
137
+ " in your project."
138
+ ] })
139
+ ] }) : /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: crons.map((cron, i) => /* @__PURE__ */ jsx(CronCard, { cron }, i)) })
140
+ ] });
141
+ }
142
+ export {
143
+ CronsPage
144
+ };