@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.
Files changed (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /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
- export type ClaudeCodeModelSource = "claude-code" | "config" | "settings" | "fallback";
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): 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 labelFromModelValue(value: string, fallbackLabel?: string): string {
118
- if (fallbackLabel) return fallbackLabel;
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 fallback = fallbackModelByFamily(family);
121
- return fallback?.label ?? value;
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 fallback = fallbackModelByFamily(family);
199
+ const version = versionFromModelText(value) ?? undefined;
136
200
  return {
137
- label: labelFromModelValue(value, fallback?.label),
201
+ label: normalizeClaudeDisplayLabel(value),
138
202
  family,
139
- value: fallback?.modelId ?? value,
140
- canonicalId: fallback?.canonicalId ?? value,
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) ?? labelFromModelValue(value, fallback?.label),
230
+ label: normalizeClaudeDisplayLabel(value, readString(raw.label ?? raw.displayName ?? raw.display_name)),
164
231
  family,
165
- value: value === fallback?.canonicalId ? fallback.modelId : value,
166
- canonicalId: readString(raw.canonicalId ?? raw.canonical_id) ?? fallback?.canonicalId ?? value,
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
- : `${model.label} - ${model.source === "claude-code" ? "Discovered from Claude Code" : "From Claude settings"}`
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
- modelSource = "claude-code";
373
- models = discoveredModels;
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-opus-4-7": {
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
- // Short alias IDs used by ANTHROPIC_FALLBACK_MODELS canonical versioned IDs in the registry.
153
- const ANTHROPIC_MODEL_ALIAS_MAP: Record<string, string> = {
154
- opus: "claude-opus-4-7",
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
- ? (ANTHROPIC_MODEL_ALIAS_MAP[modelId] ?? modelId)
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 {