@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.
Files changed (42) hide show
  1. package/README.md +25 -9
  2. package/bin/swarmclaw.js +87 -0
  3. package/electron-dist/main.js +218 -0
  4. package/package.json +2 -2
  5. package/scripts/run-next-build.mjs +1 -1
  6. package/src/app/api/setup/check-provider/route.ts +5 -62
  7. package/src/app/api/setup/doctor/route.ts +19 -9
  8. package/src/app/home/page.tsx +19 -10
  9. package/src/cli/index.js +8 -2
  10. package/src/cli/index.ts +12 -3
  11. package/src/components/agents/inspector-panel.tsx +25 -3
  12. package/src/components/auth/setup-wizard/index.tsx +6 -2
  13. package/src/components/auth/setup-wizard/step-next.tsx +46 -39
  14. package/src/components/auth/setup-wizard/step-providers.tsx +113 -140
  15. package/src/components/auth/setup-wizard/types.ts +5 -2
  16. package/src/components/auth/setup-wizard/utils.test.ts +0 -19
  17. package/src/components/auth/setup-wizard/utils.ts +0 -69
  18. package/src/components/chat/chat-card.tsx +5 -0
  19. package/src/components/home/home-launchpad.tsx +123 -71
  20. package/src/components/layout/update-banner.tsx +43 -9
  21. package/src/lib/home-launchpad.test.ts +1 -31
  22. package/src/lib/home-launchpad.ts +0 -58
  23. package/src/lib/provider-sets.test.ts +19 -0
  24. package/src/lib/provider-sets.ts +8 -3
  25. package/src/lib/providers/cli-provider-metadata.test.ts +38 -0
  26. package/src/lib/providers/cli-provider-metadata.ts +208 -0
  27. package/src/lib/providers/cli-utils.test.ts +65 -1
  28. package/src/lib/providers/cli-utils.ts +26 -44
  29. package/src/lib/providers/codex-cli.ts +71 -75
  30. package/src/lib/providers/generic-cli.ts +2 -31
  31. package/src/lib/providers/index.ts +14 -44
  32. package/src/lib/server/chat-execution/chat-execution-session-sync.test.ts +189 -0
  33. package/src/lib/server/chat-execution/chat-turn-finalization.ts +26 -19
  34. package/src/lib/server/cli-provider-readiness.test.ts +45 -0
  35. package/src/lib/server/cli-provider-readiness.ts +84 -0
  36. package/src/lib/server/provider-health.test.ts +6 -0
  37. package/src/lib/server/provider-health.ts +2 -2
  38. package/src/lib/setup-defaults.test.ts +8 -0
  39. package/src/lib/setup-defaults.ts +38 -178
  40. package/src/stores/slices/session-slice.test.ts +40 -2
  41. package/src/stores/slices/session-slice.ts +41 -1
  42. 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 { isStderrNoise, buildCliEnv, isCliProvider, CLI_PROVIDER_CAPABILITIES } from './cli-utils'
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 probe = spawnSync(binary, ['login', 'status'], {
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
- 'claude-cli': 'multi-file code editing, refactoring, debugging, code review',
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 CLI_PROVIDER_CAPABILITIES
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 temp CODEX_HOME — uses real config dir
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, session.cwd)
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
- // System prompt + MCP injection: create a temp CODEX_HOME when needed
73
- // Symlink auth files from the real config dir so auth still works
74
- let tempCodexHome: string | null = null
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 needsTempHome = (systemPrompt && !session.codexThreadId) || agentMcpServerIds.length > 0
78
- if (needsTempHome) {
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
- // 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)}`)
114
- }
115
- tomlParts.push('')
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: session.cwd,
142
+ cwd: effectiveCwd,
145
143
  promptLen: prompt.length,
146
144
  hasSystemPrompt: !!systemPrompt,
147
- tempCodexHome,
145
+ sessionCodexHome,
148
146
  })
149
147
 
150
148
  const proc = spawn(binary, args, {
151
- cwd: session.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
- if (ev.type === 'thread.started' && ev.thread_id) {
189
- session.codexThreadId = ev.thread_id
190
- log.info('codex-cli', `Got thread_id: ${ev.thread_id}`)
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
- 'aider-cli': 'aider',
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 {