@flame0510/project-aether 1.1.9

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 (290) hide show
  1. package/README.md +150 -0
  2. package/agent-templates/README.md +37 -0
  3. package/agent-templates/argus/AGENTS.md +52 -0
  4. package/agent-templates/argus/IDENTITY.md +7 -0
  5. package/agent-templates/argus/MEMORY.md +9 -0
  6. package/agent-templates/argus/SOUL.md +24 -0
  7. package/agent-templates/argus/TOOLS.md +8 -0
  8. package/agent-templates/atlas/AGENTS.md +47 -0
  9. package/agent-templates/atlas/HEARTBEAT.md +10 -0
  10. package/agent-templates/atlas/IDENTITY.md +6 -0
  11. package/agent-templates/atlas/MEMORY.md +9 -0
  12. package/agent-templates/atlas/SOUL.md +21 -0
  13. package/agent-templates/atlas/TOOLS.md +4 -0
  14. package/agent-templates/base-image/.trigger +0 -0
  15. package/agent-templates/base-image/Dockerfile +90 -0
  16. package/agent-templates/base-image/entrypoint.sh +37 -0
  17. package/agent-templates/custom/AGENTS.md +29 -0
  18. package/agent-templates/custom/BOOTSTRAP.md +56 -0
  19. package/agent-templates/custom/MEMORY.md +8 -0
  20. package/agent-templates/prometheus/AGENTS.md +37 -0
  21. package/agent-templates/prometheus/IDENTITY.md +6 -0
  22. package/agent-templates/prometheus/MEMORY.md +9 -0
  23. package/agent-templates/prometheus/SOUL.md +21 -0
  24. package/agent-templates/prometheus/TOOLS.md +4 -0
  25. package/app/agents/ChannelManager.tsx +465 -0
  26. package/app/agents/ImageDownloadBanner.tsx +159 -0
  27. package/app/agents/PageClient.tsx +1285 -0
  28. package/app/agents/create/PageClient.tsx +437 -0
  29. package/app/agents/create/page.tsx +126 -0
  30. package/app/agents/loading.tsx +14 -0
  31. package/app/agents/page.tsx +5 -0
  32. package/app/api/agents/[id]/backup/route.ts +169 -0
  33. package/app/api/agents/[id]/channels/pairing/route.ts +69 -0
  34. package/app/api/agents/[id]/channels/route.ts +14 -0
  35. package/app/api/agents/[id]/channels/telegram/route.ts +50 -0
  36. package/app/api/agents/[id]/lifecycle/route.ts +94 -0
  37. package/app/api/agents/[id]/recreate/route.ts +199 -0
  38. package/app/api/agents/[id]/restart/route.ts +33 -0
  39. package/app/api/agents/[id]/restore/route.ts +81 -0
  40. package/app/api/agents/[id]/route.ts +280 -0
  41. package/app/api/agents/channels-summary/route.ts +39 -0
  42. package/app/api/agents/config-json/route.ts +58 -0
  43. package/app/api/agents/create/route.ts +488 -0
  44. package/app/api/agents/download-image/route.ts +32 -0
  45. package/app/api/agents/image-status/route.ts +126 -0
  46. package/app/api/agents/route.ts +213 -0
  47. package/app/api/agents/token/route.ts +94 -0
  48. package/app/api/agents-active/route.ts +190 -0
  49. package/app/api/agents-config/route.ts +629 -0
  50. package/app/api/alerts/smoke/route.ts +26 -0
  51. package/app/api/aliases/add/route.ts +28 -0
  52. package/app/api/aliases/remove/route.ts +27 -0
  53. package/app/api/assistant/route.ts +204 -0
  54. package/app/api/auth/check/route.ts +38 -0
  55. package/app/api/auth/login/route.ts +85 -0
  56. package/app/api/auth/logout/route.ts +7 -0
  57. package/app/api/auth/status/route.ts +33 -0
  58. package/app/api/config/env/route.ts +132 -0
  59. package/app/api/config/restart/route.ts +26 -0
  60. package/app/api/containers/route.ts +75 -0
  61. package/app/api/cost-override/route.ts +46 -0
  62. package/app/api/costs/route.ts +84 -0
  63. package/app/api/credentials/[id]/reveal/route.ts +21 -0
  64. package/app/api/credentials/[id]/route.ts +52 -0
  65. package/app/api/credentials/[id]/sync/route.ts +58 -0
  66. package/app/api/credentials/detect/route.ts +246 -0
  67. package/app/api/credentials/route.ts +33 -0
  68. package/app/api/crons/[id]/route.ts +69 -0
  69. package/app/api/crons/route.ts +12 -0
  70. package/app/api/crons/runs/route.ts +31 -0
  71. package/app/api/debug/route.ts +11 -0
  72. package/app/api/envcheck/route.ts +18 -0
  73. package/app/api/events/route.ts +24 -0
  74. package/app/api/gateway/agent/route.ts +154 -0
  75. package/app/api/gateway/provider/balance/route.ts +38 -0
  76. package/app/api/gateway/provider/keys.ts +38 -0
  77. package/app/api/gateway/provider/oauth/route.ts +304 -0
  78. package/app/api/gateway/provider/route.ts +209 -0
  79. package/app/api/gateway/route.ts +223 -0
  80. package/app/api/gateway/sync.ts +189 -0
  81. package/app/api/lineage/route.ts +54 -0
  82. package/app/api/memory-context/route.ts +15 -0
  83. package/app/api/metrics/route.ts +35 -0
  84. package/app/api/models/route.ts +77 -0
  85. package/app/api/plugins/route.js +37 -0
  86. package/app/api/provider/upstream.ts +142 -0
  87. package/app/api/provider/v1/chat/completions/route.ts +146 -0
  88. package/app/api/provider/v1/models/route.ts +167 -0
  89. package/app/api/provider-usage/route.ts +629 -0
  90. package/app/api/session/route.ts +39 -0
  91. package/app/api/sessions/[id]/route.ts +38 -0
  92. package/app/api/sessions/route.ts +23 -0
  93. package/app/api/setup/agent-image/route.ts +102 -0
  94. package/app/api/setup/password/route.ts +111 -0
  95. package/app/api/setup/restart/route.ts +76 -0
  96. package/app/api/skills/delete/route.js +105 -0
  97. package/app/api/skills/promote/route.js +129 -0
  98. package/app/api/skills/route.js +203 -0
  99. package/app/api/skills/save/route.js +142 -0
  100. package/app/api/stats/route.ts +34 -0
  101. package/app/api/stats-since/route.ts +20 -0
  102. package/app/api/stream/route.ts +70 -0
  103. package/app/api/system-health/route.ts +233 -0
  104. package/app/api/tool-calls/route.ts +24 -0
  105. package/app/api/tools-config/route.ts +163 -0
  106. package/app/api/update-check/route.ts +127 -0
  107. package/app/api/version/route.ts +25 -0
  108. package/app/api/wizard/complete/route.ts +32 -0
  109. package/app/api/wizard/reset/route.ts +28 -0
  110. package/app/api/wizard/status/route.ts +28 -0
  111. package/app/api/workspace/route.ts +392 -0
  112. package/app/api/workspace/stream/route.ts +136 -0
  113. package/app/components/AppShell.tsx +33 -0
  114. package/app/components/AuthGuard.tsx +91 -0
  115. package/app/components/CostBreakdown.tsx +37 -0
  116. package/app/components/DashboardHeader.tsx +73 -0
  117. package/app/components/DashboardLayout.tsx +76 -0
  118. package/app/components/DashboardToolbar.tsx +61 -0
  119. package/app/components/FloatingActions.tsx +33 -0
  120. package/app/components/Icons.tsx +78 -0
  121. package/app/components/LineageGraphPage.tsx +142 -0
  122. package/app/components/LiveFeed.tsx +48 -0
  123. package/app/components/MobileBottomNav.tsx +84 -0
  124. package/app/components/ModelPickerModal.tsx +94 -0
  125. package/app/components/PasswordInput.tsx +86 -0
  126. package/app/components/PulseChat.tsx +362 -0
  127. package/app/components/Rev4aLoader.tsx +48 -0
  128. package/app/components/SessionDrawer.tsx +263 -0
  129. package/app/components/SessionTopology.tsx +289 -0
  130. package/app/components/Sidebar.tsx +294 -0
  131. package/app/components/Skeleton.tsx +133 -0
  132. package/app/components/SystemCockpit.tsx +169 -0
  133. package/app/components/VersionBanner.tsx +105 -0
  134. package/app/components/WizardButton.tsx +30 -0
  135. package/app/components/ui/Badge.tsx +30 -0
  136. package/app/components/ui/Button.tsx +59 -0
  137. package/app/components/ui/ConfirmModal.tsx +46 -0
  138. package/app/components/ui/FilterBar.tsx +33 -0
  139. package/app/components/ui/Input.tsx +25 -0
  140. package/app/components/ui/ItemList.tsx +83 -0
  141. package/app/components/ui/LoadingSpinner.tsx +30 -0
  142. package/app/components/ui/Metric.tsx +22 -0
  143. package/app/components/ui/Modal.tsx +42 -0
  144. package/app/components/ui/Page.tsx +18 -0
  145. package/app/components/ui/Pill.tsx +6 -0
  146. package/app/components/ui/PropertyList.tsx +24 -0
  147. package/app/components/ui/Select.tsx +26 -0
  148. package/app/components/ui/StatusCard.tsx +32 -0
  149. package/app/components/ui/Surface.tsx +14 -0
  150. package/app/components/ui/Tabs.tsx +55 -0
  151. package/app/components/ui/TemplateOption.tsx +59 -0
  152. package/app/components/ui/Toast.tsx +44 -0
  153. package/app/components/ui/index.ts +19 -0
  154. package/app/components/ui/tokens.ts +18 -0
  155. package/app/config/PageClient.tsx +293 -0
  156. package/app/config/loading.tsx +10 -0
  157. package/app/config/page.tsx +7 -0
  158. package/app/containers/ContainersClient.tsx +129 -0
  159. package/app/containers/loading.tsx +21 -0
  160. package/app/containers/page.tsx +5 -0
  161. package/app/containers/terminal/[id]/TerminalClient.tsx +313 -0
  162. package/app/containers/terminal/[id]/page.tsx +6 -0
  163. package/app/credentials/PageClient.tsx +598 -0
  164. package/app/credentials/loading.tsx +10 -0
  165. package/app/credentials/page.tsx +7 -0
  166. package/app/crons/PageClient.tsx +538 -0
  167. package/app/crons/loading.tsx +10 -0
  168. package/app/crons/page.tsx +5 -0
  169. package/app/design-system.ts +57 -0
  170. package/app/gateway/PageClient.tsx +1048 -0
  171. package/app/gateway/loading.tsx +24 -0
  172. package/app/gateway/page.tsx +5 -0
  173. package/app/globals.css +1474 -0
  174. package/app/layout.tsx +45 -0
  175. package/app/lib/models-context.tsx +41 -0
  176. package/app/lineage/loading.tsx +10 -0
  177. package/app/lineage/page.tsx +13 -0
  178. package/app/loading.tsx +16 -0
  179. package/app/login/page.tsx +160 -0
  180. package/app/memory/MemoryContextPageClient.tsx +196 -0
  181. package/app/memory/loading.tsx +10 -0
  182. package/app/memory/page.tsx +8 -0
  183. package/app/page.tsx +31 -0
  184. package/app/plugins/PageClient.tsx +296 -0
  185. package/app/plugins/loading.tsx +10 -0
  186. package/app/plugins/page.tsx +5 -0
  187. package/app/setup/PageClient.tsx +227 -0
  188. package/app/setup/loading.tsx +21 -0
  189. package/app/setup/page.tsx +7 -0
  190. package/app/skills/PageClient.tsx +499 -0
  191. package/app/skills/loading.tsx +10 -0
  192. package/app/skills/page.tsx +5 -0
  193. package/app/tools/ToolsPageClient.tsx +308 -0
  194. package/app/tools/loading.tsx +10 -0
  195. package/app/tools/page.tsx +62 -0
  196. package/app/tools/tools-catalog.ts +141 -0
  197. package/app/wizard/PageClient.tsx +485 -0
  198. package/app/wizard/icons.tsx +89 -0
  199. package/app/wizard/loading.tsx +24 -0
  200. package/app/wizard/page.tsx +7 -0
  201. package/app/wizard/storage.ts +33 -0
  202. package/app/wizard/useWizard.ts +101 -0
  203. package/app/workspace/WorkspaceClient.tsx +633 -0
  204. package/app/workspace/loading.tsx +36 -0
  205. package/app/workspace/page.tsx +8 -0
  206. package/bin/docker-entrypoint.sh +21 -0
  207. package/bin/postinstall.js +20 -0
  208. package/bin/rev4a.js +696 -0
  209. package/daemon.js +783 -0
  210. package/docs/ARCHITECTURE.md +557 -0
  211. package/docs/CONTAINER-TERMINAL.md +280 -0
  212. package/docs/DESIGN-SYSTEM.md +99 -0
  213. package/docs/FRONTEND-ARCHITECTURE.md +119 -0
  214. package/docs/REV4A.md +504 -0
  215. package/docs/dev/API-REFERENCE.md +1835 -0
  216. package/docs/dev/DATABASE.md +224 -0
  217. package/docs/dev/GATEWAY.md +300 -0
  218. package/docs/dev/PROVIDERS.md +229 -0
  219. package/docs/dev/SESSION-MAINTENANCE-PLAN.md +607 -0
  220. package/docs/dev/WORKSPACE.md +199 -0
  221. package/docs/rag/DATA-FRESHNESS.md +109 -0
  222. package/docs/rag/GLOSSARY.md +125 -0
  223. package/docs/rag/REV4A-OVERVIEW.md +135 -0
  224. package/docs/rag/WHAT-I-CAN-ANSWER.md +124 -0
  225. package/lib/agent-setup.ts +130 -0
  226. package/lib/alerts.ts +115 -0
  227. package/lib/apiFetch.ts +7 -0
  228. package/lib/auth.ts +28 -0
  229. package/lib/billing.ts +100 -0
  230. package/lib/buildAgentImage.ts +346 -0
  231. package/lib/channelManager.ts +386 -0
  232. package/lib/container.ts +60 -0
  233. package/lib/credentials/db.ts +86 -0
  234. package/lib/credentials/delivery.ts +448 -0
  235. package/lib/credentials/detect.ts +149 -0
  236. package/lib/credentials/hash.ts +9 -0
  237. package/lib/credentials/providers.ts +110 -0
  238. package/lib/credentials/vault.ts +195 -0
  239. package/lib/db-bootstrap.d.ts +3 -0
  240. package/lib/db-bootstrap.mjs +144 -0
  241. package/lib/db.ts +25 -0
  242. package/lib/docker-utils.ts +60 -0
  243. package/lib/hooks/useDashboard.ts +264 -0
  244. package/lib/hooks/useRev4aTimezone.ts +28 -0
  245. package/lib/hooks/useScreenshot.ts +30 -0
  246. package/lib/memory-context.ts +385 -0
  247. package/lib/model-catalogue.ts +131 -0
  248. package/lib/model-pricing.ts +63 -0
  249. package/lib/openclaw-cron.ts +350 -0
  250. package/lib/patterns/ApiAdapter.ts +73 -0
  251. package/lib/patterns/ApiClient.ts +65 -0
  252. package/lib/patterns/EventBus.ts +113 -0
  253. package/lib/patterns/FilterStrategy.ts +79 -0
  254. package/lib/patterns/SessionFactory.ts +117 -0
  255. package/lib/patterns/sessionPresentation.ts +117 -0
  256. package/lib/provider-balance.ts +163 -0
  257. package/lib/requireAuth.tsx +38 -0
  258. package/lib/rev4a-auth.d.ts +3 -0
  259. package/lib/rev4a-auth.js +36 -0
  260. package/lib/rev4a-paths.ts +145 -0
  261. package/lib/timezone.ts +46 -0
  262. package/lib/types/index.ts +96 -0
  263. package/lib/utils/format.ts +82 -0
  264. package/lineage.js +56 -0
  265. package/model-pricing.json +482 -0
  266. package/models.config.json +844 -0
  267. package/next-env.d.ts +6 -0
  268. package/next.config.mjs +7 -0
  269. package/package.json +82 -0
  270. package/public/apple-touch-icon.png +0 -0
  271. package/public/favicon-64.png +0 -0
  272. package/public/favicon.svg +58 -0
  273. package/public/icon-192-maskable.png +0 -0
  274. package/public/icon-192.png +0 -0
  275. package/public/icon-512-maskable.png +0 -0
  276. package/public/icon-512.png +0 -0
  277. package/public/manifest.json +38 -0
  278. package/public/rev4a-logo.png +0 -0
  279. package/public/rev4a-logo.svg +58 -0
  280. package/rev4a-rules/AGENTS.md +37 -0
  281. package/scripts/add-auth.cjs +185 -0
  282. package/scripts/backup.sh +66 -0
  283. package/scripts/check-providers.mjs +143 -0
  284. package/scripts/docker/traefik/docker-compose.yml +32 -0
  285. package/scripts/docker/traefik/dynamic/.gitkeep +18 -0
  286. package/scripts/docker/traefik/traefik.yml +37 -0
  287. package/scripts/remove-auth-profile.js +56 -0
  288. package/scripts/restore.sh +82 -0
  289. package/terminal-ws-server.js +139 -0
  290. package/tsconfig.json +42 -0
