@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.
Files changed (78) hide show
  1. package/README.md +167 -147
  2. package/bin/swarmclaw.js +35 -1
  3. package/package.json +2 -2
  4. package/scripts/run-next-build.mjs +87 -2
  5. package/skills/swarmclaw/SKILL.md +151 -0
  6. package/src/app/api/agents/[id]/route.ts +8 -0
  7. package/src/app/api/agents/agents-route.test.ts +114 -0
  8. package/src/app/api/agents/route.ts +10 -0
  9. package/src/app/api/connectors/route.ts +25 -13
  10. package/src/app/api/credentials/[id]/route.ts +8 -1
  11. package/src/app/api/schedules/[id]/route.ts +8 -0
  12. package/src/app/api/secrets/[id]/route.ts +10 -0
  13. package/src/app/api/setup/check-provider/route.ts +45 -0
  14. package/src/app/api/setup/doctor/route.ts +5 -0
  15. package/src/cli/binary.test.js +11 -0
  16. package/src/cli/index.js +4 -4
  17. package/src/cli/index.test.js +5 -2
  18. package/src/cli/index.ts +1 -1
  19. package/src/components/agents/agent-sheet.tsx +16 -4
  20. package/src/components/agents/inspector-panel.tsx +5 -0
  21. package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
  22. package/src/components/chat/activity-moment.tsx +3 -0
  23. package/src/components/chat/chat-header.tsx +23 -2
  24. package/src/components/chat/tool-call-bubble.tsx +20 -0
  25. package/src/components/layout/sidebar-rail.tsx +29 -0
  26. package/src/hooks/setup-done-detection.test.ts +4 -2
  27. package/src/hooks/setup-done-detection.ts +1 -1
  28. package/src/lib/orchestrator-config.ts +5 -0
  29. package/src/lib/provider-sets.ts +4 -4
  30. package/src/lib/providers/acp-client.ts +116 -0
  31. package/src/lib/providers/cli-utils.test.ts +9 -1
  32. package/src/lib/providers/cli-utils.ts +89 -4
  33. package/src/lib/providers/cursor-cli.ts +172 -0
  34. package/src/lib/providers/goose.ts +149 -0
  35. package/src/lib/providers/index.ts +29 -1
  36. package/src/lib/providers/qwen-code-cli.ts +152 -0
  37. package/src/lib/server/agents/agent-availability.ts +2 -2
  38. package/src/lib/server/agents/agent-thread-session.ts +8 -0
  39. package/src/lib/server/agents/task-session.ts +8 -0
  40. package/src/lib/server/capability-router.ts +8 -2
  41. package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
  42. package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
  43. package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
  44. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +3 -0
  45. package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
  46. package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
  47. package/src/lib/server/chat-execution/prompt-sections.ts +3 -1
  48. package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
  49. package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
  50. package/src/lib/server/chats/chat-session-service.ts +18 -0
  51. package/src/lib/server/connectors/session.ts +8 -0
  52. package/src/lib/server/context-manager.ts +5 -0
  53. package/src/lib/server/provider-health.ts +13 -3
  54. package/src/lib/server/provider-model-discovery.test.ts +8 -0
  55. package/src/lib/server/provider-model-discovery.ts +1 -1
  56. package/src/lib/server/runtime/daemon-state/core.ts +2 -2
  57. package/src/lib/server/runtime/queue/core.ts +30 -4
  58. package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
  59. package/src/lib/server/session-reset-policy.test.ts +16 -0
  60. package/src/lib/server/session-reset-policy.ts +9 -1
  61. package/src/lib/server/session-tools/context.ts +2 -2
  62. package/src/lib/server/session-tools/delegate.ts +334 -14
  63. package/src/lib/server/session-tools/index.ts +5 -2
  64. package/src/lib/server/session-tools/session-info.ts +4 -1
  65. package/src/lib/server/storage-auth-docker.test.ts +244 -0
  66. package/src/lib/server/storage-auth.test.ts +150 -0
  67. package/src/lib/server/storage-auth.ts +57 -22
  68. package/src/lib/server/storage-normalization.ts +19 -0
  69. package/src/lib/server/storage.ts +3 -0
  70. package/src/lib/server/tasks/task-resume.ts +23 -1
  71. package/src/lib/server/tool-aliases.ts +1 -1
  72. package/src/lib/server/tool-capability-policy.ts +4 -1
  73. package/src/lib/setup-defaults.test.ts +6 -0
  74. package/src/lib/setup-defaults.ts +146 -0
  75. package/src/lib/tool-definitions.ts +1 -1
  76. package/src/types/misc.ts +4 -1
  77. package/src/types/provider.ts +1 -1
  78. package/src/types/session.ts +9 -0
