@swarmclawai/swarmclaw 1.7.1 → 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.
@@ -1,22 +1,49 @@
1
1
  'use client'
2
2
 
3
3
  import { useEffect, useState } from 'react'
4
+ import { safeStorageGet, safeStorageSet } from '@/lib/app/safe-storage'
4
5
 
5
6
  const CHECK_INTERVAL = 5 * 60_000 // 5 minutes
7
+ const DISMISSED_UPDATE_KEY = 'sc_update_banner_dismissed_target'
6
8
 
7
9
  type VersionInfo = {
8
- localSha: string
9
- remoteSha: string
10
+ source: 'git' | 'package'
11
+ version: string
12
+ localSha: string | null
13
+ localTag: string | null
14
+ remoteSha: string | null
15
+ remoteTag: string | null
16
+ channel: 'stable' | 'main'
10
17
  updateAvailable: boolean
11
18
  behindBy: number
12
19
  }
13
20
 
21
+ type UpdateResponse = {
22
+ success: boolean
23
+ newSha?: string
24
+ targetTag?: string | null
25
+ channel?: 'stable' | 'main'
26
+ needsRestart?: boolean
27
+ error?: string
28
+ }
29
+
14
30
  type UpdateState = 'idle' | 'updating' | 'done' | 'error'
15
31
 
