@swarmclawai/swarmclaw 1.7.0 → 1.7.2
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 +25 -9
- package/bin/swarmclaw.js +87 -0
- package/electron-dist/main.js +218 -0
- package/package.json +2 -2
- package/scripts/run-next-build.mjs +1 -1
- package/src/app/api/setup/check-provider/route.ts +5 -62
- package/src/app/api/setup/doctor/route.ts +19 -9
- package/src/app/home/page.tsx +19 -10
- package/src/cli/index.js +8 -2
- package/src/cli/index.ts +12 -3
- package/src/components/agents/inspector-panel.tsx +25 -3
- package/src/components/auth/setup-wizard/index.tsx +6 -2
- package/src/components/auth/setup-wizard/step-next.tsx +46 -39
- package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
- package/src/components/auth/setup-wizard/types.ts +5 -2
- package/src/components/auth/setup-wizard/utils.test.ts +0 -19
- package/src/components/auth/setup-wizard/utils.ts +0 -69
- package/src/components/chat/chat-card.tsx +5 -0
- package/src/components/home/home-launchpad.tsx +123 -71
- package/src/components/layout/update-banner.tsx +43 -9
- package/src/lib/home-launchpad.test.ts +1 -31
- package/src/lib/home-launchpad.ts +0 -58
- package/src/lib/provider-sets.test.ts +19 -0
- package/src/lib/provider-sets.ts +8 -3
- package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
- package/src/lib/providers/cli-provider-metadata.ts +208 -0
- package/src/lib/providers/cli-utils.test.ts +65 -1
- package/src/lib/providers/cli-utils.ts +26 -44
- package/src/lib/providers/codex-cli.ts +71 -75
- package/src/lib/providers/generic-cli.ts +2 -31
- package/src/lib/providers/index.ts +14 -44
- package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
- package/src/lib/server/cli-provider-readiness.test.ts +45 -0
- package/src/lib/server/cli-provider-readiness.ts +84 -0
- package/src/lib/server/provider-health.test.ts +6 -0
- package/src/lib/server/provider-health.ts +2 -2
- package/src/lib/setup-defaults.test.ts +8 -0
- package/src/lib/setup-defaults.ts +38 -178
- package/src/stores/slices/session-slice.test.ts +40 -2
- package/src/stores/slices/session-slice.ts +41 -1
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ProviderType } from '../../types/provider.ts'
|
|
2
|
+
|
|
3
|
+
export type CliAuthBackend =
|
|
4
|
+
| 'claude'
|
|
5
|
+
| 'codex'
|
|
6
|
+
| 'opencode'
|
|
7
|
+
| 'gemini'
|
|
8
|
+
| 'copilot'
|
|
9
|
+
| 'droid'
|
|
10
|
+
| 'cursor'
|
|
11
|
+
| 'qwen'
|
|
12
|
+
| 'goose'
|
|
13
|
+
|
|
14
|
+
export interface CliProviderMetadata {
|
|
15
|
+
id: ProviderType
|
|
16
|
+
displayName: string
|
|
17
|
+
binaryName: string
|
|
18
|
+
capability: string
|
|
19
|
+
description: string
|
|
20
|
+
defaultModel: string
|
|
21
|
+
icon: string
|
|
22
|
+
setupBadge: string
|
|
23
|
+
generic: boolean
|
|
24
|
+
optionalApiKey?: boolean
|
|
25
|
+
authBackend?: CliAuthBackend
|
|
26
|
+
keyUrl?: string
|
|
27
|
+
keyLabel?: string
|
|
28
|
+
keyPlaceholder?: string
|
|
29
|
+
modelLibraryUrl?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const BESPOKE_CLI_PROVIDER_METADATA = [
|
|
33
|
+
{
|
|
34
|
+
id: 'claude-cli',
|
|
35
|
+
displayName: 'Claude Code CLI',
|
|
36
|
+
binaryName: 'claude',
|
|
37
|
+
capability: 'multi-file code editing, refactoring, debugging, code review',
|
|
38
|
+
description: "Anthropic's coding agent with native tools, strong edits, and first-class CLI workflows.",
|
|
39
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
40
|
+
icon: 'C',
|
|
41
|
+
setupBadge: 'CLI',
|
|
42
|
+
generic: false,
|
|
43
|
+
authBackend: 'claude',
|
|
44
|
+
modelLibraryUrl: 'https://docs.anthropic.com/en/docs/about-claude/models',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'codex-cli',
|
|
48
|
+
displayName: 'OpenAI Codex CLI',
|
|
49
|
+
binaryName: 'codex',
|
|
50
|
+
capability: 'code generation, file creation, automated coding tasks',
|
|
51
|
+
description: "OpenAI's terminal coding agent with resume support and structured headless output.",
|
|
52
|
+
defaultModel: 'gpt-5.4-codex',
|
|
53
|
+
icon: 'O',
|
|
54
|
+
setupBadge: 'CLI',
|
|
55
|
+
generic: false,
|
|
56
|
+
authBackend: 'codex',
|
|
57
|
+
modelLibraryUrl: 'https://platform.openai.com/docs/models',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'opencode-cli',
|
|
61
|
+
displayName: 'OpenCode CLI',
|
|
62
|
+
binaryName: 'opencode',
|
|
63
|
+
capability: 'code analysis, generation across multiple LLM backends',
|
|
64
|
+
description: 'A flexible coding CLI that can route across multiple model backends.',
|
|
65
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
66
|
+
icon: 'O',
|
|
67
|
+
setupBadge: 'CLI',
|
|
68
|
+
generic: false,
|
|
69
|
+
authBackend: 'opencode',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'gemini-cli',
|
|
73
|
+
displayName: 'Gemini CLI',
|
|
74
|
+
binaryName: 'gemini',
|
|
75
|
+
capability: 'code generation, analysis with Gemini models',
|
|
76
|
+
description: "Google's terminal coding agent with project-aware headless mode and resume support.",
|
|
77
|
+
defaultModel: 'gemini-3.1-pro',
|
|
78
|
+
icon: 'G',
|
|
79
|
+
setupBadge: 'CLI',
|
|
80
|
+
generic: false,
|
|
81
|
+
authBackend: 'gemini',
|
|
82
|
+
modelLibraryUrl: 'https://ai.google.dev/gemini-api/docs/models',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'copilot-cli',
|
|
86
|
+
displayName: 'GitHub Copilot CLI',
|
|
87
|
+
binaryName: 'copilot',
|
|
88
|
+
capability: 'code generation, analysis, multi-model support via GitHub Copilot',
|
|
89
|
+
description: "GitHub's multi-model terminal agent for coding and automation.",
|
|
90
|
+
defaultModel: 'claude-sonnet-4-6',
|
|
91
|
+
icon: 'P',
|
|
92
|
+
setupBadge: 'CLI',
|
|
93
|
+
generic: false,
|
|
94
|
+
authBackend: 'copilot',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'droid-cli',
|
|
98
|
+
displayName: 'Factory Droid CLI',
|
|
99
|
+
binaryName: 'droid',
|
|
100
|
+
capability: 'code generation, refactoring, and automation via Factory Droid with configurable autonomy',
|
|
101
|
+
description: "Factory.ai's terminal coding agent with headless exec mode, session resume, and autonomy controls.",
|
|
102
|
+
defaultModel: 'default',
|
|
103
|
+
icon: 'F',
|
|
104
|
+
setupBadge: 'CLI',
|
|
105
|
+
generic: false,
|
|
106
|
+
optionalApiKey: true,
|
|
107
|
+
authBackend: 'droid',
|
|
108
|
+
keyUrl: 'https://app.factory.ai/settings/api-keys',
|
|
109
|
+
keyLabel: 'app.factory.ai',
|
|
110
|
+
keyPlaceholder: 'FACTORY_API_KEY (optional if signed in via `droid`)',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'cursor-cli',
|
|
114
|
+
displayName: 'Cursor Agent CLI',
|
|
115
|
+
binaryName: 'cursor-agent',
|
|
116
|
+
capability: 'full-agent coding workflows, multi-file edits, project-aware code changes',
|
|
117
|
+
description: "Cursor's terminal agent with resume support, JSON output, and Cursor-native coding workflows.",
|
|
118
|
+
defaultModel: 'auto',
|
|
119
|
+
icon: 'U',
|
|
120
|
+
setupBadge: 'CLI',
|
|
121
|
+
generic: false,
|
|
122
|
+
authBackend: 'cursor',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'qwen-code-cli',
|
|
126
|
+
displayName: 'Qwen Code CLI',
|
|
127
|
+
binaryName: 'qwen',
|
|
128
|
+
capability: 'terminal-native coding workflows, code generation, review, and automation',
|
|
129
|
+
description: "Qwen's terminal coding agent with structured headless mode and multi-provider model config.",
|
|
130
|
+
defaultModel: 'default',
|
|
131
|
+
icon: 'Q',
|
|
132
|
+
setupBadge: 'CLI',
|
|
133
|
+
generic: false,
|
|
134
|
+
authBackend: 'qwen',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'goose',
|
|
138
|
+
displayName: 'Goose',
|
|
139
|
+
binaryName: 'goose',
|
|
140
|
+
capability: 'agentic coding workflows with extensions, tools, and runtime-managed execution',
|
|
141
|
+
description: 'A runtime-managed terminal agent with extensions, session history, and ACP support.',
|
|
142
|
+
defaultModel: 'default',
|
|
143
|
+
icon: 'G',
|
|
144
|
+
setupBadge: 'Runtime',
|
|
145
|
+
generic: false,
|
|
146
|
+
optionalApiKey: true,
|
|
147
|
+
authBackend: 'goose',
|
|
148
|
+
},
|
|
149
|
+
] as const satisfies readonly CliProviderMetadata[]
|
|
150
|
+
|
|
151
|
+
export const GENERIC_CLI_PROVIDER_METADATA = [
|
|
152
|
+
['aider-cli', 'Aider CLI', 'aider', 'paired-programming-style multi-file edits and git-aware code changes'],
|
|
153
|
+
['amp-cli', 'Amp CLI', 'amp', 'agentic coding via Sourcegraph Amp'],
|
|
154
|
+
['augment-cli', 'Augment CLI', 'augment', 'codebase-aware agentic edits via Augment'],
|
|
155
|
+
['adal-cli', 'AdaL CLI', 'adal', 'AdaL coding agent for terminal-driven workflows'],
|
|
156
|
+
['bob-cli', 'IBM Bob CLI', 'bob', 'IBM watsonx Code Assistant terminal coding workflows'],
|
|
157
|
+
['cline-cli', 'Cline CLI', 'cline', 'autonomous file-level edits and terminal automation via Cline'],
|
|
158
|
+
['codebuddy-cli', 'CodeBuddy CLI', 'codebuddy', 'CodeBuddy agentic coding workflows'],
|
|
159
|
+
['command-code-cli', 'Command Code CLI', 'commandcode', 'Command Code terminal-native coding agent'],
|
|
160
|
+
['continue-cli', 'Continue CLI', 'continue', 'agentic coding via the Continue CLI'],
|
|
161
|
+
['cortex-cli', 'Cortex Code CLI', 'cortex', 'Snowflake Cortex Code agentic workflows'],
|
|
162
|
+
['crush-cli', 'Crush CLI', 'crush', 'Crush terminal coding agent'],
|
|
163
|
+
['deepagents-cli', 'Deep Agents CLI', 'deepagents', 'long-horizon planning and multi-step coding via Deep Agents'],
|
|
164
|
+
['firebender-cli', 'Firebender CLI', 'firebender', 'Firebender JetBrains-aligned coding agent'],
|
|
165
|
+
['iflow-cli', 'iFlow CLI', 'iflow', 'iFlow CLI agentic coding workflows'],
|
|
166
|
+
['junie-cli', 'Junie CLI', 'junie', 'JetBrains Junie coding agent for terminal use'],
|
|
167
|
+
['kilo-code-cli', 'Kilo Code CLI', 'kilocode', 'Kilo Code agentic coding workflows'],
|
|
168
|
+
['kimi-cli', 'Kimi CLI', 'kimi', 'Kimi Code CLI coding agent'],
|
|
169
|
+
['kode-cli', 'Kode CLI', 'kode', 'Kode terminal coding agent'],
|
|
170
|
+
['mcpjam-cli', 'MCPJam CLI', 'mcpjam', 'MCPJam-tooled agentic coding workflows'],
|
|
171
|
+
['mistral-vibe-cli', 'Mistral Vibe CLI', 'vibe', 'Mistral Vibe coding agent'],
|
|
172
|
+
['mux-cli', 'Mux CLI', 'mux', 'Mux multi-tool coding agent'],
|
|
173
|
+
['neovate-cli', 'Neovate CLI', 'neovate', 'Neovate coding agent for terminal workflows'],
|
|
174
|
+
['openhands-cli', 'OpenHands CLI', 'openhands', 'OpenHands agentic coding via terminal'],
|
|
175
|
+
['pochi-cli', 'Pochi CLI', 'pochi', 'Pochi coding agent'],
|
|
176
|
+
['qoder-cli', 'Qoder CLI', 'qoder', 'Qoder agentic coding workflows'],
|
|
177
|
+
['replit-cli', 'Replit Agent CLI', 'replit', 'Replit Agent terminal coding workflows'],
|
|
178
|
+
['roo-code-cli', 'Roo Code CLI', 'roo', 'Roo Code agentic coding workflows'],
|
|
179
|
+
['trae-cn-cli', 'TRAE CN CLI', 'trae-cn', 'TRAE CN coding agent'],
|
|
180
|
+
['warp-cli', 'Warp Agent CLI', 'warp', 'Warp Agent terminal-native coding workflows'],
|
|
181
|
+
['windsurf-cli', 'Windsurf CLI', 'windsurf', 'Windsurf agentic coding workflows'],
|
|
182
|
+
['zencoder-cli', 'Zencoder CLI', 'zencoder', 'Zencoder agentic coding workflows'],
|
|
183
|
+
].map(([id, displayName, binaryName, capability]) => ({
|
|
184
|
+
id: id as ProviderType,
|
|
185
|
+
displayName,
|
|
186
|
+
binaryName,
|
|
187
|
+
capability,
|
|
188
|
+
description: `${displayName}: ${capability}.`,
|
|
189
|
+
defaultModel: 'default',
|
|
190
|
+
icon: displayName.charAt(0),
|
|
191
|
+
setupBadge: 'CLI',
|
|
192
|
+
generic: true,
|
|
193
|
+
optionalApiKey: true,
|
|
194
|
+
})) satisfies readonly CliProviderMetadata[]
|
|
195
|
+
|
|
196
|
+
export const CLI_PROVIDER_METADATA = [
|
|
197
|
+
...BESPOKE_CLI_PROVIDER_METADATA,
|
|
198
|
+
...GENERIC_CLI_PROVIDER_METADATA,
|
|
199
|
+
] as const satisfies readonly CliProviderMetadata[]
|
|
200
|
+
|
|
201
|
+
export type CliProviderId = (typeof CLI_PROVIDER_METADATA)[number]['id']
|
|
202
|
+
|
|
203
|
+
export const CLI_PROVIDER_METADATA_BY_ID: Record<string, CliProviderMetadata> =
|
|
204
|
+
Object.fromEntries(CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider]))
|
|
205
|
+
|
|
206
|
+
export function isCliProviderId(providerId: string): providerId is CliProviderId {
|
|
207
|
+
return providerId in CLI_PROVIDER_METADATA_BY_ID
|
|
208
|
+
}
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
2
5
|
import { describe, it } from 'node:test'
|
|
3
6
|
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isStderrNoise,
|
|
9
|
+
buildCliEnv,
|
|
10
|
+
isCliProvider,
|
|
11
|
+
CLI_PROVIDER_CAPABILITIES,
|
|
12
|
+
resolveCodexProbeInvocation,
|
|
13
|
+
ensureCliWorkingDirectory,
|
|
14
|
+
} from './cli-utils'
|
|
5
15
|
|
|
6
16
|
// ---------------------------------------------------------------------------
|
|
7
17
|
// isStderrNoise
|
|
@@ -142,3 +152,57 @@ describe('CLI_PROVIDER_CAPABILITIES', () => {
|
|
|
142
152
|
}
|
|
143
153
|
})
|
|
144
154
|
})
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// resolveCodexProbeInvocation
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe('resolveCodexProbeInvocation', () => {
|
|
161
|
+
it('uses node directly for codex JS wrappers discovered through a symlink', () => {
|
|
162
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-codex-probe-'))
|
|
163
|
+
try {
|
|
164
|
+
const realScript = path.join(tmpRoot, 'codex.js')
|
|
165
|
+
fs.writeFileSync(realScript, '#!/usr/bin/env node\nconsole.log("ok")\n')
|
|
166
|
+
fs.chmodSync(realScript, 0o755)
|
|
167
|
+
|
|
168
|
+
const wrapperPath = path.join(tmpRoot, 'codex')
|
|
169
|
+
fs.symlinkSync(realScript, wrapperPath)
|
|
170
|
+
|
|
171
|
+
const invocation = resolveCodexProbeInvocation(wrapperPath)
|
|
172
|
+
assert.equal(invocation.command, process.execPath)
|
|
173
|
+
assert.deepEqual(invocation.args, [fs.realpathSync(realScript), 'login', 'status'])
|
|
174
|
+
} finally {
|
|
175
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('uses the binary directly for non-JS executables', () => {
|
|
180
|
+
const invocation = resolveCodexProbeInvocation('/usr/local/bin/codex-bin')
|
|
181
|
+
assert.equal(invocation.command, '/usr/local/bin/codex-bin')
|
|
182
|
+
assert.deepEqual(invocation.args, ['login', 'status'])
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// ensureCliWorkingDirectory
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe('ensureCliWorkingDirectory', () => {
|
|
191
|
+
it('creates a missing working directory recursively', () => {
|
|
192
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-cwd-'))
|
|
193
|
+
const target = path.join(tmpRoot, 'nested', 'room')
|
|
194
|
+
try {
|
|
195
|
+
assert.equal(fs.existsSync(target), false)
|
|
196
|
+
const resolved = ensureCliWorkingDirectory(target)
|
|
197
|
+
assert.equal(resolved, target)
|
|
198
|
+
assert.equal(fs.existsSync(target), true)
|
|
199
|
+
assert.equal(fs.statSync(target).isDirectory(), true)
|
|
200
|
+
} finally {
|
|
201
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('falls back to process.cwd when cwd is blank', () => {
|
|
206
|
+
assert.equal(ensureCliWorkingDirectory(''), process.cwd())
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -10,7 +10,9 @@ import os from 'os'
|
|
|
10
10
|
import { findBinaryOnPath } from '../server/session-tools/context'
|
|
11
11
|
import path from 'path'
|
|
12
12
|
import { spawnSync, type ChildProcess } from 'child_process'
|
|
13
|
+
import { realpathSync } from 'fs'
|
|
13
14
|
import { log } from '../server/logger'
|
|
15
|
+
import { CLI_PROVIDER_METADATA, CLI_PROVIDER_METADATA_BY_ID } from './cli-provider-metadata'
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Binary Discovery
|
|
@@ -168,6 +170,25 @@ export interface AuthProbeResult {
|
|
|
168
170
|
errorMessage?: string
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
export function resolveCodexProbeInvocation(binary: string): { command: string; args: string[] } {
|
|
174
|
+
try {
|
|
175
|
+
const resolved = realpathSync(binary)
|
|
176
|
+
if (resolved.toLowerCase().endsWith('.js')) {
|
|
177
|
+
return { command: process.execPath, args: [resolved, 'login', 'status'] }
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Fall through to direct execution when the binary cannot be resolved.
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { command: binary, args: ['login', 'status'] }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function ensureCliWorkingDirectory(cwd?: string | null): string {
|
|
187
|
+
const resolved = typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd()
|
|
188
|
+
fs.mkdirSync(resolved, { recursive: true })
|
|
189
|
+
return resolved
|
|
190
|
+
}
|
|
191
|
+
|
|
171
192
|
/**
|
|
172
193
|
* Unified auth check for supported CLI-backed providers.
|
|
173
194
|
*/
|
|
@@ -198,7 +219,8 @@ export function probeCliAuth(
|
|
|
198
219
|
}
|
|
199
220
|
|
|
200
221
|
if (backend === 'codex') {
|
|
201
|
-
const
|
|
222
|
+
const invocation = resolveCodexProbeInvocation(binary)
|
|
223
|
+
const probe = spawnSync(invocation.command, invocation.args, {
|
|
202
224
|
cwd, env, encoding: 'utf-8', timeout: 8000,
|
|
203
225
|
})
|
|
204
226
|
const probeText = `${probe.stdout || ''}\n${probe.stderr || ''}`.toLowerCase()
|
|
@@ -447,50 +469,10 @@ export function symlinkConfigFiles(
|
|
|
447
469
|
// ---------------------------------------------------------------------------
|
|
448
470
|
|
|
449
471
|
/** Human-readable descriptions of what each CLI provider excels at. */
|
|
450
|
-
export const CLI_PROVIDER_CAPABILITIES: Record<string, string> =
|
|
451
|
-
|
|
452
|
-
'codex-cli': 'code generation, file creation, automated coding tasks',
|
|
453
|
-
'opencode-cli': 'code analysis, generation across multiple LLM backends',
|
|
454
|
-
'gemini-cli': 'code generation, analysis with Gemini models',
|
|
455
|
-
'copilot-cli': 'code generation, analysis, multi-model support via GitHub Copilot',
|
|
456
|
-
'droid-cli': 'code generation, refactoring, and automation via Factory Droid with configurable autonomy',
|
|
457
|
-
'cursor-cli': 'full-agent coding workflows, multi-file edits, project-aware code changes',
|
|
458
|
-
'qwen-code-cli': 'terminal-native coding workflows, code generation, review, and automation',
|
|
459
|
-
goose: 'agentic coding workflows with extensions, tools, and runtime-managed execution',
|
|
460
|
-
'aider-cli': 'paired-programming-style multi-file edits and git-aware code changes',
|
|
461
|
-
'amp-cli': 'agentic coding via Sourcegraph Amp',
|
|
462
|
-
'augment-cli': 'codebase-aware agentic edits via Augment',
|
|
463
|
-
'adal-cli': 'AdaL coding agent for terminal-driven workflows',
|
|
464
|
-
'bob-cli': 'IBM watsonx Code Assistant (Bob) terminal coding workflows',
|
|
465
|
-
'cline-cli': 'autonomous file-level edits and terminal automation via Cline',
|
|
466
|
-
'codebuddy-cli': 'CodeBuddy agentic coding workflows',
|
|
467
|
-
'command-code-cli': 'Command Code terminal-native coding agent',
|
|
468
|
-
'continue-cli': 'agentic coding via the Continue CLI',
|
|
469
|
-
'cortex-cli': 'Snowflake Cortex Code agentic workflows',
|
|
470
|
-
'crush-cli': 'Crush terminal coding agent',
|
|
471
|
-
'deepagents-cli': 'long-horizon planning and multi-step coding via Deep Agents',
|
|
472
|
-
'firebender-cli': 'Firebender JetBrains-aligned coding agent',
|
|
473
|
-
'iflow-cli': 'iFlow CLI agentic coding workflows',
|
|
474
|
-
'junie-cli': 'JetBrains Junie coding agent for terminal use',
|
|
475
|
-
'kilo-code-cli': 'Kilo Code agentic coding workflows',
|
|
476
|
-
'kimi-cli': 'Kimi Code CLI coding agent',
|
|
477
|
-
'kode-cli': 'Kode terminal coding agent',
|
|
478
|
-
'mcpjam-cli': 'MCPJam-tooled agentic coding workflows',
|
|
479
|
-
'mistral-vibe-cli': 'Mistral Vibe coding agent',
|
|
480
|
-
'mux-cli': 'Mux multi-tool coding agent',
|
|
481
|
-
'neovate-cli': 'Neovate coding agent for terminal workflows',
|
|
482
|
-
'openhands-cli': 'OpenHands agentic coding via terminal',
|
|
483
|
-
'pochi-cli': 'Pochi coding agent',
|
|
484
|
-
'qoder-cli': 'Qoder agentic coding workflows',
|
|
485
|
-
'replit-cli': 'Replit Agent terminal coding workflows',
|
|
486
|
-
'roo-code-cli': 'Roo Code agentic coding workflows',
|
|
487
|
-
'trae-cn-cli': 'TRAE CN coding agent',
|
|
488
|
-
'warp-cli': 'Warp Agent terminal-native coding workflows',
|
|
489
|
-
'windsurf-cli': 'Windsurf agentic coding workflows',
|
|
490
|
-
'zencoder-cli': 'Zencoder agentic coding workflows',
|
|
491
|
-
}
|
|
472
|
+
export const CLI_PROVIDER_CAPABILITIES: Record<string, string> =
|
|
473
|
+
Object.fromEntries(CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider.capability]))
|
|
492
474
|
|
|
493
475
|
/** Check if a provider ID is a CLI-based provider. */
|
|
494
476
|
export function isCliProvider(providerId: string): boolean {
|
|
495
|
-
return providerId in
|
|
477
|
+
return providerId in CLI_PROVIDER_METADATA_BY_ID
|
|
496
478
|
}
|
|
@@ -5,7 +5,7 @@ import { spawn } from 'child_process'
|
|
|
5
5
|
import type { StreamChatOptions } from './index'
|
|
6
6
|
import { log } from '../server/logger'
|
|
7
7
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
8
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise } from './cli-utils'
|
|
8
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, symlinkConfigFiles, isStderrNoise, ensureCliWorkingDirectory } from './cli-utils'
|
|
9
9
|
import { getAgent } from '@/lib/server/agents/agent-repository'
|
|
10
10
|
import { loadMcpServers } from '@/lib/server/storage'
|
|
11
11
|
|
|
@@ -27,6 +27,9 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
27
27
|
|
|
28
28
|
const prompt = message
|
|
29
29
|
const args: string[] = ['exec']
|
|
30
|
+
// Use ~/.codex-sessions/ not /tmp — codex refuses to create helper binaries under /tmp.
|
|
31
|
+
const sessionsDir = path.join(os.homedir(), '.codex-sessions')
|
|
32
|
+
const perSessionHome = path.join(sessionsDir, session.id)
|
|
30
33
|
|
|
31
34
|
// Session resume
|
|
32
35
|
if (session.codexThreadId) {
|
|
@@ -53,15 +56,16 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
53
56
|
|
|
54
57
|
// Build clean env — preserves user's CODEX_HOME for auth
|
|
55
58
|
const env = buildCliEnv()
|
|
59
|
+
const effectiveCwd = ensureCliWorkingDirectory(session.cwd)
|
|
56
60
|
|
|
57
61
|
// Pass API key if available
|
|
58
62
|
if (session.apiKey) {
|
|
59
63
|
env.OPENAI_API_KEY = session.apiKey
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
// Auth probe BEFORE creating
|
|
66
|
+
// Auth probe BEFORE creating the session CODEX_HOME — uses real config dir
|
|
63
67
|
if (!session.apiKey) {
|
|
64
|
-
const auth = probeCliAuth(binary, 'codex', env,
|
|
68
|
+
const auth = probeCliAuth(binary, 'codex', env, effectiveCwd)
|
|
65
69
|
if (!auth.authenticated) {
|
|
66
70
|
log.error('codex-cli', auth.errorMessage || 'Auth failed')
|
|
67
71
|
write(`data: ${JSON.stringify({ t: 'err', text: auth.errorMessage || 'Codex CLI is not authenticated.' })}\n\n`)
|
|
@@ -69,86 +73,80 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
76
|
+
// Always use a stable per-session CODEX_HOME for the actual Codex run. A first
|
|
77
|
+
// turn can emit a thread id even without system-prompt or MCP injection, and
|
|
78
|
+
// the next turn needs the same local metadata to resume that thread.
|
|
79
|
+
const sessionCodexHome = perSessionHome
|
|
75
80
|
const agentForMcp = session.agentId ? getAgent(session.agentId as string) : null
|
|
76
81
|
const agentMcpServerIds: string[] = agentForMcp?.mcpServerIds || []
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const realCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
|
|
80
|
-
// Use ~/.codex-sessions/ not /tmp — codex refuses to create helper binaries under /tmp
|
|
81
|
-
const sessionsDir = path.join(os.homedir(), '.codex-sessions')
|
|
82
|
-
tempCodexHome = path.join(sessionsDir, session.id)
|
|
83
|
-
fs.mkdirSync(tempCodexHome, { recursive: true })
|
|
84
|
-
|
|
85
|
-
// Symlink auth/config files from real CODEX_HOME into temp dir
|
|
86
|
-
symlinkConfigFiles(realCodexHome, tempCodexHome)
|
|
87
|
-
|
|
88
|
-
// Write system prompt as AGENTS.override.md (first turn only)
|
|
89
|
-
if (systemPrompt && !session.codexThreadId) {
|
|
90
|
-
fs.writeFileSync(path.join(tempCodexHome, 'AGENTS.override.md'), systemPrompt)
|
|
91
|
-
}
|
|
82
|
+
const realCodexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
|
|
83
|
+
fs.mkdirSync(sessionCodexHome, { recursive: true })
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
85
|
+
// Symlink auth/config files from real CODEX_HOME into session dir
|
|
86
|
+
symlinkConfigFiles(realCodexHome, sessionCodexHome)
|
|
87
|
+
|
|
88
|
+
// Write system prompt as AGENTS.override.md (first turn only)
|
|
89
|
+
if (systemPrompt && !session.codexThreadId) {
|
|
90
|
+
fs.writeFileSync(path.join(sessionCodexHome, 'AGENTS.override.md'), systemPrompt)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Inject agent-assigned MCP servers into config.toml
|
|
94
|
+
if (agentMcpServerIds.length > 0) {
|
|
95
|
+
try {
|
|
96
|
+
const allMcpServers = loadMcpServers()
|
|
97
|
+
const tomlParts: string[] = []
|
|
98
|
+
for (const serverId of agentMcpServerIds) {
|
|
99
|
+
const config = allMcpServers[serverId]
|
|
100
|
+
if (!config) continue
|
|
101
|
+
const name = config.name.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
102
|
+
if (config.transport === 'stdio' && config.command) {
|
|
103
|
+
tomlParts.push(`[mcp_servers.${name}]`)
|
|
104
|
+
tomlParts.push(`command = ${JSON.stringify(config.command)}`)
|
|
105
|
+
const argsStr = (config.args || []).map((a: string) => JSON.stringify(a)).join(', ')
|
|
106
|
+
tomlParts.push(`args = [${argsStr}]`)
|
|
107
|
+
if (config.cwd) tomlParts.push(`cwd = ${JSON.stringify(config.cwd)}`)
|
|
108
|
+
tomlParts.push('')
|
|
109
|
+
// Env vars go in a separate subsection: [mcp_servers.name.env]
|
|
110
|
+
if (config.env && Object.keys(config.env).length > 0) {
|
|
111
|
+
tomlParts.push(`[mcp_servers.${name}.env]`)
|
|
112
|
+
for (const [k, v] of Object.entries(config.env as Record<string, string>)) {
|
|
113
|
+
tomlParts.push(`${k} = ${JSON.stringify(v)}`)
|
|
116
114
|
}
|
|
117
|
-
} else if ((config.transport === 'sse' || config.transport === 'streamable-http') && config.url) {
|
|
118
|
-
tomlParts.push(`[mcp_servers.${name}]`)
|
|
119
|
-
tomlParts.push(`url = ${JSON.stringify(config.url)}`)
|
|
120
115
|
tomlParts.push('')
|
|
121
116
|
}
|
|
117
|
+
} else if ((config.transport === 'sse' || config.transport === 'streamable-http') && config.url) {
|
|
118
|
+
tomlParts.push(`[mcp_servers.${name}]`)
|
|
119
|
+
tomlParts.push(`url = ${JSON.stringify(config.url)}`)
|
|
120
|
+
tomlParts.push('')
|
|
122
121
|
}
|
|
123
|
-
if (tomlParts.length > 0) {
|
|
124
|
-
const realConfigPath = path.join(realCodexHome, 'config.toml')
|
|
125
|
-
const existingConfig = fs.existsSync(realConfigPath)
|
|
126
|
-
? fs.readFileSync(realConfigPath, 'utf-8')
|
|
127
|
-
: ''
|
|
128
|
-
const tempConfigPath = path.join(tempCodexHome, 'config.toml')
|
|
129
|
-
// Remove symlink created by symlinkConfigFiles before writing our own file
|
|
130
|
-
try { fs.unlinkSync(tempConfigPath) } catch { /* no symlink — ignore */ }
|
|
131
|
-
fs.writeFileSync(tempConfigPath, existingConfig + '\n' + tomlParts.join('\n'))
|
|
132
|
-
log.info('codex-cli', `Injecting ${agentMcpServerIds.length} MCP server(s) via config.toml`)
|
|
133
|
-
}
|
|
134
|
-
} catch (mcpErr) {
|
|
135
|
-
log.warn('codex-cli', `Failed to build MCP config: ${mcpErr}`)
|
|
136
122
|
}
|
|
123
|
+
if (tomlParts.length > 0) {
|
|
124
|
+
const realConfigPath = path.join(realCodexHome, 'config.toml')
|
|
125
|
+
const existingConfig = fs.existsSync(realConfigPath)
|
|
126
|
+
? fs.readFileSync(realConfigPath, 'utf-8')
|
|
127
|
+
: ''
|
|
128
|
+
const tempConfigPath = path.join(sessionCodexHome, 'config.toml')
|
|
129
|
+
// Remove symlink created by symlinkConfigFiles before writing our own file
|
|
130
|
+
try { fs.unlinkSync(tempConfigPath) } catch { /* no symlink — ignore */ }
|
|
131
|
+
fs.writeFileSync(tempConfigPath, existingConfig + '\n' + tomlParts.join('\n'))
|
|
132
|
+
log.info('codex-cli', `Injecting ${agentMcpServerIds.length} MCP server(s) via config.toml`)
|
|
133
|
+
}
|
|
134
|
+
} catch (mcpErr) {
|
|
135
|
+
log.warn('codex-cli', `Failed to build MCP config: ${mcpErr}`)
|
|
137
136
|
}
|
|
138
|
-
|
|
139
|
-
env.CODEX_HOME = tempCodexHome
|
|
140
137
|
}
|
|
138
|
+
env.CODEX_HOME = sessionCodexHome
|
|
141
139
|
|
|
142
140
|
log.info('codex-cli', `Spawning: ${binary}`, {
|
|
143
141
|
args: args.map(a => a.length > 100 ? a.slice(0, 100) + '...' : a),
|
|
144
|
-
cwd:
|
|
142
|
+
cwd: effectiveCwd,
|
|
145
143
|
promptLen: prompt.length,
|
|
146
144
|
hasSystemPrompt: !!systemPrompt,
|
|
147
|
-
|
|
145
|
+
sessionCodexHome,
|
|
148
146
|
})
|
|
149
147
|
|
|
150
148
|
const proc = spawn(binary, args, {
|
|
151
|
-
cwd:
|
|
149
|
+
cwd: effectiveCwd,
|
|
152
150
|
env,
|
|
153
151
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
152
|
timeout: processTimeoutMs,
|
|
@@ -185,9 +183,14 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
185
183
|
eventCount++
|
|
186
184
|
|
|
187
185
|
// Track thread ID for session resume
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
const threadId = typeof ev.thread_id === 'string'
|
|
187
|
+
? ev.thread_id
|
|
188
|
+
: (typeof ev.thread?.id === 'string' ? ev.thread.id : null)
|
|
189
|
+
if (threadId) {
|
|
190
|
+
session.codexThreadId = threadId
|
|
191
|
+
if (eventCount <= 3 || ev.type === 'thread.started') {
|
|
192
|
+
log.info('codex-cli', `Got thread_id: ${threadId} (${ev.type})`)
|
|
193
|
+
}
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
// Streaming text deltas (if codex adds streaming support)
|
|
@@ -269,10 +272,6 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
269
272
|
proc.on('close', (code, sig) => {
|
|
270
273
|
log.info('codex-cli', `Process closed: code=${code} signal=${sig} events=${eventCount} response=${fullResponse.length}chars`)
|
|
271
274
|
active.delete(session.id)
|
|
272
|
-
// Clean up temp CODEX_HOME
|
|
273
|
-
if (tempCodexHome) {
|
|
274
|
-
try { fs.rmSync(tempCodexHome, { recursive: true }) } catch { /* ignore */ }
|
|
275
|
-
}
|
|
276
275
|
if ((code ?? 0) !== 0 && !fullResponse.trim()) {
|
|
277
276
|
const msg = stderrText.trim()
|
|
278
277
|
? `Codex CLI exited with code ${code ?? 'unknown'}${sig ? ` (${sig})` : ''}: ${stderrText.trim().slice(0, 1200)}`
|
|
@@ -285,9 +284,6 @@ export function streamCodexCliChat({ session, message, imagePath, systemPrompt,
|
|
|
285
284
|
proc.on('error', (e) => {
|
|
286
285
|
log.error('codex-cli', `Process error: ${e.message}`)
|
|
287
286
|
active.delete(session.id)
|
|
288
|
-
if (tempCodexHome) {
|
|
289
|
-
try { fs.rmSync(tempCodexHome, { recursive: true }) } catch { /* ignore */ }
|
|
290
|
-
}
|
|
291
287
|
write(`data: ${JSON.stringify({ t: 'err', text: e.message })}\n\n`)
|
|
292
288
|
resolve(fullResponse)
|
|
293
289
|
})
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process'
|
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { log } from '../server/logger'
|
|
4
4
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
+
import { GENERIC_CLI_PROVIDER_METADATA } from './cli-provider-metadata'
|
|
5
6
|
import { resolveCliBinary, buildCliEnv, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -9,37 +10,7 @@ import { resolveCliBinary, buildCliEnv, attachAbortHandler, isStderrNoise } from
|
|
|
9
10
|
* Used by the generic CLI streamer for tools without a bespoke handler.
|
|
10
11
|
*/
|
|
11
12
|
export const GENERIC_CLI_BINARIES: Record<string, string> = {
|
|
12
|
-
|
|
13
|
-
'amp-cli': 'amp',
|
|
14
|
-
'augment-cli': 'augment',
|
|
15
|
-
'adal-cli': 'adal',
|
|
16
|
-
'bob-cli': 'bob',
|
|
17
|
-
'cline-cli': 'cline',
|
|
18
|
-
'codebuddy-cli': 'codebuddy',
|
|
19
|
-
'command-code-cli': 'commandcode',
|
|
20
|
-
'continue-cli': 'continue',
|
|
21
|
-
'cortex-cli': 'cortex',
|
|
22
|
-
'crush-cli': 'crush',
|
|
23
|
-
'deepagents-cli': 'deepagents',
|
|
24
|
-
'firebender-cli': 'firebender',
|
|
25
|
-
'iflow-cli': 'iflow',
|
|
26
|
-
'junie-cli': 'junie',
|
|
27
|
-
'kilo-code-cli': 'kilocode',
|
|
28
|
-
'kimi-cli': 'kimi',
|
|
29
|
-
'kode-cli': 'kode',
|
|
30
|
-
'mcpjam-cli': 'mcpjam',
|
|
31
|
-
'mistral-vibe-cli': 'vibe',
|
|
32
|
-
'mux-cli': 'mux',
|
|
33
|
-
'neovate-cli': 'neovate',
|
|
34
|
-
'openhands-cli': 'openhands',
|
|
35
|
-
'pochi-cli': 'pochi',
|
|
36
|
-
'qoder-cli': 'qoder',
|
|
37
|
-
'replit-cli': 'replit',
|
|
38
|
-
'roo-code-cli': 'roo',
|
|
39
|
-
'trae-cn-cli': 'trae-cn',
|
|
40
|
-
'warp-cli': 'warp',
|
|
41
|
-
'windsurf-cli': 'windsurf',
|
|
42
|
-
'zencoder-cli': 'zencoder',
|
|
13
|
+
...Object.fromEntries(GENERIC_CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider.binaryName])),
|
|
43
14
|
}
|
|
44
15
|
|
|
45
16
|
interface GenericCliOptions extends StreamChatOptions {
|