@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,117 @@
1
+ // Factory pattern: creates canonical Session objects from unknown raw input
2
+ import type { Session, SessionEvent, TreeNode } from '../types';
3
+ import { adaptSession, adaptEvent } from './ApiAdapter';
4
+ import { nodeLabel, shortLabel } from './sessionPresentation';
5
+
6
+ /** Produces normalized Session instances from arbitrary raw data. */
7
+ export class SessionFactory {
8
+ static create(raw: unknown): Session {
9
+ if (!raw || typeof raw !== 'object') {
10
+ return {
11
+ session_id: '',
12
+ status: 'idle',
13
+ };
14
+ }
15
+ return adaptSession(raw as Record<string, unknown>);
16
+ }
17
+
18
+ static createMany(raws: unknown[]): Session[] {
19
+ return raws.map(SessionFactory.create);
20
+ }
21
+ }
22
+
23
+ /** Produces normalized SessionEvent instances from arbitrary raw data. */
24
+ export class EventFactory {
25
+ static create(raw: unknown): SessionEvent {
26
+ if (!raw || typeof raw !== 'object') return {};
27
+ return adaptEvent(raw as Record<string, unknown>);
28
+ }
29
+
30
+ static createMany(raws: unknown[]): SessionEvent[] {
31
+ return raws.map(EventFactory.create);
32
+ }
33
+ }
34
+
35
+ // ── Helpers used by tree building ──────────────────────────────────────────
36
+
37
+ export function extractAgentId(sessionId: string): string {
38
+ return sessionId.split(':')[1] ?? 'unknown';
39
+ }
40
+
41
+
42
+ export function isCronSession(sessionId: string): boolean {
43
+ return sessionId.includes(':cron:');
44
+ }
45
+
46
+ /** Builds the D3 tree hierarchy from a flat session list. */
47
+ export function buildSessionTree(sessions: Session[], filter: string): TreeNode {
48
+ const normalizedFilter = filter.toLowerCase();
49
+ const filtered =
50
+ normalizedFilter === 'all'
51
+ ? sessions
52
+ : sessions.filter((s) => extractAgentId(s.session_id) === normalizedFilter);
53
+
54
+ const root: TreeNode = {
55
+ session_id: '__root__',
56
+ name: 'Argus',
57
+ status: 'idle',
58
+ _virtualRoot: true,
59
+ children: [],
60
+ };
61
+
62
+ const byId = new Map<string, TreeNode>();
63
+ const groups = new Map<string, Session[]>();
64
+
65
+ for (const session of filtered) {
66
+ if (!session.session_id) continue;
67
+ const node: TreeNode & { lineage_label?: string | null; task_preview?: string | null } = {
68
+ session_id: session.session_id,
69
+ name: session.label ?? shortLabel(session.session_id),
70
+ lineage_label: session.lineage_label ?? (session.label !== session.session_id ? session.label : null) ?? null,
71
+ task_preview: session.task_preview ?? null,
72
+ status: session.status,
73
+ model: session.model,
74
+ cost_usd: session.cost_usd ?? 0,
75
+ children: [],
76
+ };
77
+ byId.set(session.session_id, node);
78
+ const aid = extractAgentId(session.session_id);
79
+ if (!groups.has(aid)) groups.set(aid, []);
80
+ groups.get(aid)!.push(session);
81
+ }
82
+
83
+ const hasChild = (parent: TreeNode, childId: string) =>
84
+ (parent.children ?? []).some((c) => c.session_id === childId);
85
+
86
+ for (const [aid, agentSessions] of groups.entries()) {
87
+ const mainId = `agent:${aid}:main`;
88
+ const mainNode = byId.get(mainId);
89
+
90
+ const agentNode: TreeNode =
91
+ mainNode ?? {
92
+ session_id: `__agent__:${aid}`,
93
+ name: aid,
94
+ status: 'idle',
95
+ _agentNode: true,
96
+ children: [],
97
+ };
98
+
99
+ root.children!.push(agentNode);
100
+
101
+ for (const s of agentSessions) {
102
+ const sid = s.session_id;
103
+ if (!sid) continue;
104
+ if (mainNode && sid === mainId) continue;
105
+
106
+ const node = byId.get(sid);
107
+ if (!node) continue;
108
+
109
+ const parentNode = s.parent_id ? byId.get(s.parent_id) : undefined;
110
+ const fallbackParent = parentNode && !parentNode._virtualRoot ? parentNode : agentNode;
111
+ fallbackParent.children = fallbackParent.children ?? [];
112
+ if (!hasChild(fallbackParent, sid)) fallbackParent.children.push(node);
113
+ }
114
+ }
115
+
116
+ return root;
117
+ }
@@ -0,0 +1,117 @@
1
+ import type { Session, TreeNode } from '../types';
2
+ import { ACTIVE_STATUSES } from '../types';
3
+
4
+ function normalizeDisplayLabel(value: string | null | undefined): string {
5
+ const label = String(value ?? '').trim();
6
+ if (!label) return '';
7
+ if (/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(label)) return '';
8
+ if (/^(agent:[^\s]+:subagent:[0-9a-f-]+|subagent:[0-9a-f-]{8,}|session:[0-9a-f-]{8,})$/i.test(label)) return '';
9
+ if (/^agent:[a-z0-9_-]+(:[a-z0-9_-]+)*$/i.test(label)) return ''; // agent:ops:main, agent:ops:telegram:...
10
+ if (/^sub\s*agent\b[:\s-]*[a-z0-9-]+$/i.test(label)) return '';
11
+ return label;
12
+ }
13
+
14
+ function prettifyAgentName(value: string): string {
15
+ return value
16
+ .split(/[-_\s]+/)
17
+ .filter(Boolean)
18
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
19
+ .join(' ');
20
+ }
21
+
22
+ export function shortLabel(sessionId: string): string {
23
+ if (!sessionId) return 'session';
24
+ const parts = sessionId.split(':');
25
+ if (parts.length >= 4) return `${parts[2]}:${parts[3].slice(0, 6)}`;
26
+ if (parts.length >= 2) return `${parts[1]}:${parts[parts.length - 1].slice(0, 6)}`;
27
+ return sessionId.slice(0, 14);
28
+ }
29
+
30
+ function stripTaskBoilerplate(value: string): string {
31
+ return value
32
+ .replace(/^\[Subagent Task\]\s*/i, '')
33
+ .replace(/^Task richiesto da .*?:\s*/i, '')
34
+ .replace(/^Sei [^.\n]+\.\s*/i, '')
35
+ .replace(/^Begin\.?\s*/i, '')
36
+ .trim();
37
+ }
38
+
39
+ function summarizeTaskPreview(taskPreview: string | null | undefined): string {
40
+ const raw = String(taskPreview ?? '').replace(/\s+/g, ' ').trim();
41
+ if (!raw) return '';
42
+
43
+ const lines = String(taskPreview ?? '')
44
+ .split(/\r?\n/)
45
+ .map((line) => stripTaskBoilerplate(line.trim()))
46
+ .filter(Boolean);
47
+
48
+ const candidate = lines.find((line) => line.length >= 6 && !/^[-*#0-9.)\s]+$/.test(line)) ?? stripTaskBoilerplate(raw);
49
+ if (!candidate) return '';
50
+
51
+ const cleaned = candidate
52
+ .replace(/^[\-•*]\s*/, '')
53
+ .replace(/^\d+[.)]\s*/, '')
54
+ .replace(/\s+/g, ' ')
55
+ .trim();
56
+
57
+ if (!cleaned) return '';
58
+ return cleaned.length > 48 ? `${cleaned.slice(0, 45).trimEnd()}…` : cleaned;
59
+ }
60
+
61
+ function isSubagentSessionId(sessionId: string): boolean {
62
+ return sessionId.includes(':subagent:') || sessionId.startsWith('subagent:');
63
+ }
64
+
65
+ function subagentFallback(sessionId: string): string {
66
+ // es. agent:ops:subagent:UUID → "Ops Agent"
67
+ // es. agent:ops:cron:UUID → "Ops Cron"
68
+ const parts = sessionId.split(':');
69
+ const agentName = parts[1] ? prettifyAgentName(parts[1]) : '';
70
+ const kind = parts[2] ?? 'subagent';
71
+ if (kind === 'cron') return agentName ? `${agentName} Cron` : 'Cron';
72
+ const suffix = parts[3]?.replace(/[^a-z0-9]/gi, '').slice(0, 6) ?? '';
73
+ return agentName ? `${agentName} · ${suffix}` : suffix ? `Task ${suffix}` : 'Task';
74
+ }
75
+
76
+ const ACTIVE_WINDOW_MS = 5 * 60_000; // 5 minutes
77
+
78
+ export function isSessionActive(session: Pick<Session, 'status' | 'updated_at' | 'session_id'>): boolean {
79
+ if (ACTIVE_STATUSES.has(session.status)) return true;
80
+ // Also consider recently-updated sessions as active (just finished / heartbeat)
81
+ if (session.updated_at && Date.now() - session.updated_at < ACTIVE_WINDOW_MS) return true;
82
+ return false;
83
+ }
84
+
85
+ export function deriveSessionDisplayLabel(session: Pick<Session, 'session_id' | 'label' | 'lineage_label' | 'task_preview'>): string {
86
+ const sid = session.session_id || '';
87
+ const lineageLabel = normalizeDisplayLabel(session.lineage_label);
88
+ const directLabel = normalizeDisplayLabel(session.label);
89
+ const taskLabel = summarizeTaskPreview(session.task_preview);
90
+
91
+ if (lineageLabel) return lineageLabel;
92
+ if (directLabel && !directLabel.startsWith('agent:')) return directLabel;
93
+ if (taskLabel) return taskLabel;
94
+
95
+ if (sid.endsWith(':main')) {
96
+ const agent = sid.split(':')[1] ?? '';
97
+ if (agent) return prettifyAgentName(agent);
98
+ }
99
+
100
+ if (isSubagentSessionId(sid)) return subagentFallback(sid);
101
+ return shortLabel(sid);
102
+ }
103
+
104
+ export function nodeLabel(session: TreeNode & { lineage_label?: string | null; task_preview?: string | null }): string {
105
+ if (session._virtualRoot) return '';
106
+ if (session._agentNode) {
107
+ const raw = normalizeDisplayLabel((session as TreeNode & { name: string }).name);
108
+ return raw ? prettifyAgentName(raw) : 'Agent';
109
+ }
110
+
111
+ return deriveSessionDisplayLabel({
112
+ session_id: session.session_id,
113
+ label: session.name,
114
+ lineage_label: session.lineage_label,
115
+ task_preview: session.task_preview,
116
+ });
117
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Provider Balance — fetch account balance/credits from upstream AI providers.
3
+ *
4
+ * Each provider has its own non-standard endpoint and response format.
5
+ * We normalize them all to a common {balance, currency, raw} shape.
6
+ *
7
+ * Currently supported: deepseek, openrouter, glm (Z.AI)
8
+ * Not supported via API key: openai (needs cookie), anthropic, groq, qwen
9
+ */
10
+ import { readProviderKeys } from '@/app/api/gateway/provider/keys';
11
+
12
+ export interface BalanceResult {
13
+ /** Human-readable balance string, e.g. "$23.45" or "¥156.00" */
14
+ balance: string;
15
+ /** ISO currency code */
16
+ currency: string;
17
+ /** Raw value as number */
18
+ value: number;
19
+ /** Provider-specific extra info (optional) */
20
+ label?: string;
21
+ }
22
+
23
+ interface BalanceInfo {
24
+ currency: string;
25
+ total_balance: string;
26
+ topped_up_balance?: string;
27
+ granted_balance?: string;
28
+ }
29
+
30
+ /* ------------------------------------------------------------------ */
31
+ /* DeepSeek */
32
+ /* ------------------------------------------------------------------ */
33
+
34
+ async function fetchDeepSeekBalance(apiKey: string): Promise<BalanceResult> {
35
+ const resp = await fetch('https://api.deepseek.com/user/balance', {
36
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
37
+ });
38
+ if (!resp.ok) {
39
+ throw new Error(`DeepSeek balance API returned ${resp.status}`);
40
+ }
41
+ const json = await resp.json() as {
42
+ is_available: boolean;
43
+ balance_infos: BalanceInfo[];
44
+ };
45
+
46
+ const infos = json.balance_infos ?? [];
47
+ // Prefer CNY if available, else first entry
48
+ const cny = infos.find((i) => i.currency === 'CNY') ?? infos[0];
49
+ if (!cny) throw new Error('DeepSeek returned no balance info');
50
+
51
+ const value = parseFloat(cny.total_balance);
52
+ const currency = cny.currency;
53
+ const balance = `${currency === 'CNY' ? '¥' : '$'}${value.toFixed(2)}`;
54
+ return { balance, currency, value, label: 'Total balance' };
55
+ }
56
+
57
+ /* ------------------------------------------------------------------ */
58
+ /* OpenRouter */
59
+ /* ------------------------------------------------------------------ */
60
+
61
+ interface OpenRouterCredits {
62
+ data: {
63
+ total_credits: number;
64
+ total_usage: number;
65
+ };
66
+ }
67
+
68
+ async function fetchOpenRouterBalance(apiKey: string): Promise<BalanceResult> {
69
+ const resp = await fetch('https://openrouter.ai/api/v1/credits', {
70
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
71
+ });
72
+ if (!resp.ok) {
73
+ throw new Error(`OpenRouter credits API returned ${resp.status}`);
74
+ }
75
+ const json = (await resp.json()) as OpenRouterCredits;
76
+ const value = Math.max(0, Math.round((json.data.total_credits - json.data.total_usage) * 100) / 100);
77
+ return { balance: `$${value.toFixed(2)}`, currency: 'USD', value, label: 'Available' };
78
+ }
79
+
80
+ /* ------------------------------------------------------------------ */
81
+ /* Z.AI / GLM */
82
+ /* ------------------------------------------------------------------ */
83
+
84
+ interface ZAILimit {
85
+ /** e.g. "TOKENS_LIMIT" */
86
+ limitType: string;
87
+ limitValue: number;
88
+ /** remaining tokens */
89
+ remaining: number;
90
+ usage: number;
91
+ percentage: number;
92
+ nextResetTime?: string;
93
+ }
94
+
95
+ async function fetchZAIBalance(apiKey: string): Promise<BalanceResult> {
96
+ const resp = await fetch('https://api.z.ai/api/monitor/usage/quota/limit', {
97
+ headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
98
+ });
99
+ if (!resp.ok) {
100
+ throw new Error(`Z.AI quota API returned ${resp.status}`);
101
+ }
102
+ const json = await resp.json() as { limits?: ZAILimit[] };
103
+
104
+ const tokenLimit = json.limits?.find((l) => l.limitType === 'TOKENS_LIMIT');
105
+ if (tokenLimit) {
106
+ const usage = tokenLimit.usage ?? 0;
107
+ const limit = tokenLimit.limitValue ?? 0;
108
+ const remaining = tokenLimit.remaining ?? limit - usage;
109
+ const value = remaining;
110
+ // Show remaining + percentage
111
+ return {
112
+ balance: `${(remaining / 1_000_000).toFixed(1)}M tokens`,
113
+ currency: 'TOKENS',
114
+ value,
115
+ label: `${tokenLimit.percentage?.toFixed(0) ?? 0}% used of ${(limit / 1_000_000).toFixed(1)}M`,
116
+ };
117
+ }
118
+
119
+ // Fallback: sum all limits
120
+ const totalRemaining = (json.limits ?? []).reduce(
121
+ (sum, l) => sum + (l.remaining ?? l.limitValue - l.usage),
122
+ 0,
123
+ );
124
+ return {
125
+ balance: `${(totalRemaining / 1_000_000).toFixed(1)}M tokens`,
126
+ currency: 'TOKENS',
127
+ value: totalRemaining,
128
+ label: 'Remaining',
129
+ };
130
+ }
131
+
132
+ /* ------------------------------------------------------------------ */
133
+ /* Public API */
134
+ /* ------------------------------------------------------------------ */
135
+
136
+ export type BalanceProvider = 'deepseek' | 'openrouter' | 'glm';
137
+
138
+ const BALANCE_FETCHERS: Record<
139
+ BalanceProvider,
140
+ (apiKey: string) => Promise<BalanceResult>
141
+ > = {
142
+ deepseek: fetchDeepSeekBalance,
143
+ openrouter: fetchOpenRouterBalance,
144
+ glm: fetchZAIBalance,
145
+ };
146
+
147
+ /** Fetch balance for a single provider. Throws if not supported or API key missing. */
148
+ export async function fetchProviderBalance(
149
+ provider: BalanceProvider,
150
+ ): Promise<BalanceResult> {
151
+ const keys = readProviderKeys();
152
+ const apiKey = keys[provider];
153
+ if (!apiKey) {
154
+ throw new Error(`No API key configured for ${provider}`);
155
+ }
156
+
157
+ const fetcher = BALANCE_FETCHERS[provider];
158
+ if (!fetcher) {
159
+ throw new Error(`Balance fetching not supported for ${provider}`);
160
+ }
161
+
162
+ return fetcher(apiKey);
163
+ }
@@ -0,0 +1,38 @@
1
+ import { cookies } from 'next/headers';
2
+ import { redirect } from 'next/navigation';
3
+ import { jwtVerify } from 'jose';
4
+ import * as fs from 'fs';
5
+ import { ENV_FILE } from './rev4a-paths';
6
+
7
+ function getJwtSecret(): Uint8Array {
8
+ const fromEnv = process.env.REV4A_JWT_SECRET;
9
+ if (typeof fromEnv === 'string' && fromEnv.length > 0) {
10
+ return new TextEncoder().encode(fromEnv);
11
+ }
12
+ // Fallback: read from persistent .env
13
+ try {
14
+ if (fs.existsSync(ENV_FILE)) {
15
+ const content = fs.readFileSync(ENV_FILE, 'utf-8');
16
+ const match = content.match(/^REV4A_JWT_SECRET=(.*)$/m);
17
+ if (match && match[1].trim()) {
18
+ return new TextEncoder().encode(match[1].trim());
19
+ }
20
+ }
21
+ } catch { /* ignore */ }
22
+ return new TextEncoder().encode('');
23
+ }
24
+
25
+ export async function requireAuth(): Promise<void> {
26
+ const cookieStore = await cookies();
27
+ const token = cookieStore.get('rev4a_token')?.value;
28
+
29
+ if (token) {
30
+ try {
31
+ await jwtVerify(token, getJwtSecret());
32
+ return;
33
+ } catch {
34
+ // Invalid — redirect
35
+ }
36
+ }
37
+ redirect('/login');
38
+ }
@@ -0,0 +1,3 @@
1
+ import { NextResponse } from "next/server";
2
+ export function requireAuth(request: Request): NextResponse | null;
3
+ export function requireAuthJWT(request: Request): Promise<NextResponse | null>;
@@ -0,0 +1,36 @@
1
+ import { NextResponse } from "next/server";
2
+ import { jwtVerify } from "jose";
3
+
4
+ function getExpectedToken() {
5
+ const token = process.env.REV4A_TOKEN || process.env.NEXT_PUBLIC_REV4A_TOKEN || '';
6
+ if (!token) console.warn('REV4A_TOKEN is not set — auth will reject all requests');
7
+ return token;
8
+ }
9
+
10
+ export function requireAuth(request) {
11
+ const auth = request.headers.get("authorization");
12
+ if (!auth || auth !== `Bearer ${getExpectedToken()}`) {
13
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
+ }
15
+ return null;
16
+ }
17
+
18
+ const JWT_SECRET = new TextEncoder().encode(
19
+ process.env.REV4A_JWT_SECRET || ''
20
+ );
21
+
22
+ export async function requireAuthJWT(request) {
23
+ const cookieToken = request.cookies?.get?.("rev4a_token")?.value;
24
+ if (cookieToken) {
25
+ try {
26
+ await jwtVerify(cookieToken, JWT_SECRET);
27
+ return null;
28
+ } catch {}
29
+ }
30
+
31
+ const auth = request.headers.get("authorization");
32
+ const expectedToken = process.env.REV4A_TOKEN || process.env.NEXT_PUBLIC_REV4A_TOKEN || '';
33
+ if (auth === `Bearer ${expectedToken}`) return null;
34
+
35
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
36
+ }
@@ -0,0 +1,145 @@
1
+ import * as path from 'node:path';
2
+ import * as os from 'node:os';
3
+ import * as fs from 'node:fs';
4
+
5
+ /**
6
+ * Centralised Rev4a data directory.
7
+ *
8
+ * Cross-platform policy:
9
+ * Linux/macOS: ~/.config/rev4a/
10
+ * Windows: %APPDATA%/rev4a/
11
+ *
12
+ * All persistent files live here so npm reinstall / update
13
+ * never destroys configuration, databases, or secrets.
14
+ */
15
+ function rev4aDataDir(): string {
16
+ if (process.env.REV4A_DATA_DIR) return process.env.REV4A_DATA_DIR;
17
+ if (process.platform === 'win32') {
18
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'rev4a');
19
+ }
20
+ return path.join(os.homedir(), '.config', 'rev4a');
21
+ }
22
+
23
+ export const REV4A_DATA = rev4aDataDir();
24
+
25
+ /* ── File locations ─────────────────────────────────────────────── */
26
+
27
+ /** Persistent environment file (~/.config/rev4a/.env) */
28
+ export const ENV_FILE = path.join(REV4A_DATA, '.env');
29
+
30
+ /** SQLite events database */
31
+ export const DB_FILE = path.join(REV4A_DATA, 'data', 'events.db');
32
+
33
+ /** Credentials vault database */
34
+ export const CREDENTIALS_DB = path.join(REV4A_DATA, 'data', 'credentials.db');
35
+
36
+ /** Shared gateway token for agent containers */
37
+ export const AGENTS_TOKEN_FILE = path.join(REV4A_DATA, 'data', 'agents-token.json');
38
+
39
+ /** Provider API keys JSON */
40
+ export const PROVIDER_KEYS_FILE = path.join(REV4A_DATA, 'data', 'provider-keys.json');
41
+
42
+ /**
43
+ * Model catalogue — read-only, shipped with the package.
44
+ * Resolved at module load from the package root (alongside bin/rev4a.js).
45
+ */
46
+ function resolvePackageFile(filename: string): string {
47
+ // cwd in dev (workspace checkout)
48
+ const cwdPath = path.join(process.cwd(), filename);
49
+ if (fs.existsSync(cwdPath)) return cwdPath;
50
+
51
+ // Installed package: this file is at <pkgRoot>/lib/rev4a-paths.js
52
+ const pkgRoot = path.resolve(__dirname, '..');
53
+ const pkgPath = path.join(pkgRoot, filename);
54
+ if (fs.existsSync(pkgPath)) return pkgPath;
55
+
56
+ // Last resort: accept that it may not exist (will throw at read time)
57
+ return cwdPath;
58
+ }
59
+
60
+ /** Static model catalogue — read-only, inside the package repo */
61
+ export const MODELS_CONFIG_FILE = resolvePackageFile('models.config.json');
62
+
63
+ /** Top-level .next build directory (kept inside npm package for now — built by rev4a serve) */
64
+ export function nextDir(pkgRoot: string): string {
65
+ return path.join(pkgRoot, '.next');
66
+ }
67
+
68
+ /** Agent template directory (inside npm package — read-only) */
69
+ export function templatesDir(pkgRoot: string): string {
70
+ return path.join(pkgRoot, 'agent-templates');
71
+ }
72
+
73
+ /** Docker base image dir */
74
+ export function dockerfileDir(pkgRoot: string): string {
75
+ return path.join(pkgRoot, 'agent-templates', 'base-image');
76
+ }
77
+
78
+ /* ── Helpers ────────────────────────────────────────────────────── */
79
+
80
+ /** Ensure the data directory tree exists. */
81
+ export function ensurePaths(): void {
82
+ for (const dir of [REV4A_DATA, path.dirname(ENV_FILE), path.dirname(DB_FILE)]) {
83
+ if (!fs.existsSync(dir)) {
84
+ fs.mkdirSync(dir, { recursive: true });
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Read REV4A_* env vars from the persistent .env file.
91
+ * Returns a plain object suitable for spreading into child process env.
92
+ */
93
+ export function loadEnvVars(): Record<string, string> {
94
+ const vars: Record<string, string> = {};
95
+ if (!fs.existsSync(ENV_FILE)) return vars;
96
+
97
+ const content = fs.readFileSync(ENV_FILE, 'utf-8');
98
+ for (const line of content.split('\n')) {
99
+ const trimmed = line.trim();
100
+ if (!trimmed || trimmed.startsWith('#')) continue;
101
+ const eq = trimmed.indexOf('=');
102
+ if (eq === -1) continue;
103
+ const key = trimmed.slice(0, eq).trim();
104
+ const val = trimmed.slice(eq + 1).trim();
105
+ if (key.startsWith('REV4A_') || key.startsWith('PROVIDER_')) {
106
+ vars[key] = val;
107
+ }
108
+ }
109
+ return vars;
110
+ }
111
+
112
+ /**
113
+ * Write a key-value pair to the persistent .env file.
114
+ * Replaces existing value for the same key, appends if missing.
115
+ */
116
+ export function writeEnvVar(key: string, value: string): void {
117
+ ensurePaths();
118
+ let content = '';
119
+ if (fs.existsSync(ENV_FILE)) {
120
+ content = fs.readFileSync(ENV_FILE, 'utf-8');
121
+ }
122
+
123
+ const lines = content.split('\n');
124
+ let found = false;
125
+ const updated = lines.map((line) => {
126
+ const trimmed = line.trim();
127
+ if (trimmed.startsWith(`${key}=`)) {
128
+ found = true;
129
+ return `${key}=${value}`;
130
+ }
131
+ return line;
132
+ });
133
+
134
+ if (!found) {
135
+ updated.push(`${key}=${value}`);
136
+ }
137
+
138
+ fs.writeFileSync(ENV_FILE, updated.join('\n').trim() + '\n', {
139
+ encoding: 'utf-8',
140
+ mode: 0o600,
141
+ });
142
+
143
+ // Also set in process.env so current process picks it up immediately
144
+ process.env[key] = value;
145
+ }
@@ -0,0 +1,46 @@
1
+ export const DEFAULT_REV4A_TIMEZONE = 'Europe/Rome';
2
+
3
+ let timezoneCache: string | null = null;
4
+ let timezonePromise: Promise<string> | null = null;
5
+
6
+ export function normalizeRev4aTimezone(value: unknown): string {
7
+ return typeof value === 'string' && value ? value : DEFAULT_REV4A_TIMEZONE;
8
+ }
9
+
10
+ export async function fetchRev4aTimezone(): Promise<string> {
11
+ if (timezoneCache) return timezoneCache;
12
+ if (!timezonePromise) {
13
+ timezonePromise = fetch('/api/tools-config', {
14
+ cache: 'no-store',
15
+ credentials: 'same-origin',
16
+ })
17
+ .then(async (response) => {
18
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
19
+ const data = (await response.json()) as { timezone?: string };
20
+ const timezone = normalizeRev4aTimezone(data.timezone);
21
+ timezoneCache = timezone;
22
+ return timezone;
23
+ })
24
+ .catch(() => DEFAULT_REV4A_TIMEZONE)
25
+ .finally(() => {
26
+ timezonePromise = null;
27
+ });
28
+ }
29
+ return timezonePromise;
30
+ }
31
+
32
+ export function setCachedRev4aTimezone(timezone: string): void {
33
+ timezoneCache = normalizeRev4aTimezone(timezone);
34
+ }
35
+
36
+ export function formatDateTimeInTimezone(
37
+ value: string | number | Date,
38
+ options: Intl.DateTimeFormatOptions,
39
+ timezone = DEFAULT_REV4A_TIMEZONE,
40
+ locale = 'it-IT',
41
+ ): string {
42
+ return new Date(value).toLocaleString(locale, {
43
+ ...options,
44
+ timeZone: normalizeRev4aTimezone(timezone),
45
+ });
46
+ }