32
+ function updateTargetKey(version: VersionInfo): string {
33
+ return version.remoteTag || version.remoteSha || `${version.channel}:${version.behindBy}`
34
+ }
35
+
36
+ function updateTargetLabel(version: VersionInfo): string {
37
+ if (version.remoteTag) return version.remoteTag
38
+ if (version.remoteSha) return `${version.channel === 'stable' ? 'stable release' : 'main'} ${version.remoteSha}`
39
+ return 'latest release'
40
+ }
41
+
16
42
  export function UpdateBanner() {
17
43
  const [version, setVersion] = useState<VersionInfo | null>(null)
18
44
  const [updateState, setUpdateState] = useState<UpdateState>('idle')
19
- const [dismissed, setDismissed] = useState<string | null>(null)
45
+ const [dismissed, setDismissed] = useState<string | null>(() => safeStorageGet(DISMISSED_UPDATE_KEY))
46
+ const [appliedTarget, setAppliedTarget] = useState<string | null>(null)
20
47
  const [errorMsg, setErrorMsg] = useState('')
21
48
 
22
49
  useEffect(() => {
@@ -36,8 +63,9 @@ export function UpdateBanner() {
36
63
  setErrorMsg('')
37
64
  try {
38
65
  const res = await fetch('/api/version/update', { method: 'POST' })
39
- const data = await res.json()
66
+ const data = await res.json() as UpdateResponse
40
67
  if (data.success) {
68
+ setAppliedTarget(data.targetTag || data.newSha || (version ? updateTargetLabel(version) : null))
41
69
  setUpdateState('done')
42
70
  } else {
43
71
  setUpdateState('error')
@@ -50,12 +78,17 @@ export function UpdateBanner() {
50
78
  }
51
79
 
52
80
  const handleDismiss = () => {
53
- if (version) setDismissed(version.remoteSha)
81
+ if (!version) return
82
+ const target = updateTargetKey(version)
83
+ setDismissed(target)
84
+ safeStorageSet(DISMISSED_UPDATE_KEY, target)
54
85
  }
55
86
 
56
87
  // Don't show if no update, or user dismissed this specific remote SHA
57
88
  if (!version?.updateAvailable) return null
58
- if (dismissed === version.remoteSha && updateState === 'idle') return null
89
+ if (dismissed === updateTargetKey(version) && updateState === 'idle') return null
90
+
91
+ const targetLabel = updateTargetLabel(version)
59
92
 
60
93
  return (
61
94
  <div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] flex items-center gap-2 shrink-0 bg-accent-bright/[0.04]">
@@ -65,7 +98,8 @@ export function UpdateBanner() {
65
98
  <path d="M12 19V5M5 12l7-7 7 7" />
66
99
  </svg>
67
100
  <span className="text-text-3 flex-1 min-w-0 truncate">
68
- <span className="text-accent-bright font-600">{version.behindBy}</span> update{version.behindBy !== 1 ? 's' : ''} available
101
+ <span className="text-accent-bright font-600">{targetLabel}</span> available
102
+ {version.behindBy > 0 ? ` - ${version.behindBy} commit${version.behindBy === 1 ? '' : 's'} ahead` : ''}
69
103
  </span>
70
104
  <button
71
105
  onClick={handleUpdate}
@@ -90,7 +124,7 @@ export function UpdateBanner() {
90
124
  <>
91
125
  <span className="w-3 h-3 border-[1.5px] border-accent-bright/30 border-t-accent-bright rounded-full shrink-0"
92
126
  style={{ animation: 'spin 0.8s linear infinite' }} />
93
- <span className="text-text-3">Updating...</span>
127
+ <span className="text-text-3">Updating to {targetLabel}...</span>
94
128
  </>
95
129
  )}
96
130
 
@@ -99,7 +133,7 @@ export function UpdateBanner() {
99
133
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-success shrink-0">
100
134
  <polyline points="20 6 9 17 4 12" />
101
135
  </svg>
102
- <span className="text-text-3 flex-1">Updated! Restart to apply.</span>
136
+ <span className="text-text-3 flex-1">Updated{appliedTarget ? ` to ${appliedTarget}` : ''}. Restart SwarmClaw to apply.</span>
103
137
  </>
104
138
  )}
105
139
 
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { GENERIC_CLI_PROVIDER_METADATA } from './providers/cli-provider-metadata'
5
+ import {
6
+ NATIVE_CAPABILITY_PROVIDER_IDS,
7
+ NON_LANGGRAPH_PROVIDER_IDS,
8
+ WORKER_ONLY_PROVIDER_IDS,
9
+ } from './provider-sets'
10
+
11
+ describe('provider sets', () => {
12
+ it('routes generic CLI providers through direct provider runtime', () => {
13
+ for (const provider of GENERIC_CLI_PROVIDER_METADATA) {
14
+ assert.equal(NON_LANGGRAPH_PROVIDER_IDS.has(provider.id), true, `${provider.id} should bypass LangGraph`)
15
+ assert.equal(NATIVE_CAPABILITY_PROVIDER_IDS.has(provider.id), true, `${provider.id} should be native-capability`)
16
+ assert.equal(WORKER_ONLY_PROVIDER_IDS.has(provider.id), true, `${provider.id} should be worker-only`)
17
+ }
18
+ })
19
+ })
@@ -1,14 +1,19 @@
1
+ import { CLI_PROVIDER_METADATA } from '@/lib/providers/cli-provider-metadata'
2
+
3
+ const CLI_PROVIDER_IDS = CLI_PROVIDER_METADATA.map((provider) => provider.id)
4
+ const DIRECT_CLI_PROVIDER_IDS = CLI_PROVIDER_IDS.filter((providerId) => providerId !== 'goose')
5
+
1
6
  /** CLI providers that use their own tool execution outside the shared tool-runtime path. */
2
- export const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'opencode-web', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli'])
7
+ export const NON_LANGGRAPH_PROVIDER_IDS = new Set([...DIRECT_CLI_PROVIDER_IDS, 'opencode-web'])
3
8
 
4
9
  /** Providers that manage their own runtime/tool loop even when reached over an API endpoint. */
5
10
  export const RUNTIME_MANAGED_PROVIDER_IDS = new Set(['hermes', 'goose'])
6
11
 
7
12
  /** Providers with native tool/capability support (CLI providers + OpenClaw + Hermes). */
8
- export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
13
+ export const NATIVE_CAPABILITY_PROVIDER_IDS = new Set([...CLI_PROVIDER_IDS, 'openclaw', 'hermes'])
9
14
 
10
15
  /** Providers that can only act as workers — no coordinator role, no heartbeat, no advanced settings. */
11
- export const WORKER_ONLY_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'openclaw', 'hermes'])
16
+ export const WORKER_ONLY_PROVIDER_IDS = new Set([...CLI_PROVIDER_IDS, 'openclaw', 'hermes'])
12
17
 
13
18
  /** CLI providers that support MCP server and skill injection at runtime (via provider-specific config mechanisms). */
14
19
  export const MCP_INJECTION_PROVIDER_IDS = new Set(['copilot-cli', 'codex-cli'])
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ BESPOKE_CLI_PROVIDER_METADATA,
6
+ CLI_PROVIDER_METADATA,
7
+ CLI_PROVIDER_METADATA_BY_ID,
8
+ GENERIC_CLI_PROVIDER_METADATA,
9
+ isCliProviderId,
10
+ } from './cli-provider-metadata'
11
+ import { GENERIC_CLI_BINARIES } from './generic-cli'
12
+ import { CLI_PROVIDER_CAPABILITIES } from './cli-utils'
13
+
14
+ describe('CLI provider metadata', () => {
15
+ it('has one unique entry per CLI provider', () => {
16
+ const ids = CLI_PROVIDER_METADATA.map((provider) => provider.id)
17
+ assert.equal(new Set(ids).size, ids.length)
18
+ assert.ok(BESPOKE_CLI_PROVIDER_METADATA.length > 0)
19
+ assert.equal(GENERIC_CLI_PROVIDER_METADATA.length, 31)
20
+ })
21
+
22
+ it('drives binary and capability maps for every provider', () => {
23
+ for (const provider of CLI_PROVIDER_METADATA) {
24
+ assert.equal(CLI_PROVIDER_METADATA_BY_ID[provider.id]?.displayName, provider.displayName)
25
+ assert.equal(isCliProviderId(provider.id), true)
26
+ assert.equal(CLI_PROVIDER_CAPABILITIES[provider.id], provider.capability)
27
+ assert.ok(provider.binaryName.length > 0)
28
+ assert.ok(provider.defaultModel.length > 0)
29
+ }
30
+ })
31
+
32
+ it('keeps generic CLI binary mappings sourced from metadata', () => {
33
+ assert.deepEqual(
34
+ Object.fromEntries(GENERIC_CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider.binaryName])),
35
+ GENERIC_CLI_BINARIES,
36
+ )
37
+ })
38
+ })
@@ -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
+ }
@@ -12,6 +12,7 @@ import path from 'path'
12
12
  import { spawnSync, type ChildProcess } from 'child_process'
