UNPKG

@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

217 lines • 8.74 kB
/** * GitHub Copilot CLI client configurator. * * Copilot is the odd one out: its provider settings are read **only** from the * environment. `app.js` resolves `COPILOT_PROVIDER_BASE_URL` (and siblings) via * `process.env`, and `~/.copilot/config.json` — which announces itself as * "managed automatically" — carries no provider block. There is no file this * proxy can write that Copilot will read. * * So rather than edit a shell profile, which lives outside this proxy's blast * radius and runs on every shell the user opens, `apply()` writes a small * sourceable script inside the proxy's own directory. The user adds one line to * their profile once: * * [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh * * `restore()` deletes the script, so a sourced-but-stale file cannot outlive * the proxy — the guard above makes a missing file a no-op. * * Copilot also honours `OPENAI_API_KEY` + `OPENAI_BASE_URL`, but only when * `COPILOT_ENABLE_ALT_PROVIDERS=true` is set; the `COPILOT_PROVIDER_*` path has * no such gate, so that is the one used here. */ import { homedir } from "os"; import { join } from "path"; import { logger } from "../../utils/logger.js"; import { DEFAULT_PROXY_MODEL_ID } from "../../constants/proxyModels.js"; import { 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 getCopilotConfigDir() { return join(homedir(), ".copilot"); } function getCopilotEnvPath() { return join(homedir(), ".neurolink", "copilot-env.sh"); } /** * Shell profiles that could plausibly source the script. * * Checked to answer one question: has the user done the manual half? Writing * the file is only half the job, and until the other half happens Copilot * talks to GitHub, not the proxy. */ function getProfileCandidates() { return [".zshrc", ".zprofile", ".bashrc", ".bash_profile", ".profile"].map((name) => join(homedir(), name)); } /** A command that is `source`/`.` of a path ending in exactly `copilot-env.sh`, optionally after `then`/`else`/`do`. */ const ENV_SCRIPT_SOURCE_COMMAND = /^\s*(?:(?:then|else|do)\s+)?(?:source|\.)\s+(["']?)(?:[^\s"']*\/)?copilot-env\.sh\1(?:\s|$)/; /** * One profile line as the commands it runs, comment dropped. Quote-aware, so a * `;` or `#` inside quotes neither splits nor starts a comment. Heredocs, * multi-line strings and line continuations are not modelled, so a source line * inside one is still counted, and a source built with `eval` is missed. Both * are rare; this guards against the common mentions (a comment, an `echo`, a * file-existence test, a `.bak` name) that suppressed the note before. */ function splitProfileCommands(line) { const commands = []; let current = ""; let quote = null; for (let i = 0; i < line.length; i++) { const char = line[i]; if (quote) { current += char; if (quote === '"' && char === "\\") { current += line[i + 1] ?? ""; i++; } else if (char === quote) { quote = null; } continue; } if (char === "\\") { current += char + (line[i + 1] ?? ""); i++; continue; } if (char === '"' || char === "'") { quote = char; current += char; continue; } if (char === "#" && (current === "" || /\s/.test(current[current.length - 1]))) { break; } if (char === ";" || char === "&" || char === "|") { commands.push(current); current = ""; continue; } current += char; } commands.push(current); return commands; } /** Whether any shell profile already sources the generated script. */ async function isEnvScriptSourced() { const fs = await import("fs"); for (const profile of getProfileCandidates()) { try { if (fs .readFileSync(profile, "utf8") .split(/\r?\n/) .some((line) => splitProfileCommands(line).some((command) => ENV_SCRIPT_SOURCE_COMMAND.test(command)))) { return true; } } catch { // Absent or unreadable profile — not evidence either way, keep looking. } } return false; } function buildCopilotEnvScript(baseUrl, proxyKey) { return [ "# Generated by `neurolink proxy` — do not edit.", "# Removed automatically when the proxy stops.", "#", "# Copilot CLI reads its provider settings from the environment only, so", "# source this from your shell profile:", "# [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh", 'export COPILOT_PROVIDER_TYPE="openai"', `export COPILOT_PROVIDER_BASE_URL="${baseUrl}"`, `export COPILOT_PROVIDER_API_KEY="${proxyKey}"`, // Without a model id Copilot's BYOK path refuses to start — "BYOK // providers require an explicit model" — so a script that stops at the // base URL still needs `--model` on every invocation. Export a default and // let the user override it after sourcing. `export COPILOT_PROVIDER_MODEL_ID="\${COPILOT_PROVIDER_MODEL_ID:-${DEFAULT_PROXY_MODEL_ID}}"`, "", ].join("\n"); } export async function setCopilotProxySettings(baseUrl, proxyKey) { const fs = await import("fs"); try { fs.accessSync(getCopilotConfigDir()); } catch { // Copilot CLI not installed — report the skip so no caller prints a // success message for work that did not happen. return false; } try { const envPath = getCopilotEnvPath(); fs.mkdirSync(join(homedir(), ".neurolink"), { recursive: true }); // 0600 is applied to the temp file before the rename, so the script never // exists at the destination with wider permissions — not even briefly. // writeFileSync's own mode option would not do this: it applies only when // open() creates the file, so an overwrite kept whatever mode was there. await writeFileAtomic(envPath, buildCopilotEnvScript(baseUrl, proxyKey || "neurolink-proxy"), 0o600); return true; } catch (error) { logger.debug(`[proxy] Copilot env script not written: ${error instanceof Error ? error.message : String(error)}`); return false; } } export async function clearCopilotProxySettings(expectedBaseUrl) { const fs = await import("fs"); const envPath = getCopilotEnvPath(); let existing; try { existing = fs.readFileSync(envPath, "utf8"); } catch { return false; } // The whole file is ours to delete — but only if it is still pointing where // we pointed it. A user who hand-edited it to another endpoint keeps it. if (expectedBaseUrl && !existing.includes(expectedBaseUrl)) { return false; } try { fs.rmSync(envPath, { force: true }); return true; } catch (error) { logger.debug(`[proxy] Copilot env script not removed: ${error instanceof Error ? error.message : String(error)}`); return false; } } export const copilotConfigurator = { id: "copilot", displayName: "Copilot CLI", // Copilot is the only client whose apply() can fully succeed and still have // no effect: the script it writes does nothing until a shell profile sources // it. Without this note the proxy prints a green check for a file nothing // reads, which is indistinguishable from working. postApplyNote: async () => (await isEnvScriptSourced()) ? null : "Copilot reads provider settings from the environment only. Add this line to your shell profile, then open a new shell:\n [ -f ~/.neurolink/copilot-env.sh ] && . ~/.neurolink/copilot-env.sh", detect: async () => { const fs = await import("fs"); try { fs.accessSync(getCopilotConfigDir()); return true; } catch { return false; } }, // Copilot speaks OpenAI Chat Completions, so it points at the /v1 door. apply: (proxyBaseUrl) => setCopilotProxySettings(`${proxyBaseUrl}/v1`), restore: (proxyBaseUrl) => clearCopilotProxySettings(`${proxyBaseUrl}/v1`), }; export const __copilotTestHooks = { getCopilotConfigDir, getCopilotEnvPath, isEnvScriptSourced, setCopilotProxySettings, clearCopilotProxySettings, }; //# sourceMappingURL=copilot.js.map