@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
JavaScript
/**
* 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