13
13
  import { realpathSync } from 'fs'
14
14
  import { log } from '../server/logger'
15
+ import { CLI_PROVIDER_METADATA, CLI_PROVIDER_METADATA_BY_ID } from './cli-provider-metadata'
15
16
 
16
17
  // ---------------------------------------------------------------------------
17
18
  // Binary Discovery
@@ -468,50 +469,10 @@ export function symlinkConfigFiles(
468
469
  // ---------------------------------------------------------------------------
469
470
 
470
471
  /** Human-readable descriptions of what each CLI provider excels at. */
471
- export const CLI_PROVIDER_CAPABILITIES: Record<string, string> = {
472
- 'claude-cli': 'multi-file code editing, refactoring, debugging, code review',
473
- 'codex-cli': 'code generation, file creation, automated coding tasks',
474
- 'opencode-cli': 'code analysis, generation across multiple LLM backends',
475
- 'gemini-cli': 'code generation, analysis with Gemini models',
476
- 'copilot-cli': 'code generation, analysis, multi-model support via GitHub Copilot',
477
- 'droid-cli': 'code generation, refactoring, and automation via Factory Droid with configurable autonomy',
478
- 'cursor-cli': 'full-agent coding workflows, multi-file edits, project-aware code changes',
479
- 'qwen-code-cli': 'terminal-native coding workflows, code generation, review, and automation',
480
- goose: 'agentic coding workflows with extensions, tools, and runtime-managed execution',
481
- 'aider-cli': 'paired-programming-style multi-file edits and git-aware code changes',
482
- 'amp-cli': 'agentic coding via Sourcegraph Amp',
483
- 'augment-cli': 'codebase-aware agentic edits via Augment',
484
- 'adal-cli': 'AdaL coding agent for terminal-driven workflows',
485
- 'bob-cli': 'IBM watsonx Code Assistant (Bob) terminal coding workflows',
486
- 'cline-cli': 'autonomous file-level edits and terminal automation via Cline',
487
- 'codebuddy-cli': 'CodeBuddy agentic coding workflows',
488
- 'command-code-cli': 'Command Code terminal-native coding agent',
489
- 'continue-cli': 'agentic coding via the Continue CLI',
490
- 'cortex-cli': 'Snowflake Cortex Code agentic workflows',
491
- 'crush-cli': 'Crush terminal coding agent',
492
- 'deepagents-cli': 'long-horizon planning and multi-step coding via Deep Agents',
493
- 'firebender-cli': 'Firebender JetBrains-aligned coding agent',
494
- 'iflow-cli': 'iFlow CLI agentic coding workflows',
495
- 'junie-cli': 'JetBrains Junie coding agent for terminal use',
496
- 'kilo-code-cli': 'Kilo Code agentic coding workflows',
497
- 'kimi-cli': 'Kimi Code CLI coding agent',
498
- 'kode-cli': 'Kode terminal coding agent',
499
- 'mcpjam-cli': 'MCPJam-tooled agentic coding workflows',
500
- 'mistral-vibe-cli': 'Mistral Vibe coding agent',
501
- 'mux-cli': 'Mux multi-tool coding agent',
502
- 'neovate-cli': 'Neovate coding agent for terminal workflows',
503
- 'openhands-cli': 'OpenHands agentic coding via terminal',
504
- 'pochi-cli': 'Pochi coding agent',
505
- 'qoder-cli': 'Qoder agentic coding workflows',
506
- 'replit-cli': 'Replit Agent terminal coding workflows',
507
- 'roo-code-cli': 'Roo Code agentic coding workflows',
508
- 'trae-cn-cli': 'TRAE CN coding agent',
509
- 'warp-cli': 'Warp Agent terminal-native coding workflows',
510
- 'windsurf-cli': 'Windsurf agentic coding workflows',
511
- 'zencoder-cli': 'Zencoder agentic coding workflows',
512
- }
472
+ export const CLI_PROVIDER_CAPABILITIES: Record<string, string> =
473
+ Object.fromEntries(CLI_PROVIDER_METADATA.map((provider) => [provider.id, provider.capability]))
513
474
 
514
475
  /** Check if a provider ID is a CLI-based provider. */
515
476
  export function isCliProvider(providerId: string): boolean {
516
- return providerId in CLI_PROVIDER_CAPABILITIES
477
+ return providerId in CLI_PROVIDER_METADATA_BY_ID
517
478
  }
@@ -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 {
@@ -8,7 +8,8 @@ import { streamDroidCliChat } from './droid-cli'
8
8
  import { streamCursorCliChat } from './cursor-cli'
9
9
  import { streamQwenCodeCliChat } from './qwen-code-cli'
10
10
  import { streamGooseChat } from './goose'
11
- import { streamGenericCliChat, GENERIC_CLI_BINARIES } from './generic-cli'
11
+ import { streamGenericCliChat } from './generic-cli'
12
+ import { GENERIC_CLI_PROVIDER_METADATA, isCliProviderId } from './cli-provider-metadata'
12
13
  import { streamOpenAiChat } from './openai'
13
14
  import { streamOllamaChat } from './ollama'
14
15
  import { streamAnthropicChat } from './anthropic'
@@ -52,53 +53,22 @@ interface BuiltinProviderConfig extends ProviderInfo {
52
53
  handler: ProviderHandler
53
54
  }
54
55
 
55
- const GENERIC_CLI_DISPLAY_NAMES: Record<string, string> = {
56
- 'aider-cli': 'Aider CLI',
57
- 'amp-cli': 'Amp CLI',
58
- 'augment-cli': 'Augment CLI',
59
- 'adal-cli': 'AdaL CLI',
60
- 'bob-cli': 'IBM Bob CLI',
61
- 'cline-cli': 'Cline CLI',
62
- 'codebuddy-cli': 'CodeBuddy CLI',
63
- 'command-code-cli': 'Command Code CLI',
64
- 'continue-cli': 'Continue CLI',
65
- 'cortex-cli': 'Cortex Code CLI',
66
- 'crush-cli': 'Crush CLI',
67
- 'deepagents-cli': 'Deep Agents CLI',
68
- 'firebender-cli': 'Firebender CLI',
69
- 'iflow-cli': 'iFlow CLI',
70
- 'junie-cli': 'Junie CLI',
71
- 'kilo-code-cli': 'Kilo Code CLI',
72
- 'kimi-cli': 'Kimi Code CLI',
73
- 'kode-cli': 'Kode CLI',
74
- 'mcpjam-cli': 'MCPJam CLI',
75
- 'mistral-vibe-cli': 'Mistral Vibe CLI',
76
- 'mux-cli': 'Mux CLI',
77
- 'neovate-cli': 'Neovate CLI',
78
- 'openhands-cli': 'OpenHands CLI',
79
- 'pochi-cli': 'Pochi CLI',
80
- 'qoder-cli': 'Qoder CLI',
81
- 'replit-cli': 'Replit Agent CLI',
82
- 'roo-code-cli': 'Roo Code CLI',
83
- 'trae-cn-cli': 'TRAE CN CLI',
84
- 'warp-cli': 'Warp Agent CLI',
85
- 'windsurf-cli': 'Windsurf CLI',
86
- 'zencoder-cli': 'Zencoder CLI',
87
- }
88
-
89
56
  function buildGenericCliEntries(): Record<string, BuiltinProviderConfig> {
90
57
  const entries: Record<string, BuiltinProviderConfig> = {}
91
- for (const [providerId, binaryName] of Object.entries(GENERIC_CLI_BINARIES)) {
92
- const displayName = GENERIC_CLI_DISPLAY_NAMES[providerId] ?? providerId
93
- entries[providerId] = {
94
- id: providerId,
95
- name: displayName,
96
- models: ['default'],
58
+ for (const provider of GENERIC_CLI_PROVIDER_METADATA) {
59
+ entries[provider.id] = {
60
+ id: provider.id,
61
+ name: provider.displayName,
62
+ models: [provider.defaultModel],
97
63
  requiresApiKey: false,
98
- optionalApiKey: true,
64
+ optionalApiKey: provider.optionalApiKey,
99
65
  requiresEndpoint: false,
100
66
  handler: {
101
- streamChat: (opts) => streamGenericCliChat({ ...opts, binaryName, displayName }),
67
+ streamChat: (opts) => streamGenericCliChat({
68
+ ...opts,
69
+ binaryName: provider.binaryName,
70
+ displayName: provider.displayName,
71
+ }),
102
72
  },
103
73
  }
104
74
  }
@@ -530,7 +500,7 @@ export function getProviderList(): ProviderInfo[] {
530
500
  ...info,
531
501
  models: overrides[info.id] || info.models,
532
502
  defaultModels: info.models,
533
- supportsModelDiscovery: !['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose', 'fireworks', ...Object.keys(GENERIC_CLI_BINARIES)].includes(info.id),
503
+ supportsModelDiscovery: !isCliProviderId(info.id) && info.id !== 'fireworks',
534
504
  }
535
505
  })
536
506
 
@@ -0,0 +1,45 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { checkCliProviderReady } from './cli-provider-readiness'
5
+
6
+ describe('checkCliProviderReady', () => {
7
+ it('accepts a generic CLI provider when its binary is present', () => {
8
+ const result = checkCliProviderReady('aider-cli', {
9
+ resolveBinary: (name) => name === 'aider' ? '/usr/local/bin/aider' : null,
10
+ })
11
+
12
+ assert.equal(result.ok, true)
13
+ assert.equal(result.displayName, 'Aider CLI')
14
+ assert.equal(result.binaryName, 'aider')
15
+ assert.equal(result.generic, true)
16
+ assert.match(result.message, /binary is available/)
17
+ })
18
+
19
+ it('reports a missing generic CLI binary with install guidance', () => {
20
+ const result = checkCliProviderReady('windsurf-cli', {
21
+ resolveBinary: () => null,
22
+ })
23
+
24
+ assert.equal(result.ok, false)
25
+ assert.equal(result.displayName, 'Windsurf CLI')
26
+ assert.equal(result.binaryName, 'windsurf')
27
+ assert.match(result.message, /Install `windsurf`/)
28
+ })
29
+
30
+ it('keeps auth-aware checks for bespoke CLI providers', () => {
31
+ const result = checkCliProviderReady('claude-cli', {
32
+ resolveBinary: () => '/usr/local/bin/claude',
33
+ env: { ...process.env },
34
+ probeAuth: () => ({
35
+ authenticated: false,
36
+ errorMessage: 'Claude CLI is not authenticated.',
37
+ }),
38
+ })
39
+
40
+ assert.equal(result.ok, false)
41
+ assert.equal(result.displayName, 'Claude Code CLI')
42
+ assert.equal(result.generic, false)
43
+ assert.equal(result.message, 'Claude CLI is not authenticated.')
44
+ })
45
+ })