@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.
- package/CHANGELOG.md +1 -5
- package/dist/browser/neurolink.min.js +384 -384
- package/dist/cli/commands/proxy.d.ts +0 -18
- package/dist/cli/commands/proxy.js +30 -473
- package/dist/cli/proxy-clients/claudeCode.d.ts +24 -0
- package/dist/cli/proxy-clients/claudeCode.js +130 -0
- package/dist/cli/proxy-clients/codex.d.ts +23 -0
- package/dist/cli/proxy-clients/codex.js +217 -0
- package/dist/cli/proxy-clients/copilot.d.ts +40 -0
- package/dist/cli/proxy-clients/copilot.js +124 -0
- package/dist/cli/proxy-clients/openCode.d.ts +26 -0
- package/dist/cli/proxy-clients/openCode.js +162 -0
- package/dist/cli/proxy-clients/qwenCode.d.ts +36 -0
- package/dist/cli/proxy-clients/qwenCode.js +139 -0
- package/dist/cli/proxy-clients/registry.d.ts +19 -0
- package/dist/cli/proxy-clients/registry.js +69 -0
- package/dist/lib/providers/googleAiStudio/client.js +96 -86
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/proxyClient.d.ts +54 -0
- package/dist/lib/types/proxyClient.js +2 -0
- package/dist/providers/googleAiStudio/client.js +96 -86
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxyClient.d.ts +54 -0
- package/dist/types/proxyClient.js +1 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
(
|
|
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
|
-
|
|
2738
|
-
|
|
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
|
|
4055
|
-
|
|
4056
|
-
|
|
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
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
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
|
-
|
|
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 {};
|