@golba98/codexa 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.tsx +128 -47
- package/src/commands/handler.ts +5 -16
- package/src/config/buildInfo.ts +2 -2
- package/src/core/models/codexModelCapabilities.ts +57 -4
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/providerModelCache.ts +106 -0
- package/src/core/process/CommandRunner.ts +12 -1
- package/src/core/providerLauncher/registry.ts +37 -7
- package/src/core/providerLauncher/types.ts +2 -1
- package/src/core/providerRuntime/anthropic.ts +3 -5
- package/src/core/providerRuntime/antigravity.ts +246 -90
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -692
- package/src/core/providerRuntime/mistralVibe.ts +663 -0
- package/src/core/providerRuntime/models.ts +18 -4
- package/src/core/providerRuntime/reasoning.ts +9 -6
- package/src/core/providerRuntime/registry.ts +94 -49
- package/src/core/providerRuntime/types.ts +1 -0
- package/src/index.tsx +23 -9
- package/src/test/runtimeTestUtils.ts +90 -0
- package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
- package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
- package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
- package/src/ui/{AppShell.tsx → chrome/AppShell.tsx} +11 -11
- package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +15 -15
- package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
- package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
- package/src/ui/{RuntimeStatusBar.tsx → chrome/RuntimeStatusBar.tsx} +3 -3
- package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
- package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +12 -12
- package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +3 -3
- package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
- package/src/ui/{focus.ts → input/focus.ts} +1 -1
- package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
- package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +1 -1
- package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
- package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
- package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
- package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
- package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +11 -9
- package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
- package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
- package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
- package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
- package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
- package/src/ui/{ProviderPicker.tsx → panels/ProviderPicker.tsx} +9 -7
- package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
- package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +1 -1
- package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +3 -3
- package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +3 -3
- package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
- package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +2 -2
- package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
- package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
- package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
- package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
- package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +9 -8
- package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
- package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
- package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
- package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
- package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +8 -8
- package/src/ui/{TranscriptShell.tsx → timeline/TranscriptShell.tsx} +6 -6
- package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
- package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
- package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
- package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +14 -14
- /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
- /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
- /package/src/ui/{logoVariants.ts → render/logoVariants.ts} +0 -0
- /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
- /package/src/ui/{textLayout.ts → render/textLayout.ts} +0 -0
- /package/src/ui/{layoutListWindow.ts → timeline/layoutListWindow.ts} +0 -0
|
@@ -1,692 +1,724 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
2
|
-
import { dirname, join } from "path";
|
|
3
|
-
import { buildClaudeSpawnSpec, resolveClaudeExecutable } from "../executables/claudeExecutable.js";
|
|
4
|
-
import { runCommand } from "../process/CommandRunner.js";
|
|
5
|
-
import type { CommandResult } from "../process/CommandRunner.js";
|
|
6
|
-
import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
|
|
7
|
-
import { ANTHROPIC_FALLBACK_MODELS } from "./models.js";
|
|
8
|
-
import {
|
|
9
|
-
import type { ProviderModel } from "./types.js";
|
|
10
|
-
|
|
11
|
-
type CommandRunner = typeof runCommand;
|
|
12
|
-
|
|
13
|
-
const CLAUDE_MODEL_FAMILIES = ["opus", "sonnet", "haiku"] as const;
|
|
14
|
-
type ClaudeModelFamily = (typeof CLAUDE_MODEL_FAMILIES)[number];
|
|
15
|
-
const CLAUDE_FAMILIES_ALT = CLAUDE_MODEL_FAMILIES.join("|");
|
|
16
|
-
// Pre-compiled from CLAUDE_MODEL_FAMILIES — update that constant when new families are added.
|
|
17
|
-
const RE_VERSIONED_ID = new RegExp(`\\bclaude-(?:${CLAUDE_FAMILIES_ALT})-(\\d+)(?:-(\\d+))?\\b`);
|
|
18
|
-
const RE_VAGUE_LABEL = new RegExp(`^(?:claude\\s+)?(?:${CLAUDE_FAMILIES_ALT})$`, "i");
|
|
19
|
-
|
|
20
|
-
export type ClaudeCodeModelSource =
|
|
21
|
-
| "claude-code"
|
|
22
|
-
| "claude-code-command"
|
|
23
|
-
| "claude-code-package"
|
|
24
|
-
| "claude-code-cache"
|
|
25
|
-
| "claude-code-config"
|
|
26
|
-
| "config"
|
|
27
|
-
| "settings"
|
|
28
|
-
| "fallback";
|
|
29
|
-
|
|
30
|
-
export interface ClaudeCodeAuthInfo {
|
|
31
|
-
loggedIn: boolean;
|
|
32
|
-
authMethod?: string;
|
|
33
|
-
apiProvider?: string;
|
|
34
|
-
subscriptionType?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ClaudeCodeModel {
|
|
38
|
-
label: string;
|
|
39
|
-
family: string;
|
|
40
|
-
value: string;
|
|
41
|
-
canonicalId: string;
|
|
42
|
-
source: ClaudeCodeModelSource;
|
|
43
|
-
version?: string;
|
|
44
|
-
isFallback: boolean;
|
|
45
|
-
discoveryKind?: "models" | "aliases";
|
|
46
|
-
effortLevels: readonly string[];
|
|
47
|
-
defaultEffort: string;
|
|
48
|
-
effortSource: ClaudeCodeModelSource;
|
|
49
|
-
effortVerified: boolean;
|
|
50
|
-
description?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface ClaudeCodeCapabilityDiscovery {
|
|
54
|
-
provider: "anthropic";
|
|
55
|
-
backend: "claude-code-cli";
|
|
56
|
-
resolvedCommand: string;
|
|
57
|
-
auth: ClaudeCodeAuthInfo;
|
|
58
|
-
models: ClaudeCodeModel[];
|
|
59
|
-
modelSource: ClaudeCodeModelSource;
|
|
60
|
-
discoveredAt: string;
|
|
61
|
-
settings?: {
|
|
62
|
-
path: string;
|
|
63
|
-
model?: string;
|
|
64
|
-
effortLevel?: string;
|
|
65
|
-
availableModels?: readonly string[];
|
|
66
|
-
};
|
|
67
|
-
diagnostics?: Record<string, string | number | boolean | null>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface DiscoverClaudeCodeCapabilitiesOptions {
|
|
71
|
-
cwd: string;
|
|
72
|
-
runCommandImpl?: CommandRunner;
|
|
73
|
-
configuredPath?: string | null;
|
|
74
|
-
settingsPath?: string | null;
|
|
75
|
-
now?: () => Date;
|
|
76
|
-
timeoutMs?: number;
|
|
77
|
-
metadataPaths?: readonly string[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface ClaudeSettingsInfo {
|
|
81
|
-
path: string;
|
|
82
|
-
model?: string;
|
|
83
|
-
effortLevel?: string;
|
|
84
|
-
availableModels?: readonly string[];
|
|
85
|
-
models?: readonly ClaudeCodeModel[];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
89
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function readString(value: unknown): string | undefined {
|
|
93
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function readStringArray(value: unknown): string[] | undefined {
|
|
97
|
-
if (!Array.isArray(value)) return undefined;
|
|
98
|
-
const result = value.map(readString).filter((item): item is string => Boolean(item));
|
|
99
|
-
return result.length > 0 ? result : undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function parseClaudeAuthStatus(stdout: string): ClaudeCodeAuthInfo | null {
|
|
103
|
-
try {
|
|
104
|
-
const parsed = JSON.parse(stdout.trim()) as unknown;
|
|
105
|
-
if (!isRecord(parsed)) return null;
|
|
106
|
-
return {
|
|
107
|
-
loggedIn: parsed["loggedIn"] === true,
|
|
108
|
-
authMethod: readString(parsed["authMethod"]),
|
|
109
|
-
apiProvider: readString(parsed["apiProvider"]),
|
|
110
|
-
subscriptionType: readString(parsed["subscriptionType"]),
|
|
111
|
-
};
|
|
112
|
-
} catch {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { buildClaudeSpawnSpec, resolveClaudeExecutable } from "../executables/claudeExecutable.js";
|
|
4
|
+
import { runCommand } from "../process/CommandRunner.js";
|
|
5
|
+
import type { CommandResult } from "../process/CommandRunner.js";
|
|
6
|
+
import type { ReasoningEffortCapability } from "../models/codexModelCapabilities.js";
|
|
7
|
+
import { ANTHROPIC_FALLBACK_MODELS } from "./models.js";
|
|
8
|
+
import { getClaudeCodeEffortLevels } from "./reasoning.js";
|
|
9
|
+
import type { ProviderModel } from "./types.js";
|
|
10
|
+
|
|
11
|
+
type CommandRunner = typeof runCommand;
|
|
12
|
+
|
|
13
|
+
const CLAUDE_MODEL_FAMILIES = ["fable", "opus", "sonnet", "haiku"] as const;
|
|
14
|
+
type ClaudeModelFamily = (typeof CLAUDE_MODEL_FAMILIES)[number];
|
|
15
|
+
const CLAUDE_FAMILIES_ALT = CLAUDE_MODEL_FAMILIES.join("|");
|
|
16
|
+
// Pre-compiled from CLAUDE_MODEL_FAMILIES — update that constant when new families are added.
|
|
17
|
+
const RE_VERSIONED_ID = new RegExp(`\\bclaude-(?:${CLAUDE_FAMILIES_ALT})-(\\d+)(?:-(\\d+))?\\b`);
|
|
18
|
+
const RE_VAGUE_LABEL = new RegExp(`^(?:claude\\s+)?(?:${CLAUDE_FAMILIES_ALT})$`, "i");
|
|
19
|
+
|
|
20
|
+
export type ClaudeCodeModelSource =
|
|
21
|
+
| "claude-code"
|
|
22
|
+
| "claude-code-command"
|
|
23
|
+
| "claude-code-package"
|
|
24
|
+
| "claude-code-cache"
|
|
25
|
+
| "claude-code-config"
|
|
26
|
+
| "config"
|
|
27
|
+
| "settings"
|
|
28
|
+
| "fallback";
|
|
29
|
+
|
|
30
|
+
export interface ClaudeCodeAuthInfo {
|
|
31
|
+
loggedIn: boolean;
|
|
32
|
+
authMethod?: string;
|
|
33
|
+
apiProvider?: string;
|
|
34
|
+
subscriptionType?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClaudeCodeModel {
|
|
38
|
+
label: string;
|
|
39
|
+
family: string;
|
|
40
|
+
value: string;
|
|
41
|
+
canonicalId: string;
|
|
42
|
+
source: ClaudeCodeModelSource;
|
|
43
|
+
version?: string;
|
|
44
|
+
isFallback: boolean;
|
|
45
|
+
discoveryKind?: "models" | "aliases";
|
|
46
|
+
effortLevels: readonly string[];
|
|
47
|
+
defaultEffort: string;
|
|
48
|
+
effortSource: ClaudeCodeModelSource;
|
|
49
|
+
effortVerified: boolean;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ClaudeCodeCapabilityDiscovery {
|
|
54
|
+
provider: "anthropic";
|
|
55
|
+
backend: "claude-code-cli";
|
|
56
|
+
resolvedCommand: string;
|
|
57
|
+
auth: ClaudeCodeAuthInfo;
|
|
58
|
+
models: ClaudeCodeModel[];
|
|
59
|
+
modelSource: ClaudeCodeModelSource;
|
|
60
|
+
discoveredAt: string;
|
|
61
|
+
settings?: {
|
|
62
|
+
path: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
effortLevel?: string;
|
|
65
|
+
availableModels?: readonly string[];
|
|
66
|
+
};
|
|
67
|
+
diagnostics?: Record<string, string | number | boolean | null>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DiscoverClaudeCodeCapabilitiesOptions {
|
|
71
|
+
cwd: string;
|
|
72
|
+
runCommandImpl?: CommandRunner;
|
|
73
|
+
configuredPath?: string | null;
|
|
74
|
+
settingsPath?: string | null;
|
|
75
|
+
now?: () => Date;
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
metadataPaths?: readonly string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ClaudeSettingsInfo {
|
|
81
|
+
path: string;
|
|
82
|
+
model?: string;
|
|
83
|
+
effortLevel?: string;
|
|
84
|
+
availableModels?: readonly string[];
|
|
85
|
+
models?: readonly ClaudeCodeModel[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
89
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readString(value: unknown): string | undefined {
|
|
93
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readStringArray(value: unknown): string[] | undefined {
|
|
97
|
+
if (!Array.isArray(value)) return undefined;
|
|
98
|
+
const result = value.map(readString).filter((item): item is string => Boolean(item));
|
|
99
|
+
return result.length > 0 ? result : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseClaudeAuthStatus(stdout: string): ClaudeCodeAuthInfo | null {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(stdout.trim()) as unknown;
|
|
105
|
+
if (!isRecord(parsed)) return null;
|
|
106
|
+
return {
|
|
107
|
+
loggedIn: parsed["loggedIn"] === true,
|
|
108
|
+
authMethod: readString(parsed["authMethod"]),
|
|
109
|
+
apiProvider: readString(parsed["apiProvider"]),
|
|
110
|
+
subscriptionType: readString(parsed["subscriptionType"]),
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parseClaudeEffortLevelsFromHelp(helpText: string): string[] | null {
|
|
118
|
+
const match = helpText.match(/^\s*--effort\b[^\r\n]*\(([^)]+)\)/im);
|
|
119
|
+
if (!match?.[1]) return null;
|
|
120
|
+
const levels = match[1].split(",").map((level) => level.trim()).filter(Boolean);
|
|
121
|
+
return levels.length > 0 ? levels : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function modelFamilyFromValue(value: string): ClaudeModelFamily {
|
|
125
|
+
const normalized = value.toLowerCase();
|
|
126
|
+
if (normalized.includes("fable")) return "fable";
|
|
127
|
+
if (normalized.includes("opus")) return "opus";
|
|
128
|
+
if (normalized.includes("haiku")) return "haiku";
|
|
129
|
+
return "sonnet";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fallbackModelByFamily(family: string): ProviderModel | undefined {
|
|
133
|
+
return ANTHROPIC_FALLBACK_MODELS.find((model) => model.family === family || model.modelId === family);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function fallbackEffortIds(family: string): string[] {
|
|
137
|
+
const fallback = fallbackModelByFamily(family);
|
|
138
|
+
return fallback?.supportedReasoningLevels?.map((level) => level.id) ?? ["low", "medium", "high"];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function fallbackDefaultEffort(family: string): string {
|
|
142
|
+
return fallbackModelByFamily(family)?.defaultReasoningLevel ?? "medium";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function titleCaseFamily(family: string): string {
|
|
146
|
+
if (family === "fable") return "Fable";
|
|
147
|
+
if (family === "opus") return "Opus";
|
|
148
|
+
if (family === "haiku") return "Haiku";
|
|
149
|
+
return "Sonnet";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function versionFromModelText(value: string): string | null {
|
|
153
|
+
const normalized = value.trim().toLowerCase();
|
|
154
|
+
const idMatch = normalized.match(RE_VERSIONED_ID);
|
|
155
|
+
if (idMatch) return idMatch[2] ? `${idMatch[1]}.${idMatch[2]}` : idMatch[1] ?? null;
|
|
156
|
+
const labelMatch = normalized.match(new RegExp(`^(?:claude\\s+)?(?:${CLAUDE_FAMILIES_ALT})\\s+(\\d+(?:\\.\\d+)?)\\b`));
|
|
157
|
+
return labelMatch?.[1] ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function compareVersionStrings(left: string | null | undefined, right: string | null | undefined): number {
|
|
161
|
+
const leftParts = (left ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
162
|
+
const rightParts = (right ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
163
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
164
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
165
|
+
const l = Number.isFinite(leftParts[index]) ? leftParts[index] : 0;
|
|
166
|
+
const r = Number.isFinite(rightParts[index]) ? rightParts[index] : 0;
|
|
167
|
+
if (l !== r) return l - r;
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isVagueClaudeFamilyLabel(value: string): boolean {
|
|
173
|
+
return RE_VAGUE_LABEL.test(value.trim());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeClaudeDisplayLabel(value: string, rawLabel?: string): string {
|
|
177
|
+
const family = modelFamilyFromValue(value);
|
|
178
|
+
const familyLabel = titleCaseFamily(family);
|
|
179
|
+
const raw = rawLabel?.trim();
|
|
180
|
+
const valueVersion = versionFromModelText(value);
|
|
181
|
+
|
|
182
|
+
if (raw) {
|
|
183
|
+
const rawVersion = versionFromModelText(raw);
|
|
184
|
+
if (rawVersion) {
|
|
185
|
+
const prefixed = /^claude\b/i.test(raw) ? raw : `Claude ${raw}`;
|
|
186
|
+
return prefixed.replace(/\b(opus|sonnet|haiku)\b/i, familyLabel);
|
|
187
|
+
}
|
|
188
|
+
if (!isVagueClaudeFamilyLabel(raw) && !valueVersion) {
|
|
189
|
+
return raw;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (valueVersion) return `Claude ${familyLabel} ${valueVersion}`;
|
|
194
|
+
return `Claude ${familyLabel} (version unknown)`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeEffortIds(value: unknown, fallbackFamily: string): string[] {
|
|
198
|
+
const fromArray = readStringArray(value);
|
|
199
|
+
return fromArray && fromArray.length > 0 ? fromArray : fallbackEffortIds(fallbackFamily);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function normalizeClaudeCodeModel(raw: unknown, source: ClaudeCodeModelSource): ClaudeCodeModel | null {
|
|
203
|
+
if (typeof raw === "string") {
|
|
204
|
+
const value = raw.trim();
|
|
205
|
+
if (!value) return null;
|
|
206
|
+
const family = modelFamilyFromValue(value);
|
|
207
|
+
const version = versionFromModelText(value) ?? undefined;
|
|
208
|
+
return {
|
|
209
|
+
label: normalizeClaudeDisplayLabel(value),
|
|
210
|
+
family,
|
|
211
|
+
value,
|
|
212
|
+
canonicalId: value,
|
|
213
|
+
source,
|
|
214
|
+
...(version ? { version } : {}),
|
|
215
|
+
isFallback: source === "fallback",
|
|
216
|
+
effortLevels: fallbackEffortIds(family),
|
|
217
|
+
defaultEffort: fallbackDefaultEffort(family),
|
|
218
|
+
effortSource: "fallback",
|
|
219
|
+
effortVerified: false,
|
|
220
|
+
description: source === "settings" ? "Claude settings model" : undefined,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!isRecord(raw)) return null;
|
|
225
|
+
const value = readString(raw.value ?? raw.model ?? raw.modelId ?? raw.id ?? raw.name);
|
|
226
|
+
if (!value) return null;
|
|
227
|
+
const family = readString(raw.family) ?? modelFamilyFromValue(value);
|
|
228
|
+
// Accept both camelCase and snake_case effort field names for compatibility.
|
|
229
|
+
const rawEffortArray = raw.effortLevels ?? raw.effort_levels ?? raw.supportedEfforts ?? raw.supported_efforts;
|
|
230
|
+
const effortLevels = normalizeEffortIds(rawEffortArray, family);
|
|
231
|
+
const defaultEffort = readString(raw.defaultEffort ?? raw.default_effort ?? raw.effortLevel ?? raw.effort_level)
|
|
232
|
+
?? fallbackDefaultEffort(family);
|
|
233
|
+
const hasRawEffortArray = Array.isArray(rawEffortArray);
|
|
234
|
+
|
|
235
|
+
const version = versionFromModelText(value) ?? versionFromModelText(readString(raw.label ?? raw.displayName ?? raw.display_name) ?? "");
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
label: normalizeClaudeDisplayLabel(value, readString(raw.label ?? raw.displayName ?? raw.display_name)),
|
|
239
|
+
family,
|
|
240
|
+
value,
|
|
241
|
+
canonicalId: readString(raw.canonicalId ?? raw.canonical_id) ?? value,
|
|
242
|
+
source,
|
|
243
|
+
...(version ? { version } : {}),
|
|
244
|
+
isFallback: source === "fallback",
|
|
245
|
+
discoveryKind: readString(raw.discoveryKind ?? raw.discovery_kind) === "aliases" ? "aliases" : "models",
|
|
246
|
+
effortLevels,
|
|
247
|
+
defaultEffort: effortLevels.includes(defaultEffort) ? defaultEffort : effortLevels[0] ?? "medium",
|
|
248
|
+
effortSource: hasRawEffortArray ? source : "fallback",
|
|
249
|
+
effortVerified: (source === "claude-code" || source === "claude-code-command" || source === "claude-code-package") && hasRawEffortArray,
|
|
250
|
+
description: readString(raw.description),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function dedupeModels(models: ClaudeCodeModel[]): ClaudeCodeModel[] {
|
|
255
|
+
const seen = new Set<string>();
|
|
256
|
+
const result: ClaudeCodeModel[] = [];
|
|
257
|
+
for (const model of models) {
|
|
258
|
+
const key = model.value.toLowerCase();
|
|
259
|
+
if (seen.has(key)) continue;
|
|
260
|
+
seen.add(key);
|
|
261
|
+
result.push(model);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function fallbackClaudeModels(): ClaudeCodeModel[] {
|
|
267
|
+
return ANTHROPIC_FALLBACK_MODELS.map((model) => ({
|
|
268
|
+
label: model.label,
|
|
269
|
+
family: model.family ?? model.modelId,
|
|
270
|
+
value: model.modelId,
|
|
271
|
+
canonicalId: model.canonicalId ?? model.modelId,
|
|
272
|
+
source: "fallback",
|
|
273
|
+
isFallback: true,
|
|
274
|
+
discoveryKind: "aliases",
|
|
275
|
+
effortLevels: model.supportedReasoningLevels?.map((level) => level.id) ?? [],
|
|
276
|
+
defaultEffort: model.defaultReasoningLevel ?? "medium",
|
|
277
|
+
effortSource: "fallback",
|
|
278
|
+
effortVerified: false,
|
|
279
|
+
description: model.description ?? undefined,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function applyAvailableModelAllowlist(models: ClaudeCodeModel[], allowlist: readonly string[] | undefined): ClaudeCodeModel[] {
|
|
284
|
+
if (!allowlist || allowlist.length === 0) return models;
|
|
285
|
+
const allowed = new Set(allowlist.map((item) => item.toLowerCase()));
|
|
286
|
+
const filtered = models.filter((model) =>
|
|
287
|
+
allowed.has(model.value.toLowerCase()) ||
|
|
288
|
+
allowed.has(model.family.toLowerCase()) ||
|
|
289
|
+
allowed.has(model.canonicalId.toLowerCase()) ||
|
|
290
|
+
allowed.has(model.label.toLowerCase())
|
|
291
|
+
);
|
|
292
|
+
return filtered.length > 0 ? filtered : models;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function defaultClaudeSettingsPath(): string | null {
|
|
296
|
+
const home = process.env.USERPROFILE ?? process.env.HOME;
|
|
297
|
+
return home ? join(home, ".claude", "settings.json") : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readClaudeSettings(settingsPath: string | null | undefined): ClaudeSettingsInfo | null {
|
|
301
|
+
const path = settingsPath === undefined ? defaultClaudeSettingsPath() : settingsPath;
|
|
302
|
+
if (!path || !existsSync(path)) return null;
|
|
303
|
+
try {
|
|
304
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
305
|
+
if (!isRecord(parsed)) return null;
|
|
306
|
+
// Accept both camelCase and snake_case field names — Claude settings.json has used both across versions.
|
|
307
|
+
const availableModels = readStringArray(parsed.availableModels ?? parsed.available_models);
|
|
308
|
+
const modelOverrides = parsed.modelOverrides ?? parsed.model_overrides;
|
|
309
|
+
const overrideModels = isRecord(modelOverrides)
|
|
310
|
+
? Object.entries(modelOverrides)
|
|
311
|
+
.map(([key, value]) => normalizeClaudeCodeModel(
|
|
312
|
+
isRecord(value) ? { id: key, ...value } : key,
|
|
313
|
+
"settings",
|
|
314
|
+
))
|
|
315
|
+
.filter((model): model is ClaudeCodeModel => Boolean(model))
|
|
316
|
+
: [];
|
|
317
|
+
return {
|
|
318
|
+
path,
|
|
319
|
+
model: readString(parsed.model ?? parsed.defaultModel ?? parsed.default_model),
|
|
320
|
+
effortLevel: readString(parsed.effortLevel ?? parsed.effort_level),
|
|
321
|
+
availableModels,
|
|
322
|
+
models: overrideModels,
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function runClaudeCommand(
|
|
330
|
+
executable: string,
|
|
331
|
+
args: string[],
|
|
332
|
+
cwd: string,
|
|
333
|
+
runCommandImpl: CommandRunner,
|
|
334
|
+
timeoutMs: number,
|
|
335
|
+
): Promise<CommandResult> {
|
|
336
|
+
const spawnSpec = buildClaudeSpawnSpec(executable, args);
|
|
337
|
+
return await runCommandImpl({
|
|
338
|
+
executable: spawnSpec.executable,
|
|
339
|
+
args: spawnSpec.args,
|
|
340
|
+
cwd,
|
|
341
|
+
timeoutMs,
|
|
342
|
+
}).result;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseModelsFromJsonOutput(stdout: string): ClaudeCodeModel[] {
|
|
346
|
+
let parsed: unknown;
|
|
347
|
+
try {
|
|
348
|
+
parsed = JSON.parse(stdout.trim());
|
|
349
|
+
} catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
const rawModels = Array.isArray(parsed)
|
|
353
|
+
? parsed
|
|
354
|
+
: isRecord(parsed) && Array.isArray(parsed.models)
|
|
355
|
+
? parsed.models
|
|
356
|
+
: isRecord(parsed) && Array.isArray(parsed.data)
|
|
357
|
+
? parsed.data
|
|
358
|
+
: [];
|
|
359
|
+
return dedupeModels(
|
|
360
|
+
rawModels
|
|
361
|
+
.map((raw) => normalizeClaudeCodeModel(raw, "claude-code-command"))
|
|
362
|
+
.filter((model): model is ClaudeCodeModel => Boolean(model)),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
interface ClaudeCodePackageMetadataDiscovery {
|
|
367
|
+
sourcePath: string;
|
|
368
|
+
rawModelIds: string[];
|
|
369
|
+
models: ClaudeCodeModel[];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function uniqueStrings(values: Iterable<string>): string[] {
|
|
373
|
+
return Array.from(new Set(Array.from(values).filter(Boolean)));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function executablePathCandidates(command: string): string[] {
|
|
377
|
+
if (command.includes("/") || command.includes("\\")) return [command];
|
|
378
|
+
const pathValue = process.env.PATH ?? "";
|
|
379
|
+
const extensions = process.platform === "win32"
|
|
380
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
381
|
+
: [""];
|
|
382
|
+
const candidates: string[] = [];
|
|
383
|
+
for (const directory of pathValue.split(process.platform === "win32" ? ";" : ":")) {
|
|
384
|
+
if (!directory) continue;
|
|
385
|
+
for (const extension of extensions) {
|
|
386
|
+
candidates.push(join(directory, `${command}${extension}`));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return candidates;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function defaultClaudeMetadataPaths(resolvedCommand: string): string[] {
|
|
393
|
+
const candidates: string[] = [];
|
|
394
|
+
for (const executablePath of executablePathCandidates(resolvedCommand)) {
|
|
395
|
+
candidates.push(executablePath);
|
|
396
|
+
try {
|
|
397
|
+
const realPath = realpathSync.native(executablePath);
|
|
398
|
+
candidates.push(realPath);
|
|
399
|
+
candidates.push(join(dirname(dirname(realPath)), "package.json"));
|
|
400
|
+
} catch {
|
|
401
|
+
// Best effort only: package metadata discovery must never block CLI probing.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return uniqueStrings(candidates);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function extractClaudeModelIdsFromMetadata(raw: Buffer): string[] {
|
|
408
|
+
const text = raw.toString("latin1");
|
|
409
|
+
const ids = new Set<string>();
|
|
410
|
+
// Fresh regex per call — /g flag requires a clean lastIndex on each invocation.
|
|
411
|
+
// The minor version is optional (mirrors RE_VERSIONED_ID): major-only ids like
|
|
412
|
+
// claude-sonnet-5 must be discovered too. \d{1,2} keeps date suffixes
|
|
413
|
+
// (claude-haiku-4-5-20251001) from being absorbed into the version.
|
|
414
|
+
const pattern = new RegExp(`\\bclaude-(${CLAUDE_FAMILIES_ALT})-(\\d+)(?:-(\\d{1,2}))?\\b`, "g");
|
|
415
|
+
for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
|
|
416
|
+
ids.add(match[0]);
|
|
417
|
+
}
|
|
418
|
+
return Array.from(ids);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function latestModelIdByFamily(modelIds: readonly string[]): Map<string, { id: string; version: string }> {
|
|
422
|
+
const result = new Map<string, { id: string; version: string }>();
|
|
423
|
+
for (const id of modelIds) {
|
|
424
|
+
const family = modelFamilyFromValue(id);
|
|
425
|
+
const version = versionFromModelText(id);
|
|
426
|
+
if (!version) continue;
|
|
427
|
+
const existing = result.get(family);
|
|
428
|
+
if (!existing || compareVersionStrings(version, existing.version) > 0) {
|
|
429
|
+
result.set(family, { id, version });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function aliasResolvedModel(family: string, canonicalId: string, version: string): ClaudeCodeModel {
|
|
436
|
+
const alias = family;
|
|
437
|
+
return {
|
|
438
|
+
label: normalizeClaudeDisplayLabel(canonicalId),
|
|
439
|
+
family,
|
|
440
|
+
value: alias,
|
|
441
|
+
canonicalId,
|
|
442
|
+
source: "claude-code-package",
|
|
443
|
+
version,
|
|
444
|
+
isFallback: false,
|
|
445
|
+
discoveryKind: "aliases",
|
|
446
|
+
effortLevels: fallbackEffortIds(family),
|
|
447
|
+
defaultEffort: fallbackDefaultEffort(family),
|
|
448
|
+
effortSource: "fallback",
|
|
449
|
+
effortVerified: false,
|
|
450
|
+
description: `Claude Code alias resolved to ${canonicalId} from installed package metadata`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveAliasModelsWithPackageMetadata(
|
|
455
|
+
models: readonly ClaudeCodeModel[],
|
|
456
|
+
packageMetadata: ClaudeCodePackageMetadataDiscovery | null,
|
|
457
|
+
): ClaudeCodeModel[] {
|
|
458
|
+
if (!packageMetadata) return [...models];
|
|
459
|
+
const byFamily = new Map(packageMetadata.models.map((model) => [model.family, model]));
|
|
460
|
+
return models.map((model) => {
|
|
461
|
+
if (!isVagueClaudeFamilyLabel(model.value) || model.version) return model;
|
|
462
|
+
return byFamily.get(model.family) ?? model;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function allModelsHaveKnownVersions(models: readonly ClaudeCodeModel[]): boolean {
|
|
467
|
+
return models.length > 0 && models.every((model) => Boolean(model.version));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function discoverModelsFromClaudePackageMetadata(
|
|
471
|
+
resolvedCommand: string,
|
|
472
|
+
metadataPaths?: readonly string[],
|
|
473
|
+
): ClaudeCodePackageMetadataDiscovery | null {
|
|
474
|
+
const paths = metadataPaths
|
|
475
|
+
? uniqueStrings(metadataPaths)
|
|
476
|
+
: defaultClaudeMetadataPaths(resolvedCommand);
|
|
477
|
+
|
|
478
|
+
for (const path of paths) {
|
|
479
|
+
if (!existsSync(path)) continue;
|
|
480
|
+
try {
|
|
481
|
+
const rawModelIds = extractClaudeModelIdsFromMetadata(readFileSync(path));
|
|
482
|
+
const latestByFamily = latestModelIdByFamily(rawModelIds);
|
|
483
|
+
const models = CLAUDE_MODEL_FAMILIES
|
|
484
|
+
.map((family) => {
|
|
485
|
+
const latest = latestByFamily.get(family);
|
|
486
|
+
return latest ? aliasResolvedModel(family, latest.id, latest.version) : null;
|
|
487
|
+
})
|
|
488
|
+
.filter((model): model is ClaudeCodeModel => Boolean(model));
|
|
489
|
+
if (models.length > 0) {
|
|
490
|
+
return {
|
|
491
|
+
sourcePath: path,
|
|
492
|
+
rawModelIds,
|
|
493
|
+
models,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Claude Code CLI does not consistently advertise JSON model listing in its help text,
|
|
504
|
+
// so we probe these well-known command shapes directly first, before falling back to
|
|
505
|
+
// help-text analysis. Each probe fails fast (non-zero exit) if unsupported.
|
|
506
|
+
const PROBE_MODEL_LIST_ARGS: string[][] = [
|
|
507
|
+
["model", "list", "--json"],
|
|
508
|
+
["models", "--json"],
|
|
509
|
+
["models", "list", "--json"],
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
function candidateModelListArgs(helpOutput: string): string[][] {
|
|
513
|
+
const text = helpOutput.toLowerCase();
|
|
514
|
+
const candidates: string[][] = [];
|
|
515
|
+
if (/model\s+list/.test(text) && /--json/.test(text)) {
|
|
516
|
+
candidates.push(["model", "list", "--json"]);
|
|
517
|
+
}
|
|
518
|
+
if (/models\s+list/.test(text) && /--json/.test(text)) {
|
|
519
|
+
candidates.push(["models", "list", "--json"]);
|
|
520
|
+
}
|
|
521
|
+
if (/\bmodels\b/.test(text) && /--json/.test(text)) {
|
|
522
|
+
candidates.push(["models", "--json"]);
|
|
523
|
+
}
|
|
524
|
+
return candidates;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function discoverModelsFromClaudeHelp(
|
|
528
|
+
executable: string,
|
|
529
|
+
cwd: string,
|
|
530
|
+
runCommandImpl: CommandRunner,
|
|
531
|
+
timeoutMs: number,
|
|
532
|
+
rootHelpText: string,
|
|
533
|
+
): Promise<ClaudeCodeModel[]> {
|
|
534
|
+
// Step 1: try well-known command shapes directly, without requiring the help text
|
|
535
|
+
// to advertise them. Claude Code CLI versions vary in whether (and how) they expose
|
|
536
|
+
// a model list command — probing directly is more reliable than help-text detection.
|
|
537
|
+
const triedArgKeys = new Set<string>();
|
|
538
|
+
for (const args of PROBE_MODEL_LIST_ARGS) {
|
|
539
|
+
triedArgKeys.add(args.join("\0"));
|
|
540
|
+
const result = await runClaudeCommand(executable, args, cwd, runCommandImpl, timeoutMs);
|
|
541
|
+
if (result.status === "completed" && result.exitCode === 0) {
|
|
542
|
+
const models = parseModelsFromJsonOutput(result.stdout);
|
|
543
|
+
if (models.length > 0) return models;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Step 2: fall through to help-text-based detection as a supplementary strategy
|
|
548
|
+
// for future Claude Code versions that advertise additional model-list commands.
|
|
549
|
+
const modelHelpResult = await runClaudeCommand(executable, ["model", "--help"], cwd, runCommandImpl, timeoutMs);
|
|
550
|
+
const candidates = [
|
|
551
|
+
...candidateModelListArgs(rootHelpText),
|
|
552
|
+
...([modelHelpResult].flatMap((result) =>
|
|
553
|
+
result.status === "completed" && result.exitCode === 0
|
|
554
|
+
? candidateModelListArgs(`${result.stdout}\n${result.stderr}`)
|
|
555
|
+
: []
|
|
556
|
+
)),
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
for (const args of candidates) {
|
|
560
|
+
if (triedArgKeys.has(args.join("\0"))) continue; // skip already-probed commands
|
|
561
|
+
const result = await runClaudeCommand(executable, args, cwd, runCommandImpl, timeoutMs);
|
|
562
|
+
if (result.status !== "completed" || result.exitCode !== 0) continue;
|
|
563
|
+
const models = parseModelsFromJsonOutput(result.stdout);
|
|
564
|
+
if (models.length > 0) return models;
|
|
565
|
+
}
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function claudeCodeModelsToProviderModels(models: readonly ClaudeCodeModel[]): readonly ProviderModel[] {
|
|
570
|
+
return models.map((model) => ({
|
|
571
|
+
id: model.value,
|
|
572
|
+
modelId: model.value,
|
|
573
|
+
label: model.label,
|
|
574
|
+
description: model.description ?? (
|
|
575
|
+
model.source === "fallback"
|
|
576
|
+
? `${model.label} - Fallback defaults${model.effortVerified ? "" : "; effort metadata unverified"}`
|
|
577
|
+
: model.source === "settings" || model.source === "config" || model.source === "claude-code-config"
|
|
578
|
+
? `${model.label} - From Claude settings`
|
|
579
|
+
: `${model.label} - ${model.discoveryKind === "aliases" ? "Claude Code alias resolved" : "Discovered from Claude Code"} (${model.source})`
|
|
580
|
+
),
|
|
581
|
+
defaultReasoningLevel: model.defaultEffort,
|
|
582
|
+
supportedReasoningLevels: getClaudeCodeEffortLevels(model.effortLevels),
|
|
583
|
+
source: model.source,
|
|
584
|
+
canonicalId: model.canonicalId,
|
|
585
|
+
family: model.family,
|
|
586
|
+
version: model.version,
|
|
587
|
+
isFallback: model.isFallback,
|
|
588
|
+
discoveryKind: model.discoveryKind,
|
|
589
|
+
effortSource: model.effortSource,
|
|
590
|
+
effortVerified: model.effortVerified,
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function discoverClaudeCodeCapabilities(
|
|
595
|
+
options: DiscoverClaudeCodeCapabilitiesOptions,
|
|
596
|
+
): Promise<ClaudeCodeCapabilityDiscovery> {
|
|
597
|
+
const runCommandImpl = options.runCommandImpl ?? runCommand;
|
|
598
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
599
|
+
const resolvedCommand = await resolveClaudeExecutable({
|
|
600
|
+
runCommandImpl: options.runCommandImpl,
|
|
601
|
+
cwd: options.cwd,
|
|
602
|
+
configuredPath: options.configuredPath,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const authResult = await runClaudeCommand(resolvedCommand, ["auth", "status"], options.cwd, runCommandImpl, timeoutMs);
|
|
606
|
+
const authJson = authResult.status === "completed" && authResult.exitCode === 0
|
|
607
|
+
? parseClaudeAuthStatus(authResult.stdout)
|
|
608
|
+
: null;
|
|
609
|
+
const auth: ClaudeCodeAuthInfo = authJson?.loggedIn === true
|
|
610
|
+
? authJson
|
|
611
|
+
: { loggedIn: false };
|
|
612
|
+
|
|
613
|
+
const settings = readClaudeSettings(options.settingsPath);
|
|
614
|
+
const helpResult = await runClaudeCommand(resolvedCommand, ["--help"], options.cwd, runCommandImpl, timeoutMs);
|
|
615
|
+
const helpText = helpResult.status === "completed" && helpResult.exitCode === 0
|
|
616
|
+
? `${helpResult.stdout}\n${helpResult.stderr}`
|
|
617
|
+
: "";
|
|
618
|
+
const cliEffortLevels = parseClaudeEffortLevelsFromHelp(helpText);
|
|
619
|
+
const discoveredModels = await discoverModelsFromClaudeHelp(resolvedCommand, options.cwd, runCommandImpl, timeoutMs, helpText);
|
|
620
|
+
const packageMetadata = discoverModelsFromClaudePackageMetadata(resolvedCommand, options.metadataPaths);
|
|
621
|
+
|
|
622
|
+
let modelSource: ClaudeCodeModelSource = "fallback";
|
|
623
|
+
let models: ClaudeCodeModel[] = fallbackClaudeModels();
|
|
624
|
+
|
|
625
|
+
if (discoveredModels.length > 0) {
|
|
626
|
+
const resolvedModels = resolveAliasModelsWithPackageMetadata(discoveredModels, packageMetadata);
|
|
627
|
+
modelSource = allModelsHaveKnownVersions(resolvedModels) && resolvedModels.some((model) => model.source === "claude-code-package")
|
|
628
|
+
? "claude-code-package"
|
|
629
|
+
: "claude-code-command";
|
|
630
|
+
models = resolvedModels;
|
|
631
|
+
} else if (packageMetadata?.models && packageMetadata.models.length > 0) {
|
|
632
|
+
modelSource = "claude-code-package";
|
|
633
|
+
models = packageMetadata.models;
|
|
634
|
+
} else if (settings?.models && settings.models.length > 0) {
|
|
635
|
+
modelSource = "settings";
|
|
636
|
+
models = [...settings.models];
|
|
637
|
+
} else if (settings?.availableModels && settings.availableModels.length > 0) {
|
|
638
|
+
modelSource = "settings";
|
|
639
|
+
models = settings.availableModels
|
|
640
|
+
.map((model) => normalizeClaudeCodeModel(model, "settings"))
|
|
641
|
+
.filter((model): model is ClaudeCodeModel => Boolean(model));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (settings?.model && !models.some((model) =>
|
|
645
|
+
model.value === settings.model || model.canonicalId === settings.model || model.family === settings.model
|
|
646
|
+
)) {
|
|
647
|
+
const settingsModel = normalizeClaudeCodeModel(settings.model, "settings");
|
|
648
|
+
if (settingsModel) {
|
|
649
|
+
models = [settingsModel, ...models];
|
|
650
|
+
if (modelSource === "fallback") modelSource = "settings";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
models = applyAvailableModelAllowlist(dedupeModels(models), settings?.availableModels);
|
|
655
|
+
if (cliEffortLevels) {
|
|
656
|
+
models = models.map((model) => {
|
|
657
|
+
if (model.effortSource !== "fallback") return model;
|
|
658
|
+
const preferredDefault = settings?.effortLevel && cliEffortLevels.includes(settings.effortLevel)
|
|
659
|
+
? settings.effortLevel
|
|
660
|
+
: model.defaultEffort;
|
|
661
|
+
return {
|
|
662
|
+
...model,
|
|
663
|
+
effortLevels: cliEffortLevels,
|
|
664
|
+
defaultEffort: cliEffortLevels.includes(preferredDefault) ? preferredDefault : cliEffortLevels[0] ?? "high",
|
|
665
|
+
effortSource: "claude-code-command",
|
|
666
|
+
effortVerified: true,
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
provider: "anthropic",
|
|
673
|
+
backend: "claude-code-cli",
|
|
674
|
+
resolvedCommand,
|
|
675
|
+
auth,
|
|
676
|
+
models,
|
|
677
|
+
modelSource,
|
|
678
|
+
discoveredAt: (options.now?.() ?? new Date()).toISOString(),
|
|
679
|
+
...(settings ? {
|
|
680
|
+
settings: {
|
|
681
|
+
path: settings.path,
|
|
682
|
+
...(settings.model ? { model: settings.model } : {}),
|
|
683
|
+
...(settings.effortLevel ? { effortLevel: settings.effortLevel } : {}),
|
|
684
|
+
...(settings.availableModels ? { availableModels: settings.availableModels } : {}),
|
|
685
|
+
},
|
|
686
|
+
} : {}),
|
|
687
|
+
diagnostics: {
|
|
688
|
+
authExitCode: authResult.exitCode,
|
|
689
|
+
authStatus: authResult.status,
|
|
690
|
+
authJsonParsed: authJson !== null,
|
|
691
|
+
loggedIn: auth.loggedIn,
|
|
692
|
+
modelSource,
|
|
693
|
+
settingsPath: settings?.path ?? null,
|
|
694
|
+
packageMetadataPath: packageMetadata?.sourcePath ?? null,
|
|
695
|
+
packageMetadataModelCount: packageMetadata?.rawModelIds.length ?? 0,
|
|
696
|
+
rawPackageModelIds: packageMetadata?.rawModelIds.slice(0, 12).join(",") ?? null,
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function getClaudeModelDefaultEffort(modelId: string, models: readonly ProviderModel[]): string {
|
|
702
|
+
const normalized = modelId.toLowerCase();
|
|
703
|
+
const model = models.find((item) =>
|
|
704
|
+
item.modelId.toLowerCase() === normalized ||
|
|
705
|
+
item.id.toLowerCase() === normalized ||
|
|
706
|
+
item.family?.toLowerCase() === normalized ||
|
|
707
|
+
item.canonicalId?.toLowerCase() === normalized ||
|
|
708
|
+
(item.family ? normalized.includes(item.family.toLowerCase()) : false)
|
|
709
|
+
);
|
|
710
|
+
return model?.defaultReasoningLevel ?? "medium";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function modelSupportsClaudeEffort(modelId: string, effort: string | null | undefined, models: readonly ProviderModel[]): boolean {
|
|
714
|
+
if (!effort) return false;
|
|
715
|
+
const normalized = modelId.toLowerCase();
|
|
716
|
+
const model = models.find((item) =>
|
|
717
|
+
item.modelId.toLowerCase() === normalized ||
|
|
718
|
+
item.id.toLowerCase() === normalized ||
|
|
719
|
+
item.family?.toLowerCase() === normalized ||
|
|
720
|
+
item.canonicalId?.toLowerCase() === normalized ||
|
|
721
|
+
(item.family ? normalized.includes(item.family.toLowerCase()) : false)
|
|
722
|
+
);
|
|
723
|
+
return Boolean(model?.supportedReasoningLevels?.some((level) => level.id === effort));
|
|
724
|
+
}
|