@swarmclawai/swarmclaw 1.7.1 → 1.7.3

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.
@@ -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
+ })
@@ -0,0 +1,84 @@
1
+ import {
2
+ CLI_PROVIDER_METADATA_BY_ID,
3
+ isCliProviderId,
4
+ type CliAuthBackend,
5
+ type CliProviderMetadata,
6
+ } from '@/lib/providers/cli-provider-metadata'
7
+ import { buildCliEnv, probeCliAuth, resolveCliBinary, type AuthProbeResult } from '@/lib/providers/cli-utils'
8
+
9
+ export interface CliProviderReadyResult {
10
+ ok: boolean
11
+ message: string
12
+ providerId?: string
13
+ displayName?: string
14
+ binaryName?: string
15
+ binaryPath?: string
16
+ generic?: boolean
17
+ }
18
+
19
+ interface CheckCliProviderReadyOptions {
20
+ cwd?: string
21
+ env?: NodeJS.ProcessEnv
22
+ resolveBinary?: (name: string) => string | null
23
+ probeAuth?: (
24
+ binary: string,
25
+ backend: CliAuthBackend,
26
+ env: NodeJS.ProcessEnv,
27
+ cwd?: string,
28
+ ) => AuthProbeResult
29
+ }
30
+
31
+ function missingBinaryMessage(meta: CliProviderMetadata): string {
32
+ return `${meta.displayName} is not installed. Install \`${meta.binaryName}\` and ensure it is on your PATH.`
33
+ }
34
+
35
+ export function checkCliProviderReady(
36
+ providerId: string,
37
+ options: CheckCliProviderReadyOptions = {},
38
+ ): CliProviderReadyResult {
39
+ if (!isCliProviderId(providerId)) {
40
+ return { ok: false, message: 'Unknown CLI provider.', providerId }
41
+ }
42
+
43
+ const meta = CLI_PROVIDER_METADATA_BY_ID[providerId]
44
+ const resolveBinary = options.resolveBinary || resolveCliBinary
45
+ const binaryPath = resolveBinary(meta.binaryName)
46
+ if (!binaryPath) {
47
+ return {
48
+ ok: false,
49
+ message: missingBinaryMessage(meta),
50
+ providerId,
51
+ displayName: meta.displayName,
52
+ binaryName: meta.binaryName,
53
+ generic: meta.generic,
54
+ }
55
+ }
56
+
57
+ if (meta.authBackend) {
58
+ const env = options.env || buildCliEnv()
59
+ const auth = (options.probeAuth || probeCliAuth)(binaryPath, meta.authBackend, env, options.cwd || process.cwd())
60
+ if (!auth.authenticated) {
61
+ return {
62
+ ok: false,
63
+ message: auth.errorMessage || `${meta.displayName} is not configured.`,
64
+ providerId,
65
+ displayName: meta.displayName,
66
+ binaryName: meta.binaryName,
67
+ binaryPath,
68
+ generic: meta.generic,
69
+ }
70
+ }
71
+ }
72
+
73
+ return {
74
+ ok: true,
75
+ message: meta.authBackend
76
+ ? `${meta.displayName} is installed and ready.`
77
+ : `${meta.displayName} binary is available. If it requires account setup, complete that in \`${meta.binaryName}\` before running agent turns.`,
78
+ providerId,
79
+ displayName: meta.displayName,
80
+ binaryName: meta.binaryName,
81
+ binaryPath,
82
+ generic: meta.generic,
83
+ }
84
+ }
@@ -179,4 +179,10 @@ describe('provider-health', () => {
179
179
  globalThis.fetch = originalFetch
180
180
  }
181
181
  })
182
+
183
+ it('skips extended CLI providers instead of treating them as HTTP providers', async () => {
184
+ const result = await providerHealth.pingProvider('aider-cli', undefined, undefined)
185
+ assert.equal(result.ok, true)
186
+ assert.equal(result.message, 'CLI provider - skipped.')
187
+ })
182
188
  })
@@ -2,6 +2,7 @@ import { spawnSync } from 'child_process'
2
2
  import { errorMessage, hmrSingleton, jitteredBackoff } from '@/lib/shared-utils'
3
3
  import { upsertStoredItem, loadCollection } from './storage'
4
4
  import { log } from './logger'
5
+ import { isCliProviderId } from '@/lib/providers/cli-provider-metadata'
5
6
 
6
7
  const TAG = 'provider-health'
7
8
 
@@ -352,9 +353,8 @@ export async function pingProvider(
352
353
  apiKey: string | undefined,
353
354
  endpoint: string | undefined,
354
355
  ): Promise<{ ok: boolean; message: string }> {
355
- const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'droid-cli', 'cursor-cli', 'qwen-code-cli', 'goose']
356
356
  const OPTIONAL_OPENAI_COMPATIBLE_KEY_PROVIDERS = new Set(['hermes'])
357
- if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider skipped.' }
357
+ if (isCliProviderId(provider)) return { ok: true, message: 'CLI provider - skipped.' }
358
358
 
359
359
  try {
360
360
  if (provider === 'anthropic') {
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
+ import { CLI_PROVIDER_METADATA } from './providers/cli-provider-metadata'
3
4
  import { DEFAULT_AGENTS, getDefaultModelForProvider } from './setup-defaults'
4
5
 
5
6
  // ---------------------------------------------------------------------------
@@ -48,6 +49,13 @@ test('getDefaultModelForProvider returns expected defaults for cursor, qwen, and
48
49
  assert.equal(getDefaultModelForProvider('goose'), 'default')
49
50
  })
50
51
 
52
+ test('every CLI provider has setup default agent coverage', () => {
53
+ for (const provider of CLI_PROVIDER_METADATA) {
54
+ assert.equal(getDefaultModelForProvider(provider.id), provider.defaultModel)
55
+ assert.ok(DEFAULT_AGENTS[provider.id].description.includes(provider.displayName))
56
+ }
57
+ })
58
+
51
59
  test('getDefaultModelForProvider returns non-empty for ollama', () => {
52
60
  const model = getDefaultModelForProvider('ollama')
53
61
  assert.ok(model, 'ollama model should be truthy')