@swarmclawai/swarmclaw 0.2.0

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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,1232 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ function cmd(action, method, route, description, extra = {}) {
7
+ return { action, method, route, description, ...extra }
8
+ }
9
+
10
+ const COMMAND_GROUPS = [
11
+ {
12
+ name: 'agents',
13
+ description: 'Manage agents',
14
+ commands: [
15
+ cmd('list', 'GET', '/agents', 'List agents'),
16
+ cmd('get', 'GET', '/agents/:id', 'Get an agent by id', { virtual: true, clientGetRoute: '/agents' }),
17
+ cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
18
+ cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
19
+ cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
20
+ cmd('generate', 'POST', '/agents/generate', 'Generate agent definition from prompt', { expectsJsonBody: true }),
21
+ cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
22
+ ],
23
+ },
24
+ {
25
+ name: 'auth',
26
+ description: 'Access key auth helpers',
27
+ commands: [
28
+ cmd('status', 'GET', '/auth', 'Check auth setup status'),
29
+ cmd('login', 'POST', '/auth', 'Validate an access key', {
30
+ expectsJsonBody: true,
31
+ bodyFlagMap: { key: 'key' },
32
+ }),
33
+ ],
34
+ },
35
+ {
36
+ name: 'claude-skills',
37
+ description: 'Read local Claude skills directory metadata',
38
+ commands: [
39
+ cmd('list', 'GET', '/claude-skills', 'List Claude skills discovered on host'),
40
+ ],
41
+ },
42
+ {
43
+ name: 'clawhub',
44
+ description: 'Browse and install ClawHub skills',
45
+ commands: [
46
+ cmd('search', 'GET', '/clawhub/search', 'Search ClawHub skills catalog'),
47
+ cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }),
48
+ ],
49
+ },
50
+ {
51
+ name: 'connectors',
52
+ description: 'Manage chat connectors',
53
+ commands: [
54
+ cmd('list', 'GET', '/connectors', 'List connectors'),
55
+ cmd('get', 'GET', '/connectors/:id', 'Get connector'),
56
+ cmd('create', 'POST', '/connectors', 'Create connector', { expectsJsonBody: true }),
57
+ cmd('update', 'PUT', '/connectors/:id', 'Update connector', { expectsJsonBody: true }),
58
+ cmd('delete', 'DELETE', '/connectors/:id', 'Delete connector'),
59
+ cmd('start', 'PUT', '/connectors/:id', 'Start connector', {
60
+ expectsJsonBody: true,
61
+ defaultBody: { action: 'start' },
62
+ }),
63
+ cmd('stop', 'PUT', '/connectors/:id', 'Stop connector', {
64
+ expectsJsonBody: true,
65
+ defaultBody: { action: 'stop' },
66
+ }),
67
+ cmd('repair', 'PUT', '/connectors/:id', 'Repair connector', {
68
+ expectsJsonBody: true,
69
+ defaultBody: { action: 'repair' },
70
+ }),
71
+ ],
72
+ },
73
+ {
74
+ name: 'credentials',
75
+ description: 'Manage encrypted provider credentials',
76
+ commands: [
77
+ cmd('list', 'GET', '/credentials', 'List credentials'),
78
+ cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id', { virtual: true, clientGetRoute: '/credentials' }),
79
+ cmd('create', 'POST', '/credentials', 'Create credential', { expectsJsonBody: true }),
80
+ cmd('delete', 'DELETE', '/credentials/:id', 'Delete credential'),
81
+ ],
82
+ },
83
+ {
84
+ name: 'daemon',
85
+ description: 'Control background daemon',
86
+ commands: [
87
+ cmd('status', 'GET', '/daemon', 'Get daemon status'),
88
+ cmd('action', 'POST', '/daemon', 'Set daemon action via JSON body', { expectsJsonBody: true }),
89
+ cmd('start', 'POST', '/daemon', 'Start daemon', {
90
+ expectsJsonBody: true,
91
+ defaultBody: { action: 'start' },
92
+ }),
93
+ cmd('stop', 'POST', '/daemon', 'Stop daemon', {
94
+ expectsJsonBody: true,
95
+ defaultBody: { action: 'stop' },
96
+ }),
97
+ cmd('health-check', 'POST', '/daemon/health-check', 'Run daemon health checks immediately'),
98
+ ],
99
+ },
100
+ {
101
+ name: 'dirs',
102
+ description: 'Directory listing and native picker',
103
+ commands: [
104
+ cmd('list', 'GET', '/dirs', 'List directories (use --query path=/abs/path)'),
105
+ cmd('pick', 'POST', '/dirs/pick', 'Open native picker (mode=file|folder)', { expectsJsonBody: true }),
106
+ ],
107
+ },
108
+ {
109
+ name: 'documents',
110
+ description: 'Manage documents',
111
+ commands: [
112
+ cmd('list', 'GET', '/documents', 'List documents'),
113
+ cmd('get', 'GET', '/documents/:id', 'Get document by id'),
114
+ cmd('create', 'POST', '/documents', 'Create document', { expectsJsonBody: true }),
115
+ cmd('update', 'PUT', '/documents/:id', 'Update document', { expectsJsonBody: true }),
116
+ cmd('delete', 'DELETE', '/documents/:id', 'Delete document'),
117
+ ],
118
+ },
119
+ {
120
+ name: 'files',
121
+ description: 'Serve and manage local files',
122
+ commands: [
123
+ cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'),
124
+ ],
125
+ },
126
+ {
127
+ name: 'generate',
128
+ description: 'AI generation endpoints',
129
+ commands: [
130
+ cmd('run', 'POST', '/generate', 'Generate schedule/task/skill/provider payload', { expectsJsonBody: true }),
131
+ cmd('info', 'GET', '/generate/info', 'Get generation provider/model info'),
132
+ ],
133
+ },
134
+ {
135
+ name: 'ip',
136
+ description: 'Get local IP/port metadata',
137
+ commands: [
138
+ cmd('get', 'GET', '/ip', 'Get host IP and port'),
139
+ ],
140
+ },
141
+ {
142
+ name: 'knowledge',
143
+ description: 'Manage knowledge base entries',
144
+ commands: [
145
+ cmd('list', 'GET', '/knowledge', 'List knowledge entries'),
146
+ cmd('get', 'GET', '/knowledge/:id', 'Get knowledge entry by id'),
147
+ cmd('create', 'POST', '/knowledge', 'Create knowledge entry', { expectsJsonBody: true }),
148
+ cmd('update', 'PUT', '/knowledge/:id', 'Update knowledge entry', { expectsJsonBody: true }),
149
+ cmd('delete', 'DELETE', '/knowledge/:id', 'Delete knowledge entry'),
150
+ cmd('upload', 'POST', '/knowledge/upload', 'Upload document for knowledge extraction', {
151
+ requestType: 'upload',
152
+ inputPositional: 'filePath',
153
+ }),
154
+ ],
155
+ },
156
+ {
157
+ name: 'logs',
158
+ description: 'Read or clear app logs',
159
+ commands: [
160
+ cmd('list', 'GET', '/logs', 'List logs (use --query lines=200, --query level=INFO,ERROR)'),
161
+ cmd('clear', 'DELETE', '/logs', 'Clear logs file'),
162
+ ],
163
+ },
164
+ {
165
+ name: 'memory',
166
+ description: 'Manage memory entries',
167
+ commands: [
168
+ cmd('list', 'GET', '/memory', 'List memory entries (use --query q=, --query agentId=)'),
169
+ cmd('get', 'GET', '/memory/:id', 'Get memory by id'),
170
+ cmd('create', 'POST', '/memory', 'Create memory entry', { expectsJsonBody: true }),
171
+ cmd('update', 'PUT', '/memory/:id', 'Update memory entry', { expectsJsonBody: true }),
172
+ cmd('delete', 'DELETE', '/memory/:id', 'Delete memory entry'),
173
+ cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'),
174
+ cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }),
175
+ ],
176
+ },
177
+ {
178
+ name: 'memory-images',
179
+ description: 'Fetch stored memory image assets',
180
+ commands: [
181
+ cmd('get', 'GET', '/memory-images/:filename', 'Download memory image by filename', { responseType: 'binary' }),
182
+ ],
183
+ },
184
+ {
185
+ name: 'mcp-servers',
186
+ description: 'Manage MCP server configurations',
187
+ commands: [
188
+ cmd('list', 'GET', '/mcp-servers', 'List MCP servers'),
189
+ cmd('get', 'GET', '/mcp-servers/:id', 'Get MCP server by id'),
190
+ cmd('create', 'POST', '/mcp-servers', 'Create MCP server', { expectsJsonBody: true }),
191
+ cmd('update', 'PUT', '/mcp-servers/:id', 'Update MCP server', { expectsJsonBody: true }),
192
+ cmd('delete', 'DELETE', '/mcp-servers/:id', 'Delete MCP server'),
193
+ cmd('test', 'POST', '/mcp-servers/:id/test', 'Test MCP server connection'),
194
+ cmd('tools', 'GET', '/mcp-servers/:id/tools', 'List tools available on an MCP server'),
195
+ ],
196
+ },
197
+ {
198
+ name: 'memories',
199
+ description: 'Alias of memory command group',
200
+ aliasFor: 'memory',
201
+ commands: [],
202
+ },
203
+ {
204
+ name: 'orchestrator',
205
+ description: 'Trigger orchestrator runs',
206
+ commands: [
207
+ cmd('run', 'POST', '/orchestrator/run', 'Queue orchestrator task', {
208
+ expectsJsonBody: true,
209
+ waitEntityFrom: 'taskId',
210
+ }),
211
+ ],
212
+ },
213
+ {
214
+ name: 'preview-server',
215
+ description: 'Manage preview dev servers',
216
+ commands: [
217
+ cmd('manage', 'POST', '/preview-server', 'Start/stop/status/detect preview server', { expectsJsonBody: true }),
218
+ ],
219
+ },
220
+ {
221
+ name: 'plugins',
222
+ description: 'Manage plugins and marketplace',
223
+ commands: [
224
+ cmd('list', 'GET', '/plugins', 'List installed plugins'),
225
+ cmd('set', 'POST', '/plugins', 'Enable/disable plugin', { expectsJsonBody: true }),
226
+ cmd('install', 'POST', '/plugins/install', 'Install plugin from URL', { expectsJsonBody: true }),
227
+ cmd('marketplace', 'GET', '/plugins/marketplace', 'Get marketplace catalog'),
228
+ ],
229
+ },
230
+ {
231
+ name: 'providers',
232
+ description: 'Manage providers and model overrides',
233
+ commands: [
234
+ cmd('list', 'GET', '/providers', 'List providers'),
235
+ cmd('get', 'GET', '/providers/:id', 'Get provider config'),
236
+ cmd('create', 'POST', '/providers', 'Create custom provider', { expectsJsonBody: true }),
237
+ cmd('update', 'PUT', '/providers/:id', 'Update provider', { expectsJsonBody: true }),
238
+ cmd('delete', 'DELETE', '/providers/:id', 'Delete provider'),
239
+ cmd('configs', 'GET', '/providers/configs', 'List saved provider configs'),
240
+ cmd('ollama', 'GET', '/providers/ollama', 'List local Ollama models (use --query endpoint=http://localhost:11434)'),
241
+ cmd('openclaw-health', 'GET', '/providers/openclaw/health', 'Probe OpenClaw endpoint/auth (use --query endpoint= --query credentialId= --query model=)'),
242
+ cmd('models', 'GET', '/providers/:id/models', 'Get provider model overrides'),
243
+ cmd('models-set', 'PUT', '/providers/:id/models', 'Set provider model overrides', { expectsJsonBody: true }),
244
+ cmd('models-clear', 'DELETE', '/providers/:id/models', 'Clear provider model overrides'),
245
+ ],
246
+ },
247
+ {
248
+ name: 'runs',
249
+ description: 'Session run queue/history',
250
+ commands: [
251
+ cmd('list', 'GET', '/runs', 'List runs (use --query sessionId=, --query status=, --query limit=)'),
252
+ cmd('get', 'GET', '/runs/:id', 'Get run by id'),
253
+ ],
254
+ },
255
+ {
256
+ name: 'schedules',
257
+ description: 'Manage schedules',
258
+ commands: [
259
+ cmd('list', 'GET', '/schedules', 'List schedules'),
260
+ cmd('get', 'GET', '/schedules/:id', 'Get schedule by id', { virtual: true, clientGetRoute: '/schedules' }),
261
+ cmd('create', 'POST', '/schedules', 'Create schedule', { expectsJsonBody: true }),
262
+ cmd('update', 'PUT', '/schedules/:id', 'Update schedule', { expectsJsonBody: true }),
263
+ cmd('delete', 'DELETE', '/schedules/:id', 'Delete schedule'),
264
+ cmd('run', 'POST', '/schedules/:id/run', 'Trigger schedule now'),
265
+ ],
266
+ },
267
+ {
268
+ name: 'secrets',
269
+ description: 'Manage reusable encrypted secrets',
270
+ commands: [
271
+ cmd('list', 'GET', '/secrets', 'List secrets metadata'),
272
+ cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id', { virtual: true, clientGetRoute: '/secrets' }),
273
+ cmd('create', 'POST', '/secrets', 'Create secret', { expectsJsonBody: true }),
274
+ cmd('update', 'PUT', '/secrets/:id', 'Update secret metadata', { expectsJsonBody: true }),
275
+ cmd('delete', 'DELETE', '/secrets/:id', 'Delete secret'),
276
+ ],
277
+ },
278
+ {
279
+ name: 'sessions',
280
+ description: 'Manage chat sessions and runtime controls',
281
+ commands: [
282
+ cmd('list', 'GET', '/sessions', 'List sessions'),
283
+ cmd('get', 'GET', '/sessions/:id', 'Get session by id', { virtual: true, clientGetRoute: '/sessions' }),
284
+ cmd('create', 'POST', '/sessions', 'Create session', { expectsJsonBody: true }),
285
+ cmd('update', 'PUT', '/sessions/:id', 'Update session', { expectsJsonBody: true }),
286
+ cmd('delete', 'DELETE', '/sessions/:id', 'Delete session'),
287
+ cmd('delete-many', 'DELETE', '/sessions', 'Delete multiple sessions (body: {"ids":[...]})', { expectsJsonBody: true }),
288
+ cmd('heartbeat-disable-all', 'POST', '/sessions/heartbeat', 'Disable all session heartbeats and cancel queued heartbeat runs', {
289
+ expectsJsonBody: true,
290
+ defaultBody: { action: 'disable_all' },
291
+ }),
292
+ cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
293
+ cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
294
+ cmd('main-loop-action', 'POST', '/sessions/:id/main-loop', 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', {
295
+ expectsJsonBody: true,
296
+ }),
297
+ cmd('chat', 'POST', '/sessions/:id/chat', 'Send chat message (streaming)', {
298
+ expectsJsonBody: true,
299
+ responseType: 'sse',
300
+ }),
301
+ cmd('stop', 'POST', '/sessions/:id/stop', 'Stop session run(s)'),
302
+ cmd('clear', 'POST', '/sessions/:id/clear', 'Clear session messages'),
303
+ cmd('browser-status', 'GET', '/sessions/:id/browser', 'Check browser status'),
304
+ cmd('browser-close', 'DELETE', '/sessions/:id/browser', 'Close browser session'),
305
+ cmd('mailbox', 'GET', '/sessions/:id/mailbox', 'List session mailbox envelopes'),
306
+ cmd('mailbox-action', 'POST', '/sessions/:id/mailbox', 'Send/ack/clear mailbox envelopes', { expectsJsonBody: true }),
307
+ cmd('retry', 'POST', '/sessions/:id/retry', 'Retry last assistant message'),
308
+ cmd('deploy', 'POST', '/sessions/:id/deploy', 'Deploy current session branch', { expectsJsonBody: true }),
309
+ cmd('devserver', 'POST', '/sessions/:id/devserver', 'Dev server action via JSON body', { expectsJsonBody: true }),
310
+ cmd('devserver-start', 'POST', '/sessions/:id/devserver', 'Start session dev server', {
311
+ expectsJsonBody: true,
312
+ defaultBody: { action: 'start' },
313
+ }),
314
+ cmd('devserver-stop', 'POST', '/sessions/:id/devserver', 'Stop session dev server', {
315
+ expectsJsonBody: true,
316
+ defaultBody: { action: 'stop' },
317
+ }),
318
+ cmd('devserver-status', 'POST', '/sessions/:id/devserver', 'Check session dev server status', {
319
+ expectsJsonBody: true,
320
+ defaultBody: { action: 'status' },
321
+ }),
322
+ ],
323
+ },
324
+ {
325
+ name: 'settings',
326
+ description: 'Read/update app settings',
327
+ commands: [
328
+ cmd('get', 'GET', '/settings', 'Get settings'),
329
+ cmd('update', 'PUT', '/settings', 'Update settings', { expectsJsonBody: true }),
330
+ ],
331
+ },
332
+ {
333
+ name: 'setup',
334
+ description: 'Setup and provider validation helpers',
335
+ commands: [
336
+ cmd('check-provider', 'POST', '/setup/check-provider', 'Validate provider credentials/endpoint', { expectsJsonBody: true }),
337
+ cmd('doctor', 'GET', '/setup/doctor', 'Run local setup diagnostics'),
338
+ ],
339
+ },
340
+ {
341
+ name: 'skills',
342
+ description: 'Manage reusable skills',
343
+ commands: [
344
+ cmd('list', 'GET', '/skills', 'List skills'),
345
+ cmd('get', 'GET', '/skills/:id', 'Get skill'),
346
+ cmd('create', 'POST', '/skills', 'Create skill', { expectsJsonBody: true }),
347
+ cmd('update', 'PUT', '/skills/:id', 'Update skill', { expectsJsonBody: true }),
348
+ cmd('delete', 'DELETE', '/skills/:id', 'Delete skill'),
349
+ cmd('import', 'POST', '/skills/import', 'Import skill from URL', { expectsJsonBody: true }),
350
+ ],
351
+ },
352
+ {
353
+ name: 'tasks',
354
+ description: 'Manage task board items',
355
+ commands: [
356
+ cmd('list', 'GET', '/tasks', 'List tasks'),
357
+ cmd('get', 'GET', '/tasks/:id', 'Get task'),
358
+ cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
359
+ cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
360
+ cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
361
+ cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
362
+ ],
363
+ },
364
+ {
365
+ name: 'tts',
366
+ description: 'Text-to-speech endpoint',
367
+ commands: [
368
+ cmd('speak', 'POST', '/tts', 'Generate TTS audio', {
369
+ expectsJsonBody: true,
370
+ responseType: 'binary',
371
+ bodyFlagMap: { text: 'text' },
372
+ }),
373
+ ],
374
+ },
375
+ {
376
+ name: 'upload',
377
+ description: 'Upload raw file/blob',
378
+ commands: [
379
+ cmd('file', 'POST', '/upload', 'Upload file', {
380
+ requestType: 'upload',
381
+ inputPositional: 'filePath',
382
+ }),
383
+ ],
384
+ },
385
+ {
386
+ name: 'uploads',
387
+ description: 'Fetch uploaded artifacts',
388
+ commands: [
389
+ cmd('get', 'GET', '/uploads/:filename', 'Download uploaded artifact', { responseType: 'binary' }),
390
+ ],
391
+ },
392
+ {
393
+ name: 'usage',
394
+ description: 'Usage and cost summary',
395
+ commands: [
396
+ cmd('get', 'GET', '/usage', 'Get usage summary'),
397
+ ],
398
+ },
399
+ {
400
+ name: 'version',
401
+ description: 'Version and update checks',
402
+ commands: [
403
+ cmd('get', 'GET', '/version', 'Get local/remote version info'),
404
+ cmd('update', 'POST', '/version/update', 'Update to latest stable release tag (fallback: main) and install deps when needed'),
405
+ ],
406
+ },
407
+ {
408
+ name: 'webhooks',
409
+ description: 'Manage and trigger webhooks',
410
+ commands: [
411
+ cmd('list', 'GET', '/webhooks', 'List webhooks'),
412
+ cmd('get', 'GET', '/webhooks/:id', 'Get webhook by id'),
413
+ cmd('create', 'POST', '/webhooks', 'Create webhook', { expectsJsonBody: true }),
414
+ cmd('update', 'PUT', '/webhooks/:id', 'Update webhook', { expectsJsonBody: true }),
415
+ cmd('delete', 'DELETE', '/webhooks/:id', 'Delete webhook'),
416
+ cmd('trigger', 'POST', '/webhooks/:id', 'Trigger webhook by id', {
417
+ expectsJsonBody: true,
418
+ waitEntityFrom: 'runId',
419
+ }),
420
+ cmd('history', 'GET', '/webhooks/:id/history', 'Get webhook delivery history'),
421
+ ],
422
+ },
423
+ ]
424
+
425
+ const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group]))
426
+
427
+ function resolveGroup(name) {
428
+ const group = GROUP_MAP.get(name)
429
+ if (!group) return null
430
+ if (group.aliasFor) {
431
+ return GROUP_MAP.get(group.aliasFor) || null
432
+ }
433
+ return group
434
+ }
435
+
436
+ const COMMANDS = COMMAND_GROUPS.flatMap((group) => {
437
+ if (group.aliasFor) return []
438
+ return group.commands.map((command) => ({ ...command, group: group.name }))
439
+ })
440
+
441
+ function getCommand(groupName, action) {
442
+ const group = resolveGroup(groupName)
443
+ if (!group) return null
444
+ return group.commands.find((command) => command.action === action) || null
445
+ }
446
+
447
+ function extractPathParams(route) {
448
+ return [...route.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1])
449
+ }
450
+
451
+ function isPlainObject(value) {
452
+ return value && typeof value === 'object' && !Array.isArray(value)
453
+ }
454
+
455
+ function parseKeyValue(raw, kind) {
456
+ const idx = raw.indexOf('=')
457
+ if (idx === -1) {
458
+ throw new Error(`${kind} value must be key=value: ${raw}`)
459
+ }
460
+ const key = raw.slice(0, idx).trim()
461
+ const value = raw.slice(idx + 1)
462
+ if (!key) throw new Error(`${kind} key cannot be empty`)
463
+ return [key, value]
464
+ }
465
+
466
+ function parseDataInput(raw, stdin) {
467
+ if (raw === '-') {
468
+ return parseJsonText(readStdin(stdin), 'stdin')
469
+ }
470
+ if (raw.startsWith('@')) {
471
+ const filePath = raw.slice(1)
472
+ if (!filePath) throw new Error('Expected file path after @ for --data')
473
+ const fileText = fs.readFileSync(filePath, 'utf8')
474
+ return parseJsonText(fileText, filePath)
475
+ }
476
+ return parseJsonText(raw, '--data')
477
+ }
478
+
479
+ function parseJsonText(text, sourceName) {
480
+ try {
481
+ return JSON.parse(text)
482
+ } catch (err) {
483
+ const msg = err instanceof Error ? err.message : String(err)
484
+ throw new Error(`Invalid JSON from ${sourceName}: ${msg}`)
485
+ }
486
+ }
487
+
488
+ function readStdin(stdin) {
489
+ const fd = stdin && typeof stdin.fd === 'number' ? stdin.fd : 0
490
+ return fs.readFileSync(fd, 'utf8')
491
+ }
492
+
493
+ function normalizeBaseUrl(raw) {
494
+ const trimmed = String(raw || '').trim()
495
+ const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`
496
+ return withProtocol.replace(/\/+$/, '')
497
+ }
498
+
499
+ function resolveAccessKey(opts, env, cwd) {
500
+ if (opts.accessKey) return String(opts.accessKey).trim()
501
+ const envKey = env.SWARMCLAW_API_KEY || env.SC_ACCESS_KEY || ''
502
+ if (envKey) return String(envKey).trim()
503
+
504
+ const keyFile = path.join(cwd, 'platform-api-key.txt')
505
+ if (fs.existsSync(keyFile)) {
506
+ const content = fs.readFileSync(keyFile, 'utf8').trim()
507
+ if (content) return content
508
+ }
509
+ return ''
510
+ }
511
+
512
+ function parseArgv(argv) {
513
+ const result = {
514
+ group: '',
515
+ action: '',
516
+ positionals: [],
517
+ opts: {
518
+ baseUrl: '',
519
+ accessKey: '',
520
+ jsonOutput: false,
521
+ wait: false,
522
+ timeoutMs: 300000,
523
+ intervalMs: 2000,
524
+ out: '',
525
+ data: '',
526
+ headers: [],
527
+ query: [],
528
+ key: '',
529
+ text: '',
530
+ file: '',
531
+ filename: '',
532
+ secret: '',
533
+ event: '',
534
+ help: false,
535
+ version: false,
536
+ },
537
+ }
538
+
539
+ const valueOptions = new Set([
540
+ 'base-url',
541
+ 'access-key',
542
+ 'timeout-ms',
543
+ 'interval-ms',
544
+ 'out',
545
+ 'data',
546
+ 'header',
547
+ 'query',
548
+ 'key',
549
+ 'text',
550
+ 'file',
551
+ 'filename',
552
+ 'secret',
553
+ 'event',
554
+ ])
555
+
556
+ const tokens = [...argv]
557
+ for (let i = 0; i < tokens.length; i += 1) {
558
+ const token = tokens[i]
559
+ if (token === '--') {
560
+ result.positionals.push(...tokens.slice(i + 1))
561
+ break
562
+ }
563
+
564
+ if (token === '-h' || token === '--help') {
565
+ result.opts.help = true
566
+ continue
567
+ }
568
+
569
+ if (token === '--version') {
570
+ result.opts.version = true
571
+ continue
572
+ }
573
+
574
+ if (token === '--json') {
575
+ result.opts.jsonOutput = true
576
+ continue
577
+ }
578
+
579
+ if (token === '--wait') {
580
+ result.opts.wait = true
581
+ continue
582
+ }
583
+
584
+ if (token.startsWith('--')) {
585
+ const eqIndex = token.indexOf('=')
586
+ const hasInline = eqIndex > -1
587
+ const rawName = hasInline ? token.slice(2, eqIndex) : token.slice(2)
588
+ const rawValue = hasInline ? token.slice(eqIndex + 1) : ''
589
+
590
+ if (!valueOptions.has(rawName)) {
591
+ throw new Error(`Unknown option: --${rawName}`)
592
+ }
593
+
594
+ const value = hasInline ? rawValue : tokens[i + 1]
595
+ if (!hasInline) i += 1
596
+ if (value === undefined) {
597
+ throw new Error(`Missing value for --${rawName}`)
598
+ }
599
+
600
+ switch (rawName) {
601
+ case 'base-url':
602
+ result.opts.baseUrl = value
603
+ break
604
+ case 'access-key':
605
+ result.opts.accessKey = value
606
+ break
607
+ case 'timeout-ms':
608
+ result.opts.timeoutMs = Number.parseInt(value, 10)
609
+ if (!Number.isFinite(result.opts.timeoutMs) || result.opts.timeoutMs <= 0) {
610
+ throw new Error(`Invalid --timeout-ms value: ${value}`)
611
+ }
612
+ break
613
+ case 'interval-ms':
614
+ result.opts.intervalMs = Number.parseInt(value, 10)
615
+ if (!Number.isFinite(result.opts.intervalMs) || result.opts.intervalMs <= 0) {
616
+ throw new Error(`Invalid --interval-ms value: ${value}`)
617
+ }
618
+ break
619
+ case 'out':
620
+ result.opts.out = value
621
+ break
622
+ case 'data':
623
+ result.opts.data = value
624
+ break
625
+ case 'header':
626
+ result.opts.headers.push(value)
627
+ break
628
+ case 'query':
629
+ result.opts.query.push(value)
630
+ break
631
+ case 'key':
632
+ result.opts.key = value
633
+ break
634
+ case 'text':
635
+ result.opts.text = value
636
+ break
637
+ case 'file':
638
+ result.opts.file = value
639
+ break
640
+ case 'filename':
641
+ result.opts.filename = value
642
+ break
643
+ case 'secret':
644
+ result.opts.secret = value
645
+ break
646
+ case 'event':
647
+ result.opts.event = value
648
+ break
649
+ default:
650
+ throw new Error(`Unhandled option parser branch: --${rawName}`)
651
+ }
652
+ continue
653
+ }
654
+
655
+ result.positionals.push(token)
656
+ }
657
+
658
+ if (result.positionals.length > 0) {
659
+ result.group = result.positionals[0]
660
+ }
661
+ if (result.positionals.length > 1) {
662
+ result.action = result.positionals[1]
663
+ }
664
+
665
+ return result
666
+ }
667
+
668
+ function buildRoute(routeTemplate, args) {
669
+ const pathParams = extractPathParams(routeTemplate)
670
+ if (args.length < pathParams.length) {
671
+ throw new Error(`Missing required path args: ${pathParams.slice(args.length).join(', ')}`)
672
+ }
673
+
674
+ let route = routeTemplate
675
+ for (let i = 0; i < pathParams.length; i += 1) {
676
+ route = route.replace(`:${pathParams[i]}`, encodeURIComponent(String(args[i])))
677
+ }
678
+
679
+ const remaining = args.slice(pathParams.length)
680
+ return { route, remaining, pathParams }
681
+ }
682
+
683
+ function buildApiUrl(baseUrl, route, queryEntries) {
684
+ const normalizedBase = normalizeBaseUrl(baseUrl)
685
+ const hasApiSuffix = normalizedBase.endsWith('/api')
686
+ const url = new URL(`${normalizedBase}${hasApiSuffix ? '' : '/api'}${route}`)
687
+ for (const [key, value] of queryEntries) {
688
+ url.searchParams.set(key, value)
689
+ }
690
+ return url
691
+ }
692
+
693
+ async function parseResponse(res, forceType) {
694
+ const ct = (res.headers.get('content-type') || '').toLowerCase()
695
+
696
+ if (forceType === 'sse' || ct.includes('text/event-stream')) {
697
+ return { type: 'sse', value: res.body }
698
+ }
699
+
700
+ if (forceType === 'binary') {
701
+ const buf = Buffer.from(await res.arrayBuffer())
702
+ return { type: 'binary', value: buf, contentType: ct }
703
+ }
704
+
705
+ if (ct.includes('application/json')) {
706
+ const json = await res.json().catch(() => null)
707
+ return { type: 'json', value: json }
708
+ }
709
+
710
+ if (ct.startsWith('text/') || ct.includes('xml') || ct.includes('javascript')) {
711
+ const text = await res.text()
712
+ return { type: 'text', value: text }
713
+ }
714
+
715
+ const buf = Buffer.from(await res.arrayBuffer())
716
+ return { type: 'binary', value: buf, contentType: ct }
717
+ }
718
+
719
+ function writeJson(stdout, value, compact) {
720
+ const text = compact ? JSON.stringify(value) : JSON.stringify(value, null, 2)
721
+ stdout.write(`${text}\n`)
722
+ }
723
+
724
+ function writeText(stdout, value) {
725
+ stdout.write(String(value))
726
+ if (!String(value).endsWith('\n')) stdout.write('\n')
727
+ }
728
+
729
+ function writeBinary(stdout, stderr, buffer, outPath, cwd) {
730
+ if (outPath) {
731
+ const resolved = path.isAbsolute(outPath) ? outPath : path.join(cwd, outPath)
732
+ fs.writeFileSync(resolved, buffer)
733
+ stderr.write(`Saved ${buffer.length} bytes to ${resolved}\n`)
734
+ return
735
+ }
736
+
737
+ if (stdout.isTTY) {
738
+ throw new Error('Binary response requires --out <file> when writing to a TTY')
739
+ }
740
+ stdout.write(buffer)
741
+ }
742
+
743
+ async function consumeSse(body, stdout, stderr, jsonOutput) {
744
+ if (!body || typeof body.getReader !== 'function') {
745
+ throw new Error('Streaming response does not expose a reader')
746
+ }
747
+
748
+ const reader = body.getReader()
749
+ const decoder = new TextDecoder()
750
+ let buffer = ''
751
+
752
+ function flushChunk(rawChunk) {
753
+ const lines = rawChunk
754
+ .split('\n')
755
+ .map((line) => line.trimEnd())
756
+ .filter(Boolean)
757
+
758
+ const dataLines = lines
759
+ .filter((line) => line.startsWith('data:'))
760
+ .map((line) => line.slice(5).trim())
761
+
762
+ if (!dataLines.length) return
763
+ const payload = dataLines.join('\n')
764
+
765
+ let parsed
766
+ try {
767
+ parsed = JSON.parse(payload)
768
+ } catch {
769
+ writeText(stdout, payload)
770
+ return
771
+ }
772
+
773
+ if (jsonOutput) {
774
+ writeJson(stdout, parsed, true)
775
+ return
776
+ }
777
+
778
+ if (isPlainObject(parsed) && parsed.t === 'md' && typeof parsed.text === 'string') {
779
+ writeText(stdout, parsed.text)
780
+ return
781
+ }
782
+
783
+ if (isPlainObject(parsed) && parsed.t === 'err' && typeof parsed.text === 'string') {
784
+ writeText(stderr, parsed.text)
785
+ return
786
+ }
787
+
788
+ writeJson(stdout, parsed, false)
789
+ }
790
+
791
+ while (true) {
792
+ const { done, value } = await reader.read()
793
+ if (done) break
794
+ buffer += decoder.decode(value, { stream: true })
795
+
796
+ let splitIndex = buffer.indexOf('\n\n')
797
+ while (splitIndex >= 0) {
798
+ const chunk = buffer.slice(0, splitIndex)
799
+ buffer = buffer.slice(splitIndex + 2)
800
+ flushChunk(chunk)
801
+ splitIndex = buffer.indexOf('\n\n')
802
+ }
803
+ }
804
+
805
+ const finalText = decoder.decode()
806
+ if (finalText) buffer += finalText
807
+ if (buffer.trim()) flushChunk(buffer)
808
+ }
809
+
810
+ async function fetchJson(fetchImpl, url, headers, timeoutMs) {
811
+ const res = await fetchImpl(url, {
812
+ method: 'GET',
813
+ headers,
814
+ signal: AbortSignal.timeout(timeoutMs),
815
+ })
816
+
817
+ const parsed = await parseResponse(res)
818
+ if (!res.ok) {
819
+ throw new Error(`Request failed (${res.status}): ${serializePayload(parsed.value)}`)
820
+ }
821
+
822
+ if (parsed.type !== 'json') {
823
+ throw new Error(`Expected JSON response from ${url}`)
824
+ }
825
+
826
+ return parsed.value
827
+ }
828
+
829
+ function serializePayload(value) {
830
+ if (typeof value === 'string') return value
831
+ try {
832
+ return JSON.stringify(value)
833
+ } catch {
834
+ return String(value)
835
+ }
836
+ }
837
+
838
+ function getWaitId(payload, command) {
839
+ if (!isPlainObject(payload)) return null
840
+
841
+ if (command.waitEntityFrom && typeof payload[command.waitEntityFrom] === 'string') {
842
+ return { type: command.waitEntityFrom === 'taskId' ? 'task' : 'run', id: payload[command.waitEntityFrom] }
843
+ }
844
+
845
+ if (typeof payload.runId === 'string') return { type: 'run', id: payload.runId }
846
+ if (isPlainObject(payload.run) && typeof payload.run.id === 'string') return { type: 'run', id: payload.run.id }
847
+ if (typeof payload.taskId === 'string') return { type: 'task', id: payload.taskId }
848
+
849
+ return null
850
+ }
851
+
852
+ function isTerminalStatus(status) {
853
+ const terminal = new Set([
854
+ 'completed',
855
+ 'complete',
856
+ 'done',
857
+ 'failed',
858
+ 'error',
859
+ 'stopped',
860
+ 'cancelled',
861
+ 'canceled',
862
+ 'timeout',
863
+ 'timed_out',
864
+ ])
865
+ return terminal.has(String(status || '').toLowerCase())
866
+ }
867
+
868
+ async function waitForEntity(opts) {
869
+ const {
870
+ entityType,
871
+ entityId,
872
+ fetchImpl,
873
+ baseUrl,
874
+ headers,
875
+ timeoutMs,
876
+ intervalMs,
877
+ stdout,
878
+ jsonOutput,
879
+ } = opts
880
+
881
+ const route = entityType === 'run' ? `/runs/${encodeURIComponent(entityId)}` : `/tasks/${encodeURIComponent(entityId)}`
882
+ const deadline = Date.now() + timeoutMs
883
+
884
+ while (Date.now() <= deadline) {
885
+ const url = buildApiUrl(baseUrl, route, [])
886
+ const payload = await fetchJson(fetchImpl, url, headers, timeoutMs)
887
+
888
+ const status = isPlainObject(payload) ? payload.status : undefined
889
+ if (status !== undefined) {
890
+ stdout.write(`[wait] ${entityType} ${entityId}: ${status}\n`)
891
+ }
892
+
893
+ if (status !== undefined && isTerminalStatus(status)) {
894
+ if (jsonOutput) writeJson(stdout, payload, true)
895
+ else writeJson(stdout, payload, false)
896
+ return
897
+ }
898
+
899
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
900
+ }
901
+
902
+ throw new Error(`Timed out waiting for ${entityType} ${entityId}`)
903
+ }
904
+
905
+ function renderGeneralHelp() {
906
+ const lines = [
907
+ 'SwarmClaw CLI',
908
+ '',
909
+ 'Usage:',
910
+ ' swarmclaw <group> <command> [args] [options]',
911
+ '',
912
+ 'Global options:',
913
+ ' --base-url <url> API base URL (default: http://localhost:3456)',
914
+ ' --access-key <key> Access key override (else SWARMCLAW_API_KEY or platform-api-key.txt)',
915
+ ' --data <json|@file|-> Request JSON body',
916
+ ' --query key=value Query parameter (repeatable)',
917
+ ' --header key=value Extra HTTP header (repeatable)',
918
+ ' --json Compact JSON output',
919
+ ' --wait Wait for run/task completion when runId/taskId is returned',
920
+ ' --timeout-ms <ms> Request/wait timeout (default: 300000)',
921
+ ' --interval-ms <ms> Poll interval for --wait (default: 2000)',
922
+ ' --out <file> Write binary response to file',
923
+ ' --help Show help',
924
+ ' --version Show package version',
925
+ '',
926
+ 'Groups:',
927
+ ]
928
+
929
+ for (const group of COMMAND_GROUPS) {
930
+ if (group.aliasFor) {
931
+ lines.push(` ${group.name} (alias for ${group.aliasFor})`)
932
+ } else {
933
+ lines.push(` ${group.name}`)
934
+ }
935
+ }
936
+
937
+ lines.push('', 'Use "swarmclaw <group> --help" for group commands.')
938
+ return lines.join('\n')
939
+ }
940
+
941
+ function renderGroupHelp(groupName) {
942
+ const group = GROUP_MAP.get(groupName)
943
+ if (!group) {
944
+ throw new Error(`Unknown command group: ${groupName}`)
945
+ }
946
+
947
+ const resolved = resolveGroup(groupName)
948
+ if (!resolved) throw new Error(`Unable to resolve command group: ${groupName}`)
949
+
950
+ const lines = [
951
+ `Group: ${groupName}${group.aliasFor ? ` (alias for ${group.aliasFor})` : ''}`,
952
+ group.description ? `Description: ${group.description}` : '',
953
+ '',
954
+ 'Commands:',
955
+ ].filter(Boolean)
956
+
957
+ for (const command of resolved.commands) {
958
+ const params = extractPathParams(command.route).map((name) => `<${name}>`).join(' ')
959
+ const suffix = params ? ` ${params}` : ''
960
+ lines.push(` ${command.action}${suffix} ${command.description}`)
961
+ }
962
+
963
+ return lines.join('\n')
964
+ }
965
+
966
+ async function runCli(argv, deps = {}) {
967
+ const stdout = deps.stdout || process.stdout
968
+ const stderr = deps.stderr || process.stderr
969
+ const stdin = deps.stdin || process.stdin
970
+ const env = deps.env || process.env
971
+ const cwd = deps.cwd || process.cwd()
972
+ const fetchImpl = deps.fetchImpl || globalThis.fetch
973
+
974
+ if (typeof fetchImpl !== 'function') {
975
+ stderr.write('Global fetch is unavailable in this Node runtime. Use Node 18+ or provide a fetch implementation.\n')
976
+ return 1
977
+ }
978
+
979
+ let parsed
980
+ try {
981
+ parsed = parseArgv(argv)
982
+ } catch (err) {
983
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
984
+ return 1
985
+ }
986
+
987
+ if (parsed.opts.version) {
988
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json')
989
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
990
+ stdout.write(`${pkg.name || 'swarmclaw'} ${pkg.version || '0.0.0'}\n`)
991
+ return 0
992
+ }
993
+
994
+ if (!parsed.group || parsed.opts.help) {
995
+ if (parsed.group) {
996
+ try {
997
+ stdout.write(`${renderGroupHelp(parsed.group)}\n`)
998
+ return 0
999
+ } catch {
1000
+ // Fall through to general help for unknown group
1001
+ }
1002
+ }
1003
+ stdout.write(`${renderGeneralHelp()}\n`)
1004
+ return 0
1005
+ }
1006
+
1007
+ if (!parsed.action) {
1008
+ try {
1009
+ stdout.write(`${renderGroupHelp(parsed.group)}\n`)
1010
+ return 0
1011
+ } catch (err) {
1012
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
1013
+ return 1
1014
+ }
1015
+ }
1016
+
1017
+ const command = getCommand(parsed.group, parsed.action)
1018
+ if (!command) {
1019
+ stderr.write(`Unknown command: ${parsed.group} ${parsed.action}\n`)
1020
+ const group = resolveGroup(parsed.group)
1021
+ if (group) {
1022
+ stderr.write(`${renderGroupHelp(parsed.group)}\n`)
1023
+ } else {
1024
+ stderr.write(`${renderGeneralHelp()}\n`)
1025
+ }
1026
+ return 1
1027
+ }
1028
+
1029
+ const pathArgs = parsed.positionals.slice(2)
1030
+ let routeInfo
1031
+ try {
1032
+ routeInfo = buildRoute(command.route, pathArgs)
1033
+ } catch (err) {
1034
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
1035
+ return 1
1036
+ }
1037
+
1038
+ const accessKey = resolveAccessKey(parsed.opts, env, cwd)
1039
+ const baseUrl = parsed.opts.baseUrl || env.SWARMCLAW_BASE_URL || 'http://localhost:3456'
1040
+
1041
+ const headerEntries = []
1042
+ for (const raw of parsed.opts.headers) {
1043
+ try {
1044
+ headerEntries.push(parseKeyValue(raw, 'header'))
1045
+ } catch (err) {
1046
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
1047
+ return 1
1048
+ }
1049
+ }
1050
+
1051
+ if (parsed.opts.secret) {
1052
+ headerEntries.push(['x-webhook-secret', parsed.opts.secret])
1053
+ }
1054
+
1055
+ const queryEntries = []
1056
+ for (const raw of parsed.opts.query) {
1057
+ try {
1058
+ queryEntries.push(parseKeyValue(raw, 'query'))
1059
+ } catch (err) {
1060
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
1061
+ return 1
1062
+ }
1063
+ }
1064
+
1065
+ if (parsed.opts.event) {
1066
+ queryEntries.push(['event', parsed.opts.event])
1067
+ }
1068
+
1069
+ let url
1070
+ try {
1071
+ url = buildApiUrl(baseUrl, routeInfo.route, queryEntries)
1072
+ } catch (err) {
1073
+ stderr.write(`Invalid --base-url: ${err instanceof Error ? err.message : String(err)}\n`)
1074
+ return 1
1075
+ }
1076
+
1077
+ const headers = {
1078
+ ...Object.fromEntries(headerEntries),
1079
+ }
1080
+ if (accessKey) headers['X-Access-Key'] = accessKey
1081
+
1082
+ try {
1083
+ if (command.clientGetRoute) {
1084
+ const collectionUrl = buildApiUrl(baseUrl, command.clientGetRoute, queryEntries)
1085
+ const payload = await fetchJson(fetchImpl, collectionUrl, headers, parsed.opts.timeoutMs)
1086
+ const id = pathArgs[0]
1087
+ const entity = extractById(payload, id)
1088
+ if (!entity) {
1089
+ stderr.write(`Entity not found for id: ${id}\n`)
1090
+ return 1
1091
+ }
1092
+ if (parsed.opts.jsonOutput) writeJson(stdout, entity, true)
1093
+ else writeJson(stdout, entity, false)
1094
+ return 0
1095
+ }
1096
+
1097
+ const init = {
1098
+ method: command.method,
1099
+ headers,
1100
+ signal: AbortSignal.timeout(parsed.opts.timeoutMs),
1101
+ }
1102
+
1103
+ if (command.requestType === 'upload') {
1104
+ const uploadPath = parsed.opts.file || routeInfo.remaining[0]
1105
+ if (!uploadPath) {
1106
+ throw new Error(`Missing file path. Usage: ${parsed.group} ${parsed.action} <filePath>`) }
1107
+
1108
+ const resolvedUploadPath = path.isAbsolute(uploadPath) ? uploadPath : path.join(cwd, uploadPath)
1109
+ const fileBuffer = fs.readFileSync(resolvedUploadPath)
1110
+ const filename = parsed.opts.filename || path.basename(resolvedUploadPath)
1111
+ init.body = fileBuffer
1112
+ init.headers['x-filename'] = filename
1113
+ if (!init.headers['Content-Type']) init.headers['Content-Type'] = 'application/octet-stream'
1114
+ } else if (command.method !== 'GET' && command.method !== 'HEAD') {
1115
+ let body = undefined
1116
+ if (parsed.opts.data) {
1117
+ body = parseDataInput(parsed.opts.data, stdin)
1118
+ }
1119
+
1120
+ if (!isPlainObject(body) && command.expectsJsonBody) {
1121
+ body = {}
1122
+ }
1123
+
1124
+ if (command.defaultBody) {
1125
+ body = { ...(command.defaultBody || {}), ...(isPlainObject(body) ? body : {}) }
1126
+ }
1127
+
1128
+ if (command.bodyFlagMap) {
1129
+ const mapped = {}
1130
+ for (const [flagName, bodyKey] of Object.entries(command.bodyFlagMap)) {
1131
+ const val = parsed.opts[flagName]
1132
+ if (val !== undefined && val !== '') {
1133
+ mapped[bodyKey] = val
1134
+ }
1135
+ }
1136
+ body = { ...(isPlainObject(body) ? body : {}), ...mapped }
1137
+ }
1138
+
1139
+ if (body !== undefined) {
1140
+ init.body = JSON.stringify(body)
1141
+ init.headers['Content-Type'] = 'application/json'
1142
+ }
1143
+ }
1144
+
1145
+ const res = await fetchImpl(url, init)
1146
+ const parsedResponse = await parseResponse(res, command.responseType)
1147
+
1148
+ if (!res.ok) {
1149
+ const serialized = serializePayload(parsedResponse.value)
1150
+ stderr.write(`Request failed (${res.status} ${res.statusText}): ${serialized}\n`)
1151
+ return 1
1152
+ }
1153
+
1154
+ if (parsedResponse.type === 'sse') {
1155
+ await consumeSse(parsedResponse.value, stdout, stderr, parsed.opts.jsonOutput)
1156
+ return 0
1157
+ }
1158
+
1159
+ if (parsedResponse.type === 'binary') {
1160
+ writeBinary(stdout, stderr, parsedResponse.value, parsed.opts.out, cwd)
1161
+ return 0
1162
+ }
1163
+
1164
+ if (parsedResponse.type === 'json') {
1165
+ if (parsed.opts.jsonOutput) writeJson(stdout, parsedResponse.value, true)
1166
+ else writeJson(stdout, parsedResponse.value, false)
1167
+
1168
+ if (parsed.opts.wait) {
1169
+ const waitMeta = getWaitId(parsedResponse.value, command)
1170
+ if (waitMeta) {
1171
+ await waitForEntity({
1172
+ entityType: waitMeta.type,
1173
+ entityId: waitMeta.id,
1174
+ fetchImpl,
1175
+ baseUrl,
1176
+ headers,
1177
+ timeoutMs: parsed.opts.timeoutMs,
1178
+ intervalMs: parsed.opts.intervalMs,
1179
+ stdout,
1180
+ jsonOutput: parsed.opts.jsonOutput,
1181
+ })
1182
+ } else {
1183
+ stderr.write('--wait requested, but response did not include runId/taskId\n')
1184
+ }
1185
+ }
1186
+ return 0
1187
+ }
1188
+
1189
+ writeText(stdout, parsedResponse.value)
1190
+ return 0
1191
+ } catch (err) {
1192
+ stderr.write(`${err instanceof Error ? err.message : String(err)}\n`)
1193
+ return 1
1194
+ }
1195
+ }
1196
+
1197
+ function extractById(payload, id) {
1198
+ if (!id) return null
1199
+
1200
+ if (Array.isArray(payload)) {
1201
+ return payload.find((entry) => entry && String(entry.id) === String(id)) || null
1202
+ }
1203
+
1204
+ if (isPlainObject(payload)) {
1205
+ if (payload[id]) return payload[id]
1206
+ if (Array.isArray(payload.items)) {
1207
+ return payload.items.find((entry) => entry && String(entry.id) === String(id)) || null
1208
+ }
1209
+ }
1210
+
1211
+ return null
1212
+ }
1213
+
1214
+ function getApiCoveragePairs() {
1215
+ return COMMANDS
1216
+ .filter((command) => !command.virtual)
1217
+ .map((command) => `${command.method} ${command.route}`)
1218
+ }
1219
+
1220
+ module.exports = {
1221
+ COMMAND_GROUPS,
1222
+ COMMANDS,
1223
+ parseArgv,
1224
+ runCli,
1225
+ getCommand,
1226
+ getApiCoveragePairs,
1227
+ buildApiUrl,
1228
+ extractPathParams,
1229
+ resolveGroup,
1230
+ renderGeneralHelp,
1231
+ renderGroupHelp,
1232
+ }