UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

1,282 lines (1,277 loc) 94.3 kB
/** * Proxy CLI Commands for NeuroLink * * Implements commands for managing the Claude multi-account proxy: * - neurolink proxy start — Start the proxy server * - neurolink proxy status — Show proxy status (accounts, sessions, routing) * * The proxy creates a NeuroLink instance and builds a Hono app that registers * Claude-compatible proxy routes. All requests flow through ctx.neurolink * (generate/stream), with an optional ModelRouter for model remapping. */ import { spawn } from "node:child_process"; import { homedir } from "node:os"; import { dirname, join, resolve } from "node:path"; import chalk from "chalk"; import ora from "ora"; import { buildProxyHealthResponse, createProxyReadinessState, markProxyReady, waitForProxyReadiness, } from "../../lib/proxy/proxyHealth.js"; import { logger } from "../../lib/utils/logger.js"; import { formatUptime, isProcessRunning, StateFileManager, } from "../utils/serverUtils.js"; import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../lib/proxy/proxyEnv.js"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; const _require = createRequire(import.meta.url); const { version: PROXY_VERSION } = _require("../../../package.json"); const PROXY_TELEMETRY_SCRIPT_PATH = fileURLToPath(new URL("../../../scripts/observability/manage-local-openobserve.sh", import.meta.url)); // ============================================================================= // STATE MANAGEMENT // ============================================================================= let proxyStateManager = new StateFileManager("proxy-state.json"); /** * Reinitialise the state manager with a custom base directory. * Called when --dev redirects writable paths to .neurolink-dev/. */ function setProxyStateDir(baseDir) { proxyStateManager = new StateFileManager("proxy-state.json", baseDir); } function saveProxyState(state) { proxyStateManager.save(state); } function loadProxyState() { return proxyStateManager.load(); } function clearProxyState() { proxyStateManager.clear(); } const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json"); const PLIST_LABEL = "com.neurolink.proxy"; const PLIST_DIR = join(homedir(), "Library", "LaunchAgents"); const PLIST_PATH = join(PLIST_DIR, `${PLIST_LABEL}.plist`); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getProcessStatus(pid) { try { process.kill(pid, 0); return "running"; } catch (error) { const code = error.code; if (code === "ESRCH") { return "not_running"; } if (code === "EPERM") { return "unknown"; } return "not_running"; } } /** Resolve the primary-account info shown in /status. Reads the operator's * configured email from proxy config and cross-checks it against the token * store; falls back to the first enabled anthropic account when not set or * when the configured account isn't currently usable. */ async function resolveStatusPrimaryAccount(proxyConfig) { const configured = proxyConfig?.routing?.primaryAccount?.trim() || null; let enabledAnthropicKeys = []; try { const { tokenStore } = await import("../../lib/auth/tokenStore.js"); const all = await tokenStore.listByPrefix("anthropic:"); const filtered = []; for (const key of all) { const disabled = await tokenStore.isDisabled(key); if (!disabled) { filtered.push(key); } } enabledAnthropicKeys = filtered; } catch (err) { logger.debug(`[proxy] /status: failed to enumerate anthropic accounts: ${err instanceof Error ? err.message : String(err)}`); } if (configured) { const configuredKey = `anthropic:${configured}`; if (enabledAnthropicKeys.includes(configuredKey)) { return { configured, key: configuredKey, label: configured, source: "configured", }; } } const fallbackKey = enabledAnthropicKeys[0] ?? null; const fallbackLabel = fallbackKey ? (fallbackKey.split(":")[1] ?? null) : null; return { configured, key: fallbackKey, label: fallbackLabel, source: "fallback", }; } /** * Check if the launchd service is loaded and actively managing the proxy. * Returns true if launchctl reports the service as running. */ async function isLaunchdManaging() { if (process.platform !== "darwin") { return false; } try { const { execFileSync } = await import("node:child_process"); const uid = process.getuid?.() ?? 501; const output = execFileSync("launchctl", ["print", `gui/${uid}/${PLIST_LABEL}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); return /state\s*=\s*running/.test(output); } catch { return false; } } /** * Attempt to restart the proxy via launchd kickstart. * Returns true if the proxy comes back healthy within timeoutMs. */ async function tryLaunchdRestart(host, port, timeoutMs = 15_000) { if (process.platform !== "darwin") { return false; } try { const { existsSync } = await import("fs"); if (!existsSync(PLIST_PATH)) { return false; } } catch { return false; } try { const { execFileSync } = await import("node:child_process"); const uid = process.getuid?.() ?? 501; execFileSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], { stdio: "ignore", timeout: 5_000 }); } catch { return false; } const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { await sleep(1_000); if (await isProxyHealthy(host, port, 2_000)) { return true; } } return false; } /** Keys we manage in Claude Code's settings.env */ const PROXY_MANAGED_KEYS = ["ANTHROPIC_BASE_URL", "ENABLE_TOOL_SEARCH"]; async function setClaudeProxySettings(baseUrl) { const fs = await import("fs"); let settings = {}; try { settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf8")); } catch { // file missing/invalid — create fresh settings object } const env = (settings.env ?? {}); // Preserve original values so clearClaudeProxySettings can restore them. // Only snapshot once — subsequent calls should not overwrite the snapshot. const originals = (settings .__proxy_original_env ?? {}); for (const key of PROXY_MANAGED_KEYS) { if (!(key in originals)) { originals[key] = key in env ? env[key] : null; } } settings.__proxy_original_env = originals; env.ANTHROPIC_BASE_URL = baseUrl; env.ENABLE_TOOL_SEARCH = "true"; settings.env = env; fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2)); } async function clearClaudeProxySettings(expectedBaseUrl) { const fs = await import("fs"); let settings; try { settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS_PATH, "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; } 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 .__proxy_original_env ?? {}); for (const key of PROXY_MANAGED_KEYS) { 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.__proxy_original_env; if (Object.keys(env).length === 0) { delete settings.env; } else { settings.env = env; } fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2)); return hadBaseUrl || hadToolSearch; } async function isProxyHealthy(host, port, timeoutMs) { try { const response = await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(timeoutMs), }); return response.ok; } catch { return false; } } // --------------------------------------------------------------------------- // Stable entrypoint for launchd // --------------------------------------------------------------------------- /** * Path to a small trampoline script that the plist invokes. * The trampoline re-resolves `neurolink` via PATH on every launch, * so launchd never gets pinned to a version-specific store path. */ const TRAMPOLINE_DIR = join(homedir(), ".neurolink", "bin"); const TRAMPOLINE_PATH = join(TRAMPOLINE_DIR, "neurolink-proxy"); /** * Verify a candidate bin path actually runs by invoking `--version` on it. * Returns the version string on success, or undefined on any failure. */ function probeBinVersion(binPath) { try { const { execFileSync } = _require("node:child_process"); const out = execFileSync(binPath, ["--version"], { encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "ignore"], }).trim(); return out || undefined; } catch { return undefined; } } /** * Write (or overwrite) the trampoline shell script. * * Defensive design: the trampoline tries multiple candidates in order and * only `exec`s one whose `--version` check succeeds. If every PATH-based * candidate is broken (stale shims, missing packages), it falls back to the * baked-in `node + script` path that was verified to work at install time. */ function writeTrampoline() { const { writeFileSync, mkdirSync, existsSync, chmodSync } = _require("fs"); if (!existsSync(TRAMPOLINE_DIR)) { mkdirSync(TRAMPOLINE_DIR, { recursive: true }); } // Baked-in fallback: the specific node + JS script currently running // (guaranteed to work, since we ARE running). Used only if all PATH-based // candidates fail their --version probe. const bakedNode = process.execPath; const bakedScript = process.argv[1] ?? join(__dirname, "..", "index.js"); // Shell-escape the baked paths (they shouldn't contain quotes in practice, // but be safe for paths with spaces). const shEscape = (s) => `'${s.replace(/'/g, "'\\''")}'`; const script = `#!/bin/sh # Auto-generated by \`neurolink proxy install\` — do not edit. # Resolves a working neurolink binary on every launchd invocation so the # plist never gets pinned to a broken/stale shim. # Probe a candidate: must be executable and respond to --version cleanly. _try() { [ -n "$1" ] && [ -x "$1" ] || return 1 "$1" --version >/dev/null 2>&1 || return 1 return 0 } # 1. Explicit user override (escape hatch for broken environments). if [ -n "\${NEUROLINK_BIN:-}" ]; then if _try "$NEUROLINK_BIN"; then exec "$NEUROLINK_BIN" "$@" fi echo "[neurolink-proxy] WARN: NEUROLINK_BIN=$NEUROLINK_BIN is not runnable, trying defaults" >&2 fi # 2. PATH-based and common install locations. First working one wins. for cand in \\ "$(command -v neurolink 2>/dev/null || true)" \\ "\${PNPM_HOME:-}/neurolink" \\ "$HOME/.local/share/pnpm/neurolink" \\ "$HOME/Library/pnpm/neurolink" \\ "/usr/local/bin/neurolink" \\ "/opt/homebrew/bin/neurolink"; do if _try "$cand"; then exec "$cand" "$@" fi done # 3. Baked-in fallback: the exact node + script that worked at install time. # Always valid at install time; may become stale after package updates # (but at that point the PATH candidates above should work). BAKED_NODE=${shEscape(bakedNode)} BAKED_SCRIPT=${shEscape(bakedScript)} if [ -x "$BAKED_NODE" ] && [ -f "$BAKED_SCRIPT" ]; then exec "$BAKED_NODE" "$BAKED_SCRIPT" "$@" fi echo "[neurolink-proxy] FATAL: no working neurolink binary found." >&2 echo "[neurolink-proxy] Tried: PATH, \\$PNPM_HOME, \\$HOME/.local/share/pnpm, \\$HOME/Library/pnpm, /usr/local/bin, /opt/homebrew/bin, baked-in install path." >&2 echo "[neurolink-proxy] Fix: reinstall with 'pnpm add -g @juspay/neurolink' or set NEUROLINK_BIN=/path/to/working/neurolink." >&2 exit 127 `; writeFileSync(TRAMPOLINE_PATH, script, { mode: 0o755 }); chmodSync(TRAMPOLINE_PATH, 0o755); } /** * Check whether a pnpm binary can install into the global store. * * Multiple pnpm major versions can coexist (e.g., standalone v8 + nvm v10). * They use different store layouts (`store/v10` vs `store/v10/v3`), so a * pnpm that passes `--version` may still fail `pnpm add -g` with * ERR_PNPM_UNEXPECTED_STORE. We detect this by running `pnpm root -g` and * checking whether the resolved global root directory actually exists on disk. */ function canInstallGlobally(pnpmPath) { try { const { execFileSync } = _require("node:child_process"); const { existsSync } = _require("fs"); const globalRoot = execFileSync(pnpmPath, ["root", "-g"], { encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"], }).trim(); // If the global root exists, this pnpm version is compatible with // the current store layout and can install packages there. return !!globalRoot && existsSync(globalRoot); } catch { return false; } } /** * Resolve the `pnpm` binary defensively. * * Tries multiple candidates in order of preference. Each candidate must: * 1. Respond to `pnpm --version` (binary works) * 2. Have a compatible global store (`pnpm root -g` points to an existing dir) * * This defends against environments with multiple pnpm major versions * (e.g., standalone v8 + nvm v10) where the wrong one would fail with * ERR_PNPM_UNEXPECTED_STORE on `pnpm add -g`. * * Honors `NEUROLINK_PNPM_PATH` as an escape hatch. */ function resolveFullPnpmPath() { const candidates = []; // 1. User override if (process.env.NEUROLINK_PNPM_PATH) { candidates.push(process.env.NEUROLINK_PNPM_PATH); } // 2. PNPM_HOME (pnpm's own env variable) if (process.env.PNPM_HOME) { candidates.push(join(process.env.PNPM_HOME, "pnpm")); } // 3. `which pnpm` — whatever is on PATH try { const { execFileSync } = _require("node:child_process"); const whichOut = execFileSync("which", ["pnpm"], { encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "ignore"], }).trim(); if (whichOut) { candidates.push(whichOut); } } catch { // ignore } // 4. Common standalone installer locations candidates.push(join(homedir(), ".local", "share", "pnpm", "pnpm")); candidates.push(join(homedir(), "Library", "pnpm", "pnpm")); // Dedupe while preserving order const seen = new Set(); const unique = candidates.filter((p) => { if (!p || seen.has(p)) { return false; } seen.add(p); return true; }); // Probe each candidate: must pass --version AND have a compatible global store const tried = unique.map((path) => { const version = probeBinVersion(path); const working = version !== undefined; const globalStoreOk = working ? canInstallGlobally(path) : false; return { path, version, working, globalStoreOk }; }); // Prefer a candidate that can actually install globally const fullyWorking = tried.find((r) => r.working && r.globalStoreOk); if (fullyWorking) { return { bin: fullyWorking.path, resolved: true, version: fullyWorking.version, tried, }; } // Fall back to any candidate that at least responds to --version // (better than nothing — the install may still fail, but will be // caught and suppressed by the caller) const anyWorking = tried.find((r) => r.working); if (anyWorking) { return { bin: anyWorking.path, resolved: true, version: anyWorking.version, tried, }; } return { bin: "pnpm", resolved: false, tried }; } function spawnFailOpenGuard(host, port, parentPid) { // The guard runs the same version as this process, so process.argv[1] // (the currently-running script) is correct here — no stale-path risk. const entryScript = process.argv[1]; if (!entryScript) { return undefined; } const args = [ entryScript, "proxy", "guard", "--host", host, "--port", String(port), "--parent-pid", String(parentPid), "--quiet", ]; // Write guard stdout/stderr to a log file instead of discarding them. const { openSync, closeSync, mkdirSync, existsSync } = _require("fs"); const guardLogDir = join(homedir(), ".neurolink", "logs"); if (!existsSync(guardLogDir)) { mkdirSync(guardLogDir, { recursive: true }); } const guardLogPath = join(guardLogDir, "proxy-guard.log"); const logFd = openSync(guardLogPath, "a"); try { const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", logFd, logFd], }); child.unref(); return child.pid; } catch (error) { logger.debug(`[proxy] failed to start fail-open guard: ${error instanceof Error ? error.message : String(error)}`); return undefined; } finally { closeSync(logFd); // parent closes its copy; child keeps the fd } } async function runProxyTelemetryManager(command) { const { existsSync } = await import("fs"); if (!existsSync(PROXY_TELEMETRY_SCRIPT_PATH)) { throw new Error("Proxy telemetry helper files were not found in this installation. Reinstall NeuroLink with observability assets included."); } await new Promise((resolve, reject) => { const child = spawn("bash", [PROXY_TELEMETRY_SCRIPT_PATH, command], { stdio: "inherit", env: process.env, }); child.on("error", (error) => { reject(error); }); child.on("exit", (code, signal) => { if (signal) { reject(new Error(`proxy telemetry ${command} terminated by signal ${signal}`)); return; } if (code !== 0) { reject(new Error(`proxy telemetry ${command} exited with code ${code ?? 1}`)); return; } resolve(); }); }); } // ============================================================================= // STARTUP BANNER // ============================================================================= function printProxyBanner(url, strategy) { logger.always(""); logger.always(chalk.bold.cyan("NeuroLink Claude Proxy")); logger.always(chalk.gray("=".repeat(50))); logger.always(""); logger.always(` ${chalk.bold("URL:")} ${chalk.cyan(url)}`); logger.always(` ${chalk.bold("Strategy:")} ${chalk.cyan(strategy)}`); logger.always(` ${chalk.bold("PID:")} ${chalk.cyan(process.pid)}`); logger.always(""); logger.always(chalk.bold("Endpoints:")); logger.always(` ${chalk.blue("POST")} /v1/messages — Proxy to Anthropic`); logger.always(` ${chalk.green("GET")} /health — Health check`); logger.always(` ${chalk.green("GET")} /status — Detailed status`); logger.always(""); logger.always(chalk.bold("Set in Claude Code:")); logger.always(` ${chalk.cyan(`ANTHROPIC_BASE_URL=${url}`)}`); logger.always(""); logger.always(chalk.gray("Press Ctrl+C to stop the proxy")); logger.always(""); } export function mapClaudeErrorTypeToStatus(errorType) { switch (errorType) { case "invalid_request_error": return 400; case "authentication_error": return 401; case "permission_error": return 403; case "not_found_error": return 404; case "request_too_large": return 413; case "rate_limit_error": return 429; case "overloaded_error": return 529; case "api_error": default: return 502; } } async function ensureProxyStartAllowed(spinner) { const ignoreLaunchd = process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "1" || process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "true"; const existingState = loadProxyState(); if (existingState) { if (isProcessRunning(existingState.pid)) { // Test / dev escape hatch: when NEUROLINK_PROXY_IGNORE_LAUNCHD is set, // allow starting a second proxy on the test's requested port even if // a launchd-managed instance is using a different port (its state // file is what we hit here). The shared port-conflict surface remains // — node will fail to bind if the requested port is actually busy. if (!ignoreLaunchd) { if (spinner) { spinner.fail(chalk.red(`Proxy already running on port ${existingState.port} (PID: ${existingState.pid})`)); } logger.always(chalk.yellow("Stop it first or use 'neurolink proxy status' to inspect")); process.exit(process.ppid === 1 ? 0 : 1); } } else { clearProxyState(); } } if (process.ppid === 1 || !(await isLaunchdManaging())) { return; } // Test / dev escape hatch: when starting on an explicit non-default port, // the launchd-managed proxy (typically on its own port) cannot conflict. // Setting `NEUROLINK_PROXY_IGNORE_LAUNCHD=1` lets the test suite start a // standalone proxy alongside the launchd one without removing the daemon. if (process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "1" || process.env.NEUROLINK_PROXY_IGNORE_LAUNCHD === "true") { return; } if (spinner) { spinner.fail(chalk.red("Proxy is managed by launchd. Manual start would cause port conflicts.")); } logger.always(chalk.yellow("Use 'neurolink proxy uninstall' to remove the service first, " + "or 'launchctl kickstart gui/$(id -u)/com.neurolink.proxy' to restart.")); process.exit(1); } async function loadProxyStartEnv(argv, spinner) { try { const envResult = await loadProxyEnvFile({ explicitEnvFile: argv.envFile, }); if (spinner && envResult.path) { spinner.text = `Loaded proxy env from ${envResult.path}`; } return envResult.path; } catch (error) { if (spinner) { spinner.fail(chalk.red(error instanceof Error ? error.message : String(error))); } process.exit(1); } } async function createProxyNeurolinkRuntime(logsDir) { process.env.NEUROLINK_SKIP_MCP = "true"; const { NeuroLink } = await import("../../lib/neurolink.js"); const neurolink = new NeuroLink(); const { initRequestLogger, cleanupLogs } = await import("../../lib/proxy/requestLogger.js"); initRequestLogger(true, logsDir); cleanupLogs(7, 500); return { neurolink, cleanupLogs }; } async function loadProxyStartConfiguration(argv, spinner) { const configPath = argv.config ?? join(homedir(), ".neurolink", "proxy-config.yaml"); let proxyConfig = null; try { const { loadProxyConfig } = await import("../../lib/proxy/proxyConfig.js"); proxyConfig = (await loadProxyConfig(configPath)); if (spinner) { spinner.text = `Loaded proxy config from ${configPath}`; } } catch (configError) { if (argv.config) { if (spinner) { spinner.fail(chalk.red(`Failed to load proxy config: ${configPath}`)); } process.exit(1); } const isNotFound = configError instanceof Error && "code" in configError && configError.code === "ENOENT"; if (!isNotFound) { logger.warn(`[proxy] Ignoring default config ${configPath}: ${configError instanceof Error ? configError.message : String(configError)}`); } } const strategy = (argv.strategy ?? proxyConfig?.routing?.strategy ?? "fill-first"); let modelRouter; if (proxyConfig?.routing) { const { ModelRouter } = await import("../../lib/proxy/modelRouter.js"); modelRouter = new ModelRouter({ strategy, modelMappings: proxyConfig.routing.modelMappings ?? [], fallbackChain: proxyConfig.routing.fallbackChain ?? [], passthroughModels: proxyConfig.routing.passthroughModels, }); } const primaryAccountKey = await resolveBootPrimaryAccountKey(proxyConfig?.routing?.primaryAccount); return { configPath, proxyConfig, strategy, modelRouter, passthrough: argv.passthrough ?? false, primaryAccountKey, }; } /** Resolve the operator's configured primary email to a stable token-store * key (anthropic:<email>). Cross-checks the token store and emits a one-time * startup warning if the configured account isn't authenticated — but still * returns the key so it activates automatically once the user runs * `auth login --add`. */ async function resolveBootPrimaryAccountKey(primaryEmail) { const trimmed = primaryEmail?.trim(); if (!trimmed) { return undefined; } const key = `anthropic:${trimmed}`; try { const { tokenStore } = await import("../../lib/auth/tokenStore.js"); const known = await tokenStore.listByPrefix("anthropic:"); if (!known.includes(key)) { logger.warn(`[proxy] WARN: configured routing.primaryAccount=${trimmed} not ` + `found in token store; falling back to first enabled account. ` + `Run \`neurolink auth login --add\` to authenticate it, or ` + `\`neurolink auth clear-primary\` to remove the setting.`); } } catch (err) { logger.debug(`[proxy] could not validate primary account against token store: ${err instanceof Error ? err.message : String(err)}`); } return key; } async function createProxyStartApp(params) { const { createClaudeProxyRoutes } = await import("../../lib/server/routes/claudeProxyRoutes.js"); const { Hono } = await import("hono"); const app = new Hono(); const readiness = createProxyReadinessState(); app.onError((err, c) => { const errMsg = err instanceof Error ? err.message : String(err); logger.always(`[proxy] unhandled error: ${errMsg}`); if (err instanceof Error && err.stack) { logger.debug(`[proxy] stack: ${err.stack}`); } return c.json({ type: "error", error: { type: "api_error", message: `Proxy internal error: ${errMsg}`, }, }, 502); }); const routeGroup = createClaudeProxyRoutes(params.modelRouter, "", params.strategy, params.passthrough, params.primaryAccountKey); for (const route of routeGroup.routes) { const method = route.method.toLowerCase(); app[method](route.path, async (c) => { const emptyBody = {}; let body; let rawBody; if (method === "post") { rawBody = await c.req.text().catch(() => undefined); try { body = rawBody ? JSON.parse(rawBody) : emptyBody; } catch { return c.json({ type: "error", error: { type: "invalid_request_error", message: "Request body must be valid JSON", }, }, 400); } } const model = body?.model ?? "-"; const stream = body?.stream ? "stream" : "non-stream"; const bodyRec = body; const toolCount = Array.isArray(bodyRec?.tools) ? bodyRec.tools.length : 0; logger.always(`[proxy] ${c.req.method} ${c.req.path} → model=${model} ${stream} tools=${toolCount}`); const ctx = { requestId: crypto.randomUUID(), method: c.req.method, path: c.req.path, headers: Object.fromEntries(c.req.raw.headers.entries()), query: Object.fromEntries(new URL(c.req.url).searchParams.entries()), params: c.req.param(), body, rawBody, neurolink: params.neurolink, toolRegistry: params.neurolink.getToolRegistry(), timestamp: Date.now(), metadata: {}, }; const result = await route.handler(ctx); if (result instanceof Response) { return result; } if (result && typeof result === "object" && Symbol.asyncIterator in Object(result)) { const iterator = result[Symbol.asyncIterator](); let cancelled = false; const responseStream = new ReadableStream({ async start(controller) { try { while (!cancelled) { const { value, done } = await iterator.next(); if (done) { break; } controller.enqueue(new TextEncoder().encode(value)); } controller.close(); } catch (streamErr) { if (cancelled) { controller.close(); return; } const errMsg = streamErr instanceof Error ? streamErr.message : String(streamErr); const errorEvent = `event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: `Stream interrupted: ${errMsg}` } })}\n\n`; try { controller.enqueue(new TextEncoder().encode(errorEvent)); } catch { // Controller already errored — ignore } controller.close(); } }, async cancel() { cancelled = true; await iterator.return?.(); }, }); return new Response(responseStream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } if (result && typeof result === "object" && "httpStatus" in result) { const httpResult = result; const status = httpResult.httpStatus ?? 200; delete httpResult.httpStatus; return c.json(result, status); } if (result && typeof result === "object" && "type" in result && result.type === "error") { const errorResult = result; const status = mapClaudeErrorTypeToStatus(errorResult.error?.type); return c.json(result, status); } return c.json(result ?? {}); }); } app.get("/health", (c) => c.json(buildProxyHealthResponse(readiness, { strategy: params.strategy, passthrough: params.passthrough, version: PROXY_VERSION, }))); app.get("/status", async (c) => { const { getStats } = await import("../../lib/proxy/usageStats.js"); const stats = getStats(); const health = buildProxyHealthResponse(readiness, { strategy: params.strategy, passthrough: params.passthrough, version: PROXY_VERSION, }); const primaryAccount = await resolveStatusPrimaryAccount(params.proxyConfig); return c.json({ status: "running", ready: health.ready, acceptingConnections: health.acceptingConnections, readyAt: health.readyAt, pid: process.pid, port: params.port, host: params.host, strategy: params.strategy, uptime: process.uptime(), version: PROXY_VERSION, health, stats: { totalAttempts: stats.totalAttempts, totalRequests: stats.totalRequests, totalSuccess: stats.totalSuccess, totalErrors: stats.totalErrors, totalRateLimits: stats.totalRateLimits, accounts: Object.values(stats.accounts).map((account) => ({ label: account.label, type: account.type, attempts: account.attemptCount, requests: account.attemptCount, success: account.successCount, errors: account.errorCount, rateLimits: account.rateLimitCount, cooling: false, // No persistent cooldown — always active })), primaryAccount, }, config: params.proxyConfig ? { hasRouting: !!params.proxyConfig.routing } : null, }); }); return { app, readiness }; } async function initializeProxyOpenTelemetry() { try { const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT; if (!process.env.OTEL_SERVICE_NAME) { process.env.OTEL_SERVICE_NAME = "neurolink-proxy"; } process.env.OTEL_RESOURCE_ATTRIBUTES = [ "service.name=neurolink-proxy", `service.version=${PROXY_VERSION}`, "deployment.environment=local", process.env.OTEL_RESOURCE_ATTRIBUTES, ] .filter(Boolean) .join(","); const { initializeOpenTelemetry, isOpenTelemetryInitialized } = await import("../../lib/services/server/ai/observability/instrumentation.js"); const { buildObservabilityConfigFromEnv } = await import("../../lib/utils/observabilityHelpers.js"); if (isOpenTelemetryInitialized()) { return; } const observabilityConfig = buildObservabilityConfigFromEnv(); const langfuseConfig = observabilityConfig?.langfuse; const langfuseEnabled = langfuseConfig?.enabled === true; await initializeOpenTelemetry({ enabled: langfuseEnabled, publicKey: langfuseConfig?.publicKey || "", secretKey: langfuseConfig?.secretKey || "", baseUrl: langfuseConfig?.baseUrl, environment: "proxy", release: PROXY_VERSION, userId: "neurolink-proxy", autoDetectOperationName: true, }); if (langfuseEnabled) { logger.always(`[proxy] Langfuse enabled — exporting to ${langfuseConfig.baseUrl || "https://cloud.langfuse.com"} (environment=proxy)`); } if (otlpEndpoint) { logger.always(`[proxy] OTLP exporter enabled — exporting to ${otlpEndpoint} (service.name=neurolink-proxy)`); } if (!langfuseEnabled && !otlpEndpoint) { logger.always("[proxy] OpenTelemetry exporters disabled — set OTEL_EXPORTER_OTLP_ENDPOINT or Langfuse credentials to enable proxy observability"); } } catch (error) { logger.debug(`[proxy] OpenTelemetry init failed (non-fatal): ${error instanceof Error ? error.message : String(error)}`); } } async function refreshProxyTokensInBackground() { const { needsRefresh, refreshToken, persistTokens } = await import("../../lib/proxy/tokenRefresh.js"); const { tokenStore } = await import("../../lib/auth/tokenStore.js"); try { const allKeys = await tokenStore.listProviders(); const anthropicKeys = allKeys.filter((key) => key.startsWith("anthropic:")); for (const key of anthropicKeys) { try { const tokens = await tokenStore.loadTokens(key); if (!tokens) { continue; } const account = { label: key, token: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: tokens.expiresAt, }; if (needsRefresh(account)) { const result = await refreshToken(account); if (result.success) { await persistTokens({ providerKey: key }, account); logger.debug(`[proxy] background token refresh succeeded for ${key}`); } } } catch { // non-fatal per-account } } } catch { // non-fatal } try { const credPath = join(homedir(), ".neurolink", "anthropic-credentials.json"); const { readFileSync } = await import("fs"); const creds = JSON.parse(readFileSync(credPath, "utf8")); if (!creds.oauth) { return; } const account = { label: "background", token: creds.oauth.accessToken, refreshToken: creds.oauth.refreshToken, expiresAt: creds.oauth.expiresAt, }; if (needsRefresh(account)) { const result = await refreshToken(account); if (result.success) { await persistTokens(credPath, account); logger.debug("[proxy] background token refresh succeeded"); } } } catch { // non-fatal } } function startProxyBackgroundMaintenance(cleanupLogs) { const refreshInterval = setInterval(() => { void refreshProxyTokensInBackground(); }, 30_000); const logCleanupInterval = setInterval(() => { cleanupLogs(7, 500); }, 60 * 60 * 1000); return { refreshInterval, logCleanupInterval }; } function registerProxyShutdownHandlers(params) { const shutdown = async (signal) => { clearInterval(params.refreshInterval); clearInterval(params.logCleanupInterval); logger.always(`\nShutting down proxy (${signal})...`); try { const { flushOpenTelemetry, shutdownOpenTelemetry } = await import("../../lib/services/server/ai/observability/instrumentation.js"); await flushOpenTelemetry(); await shutdownOpenTelemetry(); } catch { // non-fatal — proxy shutdown must not block on OTel } if (signal === "SIGINT" && !params.isDev) { try { const shutdownHost = params.host === "0.0.0.0" ? "localhost" : params.host; await clearClaudeProxySettings(`http://${shutdownHost}:${params.port}`); } catch { // non-fatal } } try { params.server.close?.(); } catch { // Best-effort close } clearProxyState(); process.exit(signal === "SIGINT" ? 0 : 1); }; process.on("SIGTERM", () => { void shutdown("SIGTERM"); }); process.on("SIGINT", () => { void shutdown("SIGINT"); }); } async function startProxyRuntime(params) { const { serve } = await import("@hono/node-server"); const server = serve({ fetch: params.app.fetch, port: params.port, hostname: params.host, }); // Skip the fail-open guard in dev mode — it monitors the proxy and clears // global Claude settings on exit, which is exactly what we want to avoid. const guardPid = params.argv.dev ? undefined : spawnFailOpenGuard(params.host, params.port, process.pid); const readinessHost = params.host === "0.0.0.0" ? "127.0.0.1" : params.host; await waitForProxyReadiness({ host: readinessHost, port: params.port, }); markProxyReady(params.readiness); const fallbackChain = params.proxyConfig?.routing?.fallbackChain?.map((entry) => ({ provider: entry.provider, model: entry.model, })); saveProxyState({ pid: process.pid, port: params.port, host: params.host, strategy: params.strategy, startTime: new Date().toISOString(), ready: true, readyAt: params.readiness.readyAtMs ? new Date(params.readiness.readyAtMs).toISOString() : undefined, healthPath: "/health", statusPath: "/status", envFile: params.loadedEnvFile, fallbackChain, guardPid, managedBy: process.platform === "darwin" && process.ppid === 1 ? "launchd" : "manual", passthrough: params.passthrough, }); if (params.spinner) { params.spinner.succeed(chalk.green("Claude proxy started successfully")); } const isDev = params.argv.dev ?? false; const normalizedHost = params.host === "0.0.0.0" ? "localhost" : params.host; const url = `http://${normalizedHost}:${params.port}`; printProxyBanner(url, params.strategy); if (isDev) { logger.always(` ${chalk.bold("Mode:")} ${chalk.magenta("dev (isolated — state in .neurolink-dev/)")}`); } else { logger.always(` ${chalk.bold("Mode:")} ${chalk.cyan(params.passthrough ? "passthrough" : "full")}`); } if (params.passthrough) { logger.always(chalk.yellow(" ! Passthrough mode forwards client auth directly to Anthropic")); logger.always(chalk.dim(" Stored proxy OAuth/API credentials are ignored; clients need their own valid Anthropic auth.")); } if (params.loadedEnvFile) { logger.always(` ${chalk.bold("Env File:")} ${chalk.cyan(params.loadedEnvFile)}`); } if (!isDev) { try { await setClaudeProxySettings(url); logger.always(chalk.green(" ✓ Auto-configured Claude Code settings")); logger.always(chalk.dim(" Restart Claude Code to connect through proxy")); } catch (error) { logger.debug("[proxy] Failed to auto-configure Claude Code: " + (error instanceof Error ? error.message : String(error))); } } else { logger.always(chalk.dim(" ⊘ Dev mode: skipping client auto-configuration")); } const maintenance = startProxyBackgroundMaintenance(params.cleanupLogs); registerProxyShutdownHandlers({ server, host: params.host, port: params.port, isDev, ...maintenance, }); } async function startProxyCommandHandler(argv) { const spinner = argv.quiet ? null : ora("Starting Claude proxy...").start(); const isDev = argv.dev ?? false; try { // In dev mode: redirect writable state to .neurolink-dev/ and skip singleton check let devPaths; if (isDev) { const { resolveProxyPaths } = await import("../../lib/proxy/proxyPaths.js"); devPaths = resolveProxyPaths(true); setProxyStateDir(devPaths.stateDir); const { initAccountQuota } = await import("../../lib/proxy/accountQuota.js"); initAccountQuota(devPaths.quotaFile); // Ensure the dev state directory exists const { mkdirSync, existsSync } = await import("fs"); if (!existsSync(devPaths.stateDir)) { mkdirSync(devPaths.stateDir, { recursive: true, mode: 0o700 }); } } if (!isDev) { await ensureProxyStartAllowed(spinner); } const loadedEnvFile = await loadProxyStartEnv(argv, spinner); const { neurolink, cleanupLogs } = await createProxyNeurolinkRuntime(devPaths?.logsDir); const { proxyConfig, strategy, modelRouter, passthrough, primaryAccountKey, } = await loadProxyStartConfiguration(argv, spinner); if (spinner) { spinner.text = "Configuring server..."; } const port = argv.port ?? 55669; const host = argv.host ?? "127.0.0.1"; const { app, readiness } = await createProxyStartApp({ neurolink, modelRouter, strategy, passthrough, port, host, proxyConfig, primaryAccountKey, }); await initializeProxyOpenTelemetry(); if (spinner) { spinner.text = `Starting proxy on ${host}:${port}...`; } await startProxyRuntime({ argv, spinner, app, readiness, host, port, strategy, proxyConfig, loadedEnvFile, passthrough, cleanupLogs, }); } catch (error) { if (spinner) { spinner.fail(chalk.red("Failed to start proxy")); } logger.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`)); if (argv.debug && error instanceof Error && error.stack) { logger.error(chalk.gray(error.stack)); } process.exit(1); } } // ============================================================================= // PROXY START COMMAND // ============================================================================= export const proxyStartCommand = { command: "start", describe: "Start the Claude multi-account proxy server", builder: (yargs) => { return yargs .option("port", { type: "number", alias: "p", default: 55669, description: "Port to listen on", }) .option("host", { type: "string", alias: "H", default: "127.0.0.1", description: "Host to bind to", }) .option("strategy", { type: "string", alias: "s", choices: ["fill-first", "round-robin"], description: "Account selection strategy for routing requests (default: fill-first)", }) .option("health-interval", { type: "number", alias: "healthInterval", default: 30, description: "Health check interval in seconds", }) .option("quiet", { type: "boolean", alias: "q", default: false, description: "Suppress non-essential output", }) .option("debug", { type: "boolean", alias: "d", default: false, description: "Enable debug output", }) .option("config", { type: "string", alias: "c", description: "Path to proxy config file (YAML/JSON)", defaultDescription: "~/.neurolink/proxy-config.yaml", }) .option("env-file", { type: "string", alias: "envFile", description: "Path to proxy provider env file (overrides cwd .env for the proxy process)", }) .option("passthrough", { type: "boolean", default: false, description: "Run in transparent passthrough mode (no retry, no rotation, no polyfill)", }) .option("dev", { type: "boolean", default: false, description: "Run in isolated dev mode — state files scoped to .neurolink-dev/ in cwd, no client auto-configuration, no singleton check", }) .example("neurolink proxy start", "Start proxy on default port 55669 with fill-first strategy") .example("neurolink proxy start -p 8080 -s fill-first", "Start proxy on port 8080 with fill-first") .example("neurolink proxy start --health-interval 60", "Start proxy with 60-second health checks"); }, handler: async (argv) => { await startProxyCommandHandler(argv); }, }; // ============================================================================= // STATUS DISPLAY HELPERS // ============================================================================= function printStatusStats(stats) { console.info(`\n Stats:`); if (stats.totalAttempts !== undefined) { console.info(` Attempts: ${stats.totalAttempts}`); } console.info(` Completed: ${stats.totalRequests} total, ${stats.totalSuccess} success, ${stats.totalErrors} errors`); console.info(` Rate limits: ${stats.totalRateLimits}`); if (stats.accounts?.length) { console.info(`\n Acc