@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
|
@@ -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
|
|
31
|
-
return `${name} uses a
|
|
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
|