@@ -0,0 +1,9 @@
1
+ # MEMORY.md — Prometheus
2
+
3
+ ## Operational status
4
+ - Agent: Prometheus 🔥
5
+ - Role: Project Management Agent
6
+ - Status: Active
7
+
8
+ ## Active projects
9
+ _Fill with current project context when assigned._
@@ -0,0 +1,21 @@
1
+ # SOUL.md — Prometheus
2
+
3
+ You are **Prometheus** 🔥, a project management agent.
4
+
5
+ ## Vision
6
+ Bridge the gap between technical execution and client expectations. You manage scope, deadlines, and deliverables.
7
+
8
+ ## Core rules
9
+ - **Client first.** Every decision starts with "does this serve the client?"
10
+ - **Scope management.** Know when to say yes, how to say no, and how to escalate.
11
+ - **Delivery > perfection.** Ship on time, iterate afterwards.
12
+ - **Documentation.** Written specs prevent misunderstandings.
13
+
14
+ ## Method
15
+ 1. Clarify requirements before acting.
16
+ 2. Break projects into deliverables with clear acceptance criteria.
17
+ 3. Communicate progress before being asked.
18
+ 4. Learn from every project — capture what worked and what didn't.
19
+
20
+ ## Tone
21
+ Professional, clear, proactive. Clients trust you.
@@ -0,0 +1,4 @@
1
+ # TOOLS.md — Prometheus Tools
2
+
3
+ ## Git
4
+ - Default commit style: conventional commits, English
@@ -0,0 +1,465 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Button, Modal, Input, Badge } from '../components/ui';
5
+ import { SkeletonLines } from '../components/Skeleton';
6
+
7
+ interface TelegramChannel {
8
+ connected: boolean;
9
+ botToken: string;
10
+ dmPolicy: string;
11
+ allowFrom: string[];
12
+ }
13
+
14
+ interface PendingPairing {
15
+ code: string;
16
+ senderId: string | null;
17
+ }
18
+
19
+ interface ApprovedPairing {
20
+ senderId: string;
21
+ }
22
+
23
+ interface ChannelsData {
24
+ agentId: string;
25
+ telegram: TelegramChannel | null;
26
+ }
27
+
28
+ interface Props {
29
+ agentId: string;
30
+ agentName: string;
31
+ open: boolean;
32
+ onClose: () => void;
33
+ }
34
+
35
+ const PAIRING_POLL_MS = 5000;
36
+
37
+ export default function ChannelManager({ agentId, agentName, open, onClose }: Props) {
38
+ const [channels, setChannels] = useState<ChannelsData | null>(null);
39
+ const [channelsLoading, setChannelsLoading] = useState(true);
40
+ const [pairingsLoading, setPairingsLoading] = useState(true);
41
+ const [loading, setLoading] = useState(false);
42
+ const [disconnecting, setDisconnecting] = useState(false);
43
+ const [approvingCode, setApprovingCode] = useState<string | null>(null);
44
+ const [revokingId, setRevokingId] = useState<string | null>(null);
45
+ const [error, setError] = useState('');
46
+ const [success, setSuccess] = useState('');
47
+
48
+ const [tgToken, setTgToken] = useState('');
49
+ const [restarting, setRestarting] = useState(false);
50
+ const [confirmAction, setConfirmAction] = useState<{ type: 'revoke'; channel: string; senderId: string } | { type: 'disconnect'; channel: string } | null>(null);
51
+
52
+ const [pending, setPending] = useState<PendingPairing[]>([]);
53
+ const [approved, setApproved] = useState<ApprovedPairing[]>([]);
54
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
55
+ const pairingsAbortRef = useRef<AbortController | null>(null);
56
+ const channelsAbortRef = useRef<AbortController | null>(null);
57
+
58
+ const fetchChannels = useCallback(async () => {
59
+ // Abort any in-flight channels request
60
+ channelsAbortRef.current?.abort();
61
+ const controller = new AbortController();
62
+ channelsAbortRef.current = controller;
63
+ try {
64
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels`, { signal: controller.signal });
65
+ if (res.ok) {
66
+ const data = await res.json();
67
+ setChannels(data);
68
+ }
69
+ } catch (e: any) {
70
+ if (e?.name !== 'AbortError') { /* ignore aborted */ }
71
+ }
72
+ finally { setChannelsLoading(false); }
73
+ }, [agentId]);
74
+
75
+ // Shared fetch helper for pairings — cancels previous in-flight request
76
+ const doFetchPairings = useCallback(async (showLoading: boolean) => {
77
+ pairingsAbortRef.current?.abort();
78
+ const controller = new AbortController();
79
+ pairingsAbortRef.current = controller;
80
+ if (showLoading) setPairingsLoading(true);
81
+ try {
82
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram`, { signal: controller.signal });
83
+ if (res.ok) {
84
+ const data = await res.json();
85
+ setPending(data.pending ?? []);
86
+ setApproved(data.approved ?? []);
87
+ }
88
+ } catch (e: any) {
89
+ if (e?.name !== 'AbortError') { /* ignore aborted */ }
90
+ }
91
+ finally { if (showLoading) setPairingsLoading(false); }
92
+ }, [agentId]);
93
+
94
+ const fetchPairings = useCallback(() => doFetchPairings(true), [doFetchPairings]);
95
+ const pollPairings = useCallback(() => doFetchPairings(false), [doFetchPairings]);
96
+
97
+ useEffect(() => {
98
+ if (open) {
99
+ setChannelsLoading(true);
100
+ setPairingsLoading(true);
101
+ setChannels(null);
102
+ setPending([]);
103
+ setApproved([]);
104
+ fetchChannels();
105
+ setError('');
106
+ setSuccess('');
107
+ setTgToken('');
108
+ }
109
+ return () => {
110
+ if (pollRef.current) clearInterval(pollRef.current);
111
+ channelsAbortRef.current?.abort();
112
+ pairingsAbortRef.current?.abort();
113
+ };
114
+ }, [open, fetchChannels]);
115
+
116
+ // Initial pairings fetch when channels load
117
+ useEffect(() => {
118
+ if (!channelsLoading && channels?.telegram?.connected) {
119
+ fetchPairings();
120
+ }
121
+ }, [channelsLoading, channels?.telegram?.connected, fetchPairings]);
122
+
123
+ // Poll pairings when on the active tab and connected/linked
124
+ useEffect(() => {
125
+ if (open && channels?.telegram?.connected) {
126
+ pollRef.current = setInterval(pollPairings, PAIRING_POLL_MS);
127
+ } else {
128
+ if (pollRef.current) clearInterval(pollRef.current);
129
+ }
130
+ return () => { if (pollRef.current) clearInterval(pollRef.current); };
131
+ }, [open, channels?.telegram?.connected, pollPairings]);
132
+
133
+ const handleTelegramConnect = async () => {
134
+ setLoading(true); setError(''); setSuccess('');
135
+ try {
136
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/telegram`, {
137
+ method: 'PUT',
138
+ headers: { 'Content-Type': 'application/json' },
139
+ body: JSON.stringify({ botToken: tgToken }),
140
+ });
141
+ const data = await res.json();
142
+ if (!res.ok) {
143
+ setError(data.error || 'Failed to connect Telegram');
144
+ } else {
145
+ setSuccess('Token saved. Restart the agent to activate the bot.');
146
+ setTgToken('');
147
+ setPairingsLoading(true);
148
+ fetchChannels();
149
+ }
150
+ } catch { setError('Network error'); }
151
+ finally { setLoading(false); }
152
+ };
153
+
154
+ const handleTelegramDisconnect = async () => {
155
+ setConfirmAction({ type: 'disconnect', channel: 'telegram' });
156
+ };
157
+
158
+ const executeDisconnect = async (_channel: string) => {
159
+ setConfirmAction(null);
160
+ setDisconnecting(true); setError(''); setSuccess('');
161
+ try {
162
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/telegram`, { method: 'DELETE' });
163
+ if (!res.ok) {
164
+ const data = await res.json().catch(() => ({ error: 'Failed' }));
165
+ setError(data.error || 'Failed to disconnect Telegram');
166
+ } else {
167
+ setSuccess('Telegram disconnected.');
168
+ setPending([]);
169
+ fetchChannels();
170
+ }
171
+ } catch { setError('Network error'); }
172
+ finally { setDisconnecting(false); }
173
+ };
174
+
175
+ const handleRestartAgent = async () => {
176
+ setRestarting(true); setError(''); setSuccess('');
177
+ try {
178
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/restart`, { method: 'POST' });
179
+ const data = await res.json();
180
+ if (!res.ok) {
181
+ setError(data.error || 'Failed to restart agent');
182
+ } else {
183
+ setSuccess('Agent restarted. The bot is now active. Send /start on Telegram to get a pairing code.');
184
+ fetchChannels();
185
+ }
186
+ } catch { setError('Network error'); }
187
+ finally { setRestarting(false); }
188
+ };
189
+
190
+ const handleApprovePairing = async (code: string) => {
191
+ setApprovingCode(code); setError(''); setSuccess('');
192
+ try {
193
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram`, {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({ code }),
197
+ });
198
+ const data = await res.json();
199
+ if (!res.ok) {
200
+ setError(data.error || 'Failed to approve');
201
+ } else {
202
+ setSuccess(`Pairing ${code} approved.`);
203
+ await doFetchPairings(false);
204
+ }
205
+ } catch { setError('Network error'); }
206
+ finally { setApprovingCode(null); }
207
+ };
208
+
209
+ const handleRevokePairing = async (senderId: string) => {
210
+ setConfirmAction({ type: 'revoke', channel: 'telegram', senderId });
211
+ };
212
+
213
+ const executeRevoke = async (senderId: string, _channel: string) => {
214
+ setConfirmAction(null);
215
+ setRevokingId(senderId); setError(''); setSuccess('');
216
+ try {
217
+ const res = await fetch(
218
+ `/api/agents/${encodeURIComponent(agentId)}/channels/pairing?channel=telegram&senderId=${encodeURIComponent(senderId)}`,
219
+ { method: 'DELETE' },
220
+ );
221
+ const data = await res.json();
222
+ if (!res.ok) {
223
+ setError(data.error || 'Failed to revoke');
224
+ } else {
225
+ setSuccess(`Revoked ${senderId}.`);
226
+ await doFetchPairings(false);
227
+ }
228
+ } catch { setError('Network error'); }
229
+ finally { setRevokingId(null); }
230
+ };
231
+
232
+ const tg = channels?.telegram;
233
+
234
+ return (
235
+ <Modal open={open} onClose={onClose} title={`Channel Manager: ${agentName}`} maxWidth={520}>
236
+
237
+ {error && <div style={{ margin: '8px 0' }}><Badge tone="danger">{error}</Badge></div>}
238
+ {success && <div style={{ margin: '8px 0' }}><Badge tone="success">{success}</Badge></div>}
239
+
240
+ {/* Confirm modal */}
241
+ <Modal
242
+ open={confirmAction !== null}
243
+ onClose={() => setConfirmAction(null)}
244
+ title="Confirm"
245
+ maxWidth={340}
246
+ tone="warning"
247
+ >
248
+ <p style={{ fontSize: 12, color: 'var(--text)', margin: '0 0 16px 0' }}>
249
+ {confirmAction?.type === 'revoke'
250
+ ? `Revoke access for sender ${confirmAction.senderId}?`
251
+ : 'Disconnect Telegram? All paired users will be removed.'}
252
+ </p>
253
+ <div style={{ display: 'flex', gap: 8 }}>
254
+ <Button
255
+ variant="danger"
256
+ size="sm"
257
+ onClick={() => {
258
+ if (confirmAction?.type === 'revoke') executeRevoke(confirmAction.senderId, confirmAction.channel);
259
+ else if (confirmAction?.type === 'disconnect') executeDisconnect(confirmAction.channel);
260
+ }}
261
+ >
262
+ Confirm
263
+ </Button>
264
+ <Button variant="secondary" size="sm" onClick={() => setConfirmAction(null)}>
265
+ Cancel
266
+ </Button>
267
+ </div>
268
+ </Modal>
269
+
270
+ {channelsLoading ? (
271
+ <SkeletonLines count={4} />
272
+ ) : (
273
+ <TelegramTab
274
+ tg={tg ?? null}
275
+ tgToken={tgToken}
276
+ setTgToken={setTgToken}
277
+ loading={loading}
278
+ disconnecting={disconnecting}
279
+ pairingsLoading={pairingsLoading}
280
+ restarting={restarting}
281
+ pending={pending}
282
+ approved={approved}
283
+ approvingCode={approvingCode}
284
+ revokingId={revokingId}
285
+ onConnect={handleTelegramConnect}
286
+ onDisconnect={handleTelegramDisconnect}
287
+ onRestart={handleRestartAgent}
288
+ onApprove={handleApprovePairing}
289
+ onRevoke={handleRevokePairing}
290
+ />
291
+ )}
292
+ </Modal>
293
+ );
294
+ }
295
+
296
+ // ── Telegram Tab ─────────────────────────────
297
+
298
+ function TelegramTab({
299
+ tg, tgToken, setTgToken, loading, disconnecting, pairingsLoading, restarting,
300
+ pending, approved, approvingCode, revokingId,
301
+ onConnect, onDisconnect, onRestart, onApprove, onRevoke,
302
+ }: {
303
+ tg: TelegramChannel | null;
304
+ tgToken: string;
305
+ setTgToken: (v: string) => void;
306
+ loading: boolean;
307
+ disconnecting: boolean;
308
+ pairingsLoading: boolean;
309
+ restarting: boolean;
310
+ pending: PendingPairing[];
311
+ approved: ApprovedPairing[];
312
+ approvingCode: string | null;
313
+ revokingId: string | null;
314
+ onConnect: () => void;
315
+ onDisconnect: () => void;
316
+ onRestart: () => void;
317
+ onApprove: (code: string) => void;
318
+ onRevoke: (senderId: string) => void;
319
+ }) {
320
+ if (tg?.connected) {
321
+ return (
322
+ <div>
323
+ <Badge tone="success">Connected &mdash; {tg.botToken} &middot; DM: {tg.dmPolicy}</Badge>
324
+
325
+ <div style={{ marginTop: 12 }}>
326
+ <Button variant="secondary" size="sm" loading={restarting} disabled={restarting} onClick={onRestart}>
327
+ {restarting ? 'Restarting…' : 'Restart Agent'}
328
+ </Button>
329
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', marginLeft: 10 }}>
330
+ Required after token changes
331
+ </span>
332
+ </div>
333
+
334
+ {/* Pairing */}
335
+ <PairingSection
336
+ pending={pending}
337
+ approved={approved}
338
+ pairingsLoading={pairingsLoading}
339
+ approvingCode={approvingCode}
340
+ revokingId={revokingId}
341
+ onApprove={onApprove}
342
+ onRevoke={onRevoke}
343
+ />
344
+
345
+ <div style={{ marginTop: 16 }}>
346
+ <Button variant="danger" size="sm" onClick={onDisconnect} loading={disconnecting} disabled={disconnecting}>
347
+ {disconnecting ? 'Disconnecting…' : 'Disconnect Telegram'}
348
+ </Button>
349
+ </div>
350
+ </div>
351
+ );
352
+ }
353
+
354
+ return (
355
+ <div>
356
+ <p style={{ fontSize: 11, color: 'var(--text-dim)', margin: '0 0 10px' }}>
357
+ Get a bot token from{' '}
358
+ <a href="https://t.me/BotFather" target="_blank" rel="noopener" style={{ color: 'var(--violet)' }}>
359
+ @BotFather
360
+ </a>{' '}
361
+ on Telegram.
362
+ </p>
363
+
364
+ <Input
365
+ label="Bot Token"
366
+ type="password"
367
+ value={tgToken}
368
+ onChange={(e) => setTgToken(e.target.value)}
369
+ placeholder="1234567890:ABCdef..."
370
+ />
371
+
372
+ <div style={{ marginTop: 12 }}>
373
+ <Button variant="primary" size="sm" onClick={onConnect} loading={loading} disabled={loading || !tgToken.trim()}>
374
+ {loading ? 'Connecting…' : 'Connect Telegram'}
375
+ </Button>
376
+ </div>
377
+ </div>
378
+ );
379
+ }
380
+
381
+ // ── Pairing Section ──────────────────────────
382
+
383
+ function PairingSection({
384
+ pending, approved, pairingsLoading, approvingCode, revokingId,
385
+ onApprove, onRevoke,
386
+ }: {
387
+ pending: PendingPairing[];
388
+ approved: ApprovedPairing[];
389
+ pairingsLoading: boolean;
390
+ approvingCode: string | null;
391
+ revokingId: string | null;
392
+ onApprove: (code: string) => void;
393
+ onRevoke: (senderId: string) => void;
394
+ }) {
395
+ return (
396
+ <div style={{ marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 12 }}>
397
+ <div style={{
398
+ fontSize: 10, color: 'var(--violet)', textTransform: 'uppercase',
399
+ letterSpacing: '0.08em', marginBottom: 8,
400
+ }}>
401
+ Pairing
402
+ </div>
403
+
404
+ <p style={{ fontSize: 11, color: 'var(--text-dim)', margin: '0 0 10px 0' }}>
405
+ Send <code style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--violet)' }}>/start</code> to the bot on Telegram.
406
+ </p>
407
+
408
+ {pairingsLoading ? (
409
+ <SkeletonLines count={3} />
410
+ ) : (
411
+ <>
412
+ {/* Pending */}
413
+ {pending.length > 0 && (
414
+ <div style={{ marginBottom: 12 }}>
415
+ <div style={{ fontSize: 10, color: 'var(--yellow)', marginBottom: 6, letterSpacing: '0.04em' }}>PENDING</div>
416
+ {pending.map((p) => (
417
+ <div key={p.code} style={{
418
+ display: 'flex', alignItems: 'center', gap: 8,
419
+ padding: '6px 0', borderBottom: '1px solid var(--border)',
420
+ }}>
421
+ <code style={{
422
+ fontFamily: 'var(--font-mono)', fontSize: 12,
423
+ color: 'var(--blue)', minWidth: 72,
424
+ }}>{p.code}</code>
425
+ <Button variant="primary" size="sm" onClick={() => onApprove(p.code)} loading={approvingCode === p.code} disabled={approvingCode !== null}>{approvingCode === p.code ? 'Approving…' : 'Approve'}</Button>
426
+ </div>
427
+ ))}
428
+ </div>
429
+ )}
430
+
431
+ {pending.length === 0 && (
432
+ <div style={{ fontSize: 10, color: 'var(--text-dim)', marginBottom: 12 }}>
433
+ No pending requests
434
+ </div>
435
+ )}
436
+
437
+ {/* Approved */}
438
+ {approved.length > 0 && (
439
+ <div>
440
+ <div style={{ fontSize: 10, color: 'var(--green)', marginBottom: 6, letterSpacing: '0.04em' }}>APPROVED</div>
441
+ {approved.map((a) => (
442
+ <div key={a.senderId} style={{
443
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
444
+ padding: '6px 0', borderBottom: '1px solid var(--border)',
445
+ fontSize: 11,
446
+ }}>
447
+ <span style={{ fontFamily: 'var(--font-mono)', color: 'var(--text)' }}>
448
+ {a.senderId}
449
+ </span>
450
+ <Button variant="danger" size="sm" onClick={() => onRevoke(a.senderId)} loading={revokingId === a.senderId} disabled={revokingId !== null}>{revokingId === a.senderId ? 'Revoking…' : 'Revoke'}</Button>
451
+ </div>
452
+ ))}
453
+ </div>
454
+ )}
455
+
456
+ {approved.length === 0 && (
457
+ <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>
458
+ No approved senders
459
+ </div>
460
+ )}
461
+ </>
462
+ )}
463
+ </div>
464
+ );
465
+ }
@@ -0,0 +1,159 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Button } from '../components/ui';
5
+
6
+ interface ImageStatus {
7
+ exists: boolean;
8
+ needsUpdate: boolean;
9
+ downloading: boolean;
10
+ }
11
+
12
+ const POLL_MS = 2000;
13
+
14
+ export default function ImageDownloadBanner({ onImageReady }: { onImageReady?: () => void }) {
15
+ const [status, setStatus] = useState<ImageStatus>({ exists: true, needsUpdate: false, downloading: false });
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [justCompleted, setJustCompleted] = useState(false);
18
+ const [actionLoading, setActionLoading] = useState(false);
19
+ const wasDownloadingRef = useRef(false);
20
+ const completedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
21
+
22
+ useEffect(() => {
23
+ let active = true;
24
+
25
+ const tick = async () => {
26
+ try {
27
+ const res = await fetch('/api/agents/image-status');
28
+ if (!active) return;
29
+ if (!res.ok) return;
30
+ const data: ImageStatus = await res.json();
31
+ setStatus(data);
32
+
33
+ if (data.downloading) {
34
+ setError(null);
35
+ } else if (wasDownloadingRef.current) {
36
+ setJustCompleted(true);
37
+ setActionLoading(false);
38
+ onImageReady?.();
39
+ if (completedTimerRef.current) clearTimeout(completedTimerRef.current);
40
+ completedTimerRef.current = setTimeout(() => setJustCompleted(false), 3000);
41
+ }
42
+
43
+ wasDownloadingRef.current = data.downloading;
44
+ } catch { /* ignore */ }
45
+
46
+ if (active) setTimeout(tick, POLL_MS);
47
+ };
48
+
49
+ tick();
50
+ return () => { active = false; };
51
+ }, []);
52
+
53
+ useEffect(() => {
54
+ return () => {
55
+ if (completedTimerRef.current) clearTimeout(completedTimerRef.current);
56
+ };
57
+ }, []);
58
+
59
+ const handleDownload = useCallback(async () => {
60
+ setActionLoading(true);
61
+ setError(null);
62
+ try {
63
+ const res = await fetch('/api/agents/download-image', { method: 'POST' });
64
+ if (!res.ok) {
65
+ const data = await res.json().catch(() => ({ error: 'Failed to start download' }));
66
+ setError(data.error || 'Failed to start download');
67
+ setActionLoading(false);
68
+ }
69
+ } catch (e: unknown) {
70
+ setError((e as Error).message || 'Download failed');
71
+ setActionLoading(false);
72
+ }
73
+ }, []);
74
+
75
+ const isDownloading = status.downloading || actionLoading;
76
+ const showBanner = status.needsUpdate || isDownloading || justCompleted || error;
77
+
78
+ if (!showBanner && status.exists && !isDownloading) return null;
79
+
80
+ // ─── Downloading ───
81
+ if (isDownloading) {
82
+ return (
83
+ <Banner downloading>
84
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
85
+ <span style={{
86
+ display: 'inline-block', width: 8, height: 8,
87
+ borderRadius: '50%',
88
+ background: 'var(--violet)',
89
+ animation: 'pulse 1.5s ease-in-out infinite',
90
+ flexShrink: 0,
91
+ }} />
92
+ <span style={{ color: 'var(--violet)', fontWeight: 500 }}>Downloading agent base image…</span>
93
+ </div>
94
+ <div style={{
95
+ flex: 1, maxWidth: 120, height: 3,
96
+ background: 'var(--border)', borderRadius: 2, overflow: 'hidden',
97
+ }}>
98
+ <div style={{
99
+ height: 3, background: 'var(--violet)',
100
+ animation: 'progress-indeterminate 1.5s ease-in-out infinite',
101
+ width: '40%', borderRadius: 2,
102
+ }} />
103
+ </div>
104
+ <Button variant="secondary" size="sm" disabled>Downloading…</Button>
105
+ </Banner>
106
+ );
107
+ }
108
+
109
+ // ─── Just completed ───
110
+ if (justCompleted && !isDownloading) {
111
+ return (
112
+ <Banner success>
113
+ <div style={{ flex: 1 }}>
114
+ <span style={{ color: '#22c55e', fontWeight: 500 }}>✓ Agent base image ready</span>
115
+ </div>
116
+ </Banner>
117
+ );
118
+ }
119
+
120
+ // ─── Needs download ───
121
+ return (
122
+ <Banner warning>
123
+ <div style={{ flex: 1 }}>
124
+ <strong style={{ color: '#fbbf24' }}>
125
+ {status.exists ? '⚠ Agent base image is outdated.' : '⚠ Agent base image not found.'}
126
+ </strong>
127
+ {' '}{status.exists
128
+ ? 'A newer version is available on the registry.'
129
+ : 'Download it to create agent containers.'}
130
+ {error && <div style={{ color: '#ef4444', fontSize: 11, marginTop: 4 }}>{error}</div>}
131
+ </div>
132
+ <Button variant="primary" size="sm" onClick={handleDownload} loading={actionLoading}>
133
+ {actionLoading ? 'Starting…' : status.exists ? 'Download Update' : 'Download Image'}
134
+ </Button>
135
+ </Banner>
136
+ );
137
+ }
138
+
139
+ function Banner({
140
+ children, warning = false, downloading = false, success = false,
141
+ }: {
142
+ children: React.ReactNode; warning?: boolean; downloading?: boolean; success?: boolean;
143
+ }) {
144
+ const bg = success ? 'rgba(34,197,94,0.08)' :
145
+ downloading ? 'rgba(146,91,252,0.1)' :
146
+ warning ? 'rgba(251,191,36,0.08)' : 'rgba(146,91,252,0.08)';
147
+ const border = success ? 'rgba(34,197,94,0.25)' :
148
+ downloading ? 'rgba(146,91,252,0.25)' :
149
+ warning ? 'rgba(251,191,36,0.2)' : 'rgba(146,91,252,0.2)';
150
+ return (
151
+ <div style={{
152
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8,
153
+ padding: '12px 14px', background: bg, borderBottom: `1px solid ${border}`,
154
+ fontSize: 12, flexShrink: 0,
155
+ }}>
156
+ {children}
157
+ </div>
158
+ );
159
+ }