@@ -0,0 +1,172 @@
1
+ import { spawn } from 'child_process'
2
+ import type { StreamChatOptions } from './index'
3
+ import { log } from '../server/logger'
4
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
+
7
+ function buildCursorPrompt(message: string, systemPrompt?: string, imagePath?: string): string {
8
+ const parts: string[] = []
9
+ if (systemPrompt) parts.push(`[System instructions]\n${systemPrompt}`)
10
+ if (imagePath) parts.push(`[The user shared an image at: ${imagePath}]`)
11
+ parts.push(message)
12
+ return parts.join('\n\n')
13
+ }
14
+
15
+ function extractCursorText(event: Record<string, unknown>): string | null {
16
+ if (typeof event.result === 'string' && event.result.trim()) return event.result
17
+ if (typeof event.text === 'string' && event.text.trim()) return event.text
18
+
19
+ const message = event.message
20
+ if (typeof message === 'string' && message.trim()) return message
21
+ if (message && typeof message === 'object') {
22
+ const record = message as Record<string, unknown>
23
+ if (typeof record.text === 'string' && record.text.trim()) return record.text
24
+ if (typeof record.content === 'string' && record.content.trim()) return record.content
25
+ if (Array.isArray(record.content)) {
26
+ const text = record.content
27
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
28
+ .map((entry) => {
29
+ if (typeof entry.text === 'string') return entry.text
30
+ if (typeof entry.content === 'string') return entry.content
31
+ return ''
32
+ })
33
+ .join('')
34
+ .trim()
35
+ if (text) return text
36
+ }
37
+ }
38
+
39
+ return null
40
+ }
41
+
42
+ export function streamCursorCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
43
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
44
+ const binary = resolveCliBinary('cursor-agent')
45
+ if (!binary) {
46
+ const msg = 'Cursor Agent CLI not found. Install Cursor CLI and ensure `cursor-agent` is on your PATH.'
47
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
48
+ return Promise.resolve('')
49
+ }
50
+
51
+ const env = buildCliEnv()
52
+ if (!session.apiKey) {
53
+ const auth = probeCliAuth(binary, 'cursor', env, session.cwd)
54
+ if (!auth.authenticated) {
55
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Cursor Agent CLI is not authenticated.' })}\n\n`)
56
+ return Promise.resolve('')
57
+ }
58
+ }
59
+
60
+ const prompt = buildCursorPrompt(message, !session.cursorSessionId ? systemPrompt : undefined, imagePath)
61
+ const args = ['--print', '--output-format', 'stream-json']
62
+ if (session.cursorSessionId) args.push('--resume', session.cursorSessionId)
63
+ if (session.model && session.model !== 'auto') args.push('--model', session.model)
64
+ args.push(prompt)
65
+
66
+ log.info('cursor-cli', `Spawning: ${binary}`, {
67
+ args: args.map((value) => value.length > 100 ? `${value.slice(0, 100)}...` : value),
68
+ cwd: session.cwd,
69
+ hasSystemPrompt: Boolean(systemPrompt),
70
+ resumeSessionId: session.cursorSessionId || null,
71
+ })
72
+
73
+ const proc = spawn(binary, args, {
74
+ cwd: session.cwd,
75
+ env,
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ timeout: processTimeoutMs,
78
+ })
79
+
80
+ active.set(session.id, proc)
81
+ attachAbortHandler(proc, signal)
82
+
83
+ let fullResponse = ''
84
+ let buf = ''
85
+ let stderrText = ''
86
+ let eventCount = 0
87
+
88
+ proc.stdout?.on('data', (chunk: Buffer) => {
89
+ buf += chunk.toString()
90
+ const lines = buf.split('\n')
91
+ buf = lines.pop() || ''
92
+
93
+ for (const line of lines) {
94
+ if (!line.trim()) continue
95
+ try {
96
+ const event = JSON.parse(line) as Record<string, unknown>
97
+ eventCount += 1
98
+
99
+ const sessionId = typeof event.session_id === 'string'
100
+ ? event.session_id
101
+ : typeof event.thread_id === 'string'
102
+ ? event.thread_id
103
+ : typeof event.sessionId === 'string'
104
+ ? event.sessionId
105
+ : null
106
+ if (sessionId) session.cursorSessionId = sessionId
107
+
108
+ const eventType = String(event.type || '')
109
+ if (eventType.includes('delta')) {
110
+ const delta = (event.delta && typeof event.delta === 'object'
111
+ ? event.delta
112
+ : event.content && typeof event.content === 'object'
113
+ ? event.content
114
+ : null) as Record<string, unknown> | null
115
+ const text = typeof delta?.text === 'string' ? delta.text : null
116
+ if (text) {
117
+ fullResponse += text
118
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
119
+ continue
120
+ }
121
+ }
122
+
123
+ const text = extractCursorText(event)
124
+ if (text) {
125
+ if (eventType === 'result' || eventType === 'completed' || eventType === 'assistant') {
126
+ fullResponse = text
127
+ write(`data: ${JSON.stringify({ t: 'r', text })}\n\n`)
128
+ } else {
129
+ fullResponse += text
130
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
131
+ }
132
+ } else if (eventType === 'error' && typeof event.message === 'string') {
133
+ write(`data: ${JSON.stringify({ t: 'err', text: event.message })}\n\n`)
134
+ }
135
+ } catch {
136
+ fullResponse += `${line}\n`
137
+ write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
138
+ }
139
+ }
140
+ })
141
+
142
+ proc.stderr?.on('data', (chunk: Buffer) => {
143
+ const text = chunk.toString()
144
+ stderrText += text
145
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
146
+ if (isStderrNoise(text)) {
147
+ log.debug('cursor-cli', `stderr noise [${session.id}]`, text.slice(0, 400))
148
+ } else {
149
+ log.warn('cursor-cli', `stderr [${session.id}]`, text.slice(0, 400))
150
+ }
151
+ })
152
+
153
+ return new Promise((resolve) => {
154
+ proc.on('close', (code, sig) => {
155
+ active.delete(session.id)
156
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
157
+ const msg = stderrText.trim()
158
+ ? `Cursor Agent CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
159
+ : `Cursor Agent CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
160
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
161
+ }
162
+ log.info('cursor-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
163
+ resolve(fullResponse.trim())
164
+ })
165
+
166
+ proc.on('error', (err) => {
167
+ active.delete(session.id)
168
+ write(`data: ${JSON.stringify({ t: 'err', text: err.message })}\n\n`)
169
+ resolve(fullResponse.trim())
170
+ })
171
+ })
172
+ }
@@ -0,0 +1,149 @@
1
+ import { spawn } from 'child_process'
2
+ import type { StreamChatOptions } from './index'
3
+ import { log } from '../server/logger'
4
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
+
7
+ function buildGoosePrompt(message: string, systemPrompt?: string, imagePath?: string): string {
8
+ const parts: string[] = []
9
+ if (systemPrompt) parts.push(`[System instructions]\n${systemPrompt}`)
10
+ if (imagePath) parts.push(`[The user shared an image at: ${imagePath}]`)
11
+ parts.push(message)
12
+ return parts.join('\n\n')
13
+ }
14
+
15
+ function deriveGooseSessionName(sessionId: string): string {
16
+ return `swarmclaw-${sessionId}`
17
+ }
18
+
19
+ function extractGooseText(event: Record<string, unknown>): string | null {
20
+ if (typeof event.result === 'string' && event.result.trim()) return event.result
21
+ if (typeof event.text === 'string' && event.text.trim()) return event.text
22
+ if (typeof event.message === 'string' && event.message.trim()) return event.message
23
+
24
+ const message = event.message
25
+ if (message && typeof message === 'object') {
26
+ const record = message as Record<string, unknown>
27
+ if (typeof record.text === 'string' && record.text.trim()) return record.text
28
+ if (typeof record.content === 'string' && record.content.trim()) return record.content
29
+ }
30
+
31
+ const content = event.content
32
+ if (typeof content === 'string' && content.trim()) return content
33
+ if (content && typeof content === 'object') {
34
+ const record = content as Record<string, unknown>
35
+ if (typeof record.text === 'string' && record.text.trim()) return record.text
36
+ }
37
+
38
+ return null
39
+ }
40
+
41
+ export function streamGooseChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
42
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
43
+ const binary = resolveCliBinary('goose')
44
+ if (!binary) {
45
+ const msg = 'Goose CLI not found. Install `goose` and ensure it is on your PATH.'
46
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
47
+ return Promise.resolve('')
48
+ }
49
+
50
+ const env = buildCliEnv()
51
+ if (!session.apiKey) {
52
+ const auth = probeCliAuth(binary, 'goose', env, session.cwd)
53
+ if (!auth.authenticated) {
54
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Goose CLI is not configured.' })}\n\n`)
55
+ return Promise.resolve('')
56
+ }
57
+ }
58
+ if (session.apiKey) env.GOOSE_API_KEY = session.apiKey
59
+
60
+ const prompt = buildGoosePrompt(message, !session.acpSessionId ? systemPrompt : undefined, imagePath)
61
+ const sessionName = session.acpSessionId || deriveGooseSessionName(session.id)
62
+ const args = ['run', '-t', prompt, '--format', 'json', '--quiet', '--name', sessionName]
63
+ if (session.acpSessionId) args.push('--resume')
64
+ if (session.model && session.model !== 'default') args.push('--model', session.model)
65
+
66
+ log.info('goose', `Spawning: ${binary}`, {
67
+ args: args.map((value, index) => index === 2 && value.length > 120 ? `(${value.length} chars)` : value),
68
+ cwd: session.cwd,
69
+ resumeSessionName: session.acpSessionId || null,
70
+ })
71
+
72
+ const proc = spawn(binary, args, {
73
+ cwd: session.cwd,
74
+ env,
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ timeout: processTimeoutMs,
77
+ })
78
+
79
+ session.acpSessionId = sessionName
80
+ active.set(session.id, proc)
81
+ attachAbortHandler(proc, signal)
82
+
83
+ let fullResponse = ''
84
+ let buf = ''
85
+ let stderrText = ''
86
+ let eventCount = 0
87
+
88
+ proc.stdout?.on('data', (chunk: Buffer) => {
89
+ buf += chunk.toString()
90
+ const lines = buf.split('\n')
91
+ buf = lines.pop() || ''
92
+
93
+ for (const line of lines) {
94
+ if (!line.trim()) continue
95
+ try {
96
+ const event = JSON.parse(line) as Record<string, unknown>
97
+ eventCount += 1
98
+ const text = extractGooseText(event)
99
+ if (!text) continue
100
+
101
+ const eventType = String(event.type || event.event || '')
102
+ if (eventType === 'delta' || eventType === 'content_block_delta' || eventType === 'chunk') {
103
+ fullResponse += text
104
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
105
+ } else if (eventType === 'result' || eventType === 'completed' || eventType === 'assistant') {
106
+ fullResponse = text
107
+ write(`data: ${JSON.stringify({ t: 'r', text })}\n\n`)
108
+ } else {
109
+ fullResponse += text
110
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
111
+ }
112
+ } catch {
113
+ fullResponse += `${line}\n`
114
+ write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
115
+ }
116
+ }
117
+ })
118
+
119
+ proc.stderr?.on('data', (chunk: Buffer) => {
120
+ const text = chunk.toString()
121
+ stderrText += text
122
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
123
+ if (isStderrNoise(text)) {
124
+ log.debug('goose', `stderr noise [${session.id}]`, text.slice(0, 400))
125
+ } else {
126
+ log.warn('goose', `stderr [${session.id}]`, text.slice(0, 400))
127
+ }
128
+ })
129
+
130
+ return new Promise((resolve) => {
131
+ proc.on('close', (code, sig) => {
132
+ active.delete(session.id)
133
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
134
+ const msg = stderrText.trim()
135
+ ? `Goose CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
136
+ : `Goose CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
137
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
138
+ }
139
+ log.info('goose', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
140
+ resolve(fullResponse.trim())
141
+ })
142
+
143
+ proc.on('error', (err) => {
144
+ active.delete(session.id)
145
+ write(`data: ${JSON.stringify({ t: 'err', text: err.message })}\n\n`)
146
+ resolve(fullResponse.trim())
147
+ })
148
+ })
149
+ }
@@ -3,6 +3,9 @@ import { streamCodexCliChat } from './codex-cli'
3
3
  import { streamOpenCodeCliChat } from './opencode-cli'
