@golba98/codexa 1.0.3 → 1.0.4
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/README.md +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +533 -275
- package/src/commands/handler.ts +2 -2
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +1 -1
- package/src/config/trustStore.ts +1 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/executableResolver.ts +5 -1
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/{channel.ts → version/channel.ts} +1 -1
- package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +1 -1
- package/src/ui/AppShell.tsx +672 -706
- package/src/ui/AttachmentImportPanel.tsx +2 -2
- package/src/ui/BottomComposer.tsx +145 -144
- package/src/ui/ModelPickerScreen.tsx +216 -36
- package/src/ui/ModelReasoningPicker.tsx +1 -1
- package/src/ui/PlanReviewPanel.tsx +1 -1
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +4 -0
- package/src/ui/SettingsPanel.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +1 -1
- package/src/ui/Timeline.tsx +1619 -1470
- package/src/ui/TopHeader.tsx +46 -30
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +2 -2
- package/src/ui/UpdateAvailableCard.tsx +3 -3
- package/src/ui/UpdatePromptPanel.tsx +16 -22
- package/src/ui/layout.ts +298 -24
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +4 -8
- package/src/ui/runtimeDisplay.ts +15 -3
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/timelineMeasure.ts +194 -122
- package/src/core/codex.ts +0 -124
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
3
|
import { buildClaudeSpawnSpec, resolveClaudeExecutable } from "../executables/claudeExecutable.js";
|
|
4
4
|
import { runCommand } from "../process/CommandRunner.js";
|
|
5
5
|
import type { CommandResult } from "../process/CommandRunner.js";
|
|
@@ -9,7 +9,23 @@ import { CLAUDE_CODE_EFFORT_IDS, getClaudeCodeEffortLevels } from "./reasoning.j
|
|
|
9
9
|
import type { ProviderModel } from "./types.js";
|
|
10
10
|
|
|
11
11
|
type CommandRunner = typeof runCommand;
|
|
12
|
-
|
|
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";
|
|
13
29
|
|
|
14
30
|
export interface ClaudeCodeAuthInfo {
|
|
15
31
|
loggedIn: boolean;
|
|
@@ -24,6 +40,9 @@ export interface ClaudeCodeModel {
|
|
|
24
40
|
value: string;
|
|
25
41
|
canonicalId: string;
|
|
26
42
|
source: ClaudeCodeModelSource;
|
|
43
|
+
version?: string;
|
|
44
|
+
isFallback: boolean;
|
|
45
|
+
discoveryKind?: "models" | "aliases";
|
|
27
46
|
effortLevels: readonly string[];
|
|
28
47
|
defaultEffort: string;
|
|
29
48
|
effortSource: ClaudeCodeModelSource;
|
|
@@ -55,6 +74,7 @@ export interface DiscoverClaudeCodeCapabilitiesOptions {
|
|
|
55
74
|
settingsPath?: string | null;
|
|
56
75
|
now?: () => Date;
|
|
57
76
|
timeoutMs?: number;
|
|
77
|
+
metadataPaths?: readonly string[];
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
interface ClaudeSettingsInfo {
|
|
@@ -94,7 +114,7 @@ export function parseClaudeAuthStatus(stdout: string): ClaudeCodeAuthInfo | null
|
|
|
94
114
|
}
|
|
95
115
|
}
|
|
96
116
|
|
|
97
|
-
function modelFamilyFromValue(value: string):
|
|
117
|
+
function modelFamilyFromValue(value: string): ClaudeModelFamily {
|
|
98
118
|
const normalized = value.toLowerCase();
|
|
99
119
|
if (normalized.includes("opus")) return "opus";
|
|
100
120
|
if (normalized.includes("haiku")) return "haiku";
|
|
@@ -114,11 +134,55 @@ function fallbackDefaultEffort(family: string): string {
|
|
|
114
134
|
return fallbackModelByFamily(family)?.defaultReasoningLevel ?? "medium";
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
137
|
+
function titleCaseFamily(family: string): string {
|
|
138
|
+
if (family === "opus") return "Opus";
|
|
139
|
+
if (family === "haiku") return "Haiku";
|
|
140
|
+
return "Sonnet";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function versionFromModelText(value: string): string | null {
|
|
144
|
+
const normalized = value.trim().toLowerCase();
|
|
145
|
+
const idMatch = normalized.match(RE_VERSIONED_ID);
|
|
146
|
+
if (idMatch) return idMatch[2] ? `${idMatch[1]}.${idMatch[2]}` : idMatch[1] ?? null;
|
|
147
|
+
const labelMatch = normalized.match(/^(?:claude\s+)?(?:opus|sonnet|haiku)\s+(\d+(?:\.\d+)?)\b/);
|
|
148
|
+
return labelMatch?.[1] ?? null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function compareVersionStrings(left: string | null | undefined, right: string | null | undefined): number {
|
|
152
|
+
const leftParts = (left ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
153
|
+
const rightParts = (right ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
154
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
155
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
156
|
+
const l = Number.isFinite(leftParts[index]) ? leftParts[index] : 0;
|
|
157
|
+
const r = Number.isFinite(rightParts[index]) ? rightParts[index] : 0;
|
|
158
|
+
if (l !== r) return l - r;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isVagueClaudeFamilyLabel(value: string): boolean {
|
|
164
|
+
return RE_VAGUE_LABEL.test(value.trim());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeClaudeDisplayLabel(value: string, rawLabel?: string): string {
|
|
119
168
|
const family = modelFamilyFromValue(value);
|
|
120
|
-
const
|
|
121
|
-
|
|
169
|
+
const familyLabel = titleCaseFamily(family);
|
|
170
|
+
const raw = rawLabel?.trim();
|
|
171
|
+
const valueVersion = versionFromModelText(value);
|
|
172
|
+
|
|
173
|
+
if (raw) {
|
|
174
|
+
const rawVersion = versionFromModelText(raw);
|
|
175
|
+
if (rawVersion) {
|
|
176
|
+
const prefixed = /^claude\b/i.test(raw) ? raw : `Claude ${raw}`;
|
|
177
|
+
return prefixed.replace(/\b(opus|sonnet|haiku)\b/i, familyLabel);
|
|
178
|
+
}
|
|
179
|
+
if (!isVagueClaudeFamilyLabel(raw) && !valueVersion) {
|
|
180
|
+
return raw;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (valueVersion) return `Claude ${familyLabel} ${valueVersion}`;
|
|
185
|
+
return `Claude ${familyLabel} (version unknown)`;
|
|
122
186
|
}
|
|
123
187
|
|
|
124
188
|
function normalizeEffortIds(value: unknown, fallbackFamily: string): string[] {
|
|
@@ -132,13 +196,15 @@ function normalizeClaudeCodeModel(raw: unknown, source: ClaudeCodeModelSource):
|
|
|
132
196
|
const value = raw.trim();
|
|
133
197
|
if (!value) return null;
|
|
134
198
|
const family = modelFamilyFromValue(value);
|
|
135
|
-
const
|
|
199
|
+
const version = versionFromModelText(value) ?? undefined;
|
|
136
200
|
return {
|
|
137
|
-
label:
|
|
201
|
+
label: normalizeClaudeDisplayLabel(value),
|
|
138
202
|
family,
|
|
139
|
-
value
|
|
140
|
-
canonicalId:
|
|
203
|
+
value,
|
|
204
|
+
canonicalId: value,
|
|
141
205
|
source,
|
|
206
|
+
...(version ? { version } : {}),
|
|
207
|
+
isFallback: source === "fallback",
|
|
142
208
|
effortLevels: fallbackEffortIds(family),
|
|
143
209
|
defaultEffort: fallbackDefaultEffort(family),
|
|
144
210
|
effortSource: "fallback",
|
|
@@ -148,10 +214,9 @@ function normalizeClaudeCodeModel(raw: unknown, source: ClaudeCodeModelSource):
|
|
|
148
214
|
}
|
|
149
215
|
|
|
150
216
|
if (!isRecord(raw)) return null;
|
|
151
|
-
const value = readString(raw.value ?? raw.model ?? raw.modelId ?? raw.id);
|
|
217
|
+
const value = readString(raw.value ?? raw.model ?? raw.modelId ?? raw.id ?? raw.name);
|
|
152
218
|
if (!value) return null;
|
|
153
219
|
const family = readString(raw.family) ?? modelFamilyFromValue(value);
|
|
154
|
-
const fallback = fallbackModelByFamily(family);
|
|
155
220
|
// Accept both camelCase and snake_case effort field names for compatibility.
|
|
156
221
|
const rawEffortArray = raw.effortLevels ?? raw.effort_levels ?? raw.supportedEfforts ?? raw.supported_efforts;
|
|
157
222
|
const effortLevels = normalizeEffortIds(rawEffortArray, family);
|
|
@@ -159,16 +224,21 @@ function normalizeClaudeCodeModel(raw: unknown, source: ClaudeCodeModelSource):
|
|
|
159
224
|
?? fallbackDefaultEffort(family);
|
|
160
225
|
const hasRawEffortArray = Array.isArray(rawEffortArray);
|
|
161
226
|
|
|
227
|
+
const version = versionFromModelText(value) ?? versionFromModelText(readString(raw.label ?? raw.displayName ?? raw.display_name) ?? "");
|
|
228
|
+
|
|
162
229
|
return {
|
|
163
|
-
label: readString(raw.label ?? raw.displayName ?? raw.display_name)
|
|
230
|
+
label: normalizeClaudeDisplayLabel(value, readString(raw.label ?? raw.displayName ?? raw.display_name)),
|
|
164
231
|
family,
|
|
165
|
-
value
|
|
166
|
-
canonicalId: readString(raw.canonicalId ?? raw.canonical_id) ??
|
|
232
|
+
value,
|
|
233
|
+
canonicalId: readString(raw.canonicalId ?? raw.canonical_id) ?? value,
|
|
167
234
|
source,
|
|
235
|
+
...(version ? { version } : {}),
|
|
236
|
+
isFallback: source === "fallback",
|
|
237
|
+
discoveryKind: readString(raw.discoveryKind ?? raw.discovery_kind) === "aliases" ? "aliases" : "models",
|
|
168
238
|
effortLevels,
|
|
169
239
|
defaultEffort: effortLevels.includes(defaultEffort) ? defaultEffort : effortLevels[0] ?? "medium",
|
|
170
240
|
effortSource: hasRawEffortArray ? source : "fallback",
|
|
171
|
-
effortVerified: source === "claude-code" && hasRawEffortArray,
|
|
241
|
+
effortVerified: (source === "claude-code" || source === "claude-code-command" || source === "claude-code-package") && hasRawEffortArray,
|
|
172
242
|
description: readString(raw.description),
|
|
173
243
|
};
|
|
174
244
|
}
|
|
@@ -192,6 +262,8 @@ function fallbackClaudeModels(): ClaudeCodeModel[] {
|
|
|
192
262
|
value: model.modelId,
|
|
193
263
|
canonicalId: model.canonicalId ?? model.modelId,
|
|
194
264
|
source: "fallback",
|
|
265
|
+
isFallback: true,
|
|
266
|
+
discoveryKind: "aliases",
|
|
195
267
|
effortLevels: model.supportedReasoningLevels?.map((level) => level.id) ?? [],
|
|
196
268
|
defaultEffort: model.defaultReasoningLevel ?? "medium",
|
|
197
269
|
effortSource: "fallback",
|
|
@@ -278,11 +350,154 @@ function parseModelsFromJsonOutput(stdout: string): ClaudeCodeModel[] {
|
|
|
278
350
|
: [];
|
|
279
351
|
return dedupeModels(
|
|
280
352
|
rawModels
|
|
281
|
-
.map((raw) => normalizeClaudeCodeModel(raw, "claude-code"))
|
|
353
|
+
.map((raw) => normalizeClaudeCodeModel(raw, "claude-code-command"))
|
|
282
354
|
.filter((model): model is ClaudeCodeModel => Boolean(model)),
|
|
283
355
|
);
|
|
284
356
|
}
|
|
285
357
|
|
|
358
|
+
interface ClaudeCodePackageMetadataDiscovery {
|
|
359
|
+
sourcePath: string;
|
|
360
|
+
rawModelIds: string[];
|
|
361
|
+
models: ClaudeCodeModel[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function uniqueStrings(values: Iterable<string>): string[] {
|
|
365
|
+
return Array.from(new Set(Array.from(values).filter(Boolean)));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function executablePathCandidates(command: string): string[] {
|
|
369
|
+
if (command.includes("/") || command.includes("\\")) return [command];
|
|
370
|
+
const pathValue = process.env.PATH ?? "";
|
|
371
|
+
const extensions = process.platform === "win32"
|
|
372
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
|
|
373
|
+
: [""];
|
|
374
|
+
const candidates: string[] = [];
|
|
375
|
+
for (const directory of pathValue.split(process.platform === "win32" ? ";" : ":")) {
|
|
376
|
+
if (!directory) continue;
|
|
377
|
+
for (const extension of extensions) {
|
|
378
|
+
candidates.push(join(directory, `${command}${extension}`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return candidates;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function defaultClaudeMetadataPaths(resolvedCommand: string): string[] {
|
|
385
|
+
const candidates: string[] = [];
|
|
386
|
+
for (const executablePath of executablePathCandidates(resolvedCommand)) {
|
|
387
|
+
candidates.push(executablePath);
|
|
388
|
+
try {
|
|
389
|
+
const realPath = realpathSync.native(executablePath);
|
|
390
|
+
candidates.push(realPath);
|
|
391
|
+
candidates.push(join(dirname(dirname(realPath)), "package.json"));
|
|
392
|
+
} catch {
|
|
393
|
+
// Best effort only: package metadata discovery must never block CLI probing.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return uniqueStrings(candidates);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function extractClaudeModelIdsFromMetadata(raw: Buffer): string[] {
|
|
400
|
+
const text = raw.toString("latin1");
|
|
401
|
+
const ids = new Set<string>();
|
|
402
|
+
// Fresh regex per call — /g flag requires a clean lastIndex on each invocation.
|
|
403
|
+
const pattern = new RegExp(`\\bclaude-(${CLAUDE_FAMILIES_ALT})-(\\d+)-(\\d{1,2})\\b`, "g");
|
|
404
|
+
for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
|
|
405
|
+
ids.add(match[0]);
|
|
406
|
+
}
|
|
407
|
+
return Array.from(ids);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function latestModelIdByFamily(modelIds: readonly string[]): Map<string, { id: string; version: string }> {
|
|
411
|
+
const result = new Map<string, { id: string; version: string }>();
|
|
412
|
+
for (const id of modelIds) {
|
|
413
|
+
const family = modelFamilyFromValue(id);
|
|
414
|
+
const version = versionFromModelText(id);
|
|
415
|
+
if (!version) continue;
|
|
416
|
+
const existing = result.get(family);
|
|
417
|
+
if (!existing || compareVersionStrings(version, existing.version) > 0) {
|
|
418
|
+
result.set(family, { id, version });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function aliasResolvedModel(family: string, canonicalId: string, version: string): ClaudeCodeModel {
|
|
425
|
+
const alias = family;
|
|
426
|
+
return {
|
|
427
|
+
label: normalizeClaudeDisplayLabel(canonicalId),
|
|
428
|
+
family,
|
|
429
|
+
value: alias,
|
|
430
|
+
canonicalId,
|
|
431
|
+
source: "claude-code-package",
|
|
432
|
+
version,
|
|
433
|
+
isFallback: false,
|
|
434
|
+
discoveryKind: "aliases",
|
|
435
|
+
effortLevels: fallbackEffortIds(family),
|
|
436
|
+
defaultEffort: fallbackDefaultEffort(family),
|
|
437
|
+
effortSource: "fallback",
|
|
438
|
+
effortVerified: false,
|
|
439
|
+
description: `Claude Code alias resolved to ${canonicalId} from installed package metadata`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function resolveAliasModelsWithPackageMetadata(
|
|
444
|
+
models: readonly ClaudeCodeModel[],
|
|
445
|
+
packageMetadata: ClaudeCodePackageMetadataDiscovery | null,
|
|
446
|
+
): ClaudeCodeModel[] {
|
|
447
|
+
if (!packageMetadata) return [...models];
|
|
448
|
+
const byFamily = new Map(packageMetadata.models.map((model) => [model.family, model]));
|
|
449
|
+
return models.map((model) => {
|
|
450
|
+
if (!isVagueClaudeFamilyLabel(model.value) || model.version) return model;
|
|
451
|
+
return byFamily.get(model.family) ?? model;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function allModelsHaveKnownVersions(models: readonly ClaudeCodeModel[]): boolean {
|
|
456
|
+
return models.length > 0 && models.every((model) => Boolean(model.version));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function discoverModelsFromClaudePackageMetadata(
|
|
460
|
+
resolvedCommand: string,
|
|
461
|
+
metadataPaths?: readonly string[],
|
|
462
|
+
): ClaudeCodePackageMetadataDiscovery | null {
|
|
463
|
+
const paths = metadataPaths
|
|
464
|
+
? uniqueStrings(metadataPaths)
|
|
465
|
+
: defaultClaudeMetadataPaths(resolvedCommand);
|
|
466
|
+
|
|
467
|
+
for (const path of paths) {
|
|
468
|
+
if (!existsSync(path)) continue;
|
|
469
|
+
try {
|
|
470
|
+
const rawModelIds = extractClaudeModelIdsFromMetadata(readFileSync(path));
|
|
471
|
+
const latestByFamily = latestModelIdByFamily(rawModelIds);
|
|
472
|
+
const models = CLAUDE_MODEL_FAMILIES
|
|
473
|
+
.map((family) => {
|
|
474
|
+
const latest = latestByFamily.get(family);
|
|
475
|
+
return latest ? aliasResolvedModel(family, latest.id, latest.version) : null;
|
|
476
|
+
})
|
|
477
|
+
.filter((model): model is ClaudeCodeModel => Boolean(model));
|
|
478
|
+
if (models.length > 0) {
|
|
479
|
+
return {
|
|
480
|
+
sourcePath: path,
|
|
481
|
+
rawModelIds,
|
|
482
|
+
models,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Claude Code CLI does not consistently advertise JSON model listing in its help text,
|
|
493
|
+
// so we probe these well-known command shapes directly first, before falling back to
|
|
494
|
+
// help-text analysis. Each probe fails fast (non-zero exit) if unsupported.
|
|
495
|
+
const PROBE_MODEL_LIST_ARGS: string[][] = [
|
|
496
|
+
["model", "list", "--json"],
|
|
497
|
+
["models", "--json"],
|
|
498
|
+
["models", "list", "--json"],
|
|
499
|
+
];
|
|
500
|
+
|
|
286
501
|
function candidateModelListArgs(helpOutput: string): string[][] {
|
|
287
502
|
const text = helpOutput.toLowerCase();
|
|
288
503
|
const candidates: string[][] = [];
|
|
@@ -304,6 +519,21 @@ async function discoverModelsFromClaudeHelp(
|
|
|
304
519
|
runCommandImpl: CommandRunner,
|
|
305
520
|
timeoutMs: number,
|
|
306
521
|
): Promise<ClaudeCodeModel[]> {
|
|
522
|
+
// Step 1: try well-known command shapes directly, without requiring the help text
|
|
523
|
+
// to advertise them. Claude Code CLI versions vary in whether (and how) they expose
|
|
524
|
+
// a model list command — probing directly is more reliable than help-text detection.
|
|
525
|
+
const triedArgKeys = new Set<string>();
|
|
526
|
+
for (const args of PROBE_MODEL_LIST_ARGS) {
|
|
527
|
+
triedArgKeys.add(args.join("\0"));
|
|
528
|
+
const result = await runClaudeCommand(executable, args, cwd, runCommandImpl, timeoutMs);
|
|
529
|
+
if (result.status === "completed" && result.exitCode === 0) {
|
|
530
|
+
const models = parseModelsFromJsonOutput(result.stdout);
|
|
531
|
+
if (models.length > 0) return models;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Step 2: fall through to help-text-based detection as a supplementary strategy
|
|
536
|
+
// for future Claude Code versions that advertise additional model-list commands.
|
|
307
537
|
const helpResults = await Promise.all([
|
|
308
538
|
runClaudeCommand(executable, ["--help"], cwd, runCommandImpl, timeoutMs),
|
|
309
539
|
runClaudeCommand(executable, ["model", "--help"], cwd, runCommandImpl, timeoutMs),
|
|
@@ -315,6 +545,7 @@ async function discoverModelsFromClaudeHelp(
|
|
|
315
545
|
);
|
|
316
546
|
|
|
317
547
|
for (const args of candidates) {
|
|
548
|
+
if (triedArgKeys.has(args.join("\0"))) continue; // skip already-probed commands
|
|
318
549
|
const result = await runClaudeCommand(executable, args, cwd, runCommandImpl, timeoutMs);
|
|
319
550
|
if (result.status !== "completed" || result.exitCode !== 0) continue;
|
|
320
551
|
const models = parseModelsFromJsonOutput(result.stdout);
|
|
@@ -331,13 +562,18 @@ export function claudeCodeModelsToProviderModels(models: readonly ClaudeCodeMode
|
|
|
331
562
|
description: model.description ?? (
|
|
332
563
|
model.source === "fallback"
|
|
333
564
|
? `${model.label} - Fallback defaults${model.effortVerified ? "" : "; effort metadata unverified"}`
|
|
334
|
-
:
|
|
565
|
+
: model.source === "settings" || model.source === "config" || model.source === "claude-code-config"
|
|
566
|
+
? `${model.label} - From Claude settings`
|
|
567
|
+
: `${model.label} - ${model.discoveryKind === "aliases" ? "Claude Code alias resolved" : "Discovered from Claude Code"} (${model.source})`
|
|
335
568
|
),
|
|
336
569
|
defaultReasoningLevel: model.defaultEffort,
|
|
337
570
|
supportedReasoningLevels: getClaudeCodeEffortLevels(model.effortLevels),
|
|
338
571
|
source: model.source,
|
|
339
572
|
canonicalId: model.canonicalId,
|
|
340
573
|
family: model.family,
|
|
574
|
+
version: model.version,
|
|
575
|
+
isFallback: model.isFallback,
|
|
576
|
+
discoveryKind: model.discoveryKind,
|
|
341
577
|
effortSource: model.effortSource,
|
|
342
578
|
effortVerified: model.effortVerified,
|
|
343
579
|
}));
|
|
@@ -364,13 +600,20 @@ export async function discoverClaudeCodeCapabilities(
|
|
|
364
600
|
|
|
365
601
|
const settings = readClaudeSettings(options.settingsPath);
|
|
366
602
|
const discoveredModels = await discoverModelsFromClaudeHelp(resolvedCommand, options.cwd, runCommandImpl, timeoutMs);
|
|
603
|
+
const packageMetadata = discoverModelsFromClaudePackageMetadata(resolvedCommand, options.metadataPaths);
|
|
367
604
|
|
|
368
605
|
let modelSource: ClaudeCodeModelSource = "fallback";
|
|
369
606
|
let models: ClaudeCodeModel[] = fallbackClaudeModels();
|
|
370
607
|
|
|
371
608
|
if (discoveredModels.length > 0) {
|
|
372
|
-
|
|
373
|
-
|
|
609
|
+
const resolvedModels = resolveAliasModelsWithPackageMetadata(discoveredModels, packageMetadata);
|
|
610
|
+
modelSource = allModelsHaveKnownVersions(resolvedModels) && resolvedModels.some((model) => model.source === "claude-code-package")
|
|
611
|
+
? "claude-code-package"
|
|
612
|
+
: "claude-code-command";
|
|
613
|
+
models = resolvedModels;
|
|
614
|
+
} else if (packageMetadata?.models && packageMetadata.models.length > 0) {
|
|
615
|
+
modelSource = "claude-code-package";
|
|
616
|
+
models = packageMetadata.models;
|
|
374
617
|
} else if (settings?.models && settings.models.length > 0) {
|
|
375
618
|
modelSource = "settings";
|
|
376
619
|
models = [...settings.models];
|
|
@@ -416,6 +659,9 @@ export async function discoverClaudeCodeCapabilities(
|
|
|
416
659
|
loggedIn: auth.loggedIn,
|
|
417
660
|
modelSource,
|
|
418
661
|
settingsPath: settings?.path ?? null,
|
|
662
|
+
packageMetadataPath: packageMetadata?.sourcePath ?? null,
|
|
663
|
+
packageMetadataModelCount: packageMetadata?.rawModelIds.length ?? 0,
|
|
664
|
+
rawPackageModelIds: packageMetadata?.rawModelIds.slice(0, 12).join(",") ?? null,
|
|
419
665
|
},
|
|
420
666
|
};
|
|
421
667
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolveClaudeExecutable } from "../executables/claudeExecutable.js";
|
|
2
|
+
import { runCommand } from "../process/CommandRunner.js";
|
|
3
|
+
import {
|
|
4
|
+
claudeCodeModelsToProviderModels,
|
|
5
|
+
discoverClaudeCodeCapabilities,
|
|
6
|
+
discoverModelsFromClaudePackageMetadata,
|
|
7
|
+
} from "./claudeCodeDiscovery.js";
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const resolvedCommand = await resolveClaudeExecutable({ cwd });
|
|
12
|
+
const versionResult = await runCommand({
|
|
13
|
+
executable: resolvedCommand,
|
|
14
|
+
args: ["--version"],
|
|
15
|
+
cwd,
|
|
16
|
+
timeoutMs: 5_000,
|
|
17
|
+
}).result;
|
|
18
|
+
const packageMetadata = discoverModelsFromClaudePackageMetadata(resolvedCommand);
|
|
19
|
+
const discovery = await discoverClaudeCodeCapabilities({ cwd });
|
|
20
|
+
const normalized = claudeCodeModelsToProviderModels(discovery.models);
|
|
21
|
+
|
|
22
|
+
const report = {
|
|
23
|
+
claudeCommand: resolvedCommand,
|
|
24
|
+
claudeBinaryPath: packageMetadata?.sourcePath ?? resolvedCommand,
|
|
25
|
+
claudeVersion: versionResult.status === "completed" && versionResult.exitCode === 0
|
|
26
|
+
? versionResult.stdout.trim()
|
|
27
|
+
: null,
|
|
28
|
+
discoverySourceUsed: discovery.modelSource,
|
|
29
|
+
packageMetadataSource: packageMetadata?.sourcePath ?? null,
|
|
30
|
+
rawDiscoveredModelEntries: packageMetadata?.rawModelIds ?? [],
|
|
31
|
+
normalizedModelEntries: normalized.map((model) => ({
|
|
32
|
+
id: model.modelId,
|
|
33
|
+
label: model.label,
|
|
34
|
+
family: model.family,
|
|
35
|
+
version: model.version,
|
|
36
|
+
canonicalId: model.canonicalId,
|
|
37
|
+
source: model.source,
|
|
38
|
+
isFallback: model.isFallback,
|
|
39
|
+
discoveryKind: model.discoveryKind,
|
|
40
|
+
})),
|
|
41
|
+
fallbackReason: discovery.modelSource === "fallback"
|
|
42
|
+
? discovery.diagnostics ?? { reason: "No Claude Code command, package metadata, settings, or config model source returned versioned models." }
|
|
43
|
+
: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await main();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
process.stderr.write(`debug:claude-models failed: ${message}\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
@@ -67,14 +67,9 @@ const KNOWN_CONTEXT_REGISTRY: Record<string, KnownContextRegistryEntry> = {
|
|
|
67
67
|
// Exact documented Anthropic model IDs only. Provider aliases such as
|
|
68
68
|
// "haiku" remain unknown unless configured or discovered by CLI metadata.
|
|
69
69
|
// Source: https://docs.anthropic.com/en/docs/about-claude/models
|
|
70
|
-
"anthropic:claude-
|
|
71
|
-
contextLength: 200_000,
|
|
72
|
-
sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
73
|
-
note: "Anthropic documented context window for this exact model ID.",
|
|
74
|
-
},
|
|
75
|
-
"anthropic:claude-sonnet-4-6": {
|
|
76
|
-
contextLength: 200_000,
|
|
77
|
-
sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
70
|
+
"anthropic:claude-sonnet-4-6": {
|
|
71
|
+
contextLength: 200_000,
|
|
72
|
+
sourceUrl: "https://docs.anthropic.com/en/docs/about-claude/models",
|
|
78
73
|
note: "Anthropic documented context window for this exact model ID.",
|
|
79
74
|
},
|
|
80
75
|
"anthropic:claude-haiku-4-5": {
|
|
@@ -149,16 +144,9 @@ const KNOWN_CONTEXT_REGISTRY: Record<string, KnownContextRegistryEntry> = {
|
|
|
149
144
|
},
|
|
150
145
|
};
|
|
151
146
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
sonnet: "claude-sonnet-4-6",
|
|
156
|
-
haiku: "claude-haiku-4-5",
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Normalise OpenAI/Codex model IDs to lowercase-dashed form before registry lookup.
|
|
160
|
-
// Handles display-label variants like "GPT-5 Codex" → "gpt-5-codex".
|
|
161
|
-
function normalizeOpenAIModelId(modelId: string): string {
|
|
147
|
+
// Normalise OpenAI/Codex model IDs to lowercase-dashed form before registry lookup.
|
|
148
|
+
// Handles display-label variants like "GPT-5 Codex" → "gpt-5-codex".
|
|
149
|
+
function normalizeOpenAIModelId(modelId: string): string {
|
|
162
150
|
return modelId.toLowerCase().replace(/\s+/g, "-");
|
|
163
151
|
}
|
|
164
152
|
|
|
@@ -333,12 +321,12 @@ function resolveFromConfig(
|
|
|
333
321
|
};
|
|
334
322
|
}
|
|
335
323
|
|
|
336
|
-
function resolveFromKnownRegistry(providerId: ProviderId, modelId: string): ModelContextMetadata | null {
|
|
337
|
-
const lookupId = providerId === "anthropic"
|
|
338
|
-
?
|
|
339
|
-
: providerId === "openai"
|
|
340
|
-
? normalizeOpenAIModelId(modelId)
|
|
341
|
-
: modelId;
|
|
324
|
+
function resolveFromKnownRegistry(providerId: ProviderId, modelId: string): ModelContextMetadata | null {
|
|
325
|
+
const lookupId = providerId === "anthropic"
|
|
326
|
+
? modelId
|
|
327
|
+
: providerId === "openai"
|
|
328
|
+
? normalizeOpenAIModelId(modelId)
|
|
329
|
+
: modelId;
|
|
342
330
|
const entry = KNOWN_CONTEXT_REGISTRY[`${providerId}:${lookupId}`];
|
|
343
331
|
if (!entry) return null;
|
|
344
332
|
return {
|