@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.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- 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
|
-
|
|
205
|
-
??
|
|
206
|
-
??
|
|
207
|
-
??
|
|
208
|
-
??
|
|
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'
|
|
212
|
-
|
|
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(
|
|
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
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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: {
|
|
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
|
})
|