@swarmclawai/swarmclaw 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
|
+
import crypto from 'crypto'
|
|
4
|
+
import { spawn, spawnSync } from 'child_process'
|
|
5
|
+
import { loadAgents, loadTasks, upsertTask } from '../storage'
|
|
6
|
+
import { log } from '../logger'
|
|
7
|
+
import type { ToolBuildContext } from './context'
|
|
8
|
+
import { truncate, tail, extractResumeIdentifier, findBinaryOnPath, MAX_OUTPUT } from './context'
|
|
9
|
+
|
|
10
|
+
export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
|
|
11
|
+
const tools: StructuredToolInterface[] = []
|
|
12
|
+
const { cwd, ctx, claudeTimeoutMs, cliProcessTimeoutMs, persistDelegateResumeId, readStoredDelegateResumeId } = bctx
|
|
13
|
+
|
|
14
|
+
const wantsClaudeDelegate = bctx.hasTool('claude_code')
|
|
15
|
+
const wantsCodexDelegate = bctx.hasTool('codex_cli')
|
|
16
|
+
const wantsOpenCodeDelegate = bctx.hasTool('opencode_cli')
|
|
17
|
+
|
|
18
|
+
if (wantsClaudeDelegate || wantsCodexDelegate || wantsOpenCodeDelegate) {
|
|
19
|
+
const claudeBinary = findBinaryOnPath('claude')
|
|
20
|
+
const codexBinary = findBinaryOnPath('codex')
|
|
21
|
+
const opencodeBinary = findBinaryOnPath('opencode')
|
|
22
|
+
|
|
23
|
+
if (wantsClaudeDelegate && !claudeBinary) {
|
|
24
|
+
log.warn('session-tools', 'Claude delegation enabled but claude binary not found', {
|
|
25
|
+
sessionId: ctx?.sessionId || null,
|
|
26
|
+
agentId: ctx?.agentId || null,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
if (wantsCodexDelegate && !codexBinary) {
|
|
30
|
+
log.warn('session-tools', 'Codex delegation enabled but codex binary not found', {
|
|
31
|
+
sessionId: ctx?.sessionId || null,
|
|
32
|
+
agentId: ctx?.agentId || null,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
if (wantsOpenCodeDelegate && !opencodeBinary) {
|
|
36
|
+
log.warn('session-tools', 'OpenCode delegation enabled but opencode binary not found', {
|
|
37
|
+
sessionId: ctx?.sessionId || null,
|
|
38
|
+
agentId: ctx?.agentId || null,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (claudeBinary && wantsClaudeDelegate) {
|
|
43
|
+
tools.push(
|
|
44
|
+
tool(
|
|
45
|
+
async ({ task, resume, resumeId }) => {
|
|
46
|
+
try {
|
|
47
|
+
const env: NodeJS.ProcessEnv = { ...process.env }
|
|
48
|
+
// Running inside Claude environments can block nested `claude` launches.
|
|
49
|
+
// Strip all CLAUDE* vars so delegation can run as an independent subprocess.
|
|
50
|
+
const removedClaudeEnvKeys: string[] = []
|
|
51
|
+
for (const key of Object.keys(env)) {
|
|
52
|
+
if (key.toUpperCase().startsWith('CLAUDE')) {
|
|
53
|
+
removedClaudeEnvKeys.push(key)
|
|
54
|
+
delete env[key]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fast preflight: when Claude isn't authenticated, surface a clear error immediately.
|
|
59
|
+
const authProbe = spawnSync(claudeBinary, ['auth', 'status'], {
|
|
60
|
+
cwd,
|
|
61
|
+
env,
|
|
62
|
+
encoding: 'utf-8',
|
|
63
|
+
timeout: 8000,
|
|
64
|
+
})
|
|
65
|
+
if ((authProbe.status ?? 1) !== 0) {
|
|
66
|
+
let loggedIn = false
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(authProbe.stdout || '{}') as { loggedIn?: boolean }
|
|
69
|
+
loggedIn = parsed.loggedIn === true
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore parse issues and fall back to a generic auth guidance
|
|
72
|
+
}
|
|
73
|
+
if (!loggedIn) {
|
|
74
|
+
return 'Error: Claude Code CLI is not authenticated. Run `claude auth login` (or `claude setup-token`) on this machine, then retry.'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const storedResumeId = readStoredDelegateResumeId('claudeCode')
|
|
79
|
+
const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
|
|
80
|
+
? resumeId.trim()
|
|
81
|
+
: (resume ? storedResumeId : null)
|
|
82
|
+
|
|
83
|
+
log.info('session-tools', 'delegate_to_claude_code start', {
|
|
84
|
+
sessionId: ctx?.sessionId || null,
|
|
85
|
+
agentId: ctx?.agentId || null,
|
|
86
|
+
cwd,
|
|
87
|
+
timeoutMs: claudeTimeoutMs,
|
|
88
|
+
removedClaudeEnvKeys,
|
|
89
|
+
resumeRequested: !!resume || !!resumeId,
|
|
90
|
+
resumeId: resumeIdToUse || null,
|
|
91
|
+
taskPreview: (task || '').slice(0, 200),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return new Promise<string>((resolve) => {
|
|
95
|
+
const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
|
|
96
|
+
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
97
|
+
const child = spawn(claudeBinary, args, {
|
|
98
|
+
cwd,
|
|
99
|
+
env,
|
|
100
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
101
|
+
})
|
|
102
|
+
let stdout = ''
|
|
103
|
+
let stderr = ''
|
|
104
|
+
let stdoutBuf = ''
|
|
105
|
+
let assistantText = ''
|
|
106
|
+
let discoveredSessionId: string | null = null
|
|
107
|
+
let settled = false
|
|
108
|
+
let timedOut = false
|
|
109
|
+
const startedAt = Date.now()
|
|
110
|
+
|
|
111
|
+
const finish = (result: string) => {
|
|
112
|
+
if (settled) return
|
|
113
|
+
settled = true
|
|
114
|
+
resolve(truncate(result, MAX_OUTPUT))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const timeoutHandle = setTimeout(() => {
|
|
118
|
+
timedOut = true
|
|
119
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
try { child.kill('SIGKILL') } catch { /* ignore */ }
|
|
122
|
+
}, 5000)
|
|
123
|
+
}, claudeTimeoutMs)
|
|
124
|
+
|
|
125
|
+
log.info('session-tools', 'delegate_to_claude_code spawned', {
|
|
126
|
+
sessionId: ctx?.sessionId || null,
|
|
127
|
+
pid: child.pid || null,
|
|
128
|
+
args,
|
|
129
|
+
})
|
|
130
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
131
|
+
const text = chunk.toString()
|
|
132
|
+
stdout += text
|
|
133
|
+
if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
|
|
134
|
+
stdoutBuf += text
|
|
135
|
+
const lines = stdoutBuf.split('\n')
|
|
136
|
+
stdoutBuf = lines.pop() || ''
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (!line.trim()) continue
|
|
139
|
+
try {
|
|
140
|
+
const ev = JSON.parse(line)
|
|
141
|
+
if (typeof ev?.session_id === 'string' && ev.session_id.trim()) {
|
|
142
|
+
discoveredSessionId = ev.session_id.trim()
|
|
143
|
+
}
|
|
144
|
+
if (ev?.type === 'result' && typeof ev?.result === 'string') {
|
|
145
|
+
assistantText = ev.result
|
|
146
|
+
} else if (ev?.type === 'assistant' && Array.isArray(ev?.message?.content)) {
|
|
147
|
+
const textBlocks = ev.message.content
|
|
148
|
+
.filter((block: any) => block?.type === 'text' && typeof block?.text === 'string')
|
|
149
|
+
.map((block: any) => block.text)
|
|
150
|
+
.join('')
|
|
151
|
+
if (textBlocks) assistantText = textBlocks
|
|
152
|
+
} else if (ev?.type === 'content_block_delta' && typeof ev?.delta?.text === 'string') {
|
|
153
|
+
assistantText += ev.delta.text
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// keep raw stdout fallback when parsing fails
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
161
|
+
stderr += chunk.toString()
|
|
162
|
+
if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
|
|
163
|
+
})
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
clearTimeout(timeoutHandle)
|
|
166
|
+
log.error('session-tools', 'delegate_to_claude_code child error', {
|
|
167
|
+
sessionId: ctx?.sessionId || null,
|
|
168
|
+
error: err?.message || String(err),
|
|
169
|
+
})
|
|
170
|
+
finish(`Error: failed to start Claude Code CLI: ${err?.message || String(err)}`)
|
|
171
|
+
})
|
|
172
|
+
child.on('close', (code, signal) => {
|
|
173
|
+
clearTimeout(timeoutHandle)
|
|
174
|
+
const durationMs = Date.now() - startedAt
|
|
175
|
+
if (!discoveredSessionId) {
|
|
176
|
+
const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
|
|
177
|
+
if (guessed) discoveredSessionId = guessed
|
|
178
|
+
}
|
|
179
|
+
if (discoveredSessionId) persistDelegateResumeId('claudeCode', discoveredSessionId)
|
|
180
|
+
log.info('session-tools', 'delegate_to_claude_code child close', {
|
|
181
|
+
sessionId: ctx?.sessionId || null,
|
|
182
|
+
code,
|
|
183
|
+
signal: signal || null,
|
|
184
|
+
timedOut,
|
|
185
|
+
durationMs,
|
|
186
|
+
stdoutLen: stdout.length,
|
|
187
|
+
stderrLen: stderr.length,
|
|
188
|
+
discoveredSessionId,
|
|
189
|
+
stderrPreview: tail(stderr, 240),
|
|
190
|
+
})
|
|
191
|
+
if (timedOut) {
|
|
192
|
+
const msg = [
|
|
193
|
+
`Error: Claude Code CLI timed out after ${Math.round(claudeTimeoutMs / 1000)}s.`,
|
|
194
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
195
|
+
stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
|
|
196
|
+
'Try increasing "Claude Code Timeout (sec)" in Settings.',
|
|
197
|
+
].filter(Boolean).join('\n\n')
|
|
198
|
+
finish(msg)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const successText = assistantText.trim() || stdout.trim() || stderr.trim()
|
|
203
|
+
if (code === 0 && successText) {
|
|
204
|
+
const out = discoveredSessionId
|
|
205
|
+
? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
|
|
206
|
+
: successText
|
|
207
|
+
finish(out)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const msg = [
|
|
212
|
+
`Error: Claude Code CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
|
|
213
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
214
|
+
stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
|
|
215
|
+
].filter(Boolean).join('\n\n')
|
|
216
|
+
finish(msg || 'Error: Claude Code CLI returned no output.')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
child.stdin?.write(task)
|
|
221
|
+
child.stdin?.end()
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
clearTimeout(timeoutHandle)
|
|
224
|
+
finish(`Error: failed to send task to Claude Code CLI: ${err?.message || String(err)}`)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
} catch (err: any) {
|
|
228
|
+
return `Error delegating to Claude Code: ${err.message}`
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'delegate_to_claude_code',
|
|
233
|
+
description: 'Delegate a complex task to Claude Code CLI. Use for tasks that need deep code understanding, multi-file refactoring, or running tests. The task runs in the session working directory.',
|
|
234
|
+
schema: z.object({
|
|
235
|
+
task: z.string().describe('Detailed description of the task for Claude Code'),
|
|
236
|
+
resume: z.boolean().optional().describe('If true, try to resume the last saved Claude delegation session for this SwarmClaw session'),
|
|
237
|
+
resumeId: z.string().optional().describe('Explicit Claude session id to resume (overrides resume=true memory)'),
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (codexBinary && wantsCodexDelegate) {
|
|
245
|
+
tools.push(
|
|
246
|
+
tool(
|
|
247
|
+
async ({ task, resume, resumeId }) => {
|
|
248
|
+
try {
|
|
249
|
+
const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
|
|
250
|
+
const removedCodexEnvKeys: string[] = []
|
|
251
|
+
for (const key of Object.keys(env)) {
|
|
252
|
+
if (key.toUpperCase().startsWith('CODEX')) {
|
|
253
|
+
removedCodexEnvKeys.push(key)
|
|
254
|
+
delete env[key]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hasApiKey = typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim().length > 0
|
|
259
|
+
if (!hasApiKey) {
|
|
260
|
+
const loginProbe = spawnSync(codexBinary, ['login', 'status'], {
|
|
261
|
+
cwd,
|
|
262
|
+
env,
|
|
263
|
+
encoding: 'utf-8',
|
|
264
|
+
timeout: 8000,
|
|
265
|
+
})
|
|
266
|
+
const probeText = `${loginProbe.stdout || ''}\n${loginProbe.stderr || ''}`.toLowerCase()
|
|
267
|
+
const loggedIn = probeText.includes('logged in')
|
|
268
|
+
if ((loginProbe.status ?? 1) !== 0 || !loggedIn) {
|
|
269
|
+
return 'Error: Codex CLI is not authenticated. Run `codex login` (or set OPENAI_API_KEY), then retry.'
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const storedResumeId = readStoredDelegateResumeId('codex')
|
|
274
|
+
const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
|
|
275
|
+
? resumeId.trim()
|
|
276
|
+
: (resume ? storedResumeId : null)
|
|
277
|
+
|
|
278
|
+
log.info('session-tools', 'delegate_to_codex_cli start', {
|
|
279
|
+
sessionId: ctx?.sessionId || null,
|
|
280
|
+
agentId: ctx?.agentId || null,
|
|
281
|
+
cwd,
|
|
282
|
+
timeoutMs: cliProcessTimeoutMs,
|
|
283
|
+
removedCodexEnvKeys,
|
|
284
|
+
resumeRequested: !!resume || !!resumeId,
|
|
285
|
+
resumeId: resumeIdToUse || null,
|
|
286
|
+
taskPreview: (task || '').slice(0, 200),
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
return new Promise<string>((resolve) => {
|
|
290
|
+
const args = ['exec']
|
|
291
|
+
if (resumeIdToUse) args.push('resume', resumeIdToUse)
|
|
292
|
+
args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
|
|
293
|
+
const child = spawn(codexBinary, args, {
|
|
294
|
+
cwd,
|
|
295
|
+
env,
|
|
296
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
+
})
|
|
298
|
+
let stdout = ''
|
|
299
|
+
let stderr = ''
|
|
300
|
+
let settled = false
|
|
301
|
+
let timedOut = false
|
|
302
|
+
const startedAt = Date.now()
|
|
303
|
+
let agentText = ''
|
|
304
|
+
let discoveredThreadId: string | null = null
|
|
305
|
+
const eventErrors: string[] = []
|
|
306
|
+
let stdoutBuf = ''
|
|
307
|
+
|
|
308
|
+
const finish = (result: string) => {
|
|
309
|
+
if (settled) return
|
|
310
|
+
settled = true
|
|
311
|
+
resolve(truncate(result, MAX_OUTPUT))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const timeoutHandle = setTimeout(() => {
|
|
315
|
+
timedOut = true
|
|
316
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
try { child.kill('SIGKILL') } catch { /* ignore */ }
|
|
319
|
+
}, 5000)
|
|
320
|
+
}, cliProcessTimeoutMs)
|
|
321
|
+
|
|
322
|
+
log.info('session-tools', 'delegate_to_codex_cli spawned', {
|
|
323
|
+
sessionId: ctx?.sessionId || null,
|
|
324
|
+
pid: child.pid || null,
|
|
325
|
+
args,
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
329
|
+
const text = chunk.toString()
|
|
330
|
+
stdout += text
|
|
331
|
+
if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
|
|
332
|
+
|
|
333
|
+
stdoutBuf += text
|
|
334
|
+
const lines = stdoutBuf.split('\n')
|
|
335
|
+
stdoutBuf = lines.pop() || ''
|
|
336
|
+
for (const line of lines) {
|
|
337
|
+
if (!line.trim()) continue
|
|
338
|
+
try {
|
|
339
|
+
const ev = JSON.parse(line)
|
|
340
|
+
if (typeof ev?.thread_id === 'string' && ev.thread_id.trim()) {
|
|
341
|
+
discoveredThreadId = ev.thread_id.trim()
|
|
342
|
+
}
|
|
343
|
+
if (ev.type === 'item.completed' && ev.item?.type === 'agent_message' && typeof ev.item?.text === 'string') {
|
|
344
|
+
agentText = ev.item.text
|
|
345
|
+
} else if (ev.type === 'item.completed' && ev.item?.type === 'message' && ev.item?.role === 'assistant') {
|
|
346
|
+
const content = ev.item.content
|
|
347
|
+
if (Array.isArray(content)) {
|
|
348
|
+
const txt = content
|
|
349
|
+
.filter((c: any) => c?.type === 'output_text' && typeof c?.text === 'string')
|
|
350
|
+
.map((c: any) => c.text)
|
|
351
|
+
.join('')
|
|
352
|
+
if (txt) agentText = txt
|
|
353
|
+
} else if (typeof content === 'string') {
|
|
354
|
+
agentText = content
|
|
355
|
+
}
|
|
356
|
+
} else if (ev.type === 'error' && ev.message) {
|
|
357
|
+
eventErrors.push(String(ev.message))
|
|
358
|
+
} else if (ev.type === 'turn.failed' && ev.error?.message) {
|
|
359
|
+
eventErrors.push(String(ev.error.message))
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// Ignore non-JSON lines in parser path; raw stdout still captured above.
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
367
|
+
stderr += chunk.toString()
|
|
368
|
+
if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
|
|
369
|
+
})
|
|
370
|
+
child.on('error', (err) => {
|
|
371
|
+
clearTimeout(timeoutHandle)
|
|
372
|
+
log.error('session-tools', 'delegate_to_codex_cli child error', {
|
|
373
|
+
sessionId: ctx?.sessionId || null,
|
|
374
|
+
error: err?.message || String(err),
|
|
375
|
+
})
|
|
376
|
+
finish(`Error: failed to start Codex CLI: ${err?.message || String(err)}`)
|
|
377
|
+
})
|
|
378
|
+
child.on('close', (code, signal) => {
|
|
379
|
+
clearTimeout(timeoutHandle)
|
|
380
|
+
const durationMs = Date.now() - startedAt
|
|
381
|
+
if (!discoveredThreadId) {
|
|
382
|
+
const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
|
|
383
|
+
if (guessed) discoveredThreadId = guessed
|
|
384
|
+
}
|
|
385
|
+
if (discoveredThreadId) persistDelegateResumeId('codex', discoveredThreadId)
|
|
386
|
+
log.info('session-tools', 'delegate_to_codex_cli child close', {
|
|
387
|
+
sessionId: ctx?.sessionId || null,
|
|
388
|
+
code,
|
|
389
|
+
signal: signal || null,
|
|
390
|
+
timedOut,
|
|
391
|
+
durationMs,
|
|
392
|
+
stdoutLen: stdout.length,
|
|
393
|
+
stderrLen: stderr.length,
|
|
394
|
+
eventErrorCount: eventErrors.length,
|
|
395
|
+
discoveredThreadId,
|
|
396
|
+
stderrPreview: tail(stderr, 240),
|
|
397
|
+
})
|
|
398
|
+
if (timedOut) {
|
|
399
|
+
const msg = [
|
|
400
|
+
`Error: Codex CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
|
|
401
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
402
|
+
eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
|
|
403
|
+
'Try increasing "CLI Process Timeout (sec)" in Settings.',
|
|
404
|
+
].filter(Boolean).join('\n\n')
|
|
405
|
+
finish(msg)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
if (code === 0 && agentText.trim()) {
|
|
409
|
+
const out = discoveredThreadId
|
|
410
|
+
? `${agentText.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
|
|
411
|
+
: agentText.trim()
|
|
412
|
+
finish(out)
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
if (code === 0 && stdout.trim() && !eventErrors.length) {
|
|
416
|
+
const out = discoveredThreadId
|
|
417
|
+
? `${stdout.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
|
|
418
|
+
: stdout.trim()
|
|
419
|
+
finish(out)
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
const msg = [
|
|
423
|
+
`Error: Codex CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
|
|
424
|
+
eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
|
|
425
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
426
|
+
stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
|
|
427
|
+
].filter(Boolean).join('\n\n')
|
|
428
|
+
finish(msg || 'Error: Codex CLI returned no output.')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
child.stdin?.write(task)
|
|
433
|
+
child.stdin?.end()
|
|
434
|
+
} catch (err: any) {
|
|
435
|
+
clearTimeout(timeoutHandle)
|
|
436
|
+
finish(`Error: failed to send task to Codex CLI: ${err?.message || String(err)}`)
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
} catch (err: any) {
|
|
440
|
+
return `Error delegating to Codex CLI: ${err.message}`
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'delegate_to_codex_cli',
|
|
445
|
+
description: 'Delegate a complex task to Codex CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
|
|
446
|
+
schema: z.object({
|
|
447
|
+
task: z.string().describe('Detailed description of the task for Codex CLI'),
|
|
448
|
+
resume: z.boolean().optional().describe('If true, try to resume the last saved Codex delegation thread for this SwarmClaw session'),
|
|
449
|
+
resumeId: z.string().optional().describe('Explicit Codex thread id to resume (overrides resume=true memory)'),
|
|
450
|
+
}),
|
|
451
|
+
},
|
|
452
|
+
),
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (opencodeBinary && wantsOpenCodeDelegate) {
|
|
457
|
+
tools.push(
|
|
458
|
+
tool(
|
|
459
|
+
async ({ task, resume, resumeId }) => {
|
|
460
|
+
try {
|
|
461
|
+
const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
|
|
462
|
+
const storedResumeId = readStoredDelegateResumeId('opencode')
|
|
463
|
+
const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
|
|
464
|
+
? resumeId.trim()
|
|
465
|
+
: (resume ? storedResumeId : null)
|
|
466
|
+
|
|
467
|
+
log.info('session-tools', 'delegate_to_opencode_cli start', {
|
|
468
|
+
sessionId: ctx?.sessionId || null,
|
|
469
|
+
agentId: ctx?.agentId || null,
|
|
470
|
+
cwd,
|
|
471
|
+
timeoutMs: cliProcessTimeoutMs,
|
|
472
|
+
resumeRequested: !!resume || !!resumeId,
|
|
473
|
+
resumeId: resumeIdToUse || null,
|
|
474
|
+
taskPreview: (task || '').slice(0, 200),
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
return new Promise<string>((resolve) => {
|
|
478
|
+
const args = ['run', task, '--format', 'json']
|
|
479
|
+
if (resumeIdToUse) args.push('--session', resumeIdToUse)
|
|
480
|
+
const child = spawn(opencodeBinary, args, {
|
|
481
|
+
cwd,
|
|
482
|
+
env,
|
|
483
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
484
|
+
})
|
|
485
|
+
let stdout = ''
|
|
486
|
+
let stderr = ''
|
|
487
|
+
let discoveredSessionId: string | null = null
|
|
488
|
+
let parsedText = ''
|
|
489
|
+
const eventErrors: string[] = []
|
|
490
|
+
let stdoutBuf = ''
|
|
491
|
+
let settled = false
|
|
492
|
+
let timedOut = false
|
|
493
|
+
const startedAt = Date.now()
|
|
494
|
+
|
|
495
|
+
const finish = (result: string) => {
|
|
496
|
+
if (settled) return
|
|
497
|
+
settled = true
|
|
498
|
+
resolve(truncate(result, MAX_OUTPUT))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const timeoutHandle = setTimeout(() => {
|
|
502
|
+
timedOut = true
|
|
503
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
504
|
+
setTimeout(() => {
|
|
505
|
+
try { child.kill('SIGKILL') } catch { /* ignore */ }
|
|
506
|
+
}, 5000)
|
|
507
|
+
}, cliProcessTimeoutMs)
|
|
508
|
+
|
|
509
|
+
log.info('session-tools', 'delegate_to_opencode_cli spawned', {
|
|
510
|
+
sessionId: ctx?.sessionId || null,
|
|
511
|
+
pid: child.pid || null,
|
|
512
|
+
args: resumeIdToUse
|
|
513
|
+
? ['run', '(task hidden)', '--format', 'json', '--session', resumeIdToUse]
|
|
514
|
+
: ['run', '(task hidden)', '--format', 'json'],
|
|
515
|
+
})
|
|
516
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
517
|
+
const text = chunk.toString()
|
|
518
|
+
stdout += text
|
|
519
|
+
if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
|
|
520
|
+
stdoutBuf += text
|
|
521
|
+
const lines = stdoutBuf.split('\n')
|
|
522
|
+
stdoutBuf = lines.pop() || ''
|
|
523
|
+
for (const line of lines) {
|
|
524
|
+
if (!line.trim()) continue
|
|
525
|
+
try {
|
|
526
|
+
const ev = JSON.parse(line)
|
|
527
|
+
if (typeof ev?.sessionID === 'string' && ev.sessionID.trim()) {
|
|
528
|
+
discoveredSessionId = ev.sessionID.trim()
|
|
529
|
+
}
|
|
530
|
+
if (ev?.type === 'text' && typeof ev?.part?.text === 'string') {
|
|
531
|
+
parsedText += ev.part.text
|
|
532
|
+
} else if (ev?.type === 'error') {
|
|
533
|
+
const msg = typeof ev?.error === 'string'
|
|
534
|
+
? ev.error
|
|
535
|
+
: typeof ev?.message === 'string'
|
|
536
|
+
? ev.message
|
|
537
|
+
: 'Unknown OpenCode event error'
|
|
538
|
+
eventErrors.push(msg)
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
// keep raw stdout fallback
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
546
|
+
stderr += chunk.toString()
|
|
547
|
+
if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
|
|
548
|
+
})
|
|
549
|
+
child.on('error', (err) => {
|
|
550
|
+
clearTimeout(timeoutHandle)
|
|
551
|
+
log.error('session-tools', 'delegate_to_opencode_cli child error', {
|
|
552
|
+
sessionId: ctx?.sessionId || null,
|
|
553
|
+
error: err?.message || String(err),
|
|
554
|
+
})
|
|
555
|
+
finish(`Error: failed to start OpenCode CLI: ${err?.message || String(err)}`)
|
|
556
|
+
})
|
|
557
|
+
child.on('close', (code, signal) => {
|
|
558
|
+
clearTimeout(timeoutHandle)
|
|
559
|
+
const durationMs = Date.now() - startedAt
|
|
560
|
+
const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
|
|
561
|
+
if (guessed) discoveredSessionId = guessed
|
|
562
|
+
if (discoveredSessionId) persistDelegateResumeId('opencode', discoveredSessionId)
|
|
563
|
+
log.info('session-tools', 'delegate_to_opencode_cli child close', {
|
|
564
|
+
sessionId: ctx?.sessionId || null,
|
|
565
|
+
code,
|
|
566
|
+
signal: signal || null,
|
|
567
|
+
timedOut,
|
|
568
|
+
durationMs,
|
|
569
|
+
stdoutLen: stdout.length,
|
|
570
|
+
stderrLen: stderr.length,
|
|
571
|
+
parsedTextLen: parsedText.length,
|
|
572
|
+
eventErrorCount: eventErrors.length,
|
|
573
|
+
discoveredSessionId,
|
|
574
|
+
stderrPreview: tail(stderr, 240),
|
|
575
|
+
})
|
|
576
|
+
if (timedOut) {
|
|
577
|
+
const msg = [
|
|
578
|
+
`Error: OpenCode CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
|
|
579
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
580
|
+
eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
|
|
581
|
+
stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
|
|
582
|
+
'Try increasing "CLI Process Timeout (sec)" in Settings.',
|
|
583
|
+
].filter(Boolean).join('\n\n')
|
|
584
|
+
finish(msg)
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
const successText = parsedText.trim() || stdout.trim() || stderr.trim()
|
|
588
|
+
if (code === 0 && successText) {
|
|
589
|
+
const out = discoveredSessionId
|
|
590
|
+
? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
|
|
591
|
+
: successText
|
|
592
|
+
finish(out)
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
const msg = [
|
|
596
|
+
`Error: OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
|
|
597
|
+
eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
|
|
598
|
+
stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
|
|
599
|
+
stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
|
|
600
|
+
].filter(Boolean).join('\n\n')
|
|
601
|
+
finish(msg || 'Error: OpenCode CLI returned no output.')
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
} catch (err: any) {
|
|
605
|
+
return `Error delegating to OpenCode CLI: ${err.message}`
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
name: 'delegate_to_opencode_cli',
|
|
610
|
+
description: 'Delegate a complex task to OpenCode CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
|
|
611
|
+
schema: z.object({
|
|
612
|
+
task: z.string().describe('Detailed description of the task for OpenCode CLI'),
|
|
613
|
+
resume: z.boolean().optional().describe('If true, try to resume the last saved OpenCode delegation session for this SwarmClaw session'),
|
|
614
|
+
resumeId: z.string().optional().describe('Explicit OpenCode session id to resume (overrides resume=true memory)'),
|
|
615
|
+
}),
|
|
616
|
+
},
|
|
617
|
+
),
|
|
618
|
+
)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// delegate_to_agent: requires orchestrator capability to be enabled
|
|
623
|
+
if (bctx.activeTools.includes('orchestrator') && ctx?.agentId) {
|
|
624
|
+
tools.push(
|
|
625
|
+
tool(
|
|
626
|
+
async ({ agentId: targetAgentId, task: taskPrompt, description: taskDesc, startImmediately }) => {
|
|
627
|
+
try {
|
|
628
|
+
const agents = loadAgents()
|
|
629
|
+
let target = agents[targetAgentId]
|
|
630
|
+
let resolvedId = targetAgentId
|
|
631
|
+
// Fallback: resolve by name if the ID doesn't match directly
|
|
632
|
+
if (!target) {
|
|
633
|
+
const byName = Object.values(agents).find(
|
|
634
|
+
(a) => a.name.toLowerCase() === targetAgentId.toLowerCase(),
|
|
635
|
+
)
|
|
636
|
+
if (byName) {
|
|
637
|
+
target = byName
|
|
638
|
+
resolvedId = byName.id
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
|
|
642
|
+
|
|
643
|
+
const taskId = crypto.randomBytes(4).toString('hex')
|
|
644
|
+
const now = Date.now()
|
|
645
|
+
const newTask = {
|
|
646
|
+
id: taskId,
|
|
647
|
+
title: taskPrompt.slice(0, 100),
|
|
648
|
+
description: taskDesc || taskPrompt,
|
|
649
|
+
status: 'todo',
|
|
650
|
+
agentId: resolvedId,
|
|
651
|
+
sourceType: 'delegation' as const,
|
|
652
|
+
delegatedByAgentId: ctx.agentId!,
|
|
653
|
+
createdAt: now,
|
|
654
|
+
updatedAt: now,
|
|
655
|
+
comments: [{
|
|
656
|
+
id: crypto.randomBytes(4).toString('hex'),
|
|
657
|
+
author: agents[ctx.agentId!]?.name || 'Agent',
|
|
658
|
+
agentId: ctx.agentId!,
|
|
659
|
+
text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
|
|
660
|
+
createdAt: now,
|
|
661
|
+
}],
|
|
662
|
+
}
|
|
663
|
+
// Atomic upsert to avoid race with concurrent queue processing
|
|
664
|
+
upsertTask(taskId, newTask)
|
|
665
|
+
console.log(`[delegate] Created task ${taskId} for agent ${resolvedId}, startImmediately=${startImmediately}`)
|
|
666
|
+
|
|
667
|
+
// Verify it persisted
|
|
668
|
+
const verify = loadTasks()
|
|
669
|
+
if (!verify[taskId]) {
|
|
670
|
+
console.error(`[delegate] RACE: task ${taskId} not found after upsert!`)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (startImmediately) {
|
|
674
|
+
// Lazy import to avoid circular: session-tools → queue → chat-execution → session-tools
|
|
675
|
+
const { enqueueTask } = await import('../queue')
|
|
676
|
+
enqueueTask(taskId)
|
|
677
|
+
console.log(`[delegate] Enqueued task ${taskId}`)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return JSON.stringify({
|
|
681
|
+
ok: true,
|
|
682
|
+
taskId,
|
|
683
|
+
agentId: resolvedId,
|
|
684
|
+
agentName: target.name,
|
|
685
|
+
message: startImmediately
|
|
686
|
+
? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}.`
|
|
687
|
+
: `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo. Ask the user if they want to start it now — call again with startImmediately: true to queue it.`,
|
|
688
|
+
})
|
|
689
|
+
} catch (err: unknown) {
|
|
690
|
+
return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: 'delegate_to_agent',
|
|
695
|
+
description: 'Delegate a task to another agent. Creates a task on the task board. By default the task goes to "todo" status. Set startImmediately=true to queue it for execution right away. Ask the user to confirm before starting immediately.',
|
|
696
|
+
schema: z.object({
|
|
697
|
+
agentId: z.string().describe('ID or name of the target agent to delegate to'),
|
|
698
|
+
task: z.string().describe('What the target agent should do'),
|
|
699
|
+
description: z.string().optional().describe('Optional longer description of the task'),
|
|
700
|
+
startImmediately: z.boolean().optional().default(false).describe('If true, queue the task for immediate execution instead of putting it in todo'),
|
|
701
|
+
}),
|
|
702
|
+
},
|
|
703
|
+
),
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return tools
|
|
708
|
+
}
|