@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,350 @@
1
+ import { execFileSync } from 'child_process';
2
+
3
+ // ── Types ─────────────────────────────────────────────────────────────────
4
+
5
+ export interface CronSchedule {
6
+ kind?: string;
7
+ expr?: string;
8
+ tz?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export interface CronPayload {
13
+ kind?: string;
14
+ text?: string;
15
+ message?: string;
16
+ model?: string;
17
+ fallbacks?: string[];
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ export interface CronState {
22
+ lastStatus?: string;
23
+ nextRunAtMs?: number | null;
24
+ lastRunAtMs?: number | null;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface OpenClawCronJob {
29
+ id?: string;
30
+ name?: string;
31
+ description?: string;
32
+ agentId?: string;
33
+ containerName?: string;
34
+ enabled?: boolean;
35
+ schedule?: CronSchedule;
36
+ scheduleExpr?: string;
37
+ state?: CronState;
38
+ payload?: CronPayload;
39
+ createdAtMs?: number;
40
+ updatedAtMs?: number;
41
+ sessionTarget?: string;
42
+ wakeMode?: string;
43
+ source?: 'host' | 'container';
44
+ delivery?: {
45
+ mode?: string;
46
+ channel?: string;
47
+ to?: string;
48
+ accountId?: string;
49
+ [key: string]: unknown;
50
+ };
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ export interface OpenClawCronListResult {
55
+ ok: boolean;
56
+ jobs: OpenClawCronJob[];
57
+ total: number;
58
+ }
59
+
60
+ export interface AgentInfo {
61
+ agentId: string;
62
+ containerName: string;
63
+ }
64
+
65
+ // ── Extract schedule expression ──────────────────────────────────────────
66
+
67
+ function extractScheduleExpr(job: OpenClawCronJob): string {
68
+ if (job.scheduleExpr) return job.scheduleExpr;
69
+ if (typeof job.schedule === 'string') return job.schedule;
70
+ if (job.schedule && typeof job.schedule === 'object') {
71
+ return (job.schedule as any).expr ?? '';
72
+ }
73
+ return '';
74
+ }
75
+
76
+ // ── Discover agent containers ────────────────────────────────────────────
77
+
78
+ function discoverAgentContainers(): AgentInfo[] {
79
+ try {
80
+ const namesRaw = execFileSync('docker', [
81
+ 'ps', '--filter', 'label=AGENT_ID', '--format', '{{.Names}}'
82
+ ], { encoding: 'utf8', timeout: 5000 }).trim();
83
+ if (!namesRaw) return [];
84
+
85
+ return namesRaw.split('\n').map(name => {
86
+ const containerName = name.trim();
87
+ if (!containerName) return null;
88
+ let agentId = containerName;
89
+ try {
90
+ agentId = execFileSync('docker', [
91
+ 'inspect', containerName, '--format', '{{index .Config.Labels "AGENT_ID"}}'
92
+ ], { encoding: 'utf8', timeout: 5000 }).trim();
93
+ } catch { /* fallback */ }
94
+ return { containerName, agentId: agentId || containerName };
95
+ }).filter(Boolean) as AgentInfo[];
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ // ── Parse CLI output ─────────────────────────────────────────────────────
102
+
103
+ function parseCronListOutput(raw: string): OpenClawCronJob[] | null {
104
+ try {
105
+ const parsed = JSON.parse(raw.trim()) as unknown;
106
+ if (Array.isArray(parsed)) return parsed as OpenClawCronJob[];
107
+ if (parsed && typeof parsed === 'object' && Array.isArray((parsed as { jobs?: unknown[] }).jobs)) {
108
+ return (parsed as { jobs: OpenClawCronJob[] }).jobs;
109
+ }
110
+ return null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ function listJobsOnHost(): OpenClawCronJob[] | null {
117
+ try {
118
+ const raw = execFileSync('openclaw', ['cron', 'list', '--json', '--all'], {
119
+ encoding: 'utf8', timeout: 15000,
120
+ });
121
+ return parseCronListOutput(raw);
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ function listJobsInContainer(containerName: string): OpenClawCronJob[] | null {
128
+ // Try both common agent gateway ports. Agents default to 3000 (OpenClaw
129
+ // gateway --bind lan --port 3000) but may also run on 18789 (default).
130
+ const gatewayUrls = [
131
+ 'http://127.0.0.1:3000',
132
+ 'http://127.0.0.1:18789',
133
+ ];
134
+
135
+ for (const gwUrl of gatewayUrls) {
136
+ try {
137
+ const raw = execFileSync('docker', [
138
+ 'exec', '-e', `OPENCLAW_GATEWAY_URL=${gwUrl}`,
139
+ containerName, 'openclaw', 'cron', 'list', '--json', '--all',
140
+ ], { encoding: 'utf8', timeout: 15000 });
141
+ const parsed = parseCronListOutput(raw);
142
+ if (parsed) return parsed;
143
+ } catch { /* try next gateway port */ }
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ // ── PATCH helpers ────────────────────────────────────────────────────────
150
+
151
+ function toggleOnHost(jobId: string, enabled: boolean): boolean {
152
+ try {
153
+ const action = enabled ? 'enable' : 'disable';
154
+ execFileSync('openclaw', ['cron', action, jobId], {
155
+ encoding: 'utf8', timeout: 10000,
156
+ });
157
+ return true;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ function toggleInContainer(containerName: string, jobId: string, enabled: boolean): boolean {
164
+ const gatewayUrls = [
165
+ 'http://127.0.0.1:3000',
166
+ 'http://127.0.0.1:18789',
167
+ ];
168
+ for (const gwUrl of gatewayUrls) {
169
+ try {
170
+ const action = enabled ? 'enable' : 'disable';
171
+ execFileSync('docker', [
172
+ 'exec', '-e', `OPENCLAW_GATEWAY_URL=${gwUrl}`,
173
+ containerName, 'openclaw', 'cron', action, jobId,
174
+ ], { encoding: 'utf8', timeout: 10000 });
175
+ return true;
176
+ } catch { /* try next gateway port */ }
177
+ }
178
+ return false;
179
+ }
180
+
181
+ // ── List: host gateway + per-container cron ─────────────────────────────
182
+
183
+ export function listOpenClawCronJobs(): OpenClawCronListResult {
184
+ const seen = new Set<string>();
185
+ const jobs: OpenClawCronJob[] = [];
186
+
187
+ // 1. Host gateway cron jobs
188
+ const hostJobs = listJobsOnHost();
189
+ if (hostJobs) {
190
+ for (const job of hostJobs) {
191
+ job.scheduleExpr = extractScheduleExpr(job);
192
+ job.source = 'host';
193
+ jobs.push(job);
194
+ if (job.id) seen.add(job.id);
195
+ }
196
+ }
197
+
198
+ // 2. Agent container cron jobs
199
+ const agents = discoverAgentContainers();
200
+ for (const agent of agents) {
201
+ const containerJobs = listJobsInContainer(agent.containerName);
202
+ if (containerJobs) {
203
+ for (const job of containerJobs) {
204
+ job.scheduleExpr = extractScheduleExpr(job);
205
+ job.source = 'container';
206
+ job.containerName = agent.containerName;
207
+ if (!job.agentId) job.agentId = agent.agentId;
208
+
209
+ if (job.id && seen.has(job.id)) continue;
210
+
211
+ jobs.push(job);
212
+ if (job.id) seen.add(job.id);
213
+ }
214
+ }
215
+ }
216
+
217
+ return { ok: true, jobs, total: jobs.length };
218
+ }
219
+
220
+ // ── Patch: try container first (if containerName given), then host ──────
221
+
222
+ export interface CronPatchResult {
223
+ ok: boolean;
224
+ job?: OpenClawCronJob;
225
+ error?: string;
226
+ }
227
+
228
+ // ── Cron run history ─────────────────────────────────────────────────────
229
+
230
+ export interface CronRunEntry {
231
+ ts?: number;
232
+ jobId?: string;
233
+ action?: string;
234
+ status?: string;
235
+ error?: string;
236
+ summary?: string;
237
+ runId?: string;
238
+ runAtMs?: number;
239
+ durationMs?: number;
240
+ nextRunAtMs?: number;
241
+ model?: string;
242
+ provider?: string;
243
+ usage?: Record<string, number>;
244
+ diagnostics?: {
245
+ summary?: string;
246
+ entries?: Array<{ ts?: number; source?: string; severity?: string; message?: string }>;
247
+ };
248
+ [key: string]: unknown;
249
+ }
250
+
251
+ export interface CronRunsResult {
252
+ ok: boolean;
253
+ entries: CronRunEntry[];
254
+ error?: string;
255
+ }
256
+
257
+ export function getCronRuns(
258
+ containerName: string,
259
+ jobId: string,
260
+ limit: number = 20,
261
+ ): CronRunsResult {
262
+ const gatewayUrls = [
263
+ 'http://127.0.0.1:3000',
264
+ 'http://127.0.0.1:18789',
265
+ ];
266
+
267
+ for (const gwUrl of gatewayUrls) {
268
+ try {
269
+ const raw = execFileSync('docker', [
270
+ 'exec', '-e', `OPENCLAW_GATEWAY_URL=${gwUrl}`,
271
+ containerName, 'openclaw', 'cron', 'runs', '--id', jobId,
272
+ '--limit', String(limit),
273
+ ], { encoding: 'utf8', timeout: 15000 });
274
+
275
+ const parsed = JSON.parse(raw.trim()) as { entries?: CronRunEntry[] };
276
+ // Accept both empty and populated entries arrays
277
+ if (parsed && Array.isArray(parsed.entries)) {
278
+ return { ok: true, entries: parsed.entries };
279
+ }
280
+ // Not the expected format; try next port
281
+ } catch { /* try next gateway port */ }
282
+ }
283
+
284
+ return { ok: false, entries: [], error: 'Cannot connect to container gateway for runs' };
285
+ }
286
+
287
+ export function patchCronJob(id: string, patch: Partial<OpenClawCronJob>): CronPatchResult {
288
+ if (typeof patch.enabled !== 'boolean') {
289
+ return { ok: false, error: 'Only the enabled field can be patched' };
290
+ }
291
+
292
+ const containerName = (patch as any).containerName;
293
+
294
+ if (containerName) {
295
+ const ok = toggleInContainer(containerName, id, patch.enabled);
296
+ if (ok) return { ok: true, job: { id, enabled: patch.enabled } as OpenClawCronJob };
297
+ return { ok: false, error: `Failed to toggle cron in container ${containerName}` };
298
+ }
299
+
300
+ const ok = toggleOnHost(id, patch.enabled);
301
+ if (ok) return { ok: true, job: { id, enabled: patch.enabled } as OpenClawCronJob };
302
+ return { ok: false, error: 'Failed to toggle cron on host gateway' };
303
+ }
304
+
305
+ // ── Remove ─────────────────────────────────────────────────────────────
306
+
307
+ export interface CronRemoveResult {
308
+ ok: boolean;
309
+ error?: string;
310
+ }
311
+
312
+ function removeOnHost(jobId: string): boolean {
313
+ try {
314
+ execFileSync('openclaw', ['cron', 'remove', jobId], {
315
+ encoding: 'utf8', timeout: 10000,
316
+ });
317
+ return true;
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ function removeInContainer(containerName: string, jobId: string): boolean {
324
+ const gatewayUrls = [
325
+ 'http://127.0.0.1:3000',
326
+ 'http://127.0.0.1:18789',
327
+ ];
328
+ for (const gwUrl of gatewayUrls) {
329
+ try {
330
+ execFileSync('docker', [
331
+ 'exec', '-e', `OPENCLAW_GATEWAY_URL=${gwUrl}`,
332
+ containerName, 'openclaw', 'cron', 'remove', jobId,
333
+ ], { encoding: 'utf8', timeout: 10000 });
334
+ return true;
335
+ } catch { /* try next gateway port */ }
336
+ }
337
+ return false;
338
+ }
339
+
340
+ export function removeCronJob(id: string, containerName?: string): CronRemoveResult {
341
+ if (containerName) {
342
+ const ok = removeInContainer(containerName, id);
343
+ if (ok) return { ok: true };
344
+ return { ok: false, error: `Failed to remove cron job in container ${containerName}` };
345
+ }
346
+
347
+ const ok = removeOnHost(id);
348
+ if (ok) return { ok: true };
349
+ return { ok: false, error: 'Failed to remove cron job on host gateway' };
350
+ }
@@ -0,0 +1,73 @@
1
+ // Adapter pattern: normalizes raw API shapes into domain types
2
+ import type { Session, SessionStatus, SessionEvent, Costs, ModelCost } from '../types';
3
+
4
+ const VALID_STATUSES = new Set<SessionStatus>(['working', 'idle', 'error', 'active', 'completed']);
5
+
6
+ function toStatus(raw: unknown): SessionStatus {
7
+ const s = String(raw ?? 'idle').toLowerCase();
8
+ return VALID_STATUSES.has(s as SessionStatus) ? (s as SessionStatus) : 'idle';
9
+ }
10
+
11
+ function toNumber(raw: unknown): number | null {
12
+ if (raw === null || raw === undefined) return null;
13
+ const n = Number(raw);
14
+ return Number.isFinite(n) ? n : null;
15
+ }
16
+
17
+ function toString(raw: unknown): string | null {
18
+ if (raw === null || raw === undefined) return null;
19
+ return String(raw);
20
+ }
21
+
22
+ /** Adapts a raw API session object into the canonical Session shape. */
23
+ export function adaptSession(raw: Record<string, unknown>): Session {
24
+ return {
25
+ session_id: String(raw.session_id ?? raw.sessionId ?? raw.id ?? ''),
26
+ parent_id: toString(raw.parent_id ?? raw.parentId) ?? null,
27
+ label: toString(raw.label ?? raw.session_label) ?? null,
28
+ lineage_label: toString(raw.lineage_label ?? raw.lineageLabel ?? raw.agent_name) ?? null,
29
+ lineage_agent_name: toString(raw.lineage_agent_name ?? raw.agent_name) ?? null,
30
+ model: toString(raw.model) ?? null,
31
+ status: toStatus(raw.status),
32
+ tokens_in: toNumber(raw.tokens_in),
33
+ tokens_out: toNumber(raw.tokens_out),
34
+ cost_usd: toNumber(raw.cost_usd),
35
+ started_at: toNumber(raw.started_at),
36
+ updated_at: toNumber(raw.updated_at),
37
+ ended_at: toNumber(raw.ended_at),
38
+ task_preview: toString(raw.task_preview) ?? null,
39
+ };
40
+ }
41
+
42
+ /** Adapts a raw API event object. */
43
+ export function adaptEvent(raw: Record<string, unknown>): SessionEvent {
44
+ return {
45
+ id: (raw.id as string | number | undefined) ?? undefined,
46
+ ts: toNumber(raw.ts) ?? undefined,
47
+ type: toString(raw.type ?? raw.event) ?? undefined,
48
+ event: toString(raw.event) ?? undefined,
49
+ data: raw.data,
50
+ session_id: toString(raw.session_id) ?? undefined,
51
+ session_label: toString(raw.session_label ?? raw.label) ?? undefined,
52
+ };
53
+ }
54
+
55
+ /** Adapts a raw costs API response. */
56
+ export function adaptCosts(raw: Record<string, unknown>): Costs {
57
+ const byModel: ModelCost[] = Array.isArray(raw.byModel)
58
+ ? (raw.byModel as Record<string, unknown>[]).map((m) => ({
59
+ model: String(m.model ?? 'unknown'),
60
+ cost_usd: toNumber(m.cost_usd) ?? 0,
61
+ tokens_in: toNumber(m.tokens_in) ?? undefined,
62
+ tokens_out: toNumber(m.tokens_out) ?? undefined,
63
+ sessions: toNumber(m.sessions) ?? undefined,
64
+ }))
65
+ : [];
66
+
67
+ return {
68
+ today: toNumber(raw.today) ?? 0,
69
+ allTime: toNumber(raw.allTime) ?? 0,
70
+ allTimeSource: toString(raw.allTimeSource) ?? undefined,
71
+ byModel,
72
+ };
73
+ }
@@ -0,0 +1,65 @@
1
+ // Facade + Singleton: wraps all Rev4a API calls behind a clean interface
2
+ import type { Session, SessionDetail, Costs, SessionEvent } from '../types';
3
+ import { SessionFactory, EventFactory } from './SessionFactory';
4
+ import { adaptCosts, adaptSession, adaptEvent } from './ApiAdapter';
5
+
6
+ const API_TOKEN =
7
+ typeof process !== 'undefined'
8
+ ? (process.env.NEXT_PUBLIC_REV4A_TOKEN ?? '')
9
+ : '';
10
+
11
+ async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
12
+ const sep = path.includes('?') ? '&' : '?';
13
+ const url = `${path}${sep}token=${encodeURIComponent(API_TOKEN)}`;
14
+ const res = await fetch(url, {
15
+ ...init,
16
+ credentials: 'same-origin',
17
+ headers: { ...init?.headers },
18
+ });
19
+ const data = await res.json();
20
+ if (!res.ok) throw new Error((data as { error?: string }).error ?? `HTTP ${res.status}`);
21
+ return data as T;
22
+ }
23
+
24
+ class Rev4aApiClientClass {
25
+ private static _instance: Rev4aApiClientClass | null = null;
26
+
27
+ private constructor() {}
28
+
29
+ static get instance(): Rev4aApiClientClass {
30
+ Rev4aApiClientClass._instance ??= new Rev4aApiClientClass();
31
+ return Rev4aApiClientClass._instance;
32
+ }
33
+
34
+ async fetchSessions(): Promise<Session[]> {
35
+ const raw = await apiFetch<unknown[]>('/api/sessions');
36
+ return Array.isArray(raw) ? SessionFactory.createMany(raw) : [];
37
+ }
38
+
39
+ async fetchCosts(): Promise<Costs> {
40
+ const raw = await apiFetch<Record<string, unknown>>('/api/costs');
41
+ return adaptCosts(raw);
42
+ }
43
+
44
+ async fetchSessionDetail(id: string): Promise<SessionDetail> {
45
+ const raw = await apiFetch<Record<string, unknown>>(
46
+ `/api/session?id=${encodeURIComponent(id)}`,
47
+ );
48
+ return {
49
+ session: adaptSession((raw.session ?? raw) as Record<string, unknown>),
50
+ events: Array.isArray(raw.events)
51
+ ? EventFactory.createMany(raw.events as unknown[])
52
+ : [],
53
+ children: Array.isArray(raw.children)
54
+ ? SessionFactory.createMany(raw.children as unknown[])
55
+ : [],
56
+ };
57
+ }
58
+
59
+ async fetchEvents(limit = 50): Promise<SessionEvent[]> {
60
+ const raw = await apiFetch<unknown[]>(`/api/events?limit=${limit}`);
61
+ return Array.isArray(raw) ? EventFactory.createMany(raw) : [];
62
+ }
63
+ }
64
+
65
+ export const Rev4aApiClient = Rev4aApiClientClass.instance;
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+ // Observer + Singleton: manages one shared SSE connection; components subscribe as observers
3
+
4
+ import type { StreamMessage, Session, SessionEvent } from '../types';
5
+ import { SessionFactory, EventFactory } from './SessionFactory';
6
+
7
+ // ── Observer interface ─────────────────────────────────────────────────────
8
+
9
+ export interface IDashboardObserver {
10
+ onSessions?(sessions: Session[]): void;
11
+ onEvents?(events: SessionEvent[]): void;
12
+ onCostUpdate?(today: number): void;
13
+ }
14
+
15
+ // ── Singleton EventBus ─────────────────────────────────────────────────────
16
+
17
+ function sessionsFingerprint(data: unknown[]): string {
18
+ return (data as { session_id?: string; parent_id?: string | null; status?: string; cost_usd?: number; label?: string; lineage_label?: string | null }[])
19
+ .map((s) => `${s.session_id}|${s.parent_id ?? ''}|${s.status}|${s.cost_usd}|${s.label}|${s.lineage_label ?? ''}`)
20
+ .join('\n');
21
+ }
22
+
23
+ class Rev4aEventBusClass {
24
+ private static _instance: Rev4aEventBusClass | null = null;
25
+ private observers = new Set<IDashboardObserver>();
26
+ private source: EventSource | null = null;
27
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
28
+ private lastSessionsHash = '';
29
+ private latestSessions: Session[] | null = null;
30
+ private latestEvents: SessionEvent[] | null = null;
31
+ private latestCostToday: number | null = null;
32
+
33
+ private constructor() {}
34
+
35
+ static get instance(): Rev4aEventBusClass {
36
+ Rev4aEventBusClass._instance ??= new Rev4aEventBusClass();
37
+ return Rev4aEventBusClass._instance;
38
+ }
39
+
40
+ subscribe(observer: IDashboardObserver): () => void {
41
+ this.observers.add(observer);
42
+ this.replayTo(observer);
43
+ if (this.observers.size === 1) this.connect();
44
+ return () => this.unsubscribe(observer);
45
+ }
46
+
47
+ unsubscribe(observer: IDashboardObserver): void {
48
+ this.observers.delete(observer);
49
+ if (this.observers.size === 0) this.disconnect();
50
+ }
51
+
52
+ private connect(): void {
53
+ this.disconnect();
54
+ // EventSource cannot send headers; pass token via query param for browser auth
55
+ const token = process.env.NEXT_PUBLIC_REV4A_TOKEN ?? '';
56
+ this.source = new EventSource(`/api/stream?token=${encodeURIComponent(token)}`);
57
+
58
+ this.source.onmessage = (e: MessageEvent<string>) => {
59
+ try {
60
+ const msg = JSON.parse(e.data) as StreamMessage;
61
+ this.dispatch(msg);
62
+ } catch {
63
+ // Ignore malformed chunks.
64
+ }
65
+ };
66
+
67
+ this.source.onerror = () => {
68
+ this.source?.close();
69
+ this.source = null;
70
+ this.reconnectTimer = setTimeout(() => this.connect(), 5000);
71
+ };
72
+ }
73
+
74
+ private disconnect(): void {
75
+ if (this.reconnectTimer) {
76
+ clearTimeout(this.reconnectTimer);
77
+ this.reconnectTimer = null;
78
+ }
79
+ this.source?.close();
80
+ this.source = null;
81
+ }
82
+
83
+ private replayTo(observer: IDashboardObserver): void {
84
+ if (this.latestSessions && observer.onSessions) observer.onSessions(this.latestSessions);
85
+ if (this.latestEvents && observer.onEvents) observer.onEvents(this.latestEvents);
86
+ if (this.latestCostToday !== null && observer.onCostUpdate) observer.onCostUpdate(this.latestCostToday);
87
+ }
88
+
89
+ private dispatch(msg: StreamMessage): void {
90
+ if (msg.type === 'sessions') {
91
+ const hash = sessionsFingerprint(Array.isArray(msg.data) ? msg.data : []);
92
+ if (hash === this.lastSessionsHash) return;
93
+ this.lastSessionsHash = hash;
94
+ this.latestSessions = SessionFactory.createMany(msg.data);
95
+ } else if (msg.type === 'events') {
96
+ this.latestEvents = EventFactory.createMany(msg.data);
97
+ } else if (msg.type === 'costs') {
98
+ this.latestCostToday = Number(msg.data?.today ?? 0);
99
+ }
100
+
101
+ for (const obs of this.observers) {
102
+ if (msg.type === 'sessions' && obs.onSessions && this.latestSessions) {
103
+ obs.onSessions(this.latestSessions);
104
+ } else if (msg.type === 'events' && obs.onEvents && this.latestEvents) {
105
+ obs.onEvents(this.latestEvents);
106
+ } else if (msg.type === 'costs' && obs.onCostUpdate && this.latestCostToday !== null) {
107
+ obs.onCostUpdate(this.latestCostToday);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ export const Rev4aEventBus = Rev4aEventBusClass.instance;
@@ -0,0 +1,79 @@
1
+ // Strategy pattern: interchangeable session filter algorithms
2
+ import type { Session, FilterConfig, Period, PERIOD_MS } from '../types';
3
+ import { PERIOD_MS as PERIOD_MS_MAP } from '../types';
4
+ import { extractAgentId, isCronSession } from './SessionFactory';
5
+ import { isSessionActive } from './sessionPresentation';
6
+
7
+ /** Base interface — every filter strategy must implement this. */
8
+ export interface IFilterStrategy {
9
+ filter(sessions: Session[]): Session[];
10
+ }
11
+
12
+ // ── Concrete strategies ────────────────────────────────────────────────────
13
+
14
+ export class AgentFilterStrategy implements IFilterStrategy {
15
+ constructor(private readonly agent: string) {}
16
+
17
+ filter(sessions: Session[]): Session[] {
18
+ if (this.agent === 'all') return sessions;
19
+ return sessions.filter((s) => extractAgentId(s.session_id) === this.agent);
20
+ }
21
+ }
22
+
23
+ export class ActiveOnlyFilterStrategy implements IFilterStrategy {
24
+ filter(sessions: Session[]): Session[] {
25
+ return sessions.filter((s) => isSessionActive(s) && s.status !== 'idle');
26
+ }
27
+ }
28
+
29
+ export class NoCronFilterStrategy implements IFilterStrategy {
30
+ filter(sessions: Session[]): Session[] {
31
+ return sessions.filter((s) => !isCronSession(s.session_id));
32
+ }
33
+ }
34
+
35
+ export class PeriodFilterStrategy implements IFilterStrategy {
36
+ private readonly cutoff: number;
37
+
38
+ constructor(period: Period) {
39
+ this.cutoff =
40
+ period === 'all' ? 0 : Date.now() - PERIOD_MS_MAP[period as Exclude<Period, 'all'>];
41
+ }
42
+
43
+ filter(sessions: Session[]): Session[] {
44
+ if (this.cutoff === 0) return sessions;
45
+ return sessions.filter((s) => {
46
+ const latest = Math.max(Number(s.started_at ?? 0), Number(s.updated_at ?? 0));
47
+ return latest >= this.cutoff;
48
+ });
49
+ }
50
+ }
51
+
52
+ // ── Composite strategy ─────────────────────────────────────────────────────
53
+
54
+ /** Chains multiple strategies, applying each in sequence. */
55
+ export class CompositeFilterStrategy implements IFilterStrategy {
56
+ private readonly strategies: IFilterStrategy[];
57
+
58
+ constructor(strategies: IFilterStrategy[]) {
59
+ this.strategies = strategies;
60
+ }
61
+
62
+ filter(sessions: Session[]): Session[] {
63
+ return this.strategies.reduce((acc, s) => s.filter(acc), sessions);
64
+ }
65
+ }
66
+
67
+ // ── Factory for building the composite from a FilterConfig ────────────────
68
+
69
+ export function buildFilterStrategy(config: FilterConfig): IFilterStrategy {
70
+ const strategies: IFilterStrategy[] = [
71
+ new AgentFilterStrategy(config.agent),
72
+ new PeriodFilterStrategy(config.period),
73
+ ];
74
+
75
+ if (config.showOnlyActive) strategies.push(new ActiveOnlyFilterStrategy());
76
+ if (!config.showCron) strategies.push(new NoCronFilterStrategy());
77
+
78
+ return new CompositeFilterStrategy(strategies);
79
+ }