@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -163,6 +163,37 @@ test('runCli sends authenticated request and emits compact JSON when --json is s
163
163
  assert.equal(stderr.toString(), '')
164
164
  })
165
165
 
166
+ test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
167
+ const stdout = makeWritable()
168
+ const stderr = makeWritable()
169
+ const calls = []
170
+
171
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-keyfile-'))
172
+ fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-key\n', 'utf8')
173
+
174
+ const fetchImpl = async (url, init) => {
175
+ calls.push({ url: String(url), init })
176
+ return jsonResponse({ ok: true })
177
+ }
178
+
179
+ const exitCode = await runCli(
180
+ ['runs', 'list', '--json'],
181
+ {
182
+ fetchImpl,
183
+ stdout,
184
+ stderr,
185
+ env: {},
186
+ cwd: tmpDir,
187
+ }
188
+ )
189
+
190
+ assert.equal(exitCode, 0)
191
+ assert.equal(calls[0].init.headers['X-Access-Key'], 'file-key')
192
+ assert.equal(stderr.toString(), '')
193
+
194
+ fs.rmSync(tmpDir, { recursive: true, force: true })
195
+ })
196
+
166
197
  test('upload command sends binary body and x-filename header', async () => {
167
198
  const stdout = makeWritable()
168
199
  const stderr = makeWritable()
@@ -197,6 +228,32 @@ test('upload command sends binary body and x-filename header', async () => {
197
228
  fs.rmSync(tmpDir, { recursive: true, force: true })
198
229
  })
199
230
 
231
+ test('binary responses require --out when stdout is a TTY', async () => {
232
+ const stdout = makeWritable()
233
+ stdout.isTTY = true
234
+ const stderr = makeWritable()
235
+
236
+ const fetchImpl = async () =>
237
+ new Response(Buffer.from('hello'), {
238
+ status: 200,
239
+ headers: { 'content-type': 'application/octet-stream' },
240
+ })
241
+
242
+ const exitCode = await runCli(
243
+ ['uploads', 'get', 'artifact.bin'],
244
+ {
245
+ fetchImpl,
246
+ stdout,
247
+ stderr,
248
+ env: {},
249
+ cwd: process.cwd(),
250
+ }
251
+ )
252
+
253
+ assert.equal(exitCode, 1)
254
+ assert.match(stderr.toString(), /binary response requires --out <file>/i)
255
+ })
256
+
200
257
  test('wait polls run endpoint until terminal state', async () => {
201
258
  const stdout = makeWritable()
202
259
  const stderr = makeWritable()
@@ -236,6 +293,144 @@ test('wait polls run endpoint until terminal state', async () => {
236
293
  assert.match(stdout.toString(), /"status": "completed"/)
237
294
  })
238
295
 
296
+ test('runCli parses CRLF-delimited SSE events correctly', async () => {
297
+ const stdout = makeWritable()
298
+ const stderr = makeWritable()
299
+
300
+ const fetchImpl = async () => new Response(
301
+ 'data: {"t":"md","text":"first"}\r\n\r\ndata: {"t":"md","text":"second"}\r\n\r\n',
302
+ {
303
+ status: 200,
304
+ headers: { 'content-type': 'text/event-stream' },
305
+ }
306
+ )
307
+
308
+ const exitCode = await runCli(
309
+ ['chatrooms', 'chat', 'room-1', '--data', '{}'],
310
+ {
311
+ fetchImpl,
312
+ stdout,
313
+ stderr,
314
+ env: {},
315
+ cwd: process.cwd(),
316
+ }
317
+ )
318
+
319
+ assert.equal(exitCode, 0)
320
+ assert.equal(stdout.toString(), 'first\nsecond\n')
321
+ assert.equal(stderr.toString(), '')
322
+ })
323
+
324
+ test('binary responses require --out when stdout is a TTY', async () => {
325
+ const stdout = makeWritable()
326
+ stdout.isTTY = true
327
+ const stderr = makeWritable()
328
+
329
+ const fetchImpl = async () => new Response(Buffer.from('ok'), {
330
+ status: 200,
331
+ headers: { 'content-type': 'application/octet-stream' },
332
+ })
333
+
334
+ const exitCode = await runCli(
335
+ ['memory-images', 'get', 'image-1.png'],
336
+ {
337
+ fetchImpl,
338
+ stdout,
339
+ stderr,
340
+ env: {},
341
+ cwd: process.cwd(),
342
+ }
343
+ )
344
+
345
+ assert.equal(exitCode, 1)
346
+ assert.equal(stdout.toString(), '')
347
+ assert.match(stderr.toString(), /binary response requires --out <file>/i)
348
+ })
349
+
350
+ test('client-side collection lookups fail cleanly when the entity is missing', async () => {
351
+ const stdout = makeWritable()
352
+ const stderr = makeWritable()
353
+
354
+ const fetchImpl = async () => jsonResponse([{ id: 'agent-2', name: 'Other Agent' }])
355
+
356
+ const exitCode = await runCli(
357
+ ['agents', 'get', 'agent-1'],
358
+ {
359
+ fetchImpl,
360
+ stdout,
361
+ stderr,
362
+ env: {},
363
+ cwd: process.cwd(),
364
+ }
365
+ )
366
+
367
+ assert.equal(exitCode, 1)
368
+ assert.equal(stdout.toString(), '')
369
+ assert.match(stderr.toString(), /entity not found for id: agent-1/i)
370
+ })
371
+
372
+ test('runCli loads request JSON from @file inputs', async () => {
373
+ const stdout = makeWritable()
374
+ const stderr = makeWritable()
375
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-data-'))
376
+ const dataPath = path.join(tmpDir, 'payload.json')
377
+ fs.writeFileSync(dataPath, JSON.stringify({ title: 'From file', status: 'backlog' }), 'utf8')
378
+
379
+ const calls = []
380
+ const fetchImpl = async (url, init) => {
381
+ calls.push({ url: String(url), init })
382
+ return jsonResponse({ ok: true })
383
+ }
384
+
385
+ const exitCode = await runCli(
386
+ ['tasks', 'create', '--data', `@${dataPath}`],
387
+ {
388
+ fetchImpl,
389
+ stdout,
390
+ stderr,
391
+ env: {},
392
+ cwd: process.cwd(),
393
+ }
394
+ )
395
+
396
+ assert.equal(exitCode, 0)
397
+ assert.equal(calls.length, 1)
398
+ assert.equal(calls[0].init.headers['Content-Type'], 'application/json')
399
+ assert.deepEqual(JSON.parse(calls[0].init.body), { title: 'From file', status: 'backlog' })
400
+
401
+ fs.rmSync(tmpDir, { recursive: true, force: true })
402
+ })
403
+
404
+ test('runCli falls back to platform-api-key.txt when no env key is provided', async () => {
405
+ const stdout = makeWritable()
406
+ const stderr = makeWritable()
407
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-key-'))
408
+ fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-key\n', 'utf8')
409
+
410
+ const calls = []
411
+ const fetchImpl = async (url, init) => {
412
+ calls.push({ url: String(url), init })
413
+ return jsonResponse({ ok: true })
414
+ }
415
+
416
+ const exitCode = await runCli(
417
+ ['runs', 'list'],
418
+ {
419
+ fetchImpl,
420
+ stdout,
421
+ stderr,
422
+ env: {},
423
+ cwd: tmpDir,
424
+ }
425
+ )
426
+
427
+ assert.equal(exitCode, 0)
428
+ assert.equal(calls.length, 1)
429
+ assert.equal(calls[0].init.headers['X-Access-Key'], 'file-key')
430
+
431
+ fs.rmSync(tmpDir, { recursive: true, force: true })
432
+ })
433
+
239
434
  test('all command definitions execute with a mocked API transport', async () => {
240
435
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-all-'))
241
436
  const uploadPath = path.join(tmpDir, 'upload.txt')
package/src/cli/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Command } from 'commander'
4
4
  import { pathToFileURL } from 'node:url'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
5
7
  import {
6
8
  SETUP_PROVIDERS,
7
9
  DEFAULT_AGENTS,
@@ -17,7 +19,6 @@ interface CliContext {
17
19
 
18
20
  interface SetupAuthStatus {
19
21
  firstTime?: boolean
20
- key?: string
21
22
  }
22
23
 
23
24
  interface SetupProviderCheckResponse {
@@ -34,7 +35,21 @@ const DEFAULT_BASE_URL =
34
35
  || process.env.SWARMCLAW_BASE_URL
35
36
  || 'http://localhost:3456'
36
37
 
37
- const DEFAULT_ACCESS_KEY = process.env.SWARMCLAW_ACCESS_KEY || ''
38
+ function resolveDefaultAccessKey(cwd: string = process.cwd()): string {
39
+ const envKey = (
40
+ process.env.SWARMCLAW_ACCESS_KEY
41
+ || process.env.SWARMCLAW_API_KEY
42
+ || process.env.SC_ACCESS_KEY
43
+ || ''
44
+ ).trim()
45
+ if (envKey) return envKey
46
+
47
+ const keyFile = path.join(cwd, 'platform-api-key.txt')
48
+ if (!fs.existsSync(keyFile)) return ''
49
+ return fs.readFileSync(keyFile, 'utf8').trim()
50
+ }
51
+
52
+ const DEFAULT_ACCESS_KEY = resolveDefaultAccessKey()
38
53
 
39
54
  function normalizeBaseUrl(value: string): string {
40
55
  const trimmed = value.trim()
@@ -208,19 +223,13 @@ async function resolveSetupAccessKey(ctx: CliContext): Promise<{
208
223
  }
209
224
 
210
225
  const status = await apiRequestWithAccessKey<SetupAuthStatus>(ctx, 'GET', '/auth', undefined)
211
- const discoveredKey = typeof status?.key === 'string' ? status.key.trim() : ''
212
226
  const firstTime = status?.firstTime === true
213
227
 
214
- if (!firstTime || !discoveredKey) {
215
- throw new Error('No access key provided. Pass --key (or SWARMCLAW_ACCESS_KEY), or run setup on a fresh first-time instance.')
228
+ if (firstTime) {
229
+ throw new Error('No access key provided. Read the generated key from the launch terminal or .env.local, then pass --key, set SWARMCLAW_ACCESS_KEY / SWARMCLAW_API_KEY, or use platform-api-key.txt.')
216
230
  }
217
231
 
218
- await apiRequestWithAccessKey(ctx, 'POST', '/auth', discoveredKey, { key: discoveredKey })
219
- return {
220
- accessKey: discoveredKey,
221
- firstTime: true,
222
- autoDiscovered: true,
223
- }
232
+ throw new Error('No access key provided. Pass --key, set SWARMCLAW_ACCESS_KEY / SWARMCLAW_API_KEY, or use platform-api-key.txt.')
224
233
  }
225
234
 
226
235
  function printResult(value: unknown, rawOutput: boolean): void {
@@ -1364,7 +1373,7 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise<nu
1364
1373
  ? null
1365
1374
  : checkForUpdate(
1366
1375
  normalizeBaseUrl(process.env.SWARMCLAW_URL || process.env.SWARMCLAW_BASE_URL || 'http://localhost:3456'),
1367
- (process.env.SWARMCLAW_ACCESS_KEY || '').trim(),
1376
+ resolveDefaultAccessKey(),
1368
1377
  )
1369
1378
 
1370
1379
  await program.parseAsync(['node', 'swarmclaw', ...argv])
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+
4
+ const test = require('node:test')
5
+ const assert = require('node:assert/strict')
6
+ const fs = require('node:fs')
7
+ const os = require('node:os')
8
+ const path = require('node:path')
9
+
10
+ function loadServerCmdForHome(homeDir) {
11
+ const modPath = require.resolve('../../bin/server-cmd.js')
12
+ const previousHome = process.env.SWARMCLAW_HOME
13
+ process.env.SWARMCLAW_HOME = homeDir
14
+ delete require.cache[modPath]
15
+ const loaded = require(modPath)
16
+ if (previousHome === undefined) delete process.env.SWARMCLAW_HOME
17
+ else process.env.SWARMCLAW_HOME = previousHome
18
+ delete require.cache[modPath]
19
+ return loaded
20
+ }
21
+
22
+ test('needsBuild returns true when no build marker exists', () => {
23
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
24
+ const serverCmd = loadServerCmdForHome(homeDir)
25
+ assert.equal(serverCmd.needsBuild(false), true)
26
+ fs.rmSync(homeDir, { recursive: true, force: true })
27
+ })
28
+
29
+ test('needsBuild returns false when build marker version matches and standalone server exists', () => {
30
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
31
+ const serverCmd = loadServerCmdForHome(homeDir)
32
+
33
+ fs.mkdirSync(path.join(homeDir, '.next', 'standalone'), { recursive: true })
34
+ fs.writeFileSync(path.join(homeDir, '.next', 'standalone', 'server.js'), 'console.log("ok")\n', 'utf8')
35
+ fs.writeFileSync(
36
+ path.join(homeDir, '.built'),
37
+ JSON.stringify({ builtAt: new Date().toISOString(), version: serverCmd.getVersion() }),
38
+ 'utf8',
39
+ )
40
+
41
+ assert.equal(serverCmd.needsBuild(false), false)
42
+ fs.rmSync(homeDir, { recursive: true, force: true })
43
+ })
44
+
45
+ test('needsBuild returns true when build marker version is stale', () => {
46
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
47
+ const serverCmd = loadServerCmdForHome(homeDir)
48
+
49
+ fs.mkdirSync(path.join(homeDir, '.next', 'standalone'), { recursive: true })
50
+ fs.writeFileSync(path.join(homeDir, '.next', 'standalone', 'server.js'), 'console.log("ok")\n', 'utf8')
51
+ fs.writeFileSync(
52
+ path.join(homeDir, '.built'),
53
+ JSON.stringify({ builtAt: new Date().toISOString(), version: '0.0.0-test' }),
54
+ 'utf8',
55
+ )
56
+
57
+ assert.equal(serverCmd.needsBuild(false), true)
58
+ fs.rmSync(homeDir, { recursive: true, force: true })
59
+ })
package/src/cli/spec.js CHANGED
@@ -127,6 +127,16 @@ const COMMAND_GROUPS = {
127
127
  },
128
128
  },
129
129
  },
130
+ 'external-agents': {
131
+ description: 'Manage external agent runtimes',
132
+ commands: {
133
+ list: { description: 'List external agent runtimes', method: 'GET', path: '/external-agents' },
134
+ create: { description: 'Register an external agent runtime', method: 'POST', path: '/external-agents' },
135
+ update: { description: 'Update an external agent runtime', method: 'PUT', path: '/external-agents/:id', params: ['id'] },
136
+ delete: { description: 'Delete an external agent runtime', method: 'DELETE', path: '/external-agents/:id', params: ['id'] },
137
+ heartbeat: { description: 'Record an external agent heartbeat', method: 'POST', path: '/external-agents/:id/heartbeat', params: ['id'] },
138
+ },
139
+ },
130
140
  uploads: {
131
141
  description: 'Manage uploaded artifacts',
132
142
  commands: {
@@ -143,6 +153,16 @@ const COMMAND_GROUPS = {
143
153
  open: { description: 'Open a local file path via host default app/browser', method: 'POST', path: '/files/open' },
144
154
  },
145
155
  },
156
+ gateways: {
157
+ description: 'Manage named OpenClaw gateway profiles',
158
+ commands: {
159
+ list: { description: 'List configured gateway profiles', method: 'GET', path: '/gateways' },
160
+ create: { description: 'Create a gateway profile', method: 'POST', path: '/gateways' },
161
+ update: { description: 'Update a gateway profile', method: 'PUT', path: '/gateways/:id', params: ['id'] },
162
+ delete: { description: 'Delete a gateway profile', method: 'DELETE', path: '/gateways/:id', params: ['id'] },
163
+ health: { description: 'Run a gateway health check', method: 'GET', path: '/gateways/:id/health', params: ['id'] },
164
+ },
165
+ },
146
166
  logs: {
147
167
  description: 'Application logs',
148
168
  commands: {
@@ -291,8 +311,6 @@ const COMMAND_GROUPS = {
291
311
  'messages-delete': { description: 'Delete a message from a chat', method: 'DELETE', path: '/chats/:id/messages', params: ['id'] },
292
312
  fork: { description: 'Fork chat from a specific message index', method: 'POST', path: '/chats/:id/fork', params: ['id'] },
293
313
  'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/chats/:id/edit-resend', params: ['id'] },
294
- 'main-loop': { description: 'Get main mission loop state for a chat', method: 'GET', path: '/chats/:id/main-loop', params: ['id'] },
295
- 'main-loop-action': { description: 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', method: 'POST', path: '/chats/:id/main-loop', params: ['id'] },
296
314
  chat: { description: 'Send chat message (SSE stream)', method: 'POST', path: '/chats/:id/chat', params: ['id'], stream: true, waitable: true },
297
315
  stop: { description: 'Cancel active/running chat work', method: 'POST', path: '/chats/:id/stop', params: ['id'] },
298
316
  clear: { description: 'Clear chat history', method: 'POST', path: '/chats/:id/clear', params: ['id'] },
@@ -37,6 +37,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
37
37
  const setCurrentSession = useAppStore((s) => s.setCurrentSession)
38
38
  const setActiveView = useAppStore((s) => s.setActiveView)
39
39
  const setMessages = useChatStore((s) => s.setMessages)
40
+ const sendMessage = useChatStore((s) => s.sendMessage)
40
41
  const togglePinAgent = useAppStore((s) => s.togglePinAgent)
41
42
  const [running, setRunning] = useState(false)
42
43
  const [dialogOpen, setDialogOpen] = useState(false)
@@ -61,6 +62,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
61
62
  budget: typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0 ? agent.dailyBudget : null,
62
63
  },
63
64
  ].filter((entry) => entry.budget !== null)
65
+ const canDelegateToAgents = agent.platformAssignScope === 'all'
64
66
  useWs(`heartbeat:agent:${agent.id}`, () => {
65
67
  setHeartbeatPulse(true)
66
68
  setTimeout(() => setHeartbeatPulse(false), 1500)
@@ -78,19 +80,20 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
78
80
  }
79
81
 
80
82
  const handleConfirmRun = async () => {
81
- if (!taskInput.trim()) return
83
+ const task = taskInput.trim()
84
+ if (!task) return
82
85
  setDialogOpen(false)
83
86
  setRunning(true)
84
87
  try {
85
- const result = await api<{ ok: boolean; sessionId: string }>('POST', '/orchestrator/run', { agentId: agent.id, task: taskInput })
86
- if (result.sessionId) {
87
- await loadSessions()
88
- setMessages([])
89
- setCurrentSession(result.sessionId)
90
- setActiveView('agents')
91
- }
88
+ const session = await api<{ id: string }>('POST', `/agents/${agent.id}/thread`, { user: 'default' })
89
+ if (!session?.id) throw new Error('Agent thread not available')
90
+ await loadSessions()
91
+ setMessages([])
92
+ setCurrentSession(session.id)
93
+ setActiveView('agents')
94
+ await sendMessage(task)
92
95
  } catch (err) {
93
- console.error('Orchestrator run failed:', err)
96
+ console.error('Agent task run failed:', err)
94
97
  }
95
98
  setRunning(false)
96
99
  }
@@ -199,7 +202,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
199
202
  default
200
203
  </span>
201
204
  )}
202
- {agent.isOrchestrator && (
205
+ {canDelegateToAgents && (
203
206
  <button
204
207
  onClick={handleRunClick}
205
208
  disabled={running}
@@ -210,7 +213,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
210
213
  {running ? '...' : 'Run'}
211
214
  </button>
212
215
  )}
213
- {agent.isOrchestrator && (
216
+ {canDelegateToAgents && (
214
217
  <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px] flex items-center gap-1">
215
218
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>
216
219
  delegates
@@ -299,7 +302,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
299
302
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
300
303
  <DialogContent className="sm:max-w-[420px]">
301
304
  <DialogHeader>
302
- <DialogTitle>Run Orchestrator</DialogTitle>
305
+ <DialogTitle>Run Agent</DialogTitle>
303
306
  </DialogHeader>
304
307
  <div className="py-3">
305
308
  <label className="block text-[12px] font-600 text-text-3 mb-2">Task for {agent.name}</label>
@@ -135,6 +135,17 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
135
135
  })
136
136
  }, [sortedAgents, chatFilter, sessions, runningAgentIds, streamingSessionId, chatroomActiveAgentIds])
137
137
 
138
+ const defaultAgent = useMemo(() => {
139
+ const id = appSettings.defaultAgentId
140
+ return id ? agents[id] || null : null
141
+ }, [appSettings.defaultAgentId, agents])
142
+
143
+ const defaultAgentVisible = !!defaultAgent && filteredAgents.some((agent) => agent.id === defaultAgent.id)
144
+ const listAgents = useMemo(
145
+ () => (defaultAgentVisible ? filteredAgents.filter((agent) => agent.id !== defaultAgent?.id) : filteredAgents),
146
+ [defaultAgent?.id, defaultAgentVisible, filteredAgents],
147
+ )
148
+
138
149
  // FLIP: animate row position changes
139
150
  useLayoutEffect(() => {
140
151
  const prevTop = previousTopRef.current
@@ -258,7 +269,91 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
258
269
  </div>
259
270
  )}
260
271
  <div className="flex flex-col gap-0.5 px-2 pb-4">
261
- {filteredAgents.map((agent) => {
272
+ {defaultAgentVisible && defaultAgent && (() => {
273
+ const threadSession = defaultAgent.threadSessionId ? sessions[defaultAgent.threadSessionId] as Session | undefined : undefined
274
+ const lastMsg = threadSession?.messages?.at(-1)
275
+ const heartbeatOn = defaultAgent.heartbeatEnabled === true && (defaultAgent.plugins?.length ?? 0) > 0
276
+ const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
277
+ const isWorking = runningAgentIds.has(defaultAgent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(defaultAgent.id)
278
+ const isTyping = streamingSessionId === defaultAgent.threadSessionId
279
+ const preview = lastMsg?.text?.slice(0, 100)?.replace(/\n/g, ' ') || 'Your primary shortcut chat.'
280
+ const isActive = currentAgentId === defaultAgent.id
281
+
282
+ return (
283
+ <div className="mb-2 px-2">
284
+ <div className="px-2 pb-1 text-[10px] font-700 uppercase tracking-[0.12em] text-accent-bright/65">
285
+ Default Agent
286
+ </div>
287
+ <div
288
+ className={`group/row relative w-full text-left py-3.5 px-4 rounded-[14px] cursor-pointer transition-all duration-150 border
289
+ ${isActive
290
+ ? 'bg-accent-soft border-accent-bright/25'
291
+ : 'bg-accent-soft/40 border-accent-bright/15 hover:bg-accent-soft/55'}`}
292
+ onClick={() => bulkMode ? toggleSelected(defaultAgent.id) : handleSelect(defaultAgent)}
293
+ >
294
+ <div className="flex items-center gap-3">
295
+ {bulkMode && (
296
+ <div className={`w-5 h-5 rounded-[6px] border-2 flex items-center justify-center shrink-0 transition-colors
297
+ ${selectedIds.has(defaultAgent.id) ? 'bg-accent-bright border-accent-bright' : 'border-white/20 bg-transparent'}`}>
298
+ {selectedIds.has(defaultAgent.id) && (
299
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
300
+ <polyline points="20 6 9 17 4 12" />
301
+ </svg>
302
+ )}
303
+ </div>
304
+ )}
305
+ <div className="relative shrink-0">
306
+ <AgentAvatar seed={defaultAgent.avatarSeed || null} avatarUrl={defaultAgent.avatarUrl} name={defaultAgent.name} size={38} />
307
+ <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
308
+ isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/30'
309
+ }`} />
310
+ </div>
311
+ <div className="flex-1 min-w-0">
312
+ <div className="flex items-center gap-2">
313
+ <span className="font-display text-[14px] font-700 truncate text-text tracking-[-0.01em]">
314
+ {defaultAgent.name}
315
+ </span>
316
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/12 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em]">
317
+ Shortcut
318
+ </span>
319
+ </div>
320
+ {isTyping ? (
321
+ <div className="text-[12px] text-accent-bright/80 mt-1 flex items-center gap-1.5">
322
+ <span className="flex gap-0.5">
323
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
324
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
325
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
326
+ </span>
327
+ Typing...
328
+ </div>
329
+ ) : (
330
+ <div className="text-[12px] text-text-3/70 mt-1 truncate">
331
+ {preview}
332
+ </div>
333
+ )}
334
+ </div>
335
+ <button
336
+ onClick={async (e) => {
337
+ e.stopPropagation()
338
+ await updateSettings({ defaultAgentId: null })
339
+ toast.success('Default agent cleared')
340
+ }}
341
+ aria-label="Remove as default agent"
342
+ title="Default agent — click to clear"
343
+ className="shrink-0 p-1 rounded-[6px] transition-all bg-transparent border-none cursor-pointer hover:bg-white/[0.06] text-accent-bright"
344
+ style={{ fontFamily: 'inherit' }}
345
+ >
346
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
347
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
348
+ <path d="M9 22V12h6v10" fill="rgba(0,0,0,0.3)" stroke="none" />
349
+ </svg>
350
+ </button>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ )
355
+ })()}
356
+ {listAgents.map((agent) => {
262
357
  const threadSession = agent.threadSessionId ? sessions[agent.threadSessionId] as Session | undefined : undefined
263
358
  const lastMsg = threadSession?.messages?.at(-1)
264
359
  const isActive = currentAgentId === agent.id
@@ -300,6 +395,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
300
395
  <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
301
396
  {agent.name}
302
397
  </span>
398
+ {appSettings.defaultAgentId === agent.id && (
399
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/10 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em] shrink-0">
400
+ Default
401
+ </span>
402
+ )}
303
403
  <span className="text-[10px] text-text-3/60 font-mono shrink-0">
304
404
  {(threadSession?.model || agent.model)
305
405
  ? (threadSession?.model || agent.model)!.split('/').pop()?.split(':')[0]