4
4
  import { streamGeminiCliChat } from './gemini-cli'
5
5
  import { streamCopilotCliChat } from './copilot-cli'
6
+ import { streamCursorCliChat } from './cursor-cli'
7
+ import { streamQwenCodeCliChat } from './qwen-code-cli'
8
+ import { streamGooseChat } from './goose'
6
9
  import { streamOpenAiChat } from './openai'
7
10
  import { streamOllamaChat } from './ollama'
8
11
  import { streamAnthropicChat } from './anthropic'
@@ -148,6 +151,31 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
148
151
  requiresEndpoint: false,
149
152
  handler: { streamChat: streamCopilotCliChat },
150
153
  },
154
+ 'cursor-cli': {
155
+ id: 'cursor-cli',
156
+ name: 'Cursor Agent CLI',
157
+ models: ['auto'],
158
+ requiresApiKey: false,
159
+ requiresEndpoint: false,
160
+ handler: { streamChat: streamCursorCliChat },
161
+ },
162
+ 'qwen-code-cli': {
163
+ id: 'qwen-code-cli',
164
+ name: 'Qwen Code CLI',
165
+ models: ['default'],
166
+ requiresApiKey: false,
167
+ requiresEndpoint: false,
168
+ handler: { streamChat: streamQwenCodeCliChat },
169
+ },
170
+ goose: {
171
+ id: 'goose',
172
+ name: 'Goose',
173
+ models: ['default'],
174
+ requiresApiKey: false,
175
+ optionalApiKey: true,
176
+ requiresEndpoint: false,
177
+ handler: { streamChat: streamGooseChat },
178
+ },
151
179
  google: {
152
180
  id: 'google',
153
181
  name: 'Google Gemini',
@@ -355,7 +383,7 @@ export function getProviderList(): ProviderInfo[] {
355
383
  ...info,
356
384
  models: overrides[info.id] || info.models,
357
385
  defaultModels: info.models,
358
- supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'fireworks'].includes(info.id),
386
+ supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks'].includes(info.id),
359
387
  }
