@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
@@ -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
 
@@ -109,6 +109,195 @@ test('executeSessionChatTurn syncs updated agent runtime fields onto its thread
109
109
  assert.equal(output.connectorContext, null)
110
110
  })
111
111
 
112
+ test('executeSessionChatTurn persists codex thread id discovered on run-session object', () => {
113
+ const output = runWithTempDataDir<{
114
+ codexThreadId: string | null
115
+ }>(`
116
+ const storageMod = await import('@/lib/server/storage')
117
+ const providersMod = await import('@/lib/providers/index')
118
+ const execMod = await import('@/lib/server/chat-execution/chat-execution')
119
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
120
+ const executeSessionChatTurn = execMod.executeSessionChatTurn
121
+ || execMod.default?.executeSessionChatTurn
122
+ || execMod['module.exports']?.executeSessionChatTurn
123
+ const providers = providersMod.PROVIDERS
124
+ || providersMod.default?.PROVIDERS
125
+ || providersMod['module.exports']?.PROVIDERS
126
+
127
+ providers['test-codex-sync-provider'] = {
128
+ id: 'test-codex-sync-provider',
129
+ name: 'Codex Sync Test Provider',
130
+ models: ['unit'],
131
+ requiresApiKey: false,
132
+ requiresEndpoint: false,
133
+ handler: {
134
+ async streamChat(opts) {
135
+ opts.session.codexThreadId = 'thread_sync_123'
136
+ return 'ok'
137
+ },
138
+ },
139
+ }
140
+
141
+ const now = Date.now()
142
+ storage.saveAgents({
143
+ codexsync: {
144
+ id: 'codexsync',
145
+ name: 'Codex Sync Agent',
146
+ description: 'Codex thread id sync test',
147
+ provider: 'test-codex-sync-provider',
148
+ model: 'unit',
149
+ credentialId: null,
150
+ apiEndpoint: null,
151
+ fallbackCredentialIds: [],
152
+ disabled: false,
153
+ heartbeatEnabled: false,
154
+ heartbeatIntervalSec: null,
155
+ extensions: [],
156
+ createdAt: now,
157
+ updatedAt: now,
158
+ },
159
+ })
160
+
161
+ storage.saveSessions({
162
+ codex_session: {
163
+ id: 'codex_session',
164
+ name: 'Codex Session',
165
+ cwd: process.env.WORKSPACE_DIR,
166
+ user: 'default',
167
+ provider: 'test-codex-sync-provider',
168
+ model: 'unit',
169
+ claudeSessionId: null,
170
+ codexThreadId: null,
171
+ opencodeSessionId: null,
172
+ geminiSessionId: null,
173
+ copilotSessionId: null,
174
+ droidSessionId: null,
175
+ cursorSessionId: null,
176
+ qwenSessionId: null,
177
+ acpSessionId: null,
178
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null, copilot: null, droid: null, cursor: null, qwen: null },
179
+ messages: [],
180
+ createdAt: now,
181
+ lastActiveAt: now,
182
+ sessionType: 'human',
183
+ agentId: 'codexsync',
184
+ extensions: [],
185
+ },
186
+ })
187
+
188
+ await executeSessionChatTurn({
189
+ sessionId: 'codex_session',
190
+ message: 'hello',
191
+ runId: 'run-codex-sync',
192
+ })
193
+
194
+ const persisted = storage.loadSession('codex_session')
195
+ console.log(JSON.stringify({
196
+ codexThreadId: persisted?.codexThreadId || null,
197
+ }))
198
+ `)
199
+
200
+ assert.equal(output.codexThreadId, 'thread_sync_123')
201
+ })
202
+
203
+ test('executeSessionChatTurn persists intentional codex resume id clears from run-session object', () => {
204
+ const output = runWithTempDataDir<{
205
+ codexThreadId: string | null
206
+ delegateCodex: string | null
207
+ }>(`
208
+ const storageMod = await import('@/lib/server/storage')
209
+ const providersMod = await import('@/lib/providers/index')
210
+ const execMod = await import('@/lib/server/chat-execution/chat-execution')
211
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
212
+ const executeSessionChatTurn = execMod.executeSessionChatTurn
213
+ || execMod.default?.executeSessionChatTurn
214
+ || execMod['module.exports']?.executeSessionChatTurn
215
+ const providers = providersMod.PROVIDERS
216
+ || providersMod.default?.PROVIDERS
217
+ || providersMod['module.exports']?.PROVIDERS
218
+
219
+ providers['test-codex-clear-provider'] = {
220
+ id: 'test-codex-clear-provider',
221
+ name: 'Codex Clear Test Provider',
222
+ models: ['unit'],
223
+ requiresApiKey: false,
224
+ requiresEndpoint: false,
225
+ handler: {
226
+ async streamChat(opts) {
227
+ opts.session.codexThreadId = null
228
+ opts.session.delegateResumeIds = {
229
+ ...(opts.session.delegateResumeIds || {}),
230
+ codex: null,
231
+ }
232
+ return 'ok'
233
+ },
234
+ },
235
+ }
236
+
237
+ const now = Date.now()
238
+ storage.saveAgents({
239
+ codexclear: {
240
+ id: 'codexclear',
241
+ name: 'Codex Clear Agent',
242
+ description: 'Codex resume id clear test',
243
+ provider: 'test-codex-clear-provider',
244
+ model: 'unit',
245
+ credentialId: null,
246
+ apiEndpoint: null,
247
+ fallbackCredentialIds: [],
248
+ disabled: false,
249
+ heartbeatEnabled: false,
250
+ heartbeatIntervalSec: null,
251
+ extensions: [],
252
+ createdAt: now,
253
+ updatedAt: now,
254
+ },
255
+ })
256
+
257
+ storage.saveSessions({
258
+ codex_clear_session: {
259
+ id: 'codex_clear_session',
260
+ name: 'Codex Clear Session',
261
+ cwd: process.env.WORKSPACE_DIR,
262
+ user: 'default',
263
+ provider: 'test-codex-clear-provider',
264
+ model: 'unit',
265
+ claudeSessionId: null,
266
+ codexThreadId: 'thread_old_123',
267
+ opencodeSessionId: null,
268
+ geminiSessionId: null,
269
+ copilotSessionId: null,
270
+ droidSessionId: null,
271
+ cursorSessionId: null,
272
+ qwenSessionId: null,
273
+ acpSessionId: null,
274
+ delegateResumeIds: { claudeCode: null, codex: 'thread_old_123', opencode: null, gemini: null, copilot: null, droid: null, cursor: null, qwen: null },
275
+ messages: [],
276
+ createdAt: now,
277
+ lastActiveAt: now,
278
+ sessionType: 'human',
279
+ agentId: 'codexclear',
280
+ extensions: [],
281
+ },
282
+ })
283
+
284
+ await executeSessionChatTurn({
285
+ sessionId: 'codex_clear_session',
286
+ message: 'hello',
287
+ runId: 'run-codex-clear',
288
+ })
289
+
290
+ const persisted = storage.loadSession('codex_clear_session')
291
+ console.log(JSON.stringify({
292
+ codexThreadId: persisted?.codexThreadId ?? null,
293
+ delegateCodex: persisted?.delegateResumeIds?.codex ?? null,
294
+ }))
295
+ `)
296
+
297
+ assert.equal(output.codexThreadId, null)
298
+ assert.equal(output.delegateCodex, null)
299
+ })
300
+
112
301
  test('executeSessionChatTurn keeps tool-only heartbeats off the visible main-thread history and clears stale connector state', () => {
113
302
  const output = runWithTempDataDir<{
114
303
  connectorContext: Record<string, unknown> | null
@@ -435,18 +435,25 @@ export async function finalizeChatTurn(params: {
435
435
  ;(current as unknown as Record<string, unknown>)[key] = normalized
436
436
  }
437
437
  }
438
+ const preferRunValue = (runValue: unknown, fallbackValue: unknown) => (
439
+ runValue !== undefined ? runValue : fallbackValue
440
+ )
438
441
 
439
- persistField('claudeSessionId', session.claudeSessionId)
440
- persistField('codexThreadId', session.codexThreadId)
441
- persistField('opencodeSessionId', session.opencodeSessionId)
442
- persistField('geminiSessionId', session.geminiSessionId)
443
- persistField('copilotSessionId', session.copilotSessionId)
444
- persistField('droidSessionId', session.droidSessionId)
445
- persistField('cursorSessionId', session.cursorSessionId)
446
- persistField('qwenSessionId', session.qwenSessionId)
447
- persistField('acpSessionId', session.acpSessionId)
448
-
449
- const sourceResume = session.delegateResumeIds
442
+ // Provider handlers receive `sessionForRun` and may mutate CLI resume IDs there.
443
+ // Persist from run-session first, allowing null to intentionally clear IDs.
444
+ persistField('claudeSessionId', preferRunValue(sessionForRun.claudeSessionId, session.claudeSessionId))
445
+ persistField('codexThreadId', preferRunValue(sessionForRun.codexThreadId, session.codexThreadId))
446
+ persistField('opencodeSessionId', preferRunValue(sessionForRun.opencodeSessionId, session.opencodeSessionId))
447
+ persistField('geminiSessionId', preferRunValue(sessionForRun.geminiSessionId, session.geminiSessionId))
448
+ persistField('copilotSessionId', preferRunValue(sessionForRun.copilotSessionId, session.copilotSessionId))
449
+ persistField('droidSessionId', preferRunValue(sessionForRun.droidSessionId, session.droidSessionId))
450
+ persistField('cursorSessionId', preferRunValue(sessionForRun.cursorSessionId, session.cursorSessionId))
451
+ persistField('qwenSessionId', preferRunValue(sessionForRun.qwenSessionId, session.qwenSessionId))
452
+ persistField('acpSessionId', preferRunValue(sessionForRun.acpSessionId, session.acpSessionId))
453
+
454
+ const sourceResume = (sessionForRun.delegateResumeIds && typeof sessionForRun.delegateResumeIds === 'object')
455
+ ? sessionForRun.delegateResumeIds
456
+ : session.delegateResumeIds
450
457
  if (sourceResume && typeof sourceResume === 'object') {
451
458
  const currentResume = (current.delegateResumeIds && typeof current.delegateResumeIds === 'object')
452
459
  ? current.delegateResumeIds
@@ -454,14 +461,14 @@ export async function finalizeChatTurn(params: {
454
461
  const sr = sourceResume as Record<string, unknown>
455
462
  const cr = currentResume as Record<string, unknown>
456
463
  const nextResume = {
457
- claudeCode: normalizeResumeId(sr.claudeCode ?? cr.claudeCode),
458
- codex: normalizeResumeId(sr.codex ?? cr.codex),
459
- opencode: normalizeResumeId(sr.opencode ?? cr.opencode),
460
- gemini: normalizeResumeId(sr.gemini ?? cr.gemini),
461
- copilot: normalizeResumeId(sr.copilot ?? cr.copilot),
462
- droid: normalizeResumeId(sr.droid ?? cr.droid),
463
- cursor: normalizeResumeId(sr.cursor ?? cr.cursor),
464
- qwen: normalizeResumeId(sr.qwen ?? cr.qwen),
464
+ claudeCode: normalizeResumeId(preferRunValue(sr.claudeCode, cr.claudeCode)),
465
+ codex: normalizeResumeId(preferRunValue(sr.codex, cr.codex)),
466
+ opencode: normalizeResumeId(preferRunValue(sr.opencode, cr.opencode)),
467
+ gemini: normalizeResumeId(preferRunValue(sr.gemini, cr.gemini)),
468
+ copilot: normalizeResumeId(preferRunValue(sr.copilot, cr.copilot)),
469
+ droid: normalizeResumeId(preferRunValue(sr.droid, cr.droid)),
470
+ cursor: normalizeResumeId(preferRunValue(sr.cursor, cr.cursor)),
471
+ qwen: normalizeResumeId(preferRunValue(sr.qwen, cr.qwen)),
465
472
  }
466
473
  if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
467
474
  current.delegateResumeIds = nextResume
@@ -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')