@swarmclawai/swarmclaw 1.5.3 → 1.5.33
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 +167 -147
- package/bin/swarmclaw.js +35 -1
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +87 -2
- package/skills/swarmclaw/SKILL.md +151 -0
- package/src/app/api/agents/[id]/route.ts +8 -0
- package/src/app/api/agents/agents-route.test.ts +114 -0
- package/src/app/api/agents/route.ts +10 -0
- package/src/app/api/connectors/route.ts +25 -13
- package/src/app/api/credentials/[id]/route.ts +8 -1
- package/src/app/api/schedules/[id]/route.ts +8 -0
- package/src/app/api/secrets/[id]/route.ts +10 -0
- package/src/app/api/setup/check-provider/route.ts +45 -0
- package/src/app/api/setup/doctor/route.ts +5 -0
- package/src/cli/binary.test.js +11 -0
- package/src/cli/index.js +4 -4
- package/src/cli/index.test.js +5 -2
- package/src/cli/index.ts +1 -1
- package/src/components/agents/agent-sheet.tsx +16 -4
- package/src/components/agents/inspector-panel.tsx +5 -0
- package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
- package/src/components/chat/activity-moment.tsx +3 -0
- package/src/components/chat/chat-header.tsx +23 -2
- package/src/components/chat/tool-call-bubble.tsx +20 -0
- package/src/components/layout/sidebar-rail.tsx +29 -0
- package/src/hooks/setup-done-detection.test.ts +4 -2
- package/src/hooks/setup-done-detection.ts +1 -1
- package/src/lib/orchestrator-config.ts +5 -0
- package/src/lib/provider-sets.ts +4 -4
- package/src/lib/providers/acp-client.ts +116 -0
- package/src/lib/providers/cli-utils.test.ts +9 -1
- package/src/lib/providers/cli-utils.ts +89 -4
- package/src/lib/providers/cursor-cli.ts +172 -0
- package/src/lib/providers/goose.ts +149 -0
- package/src/lib/providers/index.ts +29 -1
- package/src/lib/providers/qwen-code-cli.ts +152 -0
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-thread-session.ts +8 -0
- package/src/lib/server/agents/task-session.ts +8 -0
- package/src/lib/server/capability-router.ts +8 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
- package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
- package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
- package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
- package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
- package/src/lib/server/chats/chat-session-service.ts +18 -0
- package/src/lib/server/connectors/session.ts +8 -0
- package/src/lib/server/context-manager.ts +5 -0
- package/src/lib/server/provider-health.ts +13 -3
- package/src/lib/server/provider-model-discovery.test.ts +8 -0
- package/src/lib/server/provider-model-discovery.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +2 -2
- package/src/lib/server/runtime/queue/core.ts +30 -4
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
- package/src/lib/server/session-reset-policy.test.ts +16 -0
- package/src/lib/server/session-reset-policy.ts +9 -1
- package/src/lib/server/session-tools/context.ts +2 -2
- package/src/lib/server/session-tools/delegate.ts +334 -14
- package/src/lib/server/session-tools/index.ts +5 -2
- package/src/lib/server/session-tools/session-info.ts +4 -1
- package/src/lib/server/storage-auth-docker.test.ts +244 -0
- package/src/lib/server/storage-auth.test.ts +150 -0
- package/src/lib/server/storage-auth.ts +57 -22
- package/src/lib/server/storage-normalization.ts +19 -0
- package/src/lib/server/storage.ts +3 -0
- package/src/lib/server/tasks/task-resume.ts +23 -1
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/setup-defaults.test.ts +6 -0
- package/src/lib/setup-defaults.ts +146 -0
- package/src/lib/tool-definitions.ts +1 -1
- package/src/types/misc.ts +4 -1
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +9 -0
|
@@ -25,7 +25,7 @@ import { markProviderFailure, markProviderSuccess } from '../provider-health'
|
|
|
25
25
|
import { loadRuntimeSettings } from '../runtime/runtime-settings'
|
|
26
26
|
import { getSessionDepth } from '../agents/subagent-runtime'
|
|
27
27
|
|
|
28
|
-
const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
|
|
28
|
+
const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini', 'copilot', 'cursor', 'qwen']
|
|
29
29
|
|
|
30
30
|
interface DelegateContext {
|
|
31
31
|
id?: string
|
|
@@ -34,8 +34,8 @@ interface DelegateContext {
|
|
|
34
34
|
jobId?: string | null
|
|
35
35
|
cwd?: string
|
|
36
36
|
claudeTimeoutMs?: number
|
|
37
|
-
readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
|
|
38
|
-
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
|
|
37
|
+
readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null
|
|
38
|
+
persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', id: string | null | undefined) => void
|
|
39
39
|
ctx?: {
|
|
40
40
|
delegationEnabled?: boolean
|
|
41
41
|
delegationTargetMode?: 'all' | 'selected'
|
|
@@ -48,7 +48,7 @@ interface DelegateContext {
|
|
|
48
48
|
hasTool?: (name: string) => boolean
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
|
|
51
|
+
type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen'
|
|
52
52
|
|
|
53
53
|
interface DelegateRuntimeState {
|
|
54
54
|
child?: ChildProcess | null
|
|
@@ -128,8 +128,11 @@ function buildDelegateResumePatch(bctx: DelegateContext) {
|
|
|
128
128
|
codex: bctx.readStoredDelegateResumeId?.('codex') || null,
|
|
129
129
|
opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
|
|
130
130
|
gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
|
|
131
|
+
copilot: bctx.readStoredDelegateResumeId?.('copilot') || null,
|
|
132
|
+
cursor: bctx.readStoredDelegateResumeId?.('cursor') || null,
|
|
133
|
+
qwen: bctx.readStoredDelegateResumeId?.('qwen') || null,
|
|
131
134
|
}
|
|
132
|
-
const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
|
|
135
|
+
const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || resumeIds.copilot || resumeIds.cursor || resumeIds.qwen || null
|
|
133
136
|
return { resumeIds, resumeId }
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -140,6 +143,9 @@ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
|
|
|
140
143
|
if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
|
|
141
144
|
if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
|
|
142
145
|
if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
|
|
146
|
+
if (['copilot', 'copilot cli', 'copilot-cli', 'copilot_cli', 'github copilot'].includes(normalized)) return 'copilot'
|
|
147
|
+
if (['cursor', 'cursor cli', 'cursor-cli', 'cursor_cli', 'cursor-agent'].includes(normalized)) return 'cursor'
|
|
148
|
+
if (['qwen', 'qwen code', 'qwen-code', 'qwen_code', 'qwen-code-cli', 'qwen_code_cli'].includes(normalized)) return 'qwen'
|
|
143
149
|
return null
|
|
144
150
|
}
|
|
145
151
|
|
|
@@ -334,18 +340,21 @@ function coerceOptionalBool(value: unknown): boolean | null {
|
|
|
334
340
|
}
|
|
335
341
|
|
|
336
342
|
function resumeStorageKeyForBackend(
|
|
337
|
-
backend: 'claude' | 'codex' | 'opencode' | 'gemini',
|
|
338
|
-
): 'claudeCode' | 'codex' | 'opencode' | 'gemini' {
|
|
343
|
+
backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen',
|
|
344
|
+
): 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen' {
|
|
339
345
|
if (backend === 'claude') return 'claudeCode'
|
|
340
346
|
if (backend === 'codex') return 'codex'
|
|
341
347
|
if (backend === 'opencode') return 'opencode'
|
|
342
|
-
return 'gemini'
|
|
348
|
+
if (backend === 'gemini') return 'gemini'
|
|
349
|
+
if (backend === 'copilot') return 'copilot'
|
|
350
|
+
if (backend === 'cursor') return 'cursor'
|
|
351
|
+
return 'qwen'
|
|
343
352
|
}
|
|
344
353
|
|
|
345
354
|
export function resolveDelegateResumeConfig(
|
|
346
355
|
normalized: Record<string, unknown>,
|
|
347
|
-
backend: 'claude' | 'codex' | 'opencode' | 'gemini',
|
|
348
|
-
bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null },
|
|
356
|
+
backend: 'claude' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen',
|
|
357
|
+
bctx: { readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen') => string | null },
|
|
349
358
|
): { resume: boolean; resumeId: string } {
|
|
350
359
|
const explicitResumeId = typeof normalized.resumeId === 'string' ? normalized.resumeId.trim() : ''
|
|
351
360
|
if (explicitResumeId) return { resume: true, resumeId: explicitResumeId }
|
|
@@ -415,6 +424,21 @@ const DELEGATE_BACKEND_ADAPTERS: Record<DelegateBackend, DelegateBackendAdapter>
|
|
|
415
424
|
binaryName: 'gemini',
|
|
416
425
|
run: runGeminiDelegate,
|
|
417
426
|
},
|
|
427
|
+
copilot: {
|
|
428
|
+
backend: 'copilot',
|
|
429
|
+
binaryName: 'copilot',
|
|
430
|
+
run: runCopilotDelegate,
|
|
431
|
+
},
|
|
432
|
+
cursor: {
|
|
433
|
+
backend: 'cursor',
|
|
434
|
+
binaryName: 'cursor-agent',
|
|
435
|
+
run: runCursorDelegate,
|
|
436
|
+
},
|
|
437
|
+
qwen: {
|
|
438
|
+
backend: 'qwen',
|
|
439
|
+
binaryName: 'qwen',
|
|
440
|
+
run: runQwenDelegate,
|
|
441
|
+
},
|
|
418
442
|
}
|
|
419
443
|
|
|
420
444
|
async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
@@ -433,7 +457,10 @@ function providerIdForBackend(backend: DelegateBackend): string {
|
|
|
433
457
|
if (backend === 'claude') return 'claude-cli'
|
|
434
458
|
if (backend === 'codex') return 'codex-cli'
|
|
435
459
|
if (backend === 'opencode') return 'opencode-cli'
|
|
436
|
-
return 'gemini-cli'
|
|
460
|
+
if (backend === 'gemini') return 'gemini-cli'
|
|
461
|
+
if (backend === 'copilot') return 'copilot-cli'
|
|
462
|
+
if (backend === 'cursor') return 'cursor-cli'
|
|
463
|
+
return 'qwen-code-cli'
|
|
437
464
|
}
|
|
438
465
|
|
|
439
466
|
function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
|
|
@@ -665,6 +692,42 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
|
|
|
665
692
|
return null
|
|
666
693
|
}
|
|
667
694
|
|
|
695
|
+
function parseCursorOutputText(ev: Record<string, unknown>): string | null {
|
|
696
|
+
if (typeof ev.result === 'string' && ev.result.trim()) return ev.result
|
|
697
|
+
if (typeof ev.text === 'string' && ev.text.trim()) return ev.text
|
|
698
|
+
if (typeof ev.message === 'string' && ev.message.trim()) return ev.message
|
|
699
|
+
const message = ev.message
|
|
700
|
+
if (message && typeof message === 'object') {
|
|
701
|
+
const record = message as Record<string, unknown>
|
|
702
|
+
if (typeof record.text === 'string' && record.text.trim()) return record.text
|
|
703
|
+
if (typeof record.content === 'string' && record.content.trim()) return record.content
|
|
704
|
+
}
|
|
705
|
+
if (ev.type === 'delta') {
|
|
706
|
+
const delta = ev.delta as Record<string, unknown> | undefined
|
|
707
|
+
if (typeof delta?.text === 'string' && delta.text.trim()) return delta.text
|
|
708
|
+
}
|
|
709
|
+
return null
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function parseQwenOutputText(ev: Record<string, unknown>): string | null {
|
|
713
|
+
if (typeof ev.result === 'string' && ev.result.trim()) return ev.result
|
|
714
|
+
if (ev.type === 'content_block_delta') {
|
|
715
|
+
const delta = ev.delta as Record<string, unknown> | undefined
|
|
716
|
+
if (typeof delta?.text === 'string' && delta.text.trim()) return delta.text
|
|
717
|
+
}
|
|
718
|
+
if (ev.type === 'assistant') {
|
|
719
|
+
const message = ev.message as Record<string, unknown> | undefined
|
|
720
|
+
const content = Array.isArray(message?.content) ? message.content : []
|
|
721
|
+
const text = content
|
|
722
|
+
.filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
|
|
723
|
+
.map((entry) => typeof entry.text === 'string' ? entry.text : '')
|
|
724
|
+
.join('')
|
|
725
|
+
.trim()
|
|
726
|
+
if (text) return text
|
|
727
|
+
}
|
|
728
|
+
return null
|
|
729
|
+
}
|
|
730
|
+
|
|
668
731
|
async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
669
732
|
try {
|
|
670
733
|
// Build clean env — preserves user's CODEX_HOME for auth
|
|
@@ -920,6 +983,263 @@ async function runGeminiDelegate(binary: string, task: string, resume: boolean,
|
|
|
920
983
|
}
|
|
921
984
|
}
|
|
922
985
|
|
|
986
|
+
async function runCopilotDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
987
|
+
try {
|
|
988
|
+
const env = buildCliEnv()
|
|
989
|
+
const auth = probeCliAuth(binary, 'copilot', env, bctx.cwd)
|
|
990
|
+
if (!auth.authenticated) {
|
|
991
|
+
return buildDelegateFailure('copilot', auth.errorMessage || 'Copilot CLI is not authenticated.', 'auth')
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const storedResumeId = bctx.readStoredDelegateResumeId?.('copilot')
|
|
995
|
+
const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
|
|
996
|
+
|
|
997
|
+
return await new Promise<DelegateBackendResult>((resolve) => {
|
|
998
|
+
const args = ['-p', task, '--output-format=json', '-s', '--yolo']
|
|
999
|
+
if (resumeIdToUse) args.push(`--resume=${resumeIdToUse}`)
|
|
1000
|
+
|
|
1001
|
+
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
1002
|
+
bindDelegateRuntime(runtime, child)
|
|
1003
|
+
let stdoutBuf = ''
|
|
1004
|
+
let stderrBuf = ''
|
|
1005
|
+
let responseText = ''
|
|
1006
|
+
let discoveredId: string | null = null
|
|
1007
|
+
let settled = false
|
|
1008
|
+
|
|
1009
|
+
const finish = (result: DelegateBackendResult) => {
|
|
1010
|
+
if (settled) return
|
|
1011
|
+
settled = true
|
|
1012
|
+
resolve(result)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const timeoutHandle = setTimeout(() => {
|
|
1016
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
1017
|
+
}, bctx.claudeTimeoutMs || 300000)
|
|
1018
|
+
|
|
1019
|
+
child.stdout?.on('data', (chunk) => {
|
|
1020
|
+
stdoutBuf += chunk.toString()
|
|
1021
|
+
const lines = stdoutBuf.split('\n')
|
|
1022
|
+
stdoutBuf = lines.pop() || ''
|
|
1023
|
+
for (const line of lines) {
|
|
1024
|
+
const trimmed = line.trim()
|
|
1025
|
+
if (!trimmed) continue
|
|
1026
|
+
try {
|
|
1027
|
+
const ev = JSON.parse(trimmed) as Record<string, unknown>
|
|
1028
|
+
const sid = typeof ev.session_id === 'string'
|
|
1029
|
+
? ev.session_id
|
|
1030
|
+
: typeof ev.sessionId === 'string'
|
|
1031
|
+
? ev.sessionId
|
|
1032
|
+
: null
|
|
1033
|
+
if (sid) discoveredId = sid
|
|
1034
|
+
const text = parseCursorOutputText(ev)
|
|
1035
|
+
if (text) {
|
|
1036
|
+
if (String(ev.type || '').includes('result') || String(ev.type || '').includes('completed')) responseText = text
|
|
1037
|
+
else responseText += text
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
responseText += `${line}\n`
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
child.stderr?.on('data', (chunk) => {
|
|
1046
|
+
stderrBuf += chunk.toString()
|
|
1047
|
+
if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
child.on('close', (code, signal) => {
|
|
1051
|
+
clearTimeout(timeoutHandle)
|
|
1052
|
+
if (discoveredId) bctx.persistDelegateResumeId?.('copilot', discoveredId)
|
|
1053
|
+
const output = responseText.trim()
|
|
1054
|
+
if (output) return finish(buildDelegateSuccess('copilot', output))
|
|
1055
|
+
const stderr = stderrBuf.trim()
|
|
1056
|
+
if (stderr) return finish(buildDelegateFailure('copilot', stderr))
|
|
1057
|
+
return finish(buildDelegateFailure('copilot', `Copilot exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
child.on('error', (err) => {
|
|
1061
|
+
clearTimeout(timeoutHandle)
|
|
1062
|
+
finish(buildDelegateFailure('copilot', err.message, 'spawn'))
|
|
1063
|
+
})
|
|
1064
|
+
})
|
|
1065
|
+
} catch (err: unknown) {
|
|
1066
|
+
return buildDelegateFailure('copilot', errorMessage(err), 'runtime')
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function runCursorDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
1071
|
+
try {
|
|
1072
|
+
const env = buildCliEnv()
|
|
1073
|
+
const auth = probeCliAuth(binary, 'cursor', env, bctx.cwd)
|
|
1074
|
+
if (!auth.authenticated) {
|
|
1075
|
+
return buildDelegateFailure('cursor', auth.errorMessage || 'Cursor Agent CLI is not authenticated.', 'auth')
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const storedResumeId = bctx.readStoredDelegateResumeId?.('cursor')
|
|
1079
|
+
const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
|
|
1080
|
+
|
|
1081
|
+
return await new Promise<DelegateBackendResult>((resolve) => {
|
|
1082
|
+
const args = ['--print', '--output-format', 'stream-json']
|
|
1083
|
+
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
1084
|
+
args.push(task)
|
|
1085
|
+
|
|
1086
|
+
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
1087
|
+
bindDelegateRuntime(runtime, child)
|
|
1088
|
+
let stdoutBuf = ''
|
|
1089
|
+
let stderrBuf = ''
|
|
1090
|
+
let responseText = ''
|
|
1091
|
+
let discoveredId: string | null = null
|
|
1092
|
+
let settled = false
|
|
1093
|
+
|
|
1094
|
+
const finish = (result: DelegateBackendResult) => {
|
|
1095
|
+
if (settled) return
|
|
1096
|
+
settled = true
|
|
1097
|
+
resolve(result)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const timeoutHandle = setTimeout(() => {
|
|
1101
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
1102
|
+
}, bctx.claudeTimeoutMs || 300000)
|
|
1103
|
+
|
|
1104
|
+
child.stdout?.on('data', (chunk) => {
|
|
1105
|
+
stdoutBuf += chunk.toString()
|
|
1106
|
+
const lines = stdoutBuf.split('\n')
|
|
1107
|
+
stdoutBuf = lines.pop() || ''
|
|
1108
|
+
for (const line of lines) {
|
|
1109
|
+
const trimmed = line.trim()
|
|
1110
|
+
if (!trimmed) continue
|
|
1111
|
+
try {
|
|
1112
|
+
const ev = JSON.parse(trimmed) as Record<string, unknown>
|
|
1113
|
+
const sid = typeof ev.session_id === 'string'
|
|
1114
|
+
? ev.session_id
|
|
1115
|
+
: typeof ev.sessionId === 'string'
|
|
1116
|
+
? ev.sessionId
|
|
1117
|
+
: typeof ev.thread_id === 'string'
|
|
1118
|
+
? ev.thread_id
|
|
1119
|
+
: null
|
|
1120
|
+
if (sid) discoveredId = sid
|
|
1121
|
+
const text = parseCursorOutputText(ev)
|
|
1122
|
+
if (text) {
|
|
1123
|
+
if (String(ev.type || '').includes('result') || String(ev.type || '').includes('completed')) responseText = text
|
|
1124
|
+
else responseText += text
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
responseText += `${line}\n`
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
child.stderr?.on('data', (chunk) => {
|
|
1133
|
+
stderrBuf += chunk.toString()
|
|
1134
|
+
if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
child.on('close', (code, signal) => {
|
|
1138
|
+
clearTimeout(timeoutHandle)
|
|
1139
|
+
if (discoveredId) bctx.persistDelegateResumeId?.('cursor', discoveredId)
|
|
1140
|
+
const output = responseText.trim()
|
|
1141
|
+
if (output) return finish(buildDelegateSuccess('cursor', output))
|
|
1142
|
+
const stderr = stderrBuf.trim()
|
|
1143
|
+
if (stderr) return finish(buildDelegateFailure('cursor', stderr))
|
|
1144
|
+
return finish(buildDelegateFailure('cursor', `Cursor exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
child.on('error', (err) => {
|
|
1148
|
+
clearTimeout(timeoutHandle)
|
|
1149
|
+
finish(buildDelegateFailure('cursor', err.message, 'spawn'))
|
|
1150
|
+
})
|
|
1151
|
+
})
|
|
1152
|
+
} catch (err: unknown) {
|
|
1153
|
+
return buildDelegateFailure('cursor', errorMessage(err), 'runtime')
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function runQwenDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
1158
|
+
try {
|
|
1159
|
+
const env = buildCliEnv()
|
|
1160
|
+
const auth = probeCliAuth(binary, 'qwen', env, bctx.cwd)
|
|
1161
|
+
if (!auth.authenticated) {
|
|
1162
|
+
return buildDelegateFailure('qwen', auth.errorMessage || 'Qwen Code CLI is not configured.', 'auth')
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const storedResumeId = bctx.readStoredDelegateResumeId?.('qwen')
|
|
1166
|
+
const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
|
|
1167
|
+
|
|
1168
|
+
return await new Promise<DelegateBackendResult>((resolve) => {
|
|
1169
|
+
const args = ['-p', task, '--output-format', 'stream-json', '--include-partial-messages', '--yolo']
|
|
1170
|
+
if (resumeIdToUse) args.push('--resume', resumeIdToUse)
|
|
1171
|
+
|
|
1172
|
+
const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
1173
|
+
bindDelegateRuntime(runtime, child)
|
|
1174
|
+
let stdoutBuf = ''
|
|
1175
|
+
let stderrBuf = ''
|
|
1176
|
+
let responseText = ''
|
|
1177
|
+
let discoveredId: string | null = null
|
|
1178
|
+
let settled = false
|
|
1179
|
+
|
|
1180
|
+
const finish = (result: DelegateBackendResult) => {
|
|
1181
|
+
if (settled) return
|
|
1182
|
+
settled = true
|
|
1183
|
+
resolve(result)
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const timeoutHandle = setTimeout(() => {
|
|
1187
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
1188
|
+
}, bctx.claudeTimeoutMs || 300000)
|
|
1189
|
+
|
|
1190
|
+
child.stdout?.on('data', (chunk) => {
|
|
1191
|
+
stdoutBuf += chunk.toString()
|
|
1192
|
+
const lines = stdoutBuf.split('\n')
|
|
1193
|
+
stdoutBuf = lines.pop() || ''
|
|
1194
|
+
for (const line of lines) {
|
|
1195
|
+
const trimmed = line.trim()
|
|
1196
|
+
if (!trimmed) continue
|
|
1197
|
+
try {
|
|
1198
|
+
const ev = JSON.parse(trimmed) as Record<string, unknown>
|
|
1199
|
+
const sid = typeof ev.session_id === 'string'
|
|
1200
|
+
? ev.session_id
|
|
1201
|
+
: typeof ev.sessionId === 'string'
|
|
1202
|
+
? ev.sessionId
|
|
1203
|
+
: null
|
|
1204
|
+
if (sid) discoveredId = sid
|
|
1205
|
+
const text = parseQwenOutputText(ev)
|
|
1206
|
+
if (text) {
|
|
1207
|
+
if (ev.type === 'assistant' || ev.type === 'result') responseText = text
|
|
1208
|
+
else responseText += text
|
|
1209
|
+
} else if (ev.type === 'result' && ev.subtype === 'error') {
|
|
1210
|
+
stderrBuf += `${typeof ev.result === 'string' ? ev.result : 'Qwen Code error'}\n`
|
|
1211
|
+
}
|
|
1212
|
+
} catch {
|
|
1213
|
+
responseText += `${line}\n`
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
child.stderr?.on('data', (chunk) => {
|
|
1219
|
+
stderrBuf += chunk.toString()
|
|
1220
|
+
if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
child.on('close', (code, signal) => {
|
|
1224
|
+
clearTimeout(timeoutHandle)
|
|
1225
|
+
if (discoveredId) bctx.persistDelegateResumeId?.('qwen', discoveredId)
|
|
1226
|
+
const output = responseText.trim()
|
|
1227
|
+
if (output) return finish(buildDelegateSuccess('qwen', output))
|
|
1228
|
+
const stderr = stderrBuf.trim()
|
|
1229
|
+
if (stderr) return finish(buildDelegateFailure('qwen', stderr))
|
|
1230
|
+
return finish(buildDelegateFailure('qwen', `Qwen Code exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`, 'runtime'))
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
child.on('error', (err) => {
|
|
1234
|
+
clearTimeout(timeoutHandle)
|
|
1235
|
+
finish(buildDelegateFailure('qwen', err.message, 'spawn'))
|
|
1236
|
+
})
|
|
1237
|
+
})
|
|
1238
|
+
} catch (err: unknown) {
|
|
1239
|
+
return buildDelegateFailure('qwen', errorMessage(err), 'runtime')
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
923
1243
|
async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<DelegateBackendResult> {
|
|
924
1244
|
try {
|
|
925
1245
|
const env = buildCliEnv()
|
|
@@ -984,19 +1304,19 @@ const DelegateExtension: Extension = {
|
|
|
984
1304
|
name: 'Core Delegate',
|
|
985
1305
|
description: 'Delegate complex multi-file tasks to specialized CLI backends or other agents.',
|
|
986
1306
|
hooks: {
|
|
987
|
-
getCapabilityDescription: () => 'I can hand off coding work to Claude Code, Codex, OpenCode, or
|
|
1307
|
+
getCapabilityDescription: () => 'I can hand off coding work to Claude Code, Codex, OpenCode, Gemini CLI, Cursor CLI, or Qwen Code CLI (`delegate`) for file creation, refactoring, debugging, code generation, and multi-file edits. Resume IDs may come back via `[delegate_meta]`.',
|
|
988
1308
|
getOperatingGuidance: () => ['CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.', 'Delegate for code tasks: writing/creating files, refactors, debugging, generation, test suites, data exports to files.'],
|
|
989
1309
|
} as ExtensionHooks,
|
|
990
1310
|
tools: [
|
|
991
1311
|
{
|
|
992
1312
|
name: 'delegate',
|
|
993
|
-
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini) for code tasks: writing files, refactoring, debugging, code generation, and multi-file edits. Supports background jobs with action=status|list|wait|cancel.',
|
|
1313
|
+
description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini, Cursor, Qwen) for code tasks: writing files, refactoring, debugging, code generation, and multi-file edits. Supports background jobs with action=status|list|wait|cancel.',
|
|
994
1314
|
parameters: {
|
|
995
1315
|
type: 'object',
|
|
996
1316
|
properties: {
|
|
997
1317
|
action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
|
|
998
1318
|
task: { type: 'string' },
|
|
999
|
-
backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
|
|
1319
|
+
backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini', 'copilot', 'cursor', 'qwen'] },
|
|
1000
1320
|
resume: { type: 'boolean' },
|
|
1001
1321
|
resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
|
|
1002
1322
|
jobId: { type: 'string' },
|
|
@@ -73,6 +73,9 @@ const DELEGATION_TOOL_NAMES = new Set([
|
|
|
73
73
|
'delegate_to_codex_cli',
|
|
74
74
|
'delegate_to_opencode_cli',
|
|
75
75
|
'delegate_to_gemini_cli',
|
|
76
|
+
'delegate_to_copilot_cli',
|
|
77
|
+
'delegate_to_cursor_cli',
|
|
78
|
+
'delegate_to_qwen_code_cli',
|
|
76
79
|
])
|
|
77
80
|
|
|
78
81
|
export async function buildSessionTools(cwd: string, enabledExtensions: string[], ctx?: ToolContext): Promise<SessionToolsResult> {
|
|
@@ -117,14 +120,14 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
|
|
|
117
120
|
return loadSession(ctx.sessionId)
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini'): string | null => {
|
|
123
|
+
const readStoredDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen'): string | null => {
|
|
121
124
|
const session = resolveCurrentSession()
|
|
122
125
|
if (!session?.delegateResumeIds || typeof session.delegateResumeIds !== 'object') return null
|
|
123
126
|
const raw = session.delegateResumeIds[key]
|
|
124
127
|
return typeof raw === 'string' && raw.trim() ? raw.trim() : null
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', resumeId: string | null | undefined): void => {
|
|
130
|
+
const persistDelegateResumeId = (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini' | 'copilot' | 'cursor' | 'qwen', resumeId: string | null | undefined): void => {
|
|
128
131
|
const normalized = typeof resumeId === 'string' ? resumeId.trim() : ''
|
|
129
132
|
if (!normalized || !ctx?.sessionId) return
|
|
130
133
|
patchSession(ctx.sessionId, (target) => {
|
|
@@ -49,6 +49,9 @@ function normalizeRuntimeExtensionId(extensionId: string): string {
|
|
|
49
49
|
if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
|
|
50
50
|
if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
|
|
51
51
|
if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
|
|
52
|
+
if (normalized === 'delegate_to_copilot_cli' || normalized === 'copilot_cli') return 'copilot_cli'
|
|
53
|
+
if (normalized === 'delegate_to_cursor_cli' || normalized === 'cursor_cli') return 'cursor_cli'
|
|
54
|
+
if (normalized === 'delegate_to_qwen_code_cli' || normalized === 'qwen_code_cli') return 'qwen_code_cli'
|
|
52
55
|
if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
|
|
53
56
|
return canonicalizeExtensionId(normalized)
|
|
54
57
|
}
|
|
@@ -100,7 +103,7 @@ export function buildSessionIdentityPayload(params: {
|
|
|
100
103
|
const activeProjectContext = params.activeProjectContext || null
|
|
101
104
|
const enabledExtensions = Array.isArray(params.enabledExtensions) ? params.enabledExtensions : []
|
|
102
105
|
const toolPolicy = params.toolPolicy || null
|
|
103
|
-
const delegationEnabled = enabledExtensions.some((extensionId) => ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli'].includes(extensionId))
|
|
106
|
+
const delegationEnabled = enabledExtensions.some((extensionId) => ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli'].includes(extensionId))
|
|
104
107
|
|
|
105
108
|
return {
|
|
106
109
|
sessionId: params.context.sessionId || undefined,
|