@swarmclawai/swarmclaw 0.7.7 → 0.8.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 (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -206,6 +206,109 @@ describe('delegate fallback', () => {
206
206
  assert.match(String(output.response || ''), /codex fallback ok/i)
207
207
  })
208
208
 
209
+ it('uses nested data.task payloads from recent tool-call wrappers', () => {
210
+ const output = runWithFakeDelegates(`
211
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
212
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
213
+
214
+ const tools = buildDelegateTools({
215
+ cwd: process.cwd(),
216
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
217
+ hasPlugin: (name) => name === 'delegate',
218
+ hasTool: (name) => name === 'delegate',
219
+ cleanupFns: [],
220
+ commandTimeoutMs: 5000,
221
+ claudeTimeoutMs: 5000,
222
+ cliProcessTimeoutMs: 5000,
223
+ persistDelegateResumeId: () => {},
224
+ readStoredDelegateResumeId: () => null,
225
+ resolveCurrentSession: () => null,
226
+ activePlugins: ['delegate'],
227
+ })
228
+
229
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
230
+ const raw = await delegateTool.invoke({
231
+ input: JSON.stringify({
232
+ data: {
233
+ task: 'Create a simple to-do list application.',
234
+ },
235
+ }),
236
+ })
237
+ console.log(raw)
238
+ `)
239
+
240
+ assert.equal(output.backend, 'codex')
241
+ assert.equal(output.status, 'completed')
242
+ assert.match(String(output.response || ''), /codex fallback ok/i)
243
+ })
244
+
245
+ it('falls back to reason text when malformed delegate wrappers omit task', () => {
246
+ const output = runWithFakeDelegates(`
247
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
248
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
249
+
250
+ const tools = buildDelegateTools({
251
+ cwd: process.cwd(),
252
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
253
+ hasPlugin: (name) => name === 'delegate',
254
+ hasTool: (name) => name === 'delegate',
255
+ cleanupFns: [],
256
+ commandTimeoutMs: 5000,
257
+ claudeTimeoutMs: 5000,
258
+ cliProcessTimeoutMs: 5000,
259
+ persistDelegateResumeId: () => {},
260
+ readStoredDelegateResumeId: () => null,
261
+ resolveCurrentSession: () => null,
262
+ activePlugins: ['delegate'],
263
+ })
264
+
265
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
266
+ const raw = await delegateTool.invoke({
267
+ input: JSON.stringify({
268
+ parameters: {
269
+ tool_id: 'delegate',
270
+ reason: 'Building a simple front-end to-do list app is well-suited for a delegated agent.',
271
+ subagent_tool_id: 'agent_coder',
272
+ subagent_name: 'Coder',
273
+ },
274
+ }),
275
+ })
276
+ console.log(raw)
277
+ `)
278
+
279
+ assert.equal(output.backend, 'codex')
280
+ assert.equal(output.status, 'completed')
281
+ assert.match(String(output.response || ''), /codex fallback ok/i)
282
+ })
283
+
284
+ it('accepts legacy id fields for lifecycle delegate actions', () => {
285
+ const output = runWithFakeDelegates(`
286
+ const mod = await import('./src/lib/server/session-tools/delegate.ts')
287
+ const { buildDelegateTools } = mod.default || mod['module.exports'] || mod
288
+
289
+ const tools = buildDelegateTools({
290
+ cwd: process.cwd(),
291
+ ctx: { sessionId: 'session-test', agentId: 'agent-test', platformAssignScope: 'self' },
292
+ hasPlugin: (name) => name === 'delegate',
293
+ hasTool: (name) => name === 'delegate',
294
+ cleanupFns: [],
295
+ commandTimeoutMs: 5000,
296
+ claudeTimeoutMs: 5000,
297
+ cliProcessTimeoutMs: 5000,
298
+ persistDelegateResumeId: () => {},
299
+ readStoredDelegateResumeId: () => null,
300
+ resolveCurrentSession: () => null,
301
+ activePlugins: ['delegate'],
302
+ })
303
+
304
+ const delegateTool = tools.find((tool) => tool.name === 'delegate')
305
+ const raw = await delegateTool.invoke({ action: 'status', id: 'job-123' })
306
+ console.log(JSON.stringify({ raw }))
307
+ `)
308
+
309
+ assert.match(String(output.raw || ''), /delegation job "job-123" not found/i)
310
+ })
311
+
209
312
  it('ranks authenticated delegate backends ahead of unauthenticated ones', () => {
210
313
  const output = runWithFakeDelegates(`
211
314
  const mod = await import('./src/lib/server/provider-health.ts')
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { resolveDelegateResumeConfig } from './delegate'
4
+
5
+ describe('resolveDelegateResumeConfig', () => {
6
+ it('auto-resumes when a stored backend resume ID exists', () => {
7
+ const config = resolveDelegateResumeConfig(
8
+ { task: 'continue the implementation' },
9
+ 'codex',
10
+ {
11
+ readStoredDelegateResumeId: (key) => key === 'codex' ? 'codex-thread-42' : null,
12
+ },
13
+ )
14
+
15
+ assert.deepEqual(config, {
16
+ resume: true,
17
+ resumeId: '',
18
+ })
19
+ })
20
+
21
+ it('respects explicit resume=false even when a stored ID exists', () => {
22
+ const config = resolveDelegateResumeConfig(
23
+ { task: 'start fresh', resume: false },
24
+ 'claude',
25
+ {
26
+ readStoredDelegateResumeId: () => 'claude-session-99',
27
+ },
28
+ )
29
+
30
+ assert.deepEqual(config, {
31
+ resume: false,
32
+ resumeId: '',
33
+ })
34
+ })
35
+
36
+ it('treats an explicit resumeId as an instruction to resume immediately', () => {
37
+ const config = resolveDelegateResumeConfig(
38
+ { task: 'continue', resumeId: 'gemini-session-5' },
39
+ 'gemini',
40
+ {
41
+ readStoredDelegateResumeId: () => null,
42
+ },
43
+ )
44
+
45
+ assert.deepEqual(config, {
46
+ resume: true,
47
+ resumeId: 'gemini-session-5',
48
+ })
49
+ })
50
+ })
@@ -147,6 +147,41 @@ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
147
147
  return null
148
148
  }
149
149
 
150
+ function asDelegateRecord(value: unknown): Record<string, unknown> | null {
151
+ return value && typeof value === 'object' && !Array.isArray(value)
152
+ ? value as Record<string, unknown>
153
+ : null
154
+ }
155
+
156
+ function pickNonEmptyDelegateString(...values: unknown[]): string | null {
157
+ for (const value of values) {
158
+ if (typeof value !== 'string') continue
159
+ const trimmed = value.trim()
160
+ if (!trimmed) continue
161
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
162
+ continue
163
+ }
164
+ return trimmed
165
+ }
166
+ return null
167
+ }
168
+
169
+ function pickDelegateTaskText(record: Record<string, unknown> | null): string | null {
170
+ if (!record) return null
171
+ return pickNonEmptyDelegateString(
172
+ record.task,
173
+ record.prompt,
174
+ record.request,
175
+ record.instructions,
176
+ record.instruction,
177
+ record.description,
178
+ record.input,
179
+ record.reason,
180
+ record.goal,
181
+ record.objective,
182
+ )
183
+ }
184
+
150
185
  function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
151
186
  const action = String(normalized.action || '').trim().toLowerCase()
152
187
  const target = [
@@ -200,20 +235,41 @@ function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): stri
200
235
 
201
236
  function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
202
237
  const normalized = normalizeToolInputArgs(rawArgs)
238
+ const nestedData = asDelegateRecord(normalized.data)
239
+ const delegatePayload = {
240
+ ...(nestedData || {}),
241
+ ...normalized,
242
+ }
203
243
  const backend = coerceDelegateBackend(
204
- normalized.backend
205
- ?? normalized.tool_name
206
- ?? normalized.toolName
207
- ?? normalized.delegate
208
- ?? normalized.provider,
244
+ delegatePayload.backend
245
+ ?? delegatePayload.tool_name
246
+ ?? delegatePayload.toolName
247
+ ?? delegatePayload.tool
248
+ ?? delegatePayload.delegate
249
+ ?? delegatePayload.provider
250
+ ?? delegatePayload.subagent_tool_id
251
+ ?? delegatePayload.subagent_name,
209
252
  )
210
253
  if (backend && !normalized.backend) normalized.backend = backend
211
- if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
212
- const action = String(normalized.action || '').trim().toLowerCase()
254
+ if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
255
+ const directTask = pickDelegateTaskText(delegatePayload) || pickDelegateTaskText(nestedData)
256
+ if (directTask) normalized.task = directTask
257
+ }
258
+ const lifecycleJobId = pickNonEmptyDelegateString(
259
+ normalized.jobId,
260
+ normalized.id,
261
+ nestedData?.jobId,
262
+ nestedData?.id,
263
+ )
264
+ if (lifecycleJobId && (!normalized.jobId || typeof normalized.jobId !== 'string')) {
265
+ normalized.jobId = lifecycleJobId
266
+ }
267
+ const action = String(normalized.action ?? nestedData?.action ?? '').trim().toLowerCase()
213
268
  const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
269
+ if (action) normalized.action = action
214
270
  if (!isLifecycleAction) {
215
271
  if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
216
- const synthesized = buildDelegateTaskFromPayload(normalized)
272
+ const synthesized = buildDelegateTaskFromPayload(delegatePayload)
217
273
  if (synthesized) normalized.task = synthesized
218
274
  }
219
275
  normalized.action = 'start'
@@ -247,12 +303,48 @@ function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: C
247
303
  child.once('error', clear)
248
304
  }
249
305
 
306
+ function coerceOptionalBool(value: unknown): boolean | null {
307
+ if (typeof value === 'boolean') return value
308
+ if (typeof value === 'string') {
309
+ const normalized = value.trim().toLowerCase()
310
+ if (['true', '1', 'yes', 'on'].includes(normalized)) return true
311
+ if (['false', '0', 'no', 'off'].includes(normalized)) return false
312
+ }
313
+ return null
314
+ }
315
+
316
+ function resumeStorageKeyForBackend(
317
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini',
318
+ ): 'claudeCode' | 'codex' | 'opencode' | 'gemini' {
319
+ if (backend === 'claude') return 'claudeCode'
320
+ if (backend === 'codex') return 'codex'
321
+ if (backend === 'opencode') return 'opencode'
322
+ return 'gemini'
323
+ }
324
+
325
+ export function resolveDelegateResumeConfig(
326
+ normalized: Record<string, unknown>,
327
+ backend: 'claude' | 'codex' | 'opencode' | 'gemini',
328
+ bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null },
329
+ ): { resume: boolean; resumeId: string } {
330
+ const explicitResumeId = typeof normalized.resumeId === 'string' ? normalized.resumeId.trim() : ''
331
+ if (explicitResumeId) return { resume: true, resumeId: explicitResumeId }
332
+
333
+ const explicitResume = coerceOptionalBool(normalized.resume)
334
+ if (explicitResume !== null) return { resume: explicitResume, resumeId: '' }
335
+
336
+ const storedResumeId = bctx.readStoredDelegateResumeId?.(resumeStorageKeyForBackend(backend))
337
+ return {
338
+ resume: Boolean(storedResumeId),
339
+ resumeId: '',
340
+ }
341
+ }
342
+
250
343
  async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
251
344
  const normalized = normalizeDelegateArgs(args)
252
345
  const task = normalized.task as string
253
346
  const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
254
- const resume = normalized.resume as boolean
255
- const resumeId = normalized.resumeId as string
347
+ const { resume, resumeId } = resolveDelegateResumeConfig(normalized, backend, bctx)
256
348
  const backends = {
257
349
  claude: findBinaryOnPath('claude'),
258
350
  codex: findBinaryOnPath('codex'),
@@ -167,4 +167,146 @@ describe('discovery approval flows', () => {
167
167
  assert.equal(output.toolNames.includes('manage_schedules'), true)
168
168
  assert.equal(output.toolNames.includes('manage_platform'), false)
169
169
  })
170
+
171
+ it('session-granted builtins disabled by default still appear in the next turn tool list', () => {
172
+ const output = runWithTempDataDir(`
173
+ const storageMod = await import('./src/lib/server/storage.ts')
174
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
175
+ const storage = storageMod.default || storageMod
176
+ const toolsApi = toolsMod.default || toolsMod
177
+
178
+ const now = Date.now()
179
+ storage.saveSessions({
180
+ session_email: {
181
+ id: 'session_email',
182
+ name: 'Email Tool Test',
183
+ cwd: process.env.WORKSPACE_DIR,
184
+ user: 'tester',
185
+ provider: 'openai',
186
+ model: 'gpt-test',
187
+ claudeSessionId: null,
188
+ messages: [],
189
+ createdAt: now,
190
+ lastActiveAt: now,
191
+ sessionType: 'human',
192
+ agentId: 'default',
193
+ plugins: ['email'],
194
+ },
195
+ })
196
+
197
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
198
+ sessionId: 'session_email',
199
+ agentId: 'default',
200
+ platformAssignScope: 'self',
201
+ })
202
+ console.log(JSON.stringify({
203
+ toolNames: built.tools.map((entry) => entry.name).sort(),
204
+ }))
205
+ `)
206
+
207
+ assert.equal(output.toolNames.includes('email'), true)
208
+ })
209
+
210
+ it('discover reports session-granted builtin tools as available now', () => {
211
+ const output = runWithTempDataDir(`
212
+ const storageMod = await import('./src/lib/server/storage.ts')
213
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
214
+ const storage = storageMod.default || storageMod
215
+ const toolsApi = toolsMod.default || toolsMod
216
+
217
+ const now = Date.now()
218
+ storage.saveSessions({
219
+ session_discover_email: {
220
+ id: 'session_discover_email',
221
+ name: 'Discovery Email Test',
222
+ cwd: process.env.WORKSPACE_DIR,
223
+ user: 'tester',
224
+ provider: 'openai',
225
+ model: 'gpt-test',
226
+ claudeSessionId: null,
227
+ messages: [],
228
+ createdAt: now,
229
+ lastActiveAt: now,
230
+ sessionType: 'human',
231
+ agentId: 'default',
232
+ plugins: ['email'],
233
+ },
234
+ })
235
+
236
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['email'], {
237
+ sessionId: 'session_discover_email',
238
+ agentId: 'default',
239
+ platformAssignScope: 'self',
240
+ })
241
+ const tool = built.tools.find((entry) => entry.name === 'manage_capabilities')
242
+ const raw = await tool.invoke({ action: 'discover', reason: 'Check runtime tool availability.' })
243
+ const plugins = JSON.parse(raw)
244
+ const email = plugins.find((entry) => entry.id === 'email')
245
+ console.log(JSON.stringify({
246
+ email,
247
+ }))
248
+ `)
249
+
250
+ assert.equal(output.email.granted, true)
251
+ assert.equal(output.email.availableNow, true)
252
+ })
253
+
254
+ it('hydrates agent-approved tools into stale connector sessions on the next turn', () => {
255
+ const output = runWithTempDataDir(`
256
+ const storageMod = await import('./src/lib/server/storage.ts')
257
+ const toolsMod = await import('./src/lib/server/session-tools/index.ts')
258
+ const approvalsMod = await import('./src/lib/server/approvals.ts')
259
+ const storage = storageMod.default || storageMod
260
+ const toolsApi = toolsMod.default || toolsMod
261
+ const approvals = approvalsMod.default || approvalsMod
262
+
263
+ const now = Date.now()
264
+ storage.saveSettings({ approvalsEnabled: true })
265
+ storage.saveSessions({
266
+ connector_session: {
267
+ id: 'connector_session',
268
+ name: 'Connector Session',
269
+ cwd: process.env.WORKSPACE_DIR,
270
+ user: 'connector',
271
+ provider: 'openai',
272
+ model: 'gpt-test',
273
+ claudeSessionId: null,
274
+ messages: [],
275
+ createdAt: now,
276
+ lastActiveAt: now,
277
+ sessionType: 'human',
278
+ agentId: 'agent_1',
279
+ plugins: ['browser'],
280
+ },
281
+ })
282
+
283
+ const approval = approvals.requestApproval({
284
+ category: 'tool_access',
285
+ title: 'Enable connector tool',
286
+ description: 'Grant connector messaging',
287
+ data: { toolId: 'connector_message_tool', pluginId: 'connector_message_tool' },
288
+ agentId: 'agent_1',
289
+ sessionId: null,
290
+ })
291
+ await approvals.submitDecision(approval.id, true)
292
+
293
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['browser'], {
294
+ sessionId: 'connector_session',
295
+ agentId: 'agent_1',
296
+ platformAssignScope: 'self',
297
+ })
298
+ try {
299
+ const session = storage.loadSessions().connector_session
300
+ console.log(JSON.stringify({
301
+ toolNames: built.tools.map((entry) => entry.name).sort(),
302
+ plugins: session.plugins || [],
303
+ }))
304
+ } finally {
305
+ await built.cleanup()
306
+ }
307
+ `)
308
+
309
+ assert.equal(output.toolNames.includes('connector_message_tool'), true)
310
+ assert.equal(output.plugins.includes('connector_message_tool'), true)
311
+ })
170
312
  })
@@ -1,12 +1,32 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { ToolBuildContext } from './context'
4
- import { getPluginManager } from '../plugins'
4
+ import { getPluginManager, normalizeMarketplacePluginUrl } from '../plugins'
5
5
  import type { Plugin, PluginHooks, ClawHubSkill } from '@/types'
6
6
  import { searchClawHub } from '../clawhub-client'
7
7
  import { normalizeToolInputArgs } from './normalize-tool-args'
8
8
  import { pluginIdMatches } from '../tool-aliases'
9
9
  import { loadSessions } from '../storage'
10
+ import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
11
+
12
+ function trimString(value: unknown): string {
13
+ return typeof value === 'string' ? value.trim() : ''
14
+ }
15
+
16
+ function buildDiscoveryApprovalResumeInput(approval: import('@/types').ApprovalRequest): Record<string, unknown> | null {
17
+ if (approval.category !== 'plugin_install') return null
18
+ const url = trimString(approval.data.url)
19
+ if (!url) return null
20
+ const pluginId = trimString(approval.data.pluginId)
21
+ const reason = trimString(approval.data.reason)
22
+ return {
23
+ action: 'install_request',
24
+ url,
25
+ pluginId: pluginId || undefined,
26
+ reason: reason || `Approved install request for ${url}`,
27
+ approved: true,
28
+ }
29
+ }
10
30
 
11
31
  /**
12
32
  * Unified Discovery Logic
@@ -41,11 +61,15 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
41
61
  case 'list':
42
62
  case 'discover': {
43
63
  const plugins = manager.listPlugins()
64
+ const currentSession = bctx?.ctx?.sessionId ? loadSessions()[bctx.ctx.sessionId] : null
65
+ const sessionPlugins = currentSession?.plugins || currentSession?.tools || []
44
66
  return JSON.stringify(plugins.map(p => ({
45
67
  id: p.filename,
46
68
  name: p.name,
47
69
  description: p.description,
48
70
  enabled: p.enabled,
71
+ granted: pluginIdMatches(sessionPlugins, p.filename),
72
+ availableNow: pluginIdMatches(sessionPlugins, p.filename) && !manager.isExplicitlyDisabled(p.filename),
49
73
  isBuiltin: !p.filename.endsWith('.js') && !p.filename.endsWith('.mjs')
50
74
  })), null, 2)
51
75
  }
@@ -62,6 +86,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
62
86
  description: s.description,
63
87
  author: s.author,
64
88
  source: 'clawhub',
89
+ catalogSource: 'clawhub',
65
90
  url: (s as ClawHubSkill & { rawUrl?: string }).rawUrl ?? s.url
66
91
  })))
67
92
  }
@@ -71,14 +96,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
71
96
 
72
97
  try {
73
98
  console.log('[discovery] Searching SwarmClaw registry...')
74
- const scRes = await fetch('https://swarmclaw.ai/registry/plugins.json', { signal: AbortSignal.timeout(5000) })
75
- if (scRes.ok) {
99
+ const registryResults = new Map<string, Record<string, unknown>>()
100
+ const registries = [
101
+ { url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
102
+ { url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
103
+ ] as const
104
+ for (const registry of registries) {
105
+ const scRes = await fetch(registry.url, { signal: AbortSignal.timeout(5000) })
106
+ if (!scRes.ok) continue
76
107
  const scPlugins = await scRes.json()
77
108
  const filtered = (scPlugins as Record<string, unknown>[]).filter((p: Record<string, unknown>) =>
78
109
  !q || (String(p.name || '')).toLowerCase().includes(q.toLowerCase()) || (String(p.description || '')).toLowerCase().includes(q.toLowerCase())
79
110
  )
80
- results.push(...filtered.map(p => ({ ...p, source: 'swarmclaw' })))
111
+ for (const p of filtered) {
112
+ const id = String(p.id || p.name || '').trim().toLowerCase().replace(/[^a-z0-9]/g, '_')
113
+ if (!id || registryResults.has(id)) continue
114
+ const url = normalizeMarketplacePluginUrl(String(p.url || ''))
115
+ registryResults.set(id, {
116
+ ...p,
117
+ id,
118
+ url,
119
+ source: inferPluginPublisherSourceFromUrl(url) || 'swarmforge',
120
+ catalogSource: registry.catalogSource,
121
+ })
122
+ }
81
123
  }
124
+ results.push(...registryResults.values())
82
125
  } catch (err: unknown) {
83
126
  console.error('[discovery] SC Registry search failed:', err instanceof Error ? err.message : String(err))
84
127
  }
@@ -99,12 +142,12 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
99
142
  const currentSession = allSessions[bctx.ctx.sessionId]
100
143
  const grantedTools = currentSession?.plugins || currentSession?.tools || []
101
144
  if (currentSession && pluginIdMatches(grantedTools, pluginId)) {
102
- return JSON.stringify({
103
- alreadyGranted: true,
104
- pluginId,
105
- message: `You already have access to "${pluginId}" proceed to use it directly.`,
106
- })
107
- }
145
+ return JSON.stringify({
146
+ alreadyGranted: true,
147
+ pluginId,
148
+ message: `You already have access to "${pluginId}". If it was just granted, it will be available on the next agent turn.`,
149
+ })
150
+ }
108
151
  }
109
152
  const { requestApprovalMaybeAutoApprove } = await import('../approvals')
110
153
  const approval = await requestApprovalMaybeAutoApprove({
@@ -121,7 +164,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
121
164
  pluginId,
122
165
  toolId: pluginId,
123
166
  autoApproved: true,
124
- message: `Access to "${pluginId}" was auto-approved and granted. Proceed to use it directly.`,
167
+ message: `Access to "${pluginId}" was auto-approved and granted. It will be available on the next agent turn.`,
125
168
  })
126
169
  }
127
170
  return JSON.stringify({
@@ -182,7 +225,32 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
182
225
  const DiscoveryPlugin: Plugin = {
183
226
  name: 'Core Discovery',
184
227
  description: 'Discover available plugins, search marketplaces, request access, or suggest new installs.',
185
- hooks: {} as PluginHooks,
228
+ hooks: {
229
+ getApprovalGuidance: ({ approval, phase, approved }) => {
230
+ if (approval.category !== 'plugin_install') return null
231
+ if (phase === 'request') {
232
+ return [
233
+ 'When this approval is granted, continue with `manage_capabilities` for the exact approved install request instead of asking again in prose.',
234
+ 'Do not change the approved plugin URL or pluginId unless newer tool evidence proves the approved source is invalid.',
235
+ ]
236
+ }
237
+ if (phase === 'connector_reminder') {
238
+ return 'Approving this lets the agent resume the approved plugin install request without repeating marketplace research.'
239
+ }
240
+ if (approved !== true) {
241
+ return 'Do not retry the rejected install request unless the plugin source or requested capability materially changes.'
242
+ }
243
+ const resumeInput = buildDiscoveryApprovalResumeInput(approval)
244
+ const lines = [
245
+ 'Resume immediately with `manage_capabilities` for the approved install request.',
246
+ 'Do not repeat the same marketplace search or install request once approval has been granted.',
247
+ ]
248
+ if (resumeInput) {
249
+ lines.push(`Exact tool input: ${JSON.stringify(resumeInput)}`)
250
+ }
251
+ return lines
252
+ },
253
+ } as PluginHooks,
186
254
  tools: [
187
255
  {
188
256
  name: 'manage_capabilities',
@@ -83,6 +83,24 @@ describe('normalizeFileArgs', () => {
83
83
  assert.deepEqual(out.files, [{ name: 'report.md', content: '# report' }])
84
84
  })
85
85
 
86
+ it('parses stringified bulk file arrays from wrapped payloads', () => {
87
+ const out = normalizeFileArgs({
88
+ input: JSON.stringify({
89
+ action: 'write',
90
+ files: JSON.stringify([
91
+ { path: 'offer-pack/offer-brief.md', content: '# brief' },
92
+ { path: 'offer-pack/landing-copy.md', content: '# landing' },
93
+ ]),
94
+ }),
95
+ })
96
+
97
+ assert.equal(out.action, 'write')
98
+ assert.deepEqual(out.files, [
99
+ { path: 'offer-pack/offer-brief.md', content: '# brief' },
100
+ { path: 'offer-pack/landing-copy.md', content: '# landing' },
101
+ ])
102
+ })
103
+
86
104
  it('treats trailing-slash write targets as directory creation', async () => {
87
105
  const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-write-dir-'))
88
106
  const out = await executeFileAction({
@@ -95,4 +113,22 @@ describe('normalizeFileArgs', () => {
95
113
  assert.equal(fs.statSync(path.join(cwd, 'weather_update')).isDirectory(), true)
96
114
  fs.rmSync(cwd, { recursive: true, force: true })
97
115
  })
116
+
117
+ it('does not inline binary screenshot data when reading image files', async () => {
118
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'file-read-binary-'))
119
+ const imagePath = path.join(cwd, 'screenshot-main.png')
120
+ fs.writeFileSync(imagePath, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0x02, 0x03]))
121
+
122
+ try {
123
+ const out = await executeFileAction({
124
+ action: 'read',
125
+ filePath: 'screenshot-main.png',
126
+ }, { cwd })
127
+
128
+ assert.match(out, /Binary file: screenshot-main\.png/)
129
+ assert.match(out, /Use send_file/)
130
+ } finally {
131
+ fs.rmSync(cwd, { recursive: true, force: true })
132
+ }
133
+ })
98
134
  })