360
388
  })
361
389
 
@@ -0,0 +1,152 @@
1
+ import { spawn } from 'child_process'
2
+ import type { StreamChatOptions } from './index'
3
+ import { log } from '../server/logger'
4
+ import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
5
+ import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
6
+
7
+ function buildQwenPrompt(message: string, systemPrompt?: string, imagePath?: string): string {
8
+ const parts: string[] = []
9
+ if (systemPrompt) parts.push(`[System instructions]\n${systemPrompt}`)
10
+ if (imagePath) parts.push(`[The user shared an image at: ${imagePath}]`)
11
+ parts.push(message)
12
+ return parts.join('\n\n')
13
+ }
14
+
15
+ function extractQwenAssistantText(event: Record<string, unknown>): string | null {
16
+ if (typeof event.result === 'string' && event.result.trim()) return event.result
17
+
18
+ if (event.type === 'assistant') {
19
+ const message = event.message as Record<string, unknown> | undefined
20
+ const content = Array.isArray(message?.content) ? message.content : []
21
+ const text = content
22
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object')
23
+ .map((entry) => typeof entry.text === 'string' ? entry.text : '')
24
+ .join('')
25
+ .trim()
26
+ if (text) return text
27
+ }
28
+
29
+ if (event.type === 'content_block_delta') {
30
+ const delta = event.delta as Record<string, unknown> | undefined
31
+ if (typeof delta?.text === 'string' && delta.text.trim()) return delta.text
32
+ }
33
+
34
+ return null
35
+ }
36
+
37
+ export function streamQwenCodeCliChat({ session, message, imagePath, systemPrompt, write, active, signal }: StreamChatOptions): Promise<string> {
38
+ const processTimeoutMs = loadRuntimeSettings().cliProcessTimeoutMs
39
+ const binary = resolveCliBinary('qwen')
40
+ if (!binary) {
41
+ const msg = 'Qwen Code CLI not found. Install `qwen` and ensure it is on your PATH.'
42
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
43
+ return Promise.resolve('')
44
+ }
45
+
46
+ const env = buildCliEnv()
47
+ if (!session.apiKey) {
48
+ const auth = probeCliAuth(binary, 'qwen', env, session.cwd)
49
+ if (!auth.authenticated) {
50
+ write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Qwen Code CLI is not configured.' })}\n\n`)
51
+ return Promise.resolve('')
52
+ }
53
+ }
54
+
55
+ const prompt = buildQwenPrompt(message, !session.qwenSessionId ? systemPrompt : undefined, imagePath)
56
+ const args = ['-p', prompt, '--output-format', 'stream-json', '--include-partial-messages', '--yolo']
57
+ if (session.qwenSessionId) args.push('--resume', session.qwenSessionId)
58
+ if (session.model && session.model !== 'default') args.push('--model', session.model)
59
+
60
+ log.info('qwen-code-cli', `Spawning: ${binary}`, {
61
+ args: args.map((value, index) => index === 1 && value.length > 120 ? `(${value.length} chars)` : value),
62
+ cwd: session.cwd,
63
+ hasSystemPrompt: Boolean(systemPrompt),
64
+ resumeSessionId: session.qwenSessionId || null,
65
+ })
66
+
67
+ const proc = spawn(binary, args, {
68
+ cwd: session.cwd,
69
+ env,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ timeout: processTimeoutMs,
72
+ })
73
+
74
+ active.set(session.id, proc)
75
+ attachAbortHandler(proc, signal)
76
+
77
+ let fullResponse = ''
78
+ let buf = ''
79
+ let stderrText = ''
80
+ let eventCount = 0
81
+
82
+ proc.stdout?.on('data', (chunk: Buffer) => {
83
+ buf += chunk.toString()
84
+ const lines = buf.split('\n')
85
+ buf = lines.pop() || ''
86
+
87
+ for (const line of lines) {
88
+ if (!line.trim()) continue
89
+ try {
90
+ const event = JSON.parse(line) as Record<string, unknown>
91
+ eventCount += 1
92
+ const sessionId = typeof event.session_id === 'string'
93
+ ? event.session_id
94
+ : typeof event.sessionId === 'string'
95
+ ? event.sessionId
96
+ : null
97
+ if (sessionId) session.qwenSessionId = sessionId
98
+
99
+ if (event.type === 'result' && event.subtype === 'error') {
100
+ const errText = typeof event.result === 'string' ? event.result : 'Qwen Code error'
101
+ write(`data: ${JSON.stringify({ t: 'err', text: errText })}\n\n`)
102
+ continue
103
+ }
104
+
105
+ const text = extractQwenAssistantText(event)
106
+ if (text) {
107
+ if (event.type === 'result' || event.type === 'assistant') {
108
+ fullResponse = text
109
+ write(`data: ${JSON.stringify({ t: 'r', text })}\n\n`)
110
+ } else {
111
+ fullResponse += text
112
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
113
+ }
114
+ }
115
+ } catch {
116
+ fullResponse += `${line}\n`
117
+ write(`data: ${JSON.stringify({ t: 'd', text: `${line}\n` })}\n\n`)
118
+ }
119
+ }
120
+ })
121
+
122
+ proc.stderr?.on('data', (chunk: Buffer) => {
123
+ const text = chunk.toString()
124
+ stderrText += text
125
+ if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
126
+ if (isStderrNoise(text)) {
127
+ log.debug('qwen-code-cli', `stderr noise [${session.id}]`, text.slice(0, 400))
128
+ } else {
129
+ log.warn('qwen-code-cli', `stderr [${session.id}]`, text.slice(0, 400))
130
+ }
131
+ })
132
+
133
+ return new Promise((resolve) => {
134
+ proc.on('close', (code, sig) => {
135
+ active.delete(session.id)
136
+ if ((code ?? 0) !== 0 && !fullResponse.trim()) {
137
+ const msg = stderrText.trim()
138
+ ? `Qwen Code CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
139
+ : `Qwen Code CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''} and returned no output.`
140
+ write(`data: ${JSON.stringify({ t: 'err', text: msg })}\n\n`)
141
+ }
142
+ log.info('qwen-code-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
143
+ resolve(fullResponse.trim())
144
+ })
145
+
146
+ proc.on('error', (err) => {
147
+ active.delete(session.id)
148
+ write(`data: ${JSON.stringify({ t: 'err', text: err.message })}\n\n`)
149
+ resolve(fullResponse.trim())
150
+ })
151
+ })
152
+ }
@@ -27,6 +27,6 @@ export function buildWorkerOnlyAgentMessage(
27
27
  const name = typeof agent?.name === 'string' && agent.name.trim()
28
28
  ? agent.name.trim()
29
29
  : 'This agent'
30
- if (action) return `${name} uses a runtime-managed provider and cannot ${action}. Runtime-managed agents can only be used for direct chats and delegation.`
31
- return `${name} uses a runtime-managed provider and cannot join chatrooms. Runtime-managed agents can only be used for direct chats and delegation.`
30
+ if (action) return `${name} uses a worker-only provider and cannot ${action}. Worker-only agents can only be used for direct chats and delegation.`
31
+ return `${name} uses a worker-only provider and cannot join chatrooms. Worker-only agents can only be used for direct chats and delegation.`
32
32
  }
@@ -13,6 +13,9 @@ function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']
13
13
  codex: null,
14
14
  opencode: null,
15
15
  gemini: null,
16
+ copilot: null,
17
+ cursor: null,
18
+ qwen: null,
16
19
  }
17
20
  }
18
21
 
@@ -40,6 +43,11 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
40
43
  claudeSessionId: existing?.claudeSessionId || null,
41
44
  codexThreadId: existing?.codexThreadId || null,
42
45
  opencodeSessionId: existing?.opencodeSessionId || null,
46
+ geminiSessionId: existing?.geminiSessionId || null,
47
+ copilotSessionId: existing?.copilotSessionId || null,
48
+ cursorSessionId: existing?.cursorSessionId || null,
49
+ qwenSessionId: existing?.qwenSessionId || null,
50
+ acpSessionId: existing?.acpSessionId || null,
43
51
  delegateResumeIds: existing?.delegateResumeIds || buildEmptyDelegateResumeIds(),
44
52
  messages: Array.isArray(existing?.messages) ? existing.messages : [],
45
53
  createdAt: existing?.createdAt || createdAt,
@@ -40,11 +40,19 @@ export function createAgentTaskSession(
40
40
  claudeSessionId: null,
41
41
  codexThreadId: null,
42
42
  opencodeSessionId: null,
43
+ geminiSessionId: null,
44
+ copilotSessionId: null,
45
+ cursorSessionId: null,
46
+ qwenSessionId: null,
47
+ acpSessionId: null,
43
48
  delegateResumeIds: {
44
49
  claudeCode: null,
45
50
  codex: null,
46
51
  opencode: null,
47
52
  gemini: null,
53
+ copilot: null,
54
+ cursor: null,
55
+ qwen: null,
48
56
  },
49
57
  messages: [],
50
58
  createdAt: Date.now(),
@@ -19,7 +19,7 @@ export interface CapabilityRoutingDecision {
19
19
  primaryUrl?: string
20
20
  }
21
21
 
22
- type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
22
+ type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli' | 'delegate_to_copilot_cli' | 'delegate_to_cursor_cli' | 'delegate_to_qwen_code_cli'
23
23
 
24
24
  function findFirstUrl(text: string): string | undefined {
25
25
  const m = text.match(/https?:\/\/[^\s<>"')]+/i)
@@ -41,6 +41,9 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
41
41
  'delegate_to_codex_cli',
42
42
  'delegate_to_opencode_cli',
43
43
  'delegate_to_gemini_cli',
44
+ 'delegate_to_copilot_cli',
45
+ 'delegate_to_cursor_cli',
46
+ 'delegate_to_qwen_code_cli',
44
47
  ]
45
48
  if (!Array.isArray(value) || !value.length) return fallback
46
49
 
@@ -50,6 +53,9 @@ function normalizeDelegateOrder(value: unknown): DelegateTool[] {
50
53
  else if (raw === 'codex') mapped.push('delegate_to_codex_cli')
51
54
  else if (raw === 'opencode') mapped.push('delegate_to_opencode_cli')
52
55
  else if (raw === 'gemini') mapped.push('delegate_to_gemini_cli')
56
+ else if (raw === 'copilot') mapped.push('delegate_to_copilot_cli')
57
+ else if (raw === 'cursor') mapped.push('delegate_to_cursor_cli')
58
+ else if (raw === 'qwen') mapped.push('delegate_to_qwen_code_cli')
53
59
  }
54
60
  if (!mapped.length) return fallback
55
61
  const deduped = dedup(mapped)
@@ -77,7 +83,7 @@ export function routeTaskIntent(
77
83
  return {
78
84
  intent: 'coding',
79
85
  confidence,
80
- preferredTools: ['claude_code', 'codex_cli', 'opencode_cli', 'shell', 'files', 'edit_file'],
86
+ preferredTools: ['claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli', 'cursor_cli', 'qwen_code_cli', 'shell', 'files', 'edit_file'],
81
87
  preferredDelegates: delegateOrder,
82
88
  primaryUrl: url,
83
89
  }
@@ -30,6 +30,9 @@ export type DelegateTool =
30
30
  | 'delegate_to_codex_cli'
31
31
  | 'delegate_to_opencode_cli'
32
32
  | 'delegate_to_gemini_cli'
33
+ | 'delegate_to_copilot_cli'
34
+ | 'delegate_to_cursor_cli'
35
+ | 'delegate_to_qwen_code_cli'
33
36
 
34
37
  export function applyContextClearBoundary(messages: Message[]): Message[] {
35
38
  const filterModelHistory = (items: Message[]) => items.filter((message) => message.historyExcluded !== true)
@@ -135,6 +138,12 @@ export function translateRequestedToolInvocation(
135
138
  if (requestedName === 'delegate_to_gemini_cli') {
136
139
  return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
137
140
  }
141
+ if (requestedName === 'delegate_to_cursor_cli') {
142
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'cursor' } }
143
+ }
144
+ if (requestedName === 'delegate_to_qwen_code_cli') {
145
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'qwen' } }
146
+ }
138
147
 
139
148
  const managePrefix = 'manage_'
140
149
  if (requestedName === 'manage_platform') {
@@ -297,6 +306,8 @@ export function requestedToolNamesFromMessage(message: string): string[] {
297
306
  'delegate_to_codex_cli',
298
307
  'delegate_to_opencode_cli',
299
308
  'delegate_to_gemini_cli',
309
+ 'delegate_to_cursor_cli',
310
+ 'delegate_to_qwen_code_cli',
300
311
  'connector_message_tool',
301
312
  'sessions_tool',
302
313
  'whoami_tool',
@@ -361,6 +372,8 @@ export function enabledDelegationTools(session: SessionWithTools): DelegateTool[
361
372
  if (hasToolEnabled(session, 'codex_cli')) tools.push('delegate_to_codex_cli')
362
373
  if (hasToolEnabled(session, 'opencode_cli')) tools.push('delegate_to_opencode_cli')
363
374
  if (hasToolEnabled(session, 'gemini_cli')) tools.push('delegate_to_gemini_cli')
375
+ if (hasToolEnabled(session, 'cursor_cli')) tools.push('delegate_to_cursor_cli')
376
+ if (hasToolEnabled(session, 'qwen_code_cli')) tools.push('delegate_to_qwen_code_cli')
364
377
  return tools
365
378
  }
366
379
 
@@ -396,6 +396,11 @@ export async function finalizeChatTurn(params: {
396
396
  persistField('claudeSessionId', session.claudeSessionId)
397
397
  persistField('codexThreadId', session.codexThreadId)
398
398
  persistField('opencodeSessionId', session.opencodeSessionId)
399
+ persistField('geminiSessionId', session.geminiSessionId)
400
+ persistField('copilotSessionId', session.copilotSessionId)
401
+ persistField('cursorSessionId', session.cursorSessionId)
402
+ persistField('qwenSessionId', session.qwenSessionId)
403
+ persistField('acpSessionId', session.acpSessionId)
399
404
 
400
405
  const sourceResume = session.delegateResumeIds
401
406
  if (sourceResume && typeof sourceResume === 'object') {
@@ -409,6 +414,9 @@ export async function finalizeChatTurn(params: {
409
414
  codex: normalizeResumeId(sr.codex ?? cr.codex),
410
415
  opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
411
416
  gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
417
+ copilot: normalizeResumeId(sr.copilot ?? cr.copilot),
418
+ cursor: normalizeResumeId(sr.cursor ?? cr.cursor),
419
+ qwen: normalizeResumeId(sr.qwen ?? cr.qwen),
412
420
  }
413
421
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
414
422
  current.delegateResumeIds = nextResume