linkshell-cli 0.5.6 → 0.5.8

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.
@@ -434,8 +434,19 @@ export function claudePermissionModeFor(input: {
434
434
  return "default";
435
435
  }
436
436
 
437
+ const FALLBACK_MODEL_LABELS: Record<string, string> = {
438
+ default: "默认模型",
439
+ sonnet: "Sonnet",
440
+ opus: "Opus",
441
+ opusplan: "Opus Plan",
442
+ haiku: "Haiku",
443
+ "sonnet[1m]": "Sonnet 1M",
444
+ "opus[1m]": "Opus 1M",
445
+ };
446
+
437
447
  export class ClaudeSdkClient {
438
448
  private query: ClaudeQuery | undefined;
449
+ private startup: ((params?: { options?: Record<string, unknown>; initializeTimeoutMs?: number }) => Promise<{ query(prompt: string): AsyncGenerator<unknown> & { supportedModels(): Promise<{ value: string; displayName: string }[]>; interrupt(): Promise<void> }; close(): void }>) | undefined;
439
450
  private claudeSessionId: string | undefined;
440
451
  private abortController: AbortController | undefined;
441
452
  private permissionWaiters = new Map<string, (outcome: "allow" | "deny") => void>();
@@ -461,6 +472,12 @@ export class ClaudeSdkClient {
461
472
  throw new Error("Claude Agent SDK does not export query()");
462
473
  }
463
474
  this.query = query as ClaudeQuery;
475
+ // startup() pre-warms the CLI subprocess so we can discover available models
476
+ // without starting a full conversation (via supportedModels() on the Query).
477
+ const startup = (mod as { startup?: unknown }).startup;
478
+ if (typeof startup === "function") {
479
+ this.startup = startup as typeof this.startup;
480
+ }
464
481
  return { status: "ok", protocol: "claude-agent-sdk" };
465
482
  } catch (error) {
466
483
  throw new Error(
@@ -675,27 +692,63 @@ export class ClaudeSdkClient {
675
692
 
676
693
  async listModels(): Promise<unknown> {
677
694
  let defaultModel: string = "default";
695
+ // Read the user's configured default model from settings.json. This is always
696
+ // consulted (both for live discovery and for the fallback path).
697
+ let availableModels: string[] | undefined;
678
698
  try {
679
699
  const settingsPath = join(homedir(), ".claude", "settings.json");
680
700
  if (existsSync(settingsPath)) {
681
- const raw = JSON.parse(readFileSync(settingsPath, "utf8")) as { model?: unknown };
701
+ const raw = JSON.parse(readFileSync(settingsPath, "utf8")) as { model?: unknown; availableModels?: unknown };
682
702
  if (typeof raw.model === "string" && raw.model.trim().length > 0) {
683
703
  defaultModel = raw.model.trim();
684
704
  }
705
+ if (Array.isArray(raw.availableModels)) {
706
+ availableModels = raw.availableModels.filter((m): m is string => typeof m === "string");
707
+ }
685
708
  }
686
709
  } catch {
687
710
  // Fallback to "default" if settings file is unreadable.
688
711
  }
712
+
713
+ // Dynamic model discovery: use the Claude Agent SDK's startup() to get the
714
+ // REAL model list for THIS API key (different keys/tiers see different models).
715
+ // Falls back to settings.json availableModels → hardcoded list on any failure.
716
+ if (this.startup) {
717
+ let warm: Awaited<ReturnType<NonNullable<typeof this.startup>>> | undefined;
718
+ try {
719
+ warm = await this.startup({ initializeTimeoutMs: 10_000 });
720
+ const q = warm.query("");
721
+ try {
722
+ const sdkModels: { value: string; displayName: string }[] = await (q as unknown as { supportedModels(): Promise<{ value: string; displayName: string }[]> }).supportedModels();
723
+ if (sdkModels.length > 0) {
724
+ // Filter to admin-configured allowlist if present, then map to our format.
725
+ const allow = availableModels;
726
+ const filtered = allow
727
+ ? sdkModels.filter((m) => allow.includes(m.value) || m.value === "default")
728
+ : sdkModels;
729
+ return {
730
+ defaultModel,
731
+ models: filtered.map((m) => ({ id: m.value, label: m.displayName || m.value })),
732
+ };
733
+ }
734
+ } catch {
735
+ // supportedModels() failed — the subprocess may be in a bad state. Close it.
736
+ } finally {
737
+ try { warm.close(); } catch { /* ignore */ }
738
+ }
739
+ } catch {
740
+ // startup() failed — clean up if it partially succeeded.
741
+ try { warm?.close(); } catch { /* ignore */ }
742
+ }
743
+ }
744
+
745
+ // Fallback: admin-configured allowlist, or the full Claude Code alias set.
746
+ const fallbackModels = availableModels ?? [
747
+ "default", "sonnet", "opus", "opusplan", "haiku", "sonnet[1m]", "opus[1m]",
748
+ ];
689
749
  return {
690
750
  defaultModel,
691
- models: [
692
- { id: "default", label: "默认模型" },
693
- { id: "sonnet", label: "Sonnet" },
694
- { id: "opus", label: "Opus" },
695
- { id: "haiku", label: "Haiku" },
696
- { id: "sonnet[1m]", label: "Sonnet 1M" },
697
- { id: "opusplan", label: "Opus Plan" },
698
- ],
751
+ models: fallbackModels.map((id) => ({ id, label: FALLBACK_MODEL_LABELS[id] ?? id })),
699
752
  };
700
753
  }
701
754
 
@@ -574,16 +574,30 @@ export class ClaudeStreamJsonClient {
574
574
  }
575
575
 
576
576
  async listModels(): Promise<unknown> {
577
+ let defaultModel: string = "default";
578
+ let availableModels: string[] | undefined;
579
+ try {
580
+ const settingsPath = join(homedir(), ".claude", "settings.json");
581
+ if (existsSync(settingsPath)) {
582
+ const raw = JSON.parse(readFileSync(settingsPath, "utf8")) as { model?: unknown; availableModels?: unknown };
583
+ if (typeof raw.model === "string" && raw.model.trim().length > 0) {
584
+ defaultModel = raw.model.trim();
585
+ }
586
+ if (Array.isArray(raw.availableModels)) {
587
+ availableModels = raw.availableModels.filter((m): m is string => typeof m === "string");
588
+ }
589
+ }
590
+ } catch {
591
+ // Fallback to defaults if settings file is unreadable.
592
+ }
593
+ const ids = availableModels ?? ["default", "sonnet", "opus", "opusplan", "haiku", "sonnet[1m]", "opus[1m]"];
594
+ const LABELS: Record<string, string> = {
595
+ default: "默认模型", sonnet: "Sonnet", opus: "Opus", opusplan: "Opus Plan",
596
+ haiku: "Haiku", "sonnet[1m]": "Sonnet 1M", "opus[1m]": "Opus 1M",
597
+ };
577
598
  return {
578
- defaultModel: "default",
579
- models: [
580
- { id: "default", label: "默认模型" },
581
- { id: "sonnet", label: "Sonnet" },
582
- { id: "opus", label: "Opus" },
583
- { id: "haiku", label: "Haiku" },
584
- { id: "sonnet[1m]", label: "Sonnet 1M" },
585
- { id: "opusplan", label: "Opus Plan" },
586
- ],
599
+ defaultModel,
600
+ models: ids.map((id) => ({ id, label: LABELS[id] ?? id })),
587
601
  };
588
602
  }
589
603