@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
162 lines • 6.25 kB
JavaScript
/**
* Claude Code client configurator.
*
* Moved verbatim out of `proxy.ts`, with one behaviour change: `detect()` is
* new. This was the only writer that created its config file for a CLI that
* may never have been installed.
*/
import { homedir } from "os";
import { join } from "path";
import { logger } from "../../utils/logger.js";
import { isProxyOwnedValue, shouldCaptureSnapshot, writeFileAtomic, } from "./snapshot.js";
/**
* Resolved per call rather than at module load so `detect()` and `apply()`
* agree when HOME changes — under test, and on the `--dev` isolation path.
*/
function getClaudeSettingsDir() {
return join(homedir(), ".claude");
}
function getClaudeSettingsPath() {
return join(getClaudeSettingsDir(), "settings.json");
}
/** Keys we manage in Claude Code's settings.env */
const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"];
/** The user's values for those keys, as they were before the proxy first ran. */
const CLAUDE_ORIGINAL_KEY = "__proxy_original_env";
/**
* The values this writer last wrote. Lets apply() tell its own values from ones
* the user substituted while the proxy was not running.
*/
const CLAUDE_WRITTEN_KEY = "__proxy_written_env";
export async function setClaudeProxySettings(baseUrl) {
const fs = await import("fs");
let settings = {};
try {
settings = JSON.parse(fs.readFileSync(getClaudeSettingsPath(), "utf8"));
}
catch {
// file missing/invalid — create fresh settings object
}
const env = (settings.env ?? {});
// Preserve original values so clearClaudeProxySettings can restore them.
// A repeat apply() must not overwrite the snapshot with the proxy's own
// values — but a value the user set while the proxy was gone must replace
// it. See shouldCaptureSnapshot.
const originals = (settings[CLAUDE_ORIGINAL_KEY] ?? {});
const written = (settings[CLAUDE_WRITTEN_KEY] ??
{});
for (const key of PROXY_MANAGED_KEYS) {
const current = key in env ? env[key] : undefined;
if (shouldCaptureSnapshot({
hasSnapshot: key in originals,
written: written[key],
current,
})) {
originals[key] = current ?? null;
}
}
settings[CLAUDE_ORIGINAL_KEY] = originals;
env.ANTHROPIC_BASE_URL = baseUrl;
env.ENABLE_TOOL_SEARCH = "true";
settings.env = env;
settings[CLAUDE_WRITTEN_KEY] = {
ANTHROPIC_BASE_URL: baseUrl,
ENABLE_TOOL_SEARCH: "true",
};
await writeFileAtomic(getClaudeSettingsPath(), JSON.stringify(settings, null, 2));
}
export async function clearClaudeProxySettings(expectedBaseUrl) {
const fs = await import("fs");
let settings;
try {
settings = JSON.parse(fs.readFileSync(getClaudeSettingsPath(), "utf8"));
}
catch {
return false;
}
const env = settings.env;
if (!env) {
return false;
}
if (expectedBaseUrl &&
typeof env.ANTHROPIC_BASE_URL === "string" &&
env.ANTHROPIC_BASE_URL !== expectedBaseUrl) {
// User switched to a different proxy URL; do not clobber.
return false;
}
if (!(CLAUDE_ORIGINAL_KEY in settings)) {
// No snapshot means we cannot prove these keys are ours. Without this
// guard the loop below reads every managed key as "did not exist before"
// and deletes it — wiping a real user value. Leaving a stale proxy URL
// behind is recoverable; destroying the user's own setting is not.
// OpenCode and Qwen refuse for the same reason.
logger.debug("[proxy] Claude clear: no original-env snapshot found, leaving settings.env intact");
return false;
}
const hadBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string";
const hadToolSearch = env.ENABLE_TOOL_SEARCH === "true";
// Restore original values if they were saved, otherwise delete the keys
const originals = (settings[CLAUDE_ORIGINAL_KEY] ?? {});
// Only restore keys we can prove are ours. A user who kept the proxy's base
// URL but changed one of the other managed values made a deliberate edit;
// reverting it to the snapshot — or deleting it, when the snapshot says the
// key was absent — would silently undo them.
const writtenValues = (settings[CLAUDE_WRITTEN_KEY] ?? {});
for (const key of PROXY_MANAGED_KEYS) {
if (!isProxyOwnedValue({
written: writtenValues[key],
current: key in env ? env[key] : undefined,
})) {
logger.debug(`[proxy] Claude clear: ${key} was edited after the proxy wrote it, leaving it intact`);
continue;
}
const original = originals[key];
if (original !== undefined && original !== null) {
// Restore the value that existed before the proxy was started
env[key] = original;
}
else {
// Key did not exist before — remove it
delete env[key];
}
}
delete settings[CLAUDE_ORIGINAL_KEY];
delete settings[CLAUDE_WRITTEN_KEY];
if (Object.keys(env).length === 0) {
delete settings.env;
}
else {
settings.env = env;
}
await writeFileAtomic(getClaudeSettingsPath(), JSON.stringify(settings, null, 2));
return hadBaseUrl || hadToolSearch;
}
export const claudeCodeConfigurator = {
id: "claude-code",
displayName: "Claude Code",
// New: previously this writer created ~/.claude/settings.json even when
// Claude Code had never been installed. The other two writers already
// probed; this one did not.
detect: async () => {
const fs = await import("fs");
try {
fs.accessSync(getClaudeSettingsDir());
return true;
}
catch {
return false;
}
},
apply: async (proxyBaseUrl) => {
await setClaudeProxySettings(proxyBaseUrl);
return true;
},
restore: (proxyBaseUrl) => clearClaudeProxySettings(proxyBaseUrl),
};
export const __claudeCodeTestHooks = {
getClaudeSettingsDir,
getClaudeSettingsPath,
setClaudeProxySettings,
clearClaudeProxySettings,
};
//# sourceMappingURL=claudeCode.js.map