@juspay/neurolink 11.11.4 → 11.11.6

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.
@@ -58,23 +58,6 @@ export declare function isRollingHandoffCapable(state: ProxySupervisorState | nu
58
58
  * confirm a mismatch" and falls through to the args-only result.
59
59
  */
60
60
  export declare function processLooksLikeProxySupervisor(pid: number, expectedStartTimeIso?: string): Promise<boolean>;
61
- declare function getOpenCodeConfigDir(): string;
62
- declare function getOpenCodeConfigPath(): string;
63
- declare function setOpenCodeProxySettings(baseUrl: string, proxyKey?: string): Promise<boolean>;
64
- declare function clearOpenCodeProxySettings(expectedBaseUrl?: string): Promise<boolean>;
65
- /**
66
- * Test-only export (CLAUDE.md rule 15 determinism exception). The OpenCode
67
- * client writers resolve paths from the environment and are only reachable
68
- * from `proxy start` / `proxy setup`, neither of which can be driven against a
69
- * throwaway HOME without starting a real server. Consumed by
70
- * test/continuous-test-suite-proxy.ts.
71
- */
72
- export declare const __openCodeTestHooks: {
73
- getOpenCodeConfigDir: typeof getOpenCodeConfigDir;
74
- getOpenCodeConfigPath: typeof getOpenCodeConfigPath;
75
- setOpenCodeProxySettings: typeof setOpenCodeProxySettings;
76
- clearOpenCodeProxySettings: typeof clearOpenCodeProxySettings;
77
- };
78
61
  export declare function probeProxyHealth(host: string, port: number, timeoutMs: number): Promise<ProxyHealthProbe>;
79
62
  export declare function mapClaudeErrorTypeToStatus(errorType?: string): number;
80
63
  export declare function createProxyStartApp(params: {
@@ -101,4 +84,3 @@ export declare const proxyGuardCommand: CommandModule<object, ProxyGuardArgs>;
101
84
  export declare const proxySetupCommand: CommandModule;
102
85
  export declare const proxyInstallCommand: CommandModule;
103
86
  export declare const proxyUninstallCommand: CommandModule;
104
- export {};
@@ -17,6 +17,7 @@ import chalk from "chalk";
17
17
  import ora from "ora";
18
18
  import { buildProxyHealthResponse, createProxyReadinessState, markProxyDrainingForUpdate, markProxyReady, resumeProxyConnections, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js";
19
19
  import { logger } from "../../lib/utils/logger.js";
20
+ import { applyAllClients, restoreAllClients, } from "../proxy-clients/registry.js";
20
21
  import { redactUrlsInText, sanitizeForLog, } from "../../lib/utils/logSanitize.js";
21
22
  import { withTimeout } from "../../lib/utils/async/withTimeout.js";
22
23
  import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js";
@@ -125,7 +126,6 @@ function loadProxySupervisorState() {
125
126
  function clearProxySupervisorState() {
126
127
  proxySupervisorStateManager.clear();
127
128
  }
128
- const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
129
129
  const PLIST_LABEL = "com.neurolink.proxy";
130
130
  const PLIST_DIR = join(homedir(), "Library", "LaunchAgents");
131
131
  const PLIST_PATH = join(PLIST_DIR, `${PLIST_LABEL}.plist`);
@@ -437,398 +437,6 @@ function getProxyWorkerGeneration() {
437
437
  function isProxyAutoUpdateEnabled(value = process.env.NEUROLINK_PROXY_AUTO_UPDATE) {
438
438
  return !["0", "off", "false"].includes((value ?? "").trim().toLowerCase());
439
439
  }
440
- /** Keys we manage in Claude Code's settings.env */
441
- const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"];
442
- async function setClaudeProxySettings(baseUrl) {
443
- const fs = await import("fs");
444
- let settings = {};
445
- try {
446
- settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
447
- }
448
- catch {
449
- // file missing/invalid — create fresh settings object
450
- }
451
- const env = (settings.env ?? {});
452
- // Preserve original values so clearClaudeProxySettings can restore them.
453
- // Only snapshot once — subsequent calls should not overwrite the snapshot.
454
- const originals = (settings
455
- .__proxy_original_env ?? {});
456
- for (const key of PROXY_MANAGED_KEYS) {
457
- if (!(key in originals)) {
458
- originals[key] = key in env ? env[key] : null;
459
- }
460
- }
461
- settings.__proxy_original_env = originals;
462
- env.ANTHROPIC_BASE_URL = baseUrl;
463
- env.ENABLE_TOOL_SEARCH = "true";
464
- settings.env = env;
465
- fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
466
- }
467
- async function clearClaudeProxySettings(expectedBaseUrl) {
468
- const fs = await import("fs");
469
- let settings;
470
- try {
471
- settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8"));
472
- }
473
- catch {
474
- return false;
475
- }
476
- const env = settings.env;
477
- if (!env) {
478
- return false;
479
- }
480
- if (expectedBaseUrl &&
481
- typeof env.ANTHROPIC_BASE_URL === "string" &&
482
- env.ANTHROPIC_BASE_URL !== expectedBaseUrl) {
483
- // User switched to a different proxy URL; do not clobber.
484
- return false;
485
- }
486
- const hadBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string";
487
- const hadToolSearch = env.ENABLE_TOOL_SEARCH === "true";
488
- // Restore original values if they were saved, otherwise delete the keys
489
- const originals = (settings
490
- .__proxy_original_env ?? {});
491
- for (const key of PROXY_MANAGED_KEYS) {
492
- const original = originals[key];
493
- if (original !== undefined && original !== null) {
494
- // Restore the value that existed before the proxy was started
495
- env[key] = original;
496
- }
497
- else {
498
- // Key did not exist before — remove it
499
- delete env[key];
500
- }
501
- }
502
- delete settings.__proxy_original_env;
503
- if (Object.keys(env).length === 0) {
504
- delete settings.env;
505
- }
506
- else {
507
- settings.env = env;
508
- }
509
- fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
510
- return hadBaseUrl || hadToolSearch;
511
- }
512
- // =============================================================================
513
- // OPENCODE AUTO-CONFIGURATION
514
- // =============================================================================
515
- function getOpenCodeConfigDir() {
516
- // OpenCode resolves this with the unmodified `xdg-basedir` package —
517
- // `XDG_CONFIG_HOME || ~/.config` — on every platform, macOS included. There
518
- // is deliberately no darwin branch here: `~/Library/Application Support/
519
- // opencode` is not a path OpenCode reads. (The similar-looking literal in
520
- // OpenCode's binary is `systemManagedConfigDir()`, an MDM policy directory
521
- // at the filesystem root with no $HOME prefix.)
522
- return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
523
- }
524
- function getOpenCodeConfigPath() {
525
- return join(getOpenCodeConfigDir(), "opencode.json");
526
- }
527
- /**
528
- * Key under which we persist the snapshot of the user's pre-existing
529
- * `provider.neurolink` config inside `opencode.json` itself. Persisting (rather
530
- * than relying on in-process state) means restoration still works even if the
531
- * proxy crashes or shutdown handlers run in a different process.
532
- *
533
- * Mirrors the Claude pattern (`__proxy_original_env` inside Claude's settings).
534
- */
535
- const OPENCODE_ORIGINAL_KEY = "__proxy_original_neurolink";
536
- async function setOpenCodeProxySettings(baseUrl, proxyKey) {
537
- const fs = await import("fs");
538
- const configDir = getOpenCodeConfigDir();
539
- try {
540
- fs.accessSync(configDir);
541
- }
542
- catch {
543
- // OpenCode not installed — config directory does not exist. Report the
544
- // skip so the caller does not print a success message for work that did
545
- // not happen.
546
- return false;
547
- }
548
- let config;
549
- try {
550
- config = JSON.parse(fs.readFileSync(getOpenCodeConfigPath(), "utf8"));
551
- }
552
- catch {
553
- // file missing/invalid — create fresh config object
554
- config = { provider: {} };
555
- }
556
- const provider = (config.provider ?? {});
557
- // Persist a snapshot of the user's pre-existing provider.neurolink — but
558
- // only the first time we touch the file. Subsequent set() calls must NOT
559
- // overwrite the snapshot (otherwise after the proxy writes its own block,
560
- // the next set() would store the proxy's block as the "original" and
561
- // permanently lose the user's real config on the next clear()).
562
- if (!(OPENCODE_ORIGINAL_KEY in config)) {
563
- config[OPENCODE_ORIGINAL_KEY] =
564
- "neurolink" in provider
565
- ? JSON.parse(JSON.stringify(provider.neurolink))
566
- : null;
567
- }
568
- provider.neurolink = {
569
- id: "neurolink",
570
- name: "NeuroLink Proxy",
571
- npm: "@ai-sdk/openai-compatible",
572
- env: [],
573
- models: {},
574
- options: {
575
- baseURL: baseUrl,
576
- apiKey: proxyKey || "neurolink-proxy",
577
- },
578
- };
579
- config.provider = provider;
580
- fs.writeFileSync(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
581
- return true;
582
- }
583
- async function clearOpenCodeProxySettings(expectedBaseUrl) {
584
- const fs = await import("fs");
585
- let config;
586
- try {
587
- config = JSON.parse(fs.readFileSync(getOpenCodeConfigPath(), "utf8"));
588
- }
589
- catch {
590
- return false;
591
- }
592
- const provider = config.provider;
593
- if (!provider || !("neurolink" in provider)) {
594
- return false;
595
- }
596
- // Check if our proxy URL matches before removing
597
- const existing = provider.neurolink;
598
- if (expectedBaseUrl && existing) {
599
- const options = existing.options;
600
- if (options && typeof options.baseURL === "string") {
601
- if (options.baseURL !== expectedBaseUrl) {
602
- // User configured a different URL; do not clobber
603
- return false;
604
- }
605
- }
606
- }
607
- const hadNeurolink = "neurolink" in provider;
608
- // Restore from the snapshot persisted at first set(), regardless of process
609
- // identity. Only delete provider.neurolink when the snapshot says the user
610
- // explicitly had no entry before — never on an "undefined" snapshot, since
611
- // that would mean the snapshot was lost and we cannot prove the entry is ours.
612
- if (OPENCODE_ORIGINAL_KEY in config) {
613
- const snapshot = config[OPENCODE_ORIGINAL_KEY];
614
- if (snapshot === null) {
615
- // User had no provider.neurolink before the proxy started — safe to remove.
616
- delete provider.neurolink;
617
- }
618
- else {
619
- provider.neurolink = snapshot;
620
- }
621
- delete config[OPENCODE_ORIGINAL_KEY];
622
- }
623
- else {
624
- // No snapshot present — refuse to delete to avoid destroying a config
625
- // the proxy may not own (e.g. a user wrote their own `neurolink` block
626
- // before the snapshot key was introduced, or this is being cleared from
627
- // a process that never ran set()).
628
- logger.debug("[proxy] OpenCode clear: no original-provider snapshot found, leaving provider.neurolink intact");
629
- return false;
630
- }
631
- config.provider = provider;
632
- fs.writeFileSync(getOpenCodeConfigPath(), JSON.stringify(config, null, 2));
633
- return hadNeurolink;
634
- }
635
- /**
636
- * Test-only export (CLAUDE.md rule 15 determinism exception). The OpenCode
637
- * client writers resolve paths from the environment and are only reachable
638
- * from `proxy start` / `proxy setup`, neither of which can be driven against a
639
- * throwaway HOME without starting a real server. Consumed by
640
- * test/continuous-test-suite-proxy.ts.
641
- */
642
- export const __openCodeTestHooks = {
643
- getOpenCodeConfigDir,
644
- getOpenCodeConfigPath,
645
- setOpenCodeProxySettings,
646
- clearOpenCodeProxySettings,
647
- };
648
- // =============================================================================
649
- // CODEX (ChatGPT) AUTO-CONFIGURATION
650
- // =============================================================================
651
- //
652
- // Points the Codex CLI at the proxy by managing `~/.codex/config.toml`:
653
- // - appends a marker-delimited `[model_providers.neurolink]` table
654
- // - flips the top-level `model_provider` to "neurolink"
655
- // The original `model_provider` value is snapshotted to a sidecar JSON so the
656
- // restore works even across process crashes. All edits are guarded and wrapped
657
- // so a failure never aborts proxy start/stop. Codex talks the Responses API to
658
- // the proxy's /backend-api/codex path.
659
- const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml");
660
- const CODEX_SNAPSHOT_PATH = join(homedir(), ".neurolink", "codex-proxy-snapshot.json");
661
- const CODEX_BLOCK_BEGIN = "# >>> neurolink-proxy (managed) >>>";
662
- const CODEX_BLOCK_END = "# <<< neurolink-proxy (managed) <<<";
663
- const CODEX_PROVIDER_LINE_RE = /^[ \t]*model_provider[ \t]*=.*$/m;
664
- const CODEX_MODEL_LINE_RE = /^[ \t]*model[ \t]*=.*$/m;
665
- /** Strip any previously-managed block + our injected provider line. */
666
- function stripCodexManagedConfig(text) {
667
- const blockRe = new RegExp(`\\n?${escapeRegExp(CODEX_BLOCK_BEGIN)}[\\s\\S]*?${escapeRegExp(CODEX_BLOCK_END)}\\n?`, "g");
668
- let out = text.replace(blockRe, "\n");
669
- // Remove our injected selector line (only the exact "neurolink" one).
670
- out = out.replace(/^[ \t]*model_provider[ \t]*=[ \t]*"neurolink"[ \t]*$\n?/m, "");
671
- return out;
672
- }
673
- function escapeRegExp(value) {
674
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
675
- }
676
- /**
677
- * Apply a top-level TOML key edit, confined to the document preamble.
678
- *
679
- * In TOML every key after a `[table]` header belongs to that table. A
680
- * document-wide regex therefore happily rewrites `model_provider` inside
681
- * `[model_providers.foo]`, which sets a table key instead of the top-level
682
- * selector: Codex keeps using its original provider while the command reports
683
- * success. Only the text before the first header can hold top-level keys.
684
- */
685
- function editTomlPreamble(text, edit) {
686
- const headerMatch = /^[ \t]*\[/m.exec(text);
687
- const boundary = headerMatch ? headerMatch.index : text.length;
688
- const preamble = text.slice(0, boundary);
689
- const edited = edit(preamble);
690
- return edited === null ? text : edited + text.slice(boundary);
691
- }
692
- function buildCodexProviderBlock(baseUrl) {
693
- return [
694
- CODEX_BLOCK_BEGIN,
695
- "[model_providers.neurolink]",
696
- 'name = "NeuroLink Proxy"',
697
- `base_url = "${baseUrl}/backend-api/codex"`,
698
- 'wire_api = "responses"',
699
- "requires_openai_auth = true",
700
- CODEX_BLOCK_END,
701
- "",
702
- ].join("\n");
703
- }
704
- async function setCodexProxySettings(baseUrl) {
705
- try {
706
- const fs = await import("fs");
707
- if (!fs.existsSync(CODEX_CONFIG_PATH)) {
708
- // Codex not installed / never configured — skip silently.
709
- return false;
710
- }
711
- const original = fs.readFileSync(CODEX_CONFIG_PATH, "utf8");
712
- // Snapshot the user's original selector once (survives crashes/restarts).
713
- if (!fs.existsSync(CODEX_SNAPSHOT_PATH)) {
714
- // Same preamble boundary the edit paths use. Matching document-wide would
715
- // capture a `model_provider` belonging to a table — a legacy
716
- // `[profiles.<name>]` block, say — and the restore would then write that
717
- // profile's provider back as the top-level selector.
718
- let providerMatch = null;
719
- editTomlPreamble(original, (preamble) => {
720
- providerMatch = preamble.match(CODEX_PROVIDER_LINE_RE);
721
- return null;
722
- });
723
- // Ignore a stale managed selector line if present in the original.
724
- const originalProviderLine = providerMatch && !/"neurolink"/.test(providerMatch[0])
725
- ? providerMatch[0]
726
- : null;
727
- fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true });
728
- fs.writeFileSync(CODEX_SNAPSHOT_PATH, JSON.stringify({ originalProviderLine }, null, 2), { mode: 0o600 });
729
- }
730
- let text = stripCodexManagedConfig(original);
731
- // Set the selector: replace an existing top-level model_provider or insert
732
- // one right after the top-level `model = ...` line (stays before any table).
733
- let selectorPlaced = false;
734
- text = editTomlPreamble(text, (preamble) => {
735
- if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
736
- selectorPlaced = true;
737
- return preamble.replace(CODEX_PROVIDER_LINE_RE, 'model_provider = "neurolink"');
738
- }
739
- if (CODEX_MODEL_LINE_RE.test(preamble)) {
740
- selectorPlaced = true;
741
- return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\nmodel_provider = "neurolink"`);
742
- }
743
- return null;
744
- });
745
- if (!selectorPlaced) {
746
- text = `model_provider = "neurolink"\n${text}`;
747
- }
748
- const trimmed = text.replace(/\s*$/, "\n");
749
- fs.writeFileSync(CODEX_CONFIG_PATH, `${trimmed}\n${buildCodexProviderBlock(baseUrl)}`);
750
- return true;
751
- }
752
- catch (error) {
753
- logger.debug(`[proxy] Codex client config not updated: ${error instanceof Error ? error.message : String(error)}`);
754
- return false;
755
- }
756
- }
757
- async function clearCodexProxySettings(expectedBaseUrl) {
758
- try {
759
- const fs = await import("fs");
760
- if (!fs.existsSync(CODEX_CONFIG_PATH)) {
761
- return false;
762
- }
763
- const original = fs.readFileSync(CODEX_CONFIG_PATH, "utf8");
764
- // The managed block records its owner in `base_url`. Without this check a
765
- // second proxy shutting down on another port would strip the block that the
766
- // still-running proxy installed, and restore the snapshot selector on top —
767
- // silently taking Codex off the live proxy. Mirrors the Claude and OpenCode
768
- // clear paths.
769
- if (expectedBaseUrl) {
770
- const ownerMatch = original.match(new RegExp(`${escapeRegExp(CODEX_BLOCK_BEGIN)}[\\s\\S]*?base_url\\s*=\\s*"([^"]*)"`));
771
- if (ownerMatch &&
772
- ownerMatch[1] !== `${expectedBaseUrl}/backend-api/codex`) {
773
- return false;
774
- }
775
- }
776
- let text = stripCodexManagedConfig(original);
777
- // Restore the user's original selector line if we snapshotted one.
778
- let restored = null;
779
- if (fs.existsSync(CODEX_SNAPSHOT_PATH)) {
780
- try {
781
- const snap = JSON.parse(fs.readFileSync(CODEX_SNAPSHOT_PATH, "utf8"));
782
- restored = snap.originalProviderLine ?? null;
783
- }
784
- catch {
785
- restored = null;
786
- }
787
- }
788
- if (restored) {
789
- const restoredLine = restored;
790
- let restorePlaced = false;
791
- text = editTomlPreamble(text, (preamble) => {
792
- if (CODEX_PROVIDER_LINE_RE.test(preamble)) {
793
- restorePlaced = true;
794
- return preamble.replace(CODEX_PROVIDER_LINE_RE, restoredLine);
795
- }
796
- if (CODEX_MODEL_LINE_RE.test(preamble)) {
797
- restorePlaced = true;
798
- return preamble.replace(CODEX_MODEL_LINE_RE, (line) => `${line}\n${restoredLine}`);
799
- }
800
- return null;
801
- });
802
- if (!restorePlaced) {
803
- text = `${restoredLine}\n${text}`;
804
- }
805
- }
806
- if (text === original) {
807
- // Nothing managed remains, so the snapshot can no longer describe the
808
- // user's current selector. Keeping it would let a later clear restore a
809
- // value the user has since changed by hand.
810
- try {
811
- fs.rmSync(CODEX_SNAPSHOT_PATH, { force: true });
812
- }
813
- catch {
814
- // best-effort
815
- }
816
- return false;
817
- }
818
- fs.writeFileSync(CODEX_CONFIG_PATH, text.replace(/\s*$/, "\n"));
819
- try {
820
- fs.rmSync(CODEX_SNAPSHOT_PATH, { force: true });
821
- }
822
- catch {
823
- // best-effort
824
- }
825
- return true;
826
- }
827
- catch (error) {
828
- logger.debug(`[proxy] Codex client config not cleared: ${error instanceof Error ? error.message : String(error)}`);
829
- return false;
830
- }
831
- }
832
440
  export async function probeProxyHealth(host, port, timeoutMs) {
833
441
  const startedAt = Date.now();
834
442
  try {
@@ -2472,27 +2080,10 @@ function registerProxyShutdownHandlers(params) {
2472
2080
  // non-fatal — proxy shutdown must not block on OTel
2473
2081
  }
2474
2082
  if (signal === "SIGINT" && !params.isDev) {
2475
- try {
2476
- const shutdownHost = params.host === "0.0.0.0" ? "localhost" : params.host;
2477
- await clearClaudeProxySettings(`http://${shutdownHost}:${params.port}`);
2478
- }
2479
- catch {
2480
- // non-fatal
2481
- }
2482
2083
  const shutdownHost = params.host === "0.0.0.0" ? "localhost" : params.host;
2483
- const shutdownBaseUrl = `http://${shutdownHost}:${params.port}`;
2484
- try {
2485
- await clearOpenCodeProxySettings(`${shutdownBaseUrl}/v1`);
2486
- }
2487
- catch {
2488
- // non-fatal
2489
- }
2490
- try {
2491
- await clearCodexProxySettings(shutdownBaseUrl);
2492
- }
2493
- catch {
2494
- // non-fatal
2495
- }
2084
+ // restoreAllClients wraps each client, so one failure cannot stop the
2085
+ // others and none can abort shutdown.
2086
+ await restoreAllClients(`http://${shutdownHost}:${params.port}`);
2496
2087
  }
2497
2088
  try {
2498
2089
  const state = loadProxyState();
@@ -2719,35 +2310,21 @@ async function startProxyRuntime(params) {
2719
2310
  logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`);
2720
2311
  }
2721
2312
  if (!isDev) {
2722
- try {
2723
- await setClaudeProxySettings(url);
2724
- logger.always(chalk.green(" ✓ Auto-configured Claude Code settings"));
2725
- logger.always(chalk.dim(" Restart Claude Code to connect through proxy"));
2726
- }
2727
- catch (error) {
2728
- logger.debug("[proxy] Failed to auto-configure Claude Code: " +
2729
- (error instanceof Error ? error.message : String(error)));
2730
- }
2731
- try {
2732
- if (await setOpenCodeProxySettings(`${url}/v1`)) {
2733
- logger.always(chalk.green(" ✓ Auto-configured OpenCode settings"));
2734
- logger.always(chalk.dim(" Restart OpenCode to connect through proxy"));
2313
+ for (const result of await applyAllClients(url)) {
2314
+ if (result.error) {
2315
+ // Visible, not debug-level. A client whose config could not be written
2316
+ // will keep talking to its own upstream, which looks like the proxy
2317
+ // silently not being used — the failure has to be actionable at the
2318
+ // level the user actually reads. The setup wizard already reported
2319
+ // this failure loudly; these two paths used to disagree.
2320
+ logger.always(chalk.yellow(` ⚠ Could not auto-configure ${result.displayName}: ${result.error.message}`));
2321
+ continue;
2735
2322
  }
2736
- }
2737
- catch (error) {
2738
- logger.debug("[proxy] Failed to auto-configure OpenCode: " +
2739
- (error instanceof Error ? error.message : String(error)));
2740
- }
2741
- try {
2742
- if (await setCodexProxySettings(url)) {
2743
- logger.always(chalk.green(" ✓ Auto-configured Codex settings"));
2744
- logger.always(chalk.dim(" Restart Codex to connect through proxy"));
2323
+ if (result.applied) {
2324
+ logger.always(chalk.green(` ✓ Auto-configured ${result.displayName} settings`));
2325
+ logger.always(chalk.dim(` Restart ${result.displayName} to connect through proxy`));
2745
2326
  }
2746
2327
  }
2747
- catch (error) {
2748
- logger.debug("[proxy] Failed to auto-configure Codex: " +
2749
- (error instanceof Error ? error.message : String(error)));
2750
- }
2751
2328
  }
2752
2329
  else {
2753
2330
  logger.always(chalk.dim(" ⊘ Dev mode: skipping client auto-configuration"));
@@ -4051,19 +3628,9 @@ export const proxyGuardCommand = {
4051
3628
  const expectedBaseUrl = `http://${guardHost}:${port}`;
4052
3629
  // The parent is confirmed gone and no replacement is healthy. Foreground
4053
3630
  // guards are cleanup-only; they never restart or signal proxy processes.
4054
- const cleared = await clearClaudeProxySettings(expectedBaseUrl);
4055
- try {
4056
- await clearOpenCodeProxySettings(`${expectedBaseUrl}/v1`);
4057
- }
4058
- catch {
4059
- // non-fatal
4060
- }
4061
- try {
4062
- await clearCodexProxySettings(expectedBaseUrl);
4063
- }
4064
- catch {
4065
- // non-fatal
4066
- }
3631
+ const restored = await restoreAllClients(expectedBaseUrl);
3632
+ // Downstream logic keys off whether Claude Code specifically was restored.
3633
+ const cleared = restored.find((r) => r.id === "claude-code")?.restored ?? false;
4067
3634
  const state = loadProxyState();
4068
3635
  if (state &&
4069
3636
  state.host === host &&
@@ -4167,30 +3734,20 @@ export const proxySetupCommand = {
4167
3734
  const nextStep = stepNum + 1;
4168
3735
  console.info(chalk.blue(`\nStep ${nextStep}:`) + " Configuring Claude Code...");
4169
3736
  const url = `http://127.0.0.1:${port}`;
4170
- try {
4171
- await setClaudeProxySettings(url);
4172
- console.info(chalk.green(" Claude Code configured"));
4173
- }
4174
- catch (e) {
4175
- console.info(chalk.yellow(` ⚠ Could not auto-configure Claude Code: ${e instanceof Error ? e.message : String(e)}`));
4176
- console.info(chalk.yellow(` Set manually: ANTHROPIC_BASE_URL=${url}`));
4177
- }
4178
- try {
4179
- if (await setOpenCodeProxySettings(`${url}/v1`)) {
4180
- console.info(chalk.green(" ✓ OpenCode configured"));
3737
+ for (const result of await applyAllClients(url)) {
3738
+ if (result.error) {
3739
+ console.info(chalk.yellow(` Could not auto-configure ${result.displayName}: ${result.error.message}`));
3740
+ // Claude Code is the one client whose manual fallback is a single
3741
+ // env var, so it is worth spelling out.
3742
+ if (result.id === "claude-code") {
3743
+ console.info(chalk.yellow(` Set manually: ANTHROPIC_BASE_URL=${url}`));
3744
+ }
3745
+ continue;
4181
3746
  }
4182
- }
4183
- catch (e) {
4184
- console.info(chalk.yellow(` ⚠ Could not auto-configure OpenCode: ${e instanceof Error ? e.message : String(e)}`));
4185
- }
4186
- try {
4187
- if (await setCodexProxySettings(url)) {
4188
- console.info(chalk.green(" ✓ Codex configured"));
3747
+ if (result.applied) {
3748
+ console.info(chalk.green(` ✓ ${result.displayName} configured`));
4189
3749
  }
4190
3750
  }
4191
- catch (e) {
4192
- console.info(chalk.yellow(` ⚠ Could not auto-configure Codex: ${e instanceof Error ? e.message : String(e)}`));
4193
- }
4194
3751
  // Done!
4195
3752
  console.info("");
4196
3753
  console.info(chalk.bold.green("Setup complete!"));
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Claude Code client configurator.
3
+ *
4
+ * Moved verbatim out of `proxy.ts`, with one behaviour change: `detect()` is
5
+ * new. This was the only writer that created its config file for a CLI that
6
+ * may never have been installed.
7
+ */
8
+ import type { CliProxyClientConfigurator } from "../../lib/types/index.js";
9
+ /**
10
+ * Resolved per call rather than at module load so `detect()` and `apply()`
11
+ * agree when HOME changes — under test, and on the `--dev` isolation path.
12
+ */
13
+ declare function getClaudeSettingsDir(): string;
14
+ declare function getClaudeSettingsPath(): string;
15
+ export declare function setClaudeProxySettings(baseUrl: string): Promise<void>;
16
+ export declare function clearClaudeProxySettings(expectedBaseUrl?: string): Promise<boolean>;
17
+ export declare const claudeCodeConfigurator: CliProxyClientConfigurator;
18
+ export declare const __claudeCodeTestHooks: {
19
+ getClaudeSettingsDir: typeof getClaudeSettingsDir;
20
+ getClaudeSettingsPath: typeof getClaudeSettingsPath;
21
+ setClaudeProxySettings: typeof setClaudeProxySettings;
22
+ clearClaudeProxySettings: typeof clearClaudeProxySettings;
23
+ };
24
+ export {};