@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,203 @@
1
+ import { NextResponse } from 'next/server';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync, execFileSync } from 'child_process';
5
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
6
+
7
+ const SHARED_SKILLS_DIR = '/docker/shared-skills';
8
+ const CONTAINER_BUNDLED_SKILLS_DIR = '/usr/local/lib/node_modules/openclaw/skills';
9
+
10
+ function readSkillMeta(skillDir, name, type, agentId) {
11
+ const skillMdPath = path.join(skillDir, name, 'SKILL.md');
12
+ let description = '';
13
+ let version = '';
14
+ let hasSkillMd = false;
15
+
16
+ try {
17
+ const content = fs.readFileSync(skillMdPath, 'utf8');
18
+ hasSkillMd = true;
19
+ const descMatch = content.match(/^description:\s*(.+)$/m);
20
+ const verMatch = content.match(/^\*\*Version:\*\*\s*(.+)$/m) || content.match(/^version:\s*(.+)$/m);
21
+ if (descMatch) description = descMatch[1].trim();
22
+ if (verMatch) version = verMatch[1].trim();
23
+ } catch {}
24
+
25
+ const result = {
26
+ name,
27
+ type,
28
+ path: path.join(skillDir, name),
29
+ skillMdPath: hasSkillMd ? skillMdPath : null,
30
+ hasSkillMd,
31
+ description,
32
+ version,
33
+ };
34
+
35
+ if (agentId) result.agentId = agentId;
36
+
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * Discover agent containers with AGENT_ID label.
42
+ * Returns [{ containerName, agentId }]
43
+ */
44
+ function discoverAgentContainers() {
45
+ try {
46
+ // Get container names with AGENT_ID label
47
+ const namesRaw = execFileSync('docker', [
48
+ 'ps', '--filter', 'label=AGENT_ID', '--format', '{{.Names}}'
49
+ ], { encoding: 'utf8', timeout: 5000 }).trim();
50
+ if (!namesRaw) return [];
51
+
52
+ return namesRaw.split('\n').map(name => {
53
+ const containerName = name.trim();
54
+ if (!containerName) return null;
55
+ // Get agent ID from label using docker inspect
56
+ const agentId = execFileSync('docker', [
57
+ 'inspect', containerName, '--format', '{{index .Config.Labels "AGENT_ID"}}'
58
+ ], { encoding: 'utf8', timeout: 5000 }).trim();
59
+ return { containerName, agentId: agentId || containerName };
60
+ }).filter(Boolean);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Read skill directories from a container agent's workspace via docker exec.
68
+ * Returns an array of skill names found.
69
+ */
70
+ function readContainerSkillDirs(containerName) {
71
+ const workspaceSkillsPath = '/root/.openclaw/workspace/skills';
72
+ try {
73
+ // Check if the skills directory exists
74
+ execFileSync('docker', ['exec', containerName, 'test', '-d', workspaceSkillsPath], {
75
+ encoding: 'utf8', timeout: 5000
76
+ });
77
+
78
+ // List subdirectories (each is a potential skill)
79
+ const raw = execFileSync('docker', ['exec', containerName, 'ls', '-1', workspaceSkillsPath], {
80
+ encoding: 'utf8', timeout: 5000
81
+ }).trim();
82
+
83
+ if (!raw) return [];
84
+
85
+ return raw.split('\n').map(line => line.trim()).filter(Boolean);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Read SKILL.md content from inside a container.
93
+ */
94
+ function readContainerSkillMd(containerName, skillName) {
95
+ const filePath = `/root/.openclaw/workspace/skills/${skillName}/SKILL.md`;
96
+ try {
97
+ return execFileSync('docker', ['exec', containerName, 'cat', filePath], {
98
+ encoding: 'utf8', timeout: 5000
99
+ });
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Parse description and version from SKILL.md content string.
107
+ */
108
+ function parseSkillContent(content) {
109
+ let description = '';
110
+ let version = '';
111
+ if (!content) return { description, version };
112
+
113
+ const descMatch = content.match(/^description:\s*(.+)$/m);
114
+ const verMatch = content.match(/^\*\*Version:\*\*\s*(.+)$/m) || content.match(/^version:\s*(.+)$/m);
115
+ if (descMatch) description = descMatch[1].trim();
116
+ if (verMatch) version = verMatch[1].trim();
117
+
118
+ return { description, version };
119
+ }
120
+
121
+ export async function GET(request) {
122
+ const denied = await requireAuthJWT(request); if (denied) return denied;
123
+ const skills = [];
124
+
125
+ // 1. Shared skills
126
+ try {
127
+ if (fs.existsSync(SHARED_SKILLS_DIR)) {
128
+ const entries = fs.readdirSync(SHARED_SKILLS_DIR, { withFileTypes: true });
129
+ for (const entry of entries) {
130
+ if (!entry.isDirectory()) continue;
131
+ const skillMd = path.join(SHARED_SKILLS_DIR, entry.name, 'SKILL.md');
132
+ if (fs.existsSync(skillMd)) {
133
+ skills.push(readSkillMeta(SHARED_SKILLS_DIR, entry.name, 'shared', null));
134
+ }
135
+ }
136
+ }
137
+ } catch {}
138
+
139
+ // 2. Agent container skills (workspace-local)
140
+ const agents = discoverAgentContainers();
141
+ for (const agent of agents) {
142
+ const dirs = readContainerSkillDirs(agent.containerName);
143
+ for (const dir of dirs) {
144
+ const content = readContainerSkillMd(agent.containerName, dir);
145
+ if (content) {
146
+ const { description, version } = parseSkillContent(content);
147
+ skills.push({
148
+ name: dir,
149
+ type: 'workspace',
150
+ agentId: agent.agentId,
151
+ containerName: agent.containerName,
152
+ path: `/root/.openclaw/workspace/skills/${dir}`,
153
+ skillMdPath: `/root/.openclaw/workspace/skills/${dir}/SKILL.md`,
154
+ hasSkillMd: true,
155
+ description,
156
+ version,
157
+ });
158
+ }
159
+ }
160
+ }
161
+
162
+ // 3. Bundled skills (read-only) — discover from any agent container
163
+ try {
164
+ let bundledDirNames = [];
165
+ let bundledSourceContainer = null;
166
+
167
+ if (agents.length > 0) {
168
+ for (const agent of agents) {
169
+ try {
170
+ const raw = execFileSync('docker', [
171
+ 'exec', agent.containerName, 'ls', '-1', CONTAINER_BUNDLED_SKILLS_DIR
172
+ ], { encoding: 'utf8', timeout: 5000 }).trim();
173
+ if (raw) {
174
+ bundledDirNames = raw.split('\n').map(l => l.trim()).filter(Boolean);
175
+ bundledSourceContainer = agent.containerName;
176
+ break;
177
+ }
178
+ } catch { /* try next container */ }
179
+ }
180
+ }
181
+
182
+ for (const name of bundledDirNames) {
183
+ try {
184
+ const skillMdPath = `${CONTAINER_BUNDLED_SKILLS_DIR}/${name}/SKILL.md`;
185
+ const content = execFileSync('docker', [
186
+ 'exec', bundledSourceContainer, 'cat', skillMdPath
187
+ ], { encoding: 'utf8', timeout: 5000 });
188
+ const { description, version } = parseSkillContent(content);
189
+ skills.push({
190
+ name,
191
+ type: 'bundled',
192
+ path: `${CONTAINER_BUNDLED_SKILLS_DIR}/${name}`,
193
+ skillMdPath,
194
+ hasSkillMd: true,
195
+ description,
196
+ version,
197
+ });
198
+ } catch { /* skip this skill */ }
199
+ }
200
+ } catch {}
201
+
202
+ return NextResponse.json({ skills });
203
+ }
@@ -0,0 +1,142 @@
1
+ import { NextResponse } from 'next/server';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { execSync, execFileSync } from 'child_process';
5
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
6
+
7
+ const SHARED_SKILLS_DIR = '/docker/shared-skills';
8
+ const BUNDLED_SKILLS_DIR = '/usr/lib/node_modules/openclaw/skills';
9
+
10
+ /**
11
+ * Read SKILL.md content from various sources:
12
+ * - Host path (shared / bundled)
13
+ * - Container path (via docker exec)
14
+ */
15
+ function readSkillContent(skillPath, containerName) {
16
+ if (containerName) {
17
+ // Container skill — read via docker exec
18
+ try {
19
+ return execFileSync('docker', ['exec', containerName, 'cat', skillPath], {
20
+ encoding: 'utf8', timeout: 5000
21
+ });
22
+ } catch {
23
+ throw Object.assign(new Error(`Skill not found in container ${containerName}`), { status: 404 });
24
+ }
25
+ }
26
+
27
+ // Host skill
28
+ if (!fs.existsSync(skillPath)) {
29
+ throw Object.assign(new Error('Skill not found'), { status: 404 });
30
+ }
31
+
32
+ return fs.readFileSync(skillPath, 'utf8');
33
+ }
34
+
35
+ /**
36
+ * Write SKILL.md content.
37
+ * Shared skills — write to host filesystem.
38
+ * Container workspace skills — write via docker exec.
39
+ */
40
+ function writeSkillContent(skillPath, containerName, content) {
41
+ if (containerName) {
42
+ // Container skill — write via docker exec heredoc
43
+ const escapedContent = content
44
+ .replace(/\\/g, '\\\\')
45
+ .replace(/'/g, "'\\''");
46
+ execSync(
47
+ `docker exec ${containerName} sh -c "cat > '${skillPath}' << 'NEXUS_EOF'\n${escapedContent}\nNEXUS_EOF"`,
48
+ { encoding: 'utf8', timeout: 10000 }
49
+ );
50
+ return;
51
+ }
52
+
53
+ // Host skill
54
+ fs.mkdirSync(path.dirname(skillPath), { recursive: true });
55
+ if (fs.existsSync(skillPath)) {
56
+ fs.copyFileSync(skillPath, skillPath + '.bak');
57
+ }
58
+ fs.writeFileSync(skillPath, content, 'utf8');
59
+ }
60
+
61
+ export async function POST(request) {
62
+ const denied = await requireAuthJWT(request);
63
+ if (denied) return denied;
64
+
65
+ try {
66
+ const { skillPath, content, containerName } = await request.json();
67
+
68
+ if (!skillPath || !content) {
69
+ return NextResponse.json({ error: 'Missing skillPath or content' }, { status: 400 });
70
+ }
71
+
72
+ // Validate path ends with SKILL.md
73
+ if (!skillPath.endsWith('SKILL.md')) {
74
+ return NextResponse.json({ error: 'Only SKILL.md files can be saved' }, { status: 403 });
75
+ }
76
+
77
+ // Determine if this is a shared, bundled, or container skill
78
+ const absPath = path.resolve(skillPath);
79
+ const absShared = path.resolve(SHARED_SKILLS_DIR);
80
+ const absBundled = path.resolve(BUNDLED_SKILLS_DIR);
81
+
82
+ const isShared = absPath.startsWith(absShared + path.sep) || absPath === absShared;
83
+ const isBundled = absPath.startsWith(absBundled + path.sep) || absPath === absBundled;
84
+ const isContainer = !!containerName;
85
+
86
+ if (isBundled) {
87
+ return NextResponse.json({ error: 'Bundled skills are read-only' }, { status: 403 });
88
+ }
89
+
90
+ if (!isShared && !isContainer) {
91
+ return NextResponse.json({ error: 'Path not in allowed roots and no containerName provided' }, { status: 403 });
92
+ }
93
+
94
+ writeSkillContent(isContainer ? skillPath : absPath, containerName || null, content);
95
+
96
+ return NextResponse.json({ ok: true, saved: isContainer ? skillPath : absPath });
97
+ } catch (err) {
98
+ const msg = err instanceof Error ? err.message : 'Internal error';
99
+ const status = err.status || 500;
100
+ return NextResponse.json({ error: msg }, { status });
101
+ }
102
+ }
103
+
104
+ export async function GET(request) {
105
+ const denied = await requireAuthJWT(request);
106
+ if (denied) return denied;
107
+
108
+ const { searchParams } = new URL(request.url);
109
+ const skillPath = searchParams.get('path');
110
+ const containerName = searchParams.get('containerName');
111
+
112
+ if (!skillPath) {
113
+ return NextResponse.json({ error: 'Missing path' }, { status: 400 });
114
+ }
115
+
116
+ try {
117
+ if (containerName) {
118
+ // Read from container via docker exec
119
+ const content = readSkillContent(skillPath, containerName);
120
+ return NextResponse.json({ content });
121
+ }
122
+
123
+ // Read from host path
124
+ const absPath = path.resolve(skillPath);
125
+ const absShared = path.resolve(SHARED_SKILLS_DIR);
126
+ const absBundled = path.resolve(BUNDLED_SKILLS_DIR);
127
+
128
+ const isShared = absPath.startsWith(absShared + path.sep) || absPath === absShared;
129
+ const isBundled = absPath.startsWith(absBundled + path.sep) || absPath === absBundled;
130
+
131
+ if (!isShared && !isBundled) {
132
+ return NextResponse.json({ error: 'Host path not in allowed skill roots' }, { status: 403 });
133
+ }
134
+
135
+ const content = readSkillContent(absPath, null);
136
+ return NextResponse.json({ content });
137
+ } catch (err) {
138
+ const msg = err instanceof Error ? err.message : 'Internal error';
139
+ const status = err.status || 404;
140
+ return NextResponse.json({ error: msg }, { status });
141
+ }
142
+ }
@@ -0,0 +1,34 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { openDb } from '@/lib/db';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function GET(request: NextRequest): Promise<NextResponse> {
8
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
9
+ try {
10
+ const db = openDb();
11
+ const now = new Date();
12
+ const startOfMonth = Math.floor(
13
+ new Date(now.getFullYear(), now.getMonth(), 1).getTime() / 1000,
14
+ );
15
+ const total = db
16
+ .prepare(
17
+ `SELECT COALESCE(SUM(cost_usd),0) as total, COALESCE(SUM(tokens_in),0) as total_in,
18
+ COALESCE(SUM(tokens_out),0) as total_out, COUNT(*) as sessions
19
+ FROM sessions WHERE started_at >= ?`,
20
+ )
21
+ .get(startOfMonth);
22
+ const byModel = db
23
+ .prepare(
24
+ `SELECT model, COALESCE(SUM(cost_usd),0) as cost,
25
+ COALESCE(SUM(tokens_in),0) as tokens_in, COALESCE(SUM(tokens_out),0) as tokens_out,
26
+ COUNT(*) as sessions FROM sessions WHERE started_at >= ? GROUP BY model ORDER BY cost DESC`,
27
+ )
28
+ .all(startOfMonth);
29
+ db.close();
30
+ return NextResponse.json({ total, byModel });
31
+ } catch (e: unknown) {
32
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ import { NextResponse, type NextRequest } from 'next/server';
2
+ import { openDb } from '@/lib/db';
3
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export async function GET(request: NextRequest): Promise<NextResponse> {
8
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
9
+ const ts = Number.parseInt(new URL(request.url).searchParams.get('ts') ?? '0', 10);
10
+ try {
11
+ const db = openDb();
12
+ const row = db
13
+ .prepare('SELECT COALESCE(SUM(cost_usd),0) as total FROM sessions WHERE started_at >= ?')
14
+ .get(Math.floor(ts / 1000)) as { total: number } | undefined;
15
+ db.close();
16
+ return NextResponse.json({ total: row?.total ?? 0 });
17
+ } catch (e: unknown) {
18
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
19
+ }
20
+ }
@@ -0,0 +1,70 @@
1
+ import { type NextRequest } from 'next/server';
2
+ import { DB_PATH } from '@/lib/db';
3
+ import Database from 'better-sqlite3';
4
+ import { requireAuthJWT } from '@/lib/rev4a-auth';
5
+
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ export async function GET(request: NextRequest): Promise<Response> {
9
+ const denied = await requireAuthJWT(request); if (denied) return denied as any;
10
+ let lastEventId = 0;
11
+ const encoder = new TextEncoder();
12
+
13
+ const stream = new ReadableStream({
14
+ start(controller) {
15
+ const send = (data: unknown) => {
16
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
17
+ };
18
+
19
+ const poll = () => {
20
+ try {
21
+ const db = new Database(DB_PATH, { readonly: true });
22
+ const newEvents = db
23
+ .prepare(
24
+ `SELECT e.*, s.label AS session_label FROM events e
25
+ LEFT JOIN sessions s ON e.session_id = s.session_id
26
+ WHERE e.id > ? ORDER BY e.ts ASC LIMIT 20`,
27
+ )
28
+ .all(lastEventId) as { id: number }[];
29
+ const sessions = db
30
+ .prepare(`SELECT s.*, l.label AS lineage_label, l.agent_name AS lineage_agent_name
31
+ FROM sessions s
32
+ LEFT JOIN lineage l ON s.session_id = l.child_id
33
+ ORDER BY s.started_at DESC LIMIT 100`)
34
+ .all();
35
+ const costs = db
36
+ .prepare('SELECT SUM(cost_usd) AS today FROM sessions WHERE started_at >= ?')
37
+ .get(Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)) as
38
+ | { today: number | null }
39
+ | undefined;
40
+ db.close();
41
+
42
+ if (newEvents.length > 0) {
43
+ lastEventId = newEvents[newEvents.length - 1].id;
44
+ send({ type: 'events', data: newEvents });
45
+ }
46
+ send({ type: 'sessions', data: sessions });
47
+ send({ type: 'costs', data: { today: costs?.today ?? 0 } });
48
+ } catch {
49
+ // Ignore DB read errors to keep stream alive.
50
+ }
51
+ };
52
+
53
+ poll();
54
+ const interval = setInterval(poll, 5000);
55
+
56
+ request.signal.addEventListener('abort', () => {
57
+ clearInterval(interval);
58
+ controller.close();
59
+ });
60
+ },
61
+ });
62
+
63
+ return new Response(stream, {
64
+ headers: {
65
+ 'Content-Type': 'text/event-stream',
66
+ 'Cache-Control': 'no-cache',
67
+ Connection: 'keep-alive',
68
+ },
69
+ });
70
+ }