Spaces:
Runtime error
Runtime error
File size: 6,186 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | import { spawn } from "node:child_process";
import { join } from "node:path";
import os from "node:os";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
function stripTrailingSlash(value) {
let s = String(value);
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
/**
* Build a clean child env for Claude Code pointed at OmniRoute.
*
* Strips inherited ANTHROPIC_* (avoids a stale shell token leaking through), then
* injects the base URL, gateway model discovery, and auto-compact window.
*
* @param {Record<string,string>} baseEnv
* @param {number|string} baseUrlOrPort a port (β http://localhost:<port>) or a full base URL
* @param {string|undefined} authToken
* @param {{ configDir?:string, model?:string }} [opts]
* @returns {Record<string,string>}
*/
export function buildClaudeEnv(baseEnv, baseUrlOrPort, authToken, opts = {}) {
const env = { ...baseEnv };
for (const key of Object.keys(env)) {
if (key.startsWith("ANTHROPIC_")) delete env[key];
}
// Accept a bare port (number/numeric string β localhost) or a full base URL.
// Claude Code wants the ROOT URL (it appends /v1/messages itself) β no /v1 here.
let baseUrl;
if (typeof baseUrlOrPort === "number" || /^\d+$/.test(String(baseUrlOrPort))) {
baseUrl = `http://localhost:${Number(baseUrlOrPort) || 20128}`;
} else {
baseUrl = stripTrailingSlash(String(baseUrlOrPort)).replace(/\/v1$/, "");
}
env.ANTHROPIC_BASE_URL = baseUrl;
// Always set a token: when none is resolved, a sentinel keeps newer Claude Code
// from stopping at its local login gate before it ever contacts OmniRoute (an
// open backend ignores the value). Mirrors free-claude-code. ANTHROPIC_API_KEY
// stays stripped (above) so it can't shadow the Bearer token.
env.ANTHROPIC_AUTH_TOKEN = (authToken && String(authToken).trim()) || "omniroute-no-auth";
env.CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY = "1";
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = "190000";
// Profile isolation (Claude Code has no native profiles β CLAUDE_CONFIG_DIR is
// the idiomatic mechanism: separate settings/credentials/history/cache per dir).
if (opts.configDir) env.CLAUDE_CONFIG_DIR = opts.configDir;
if (opts.model) env.ANTHROPIC_MODEL = opts.model;
return env;
}
/**
* Resolve the OmniRoute base URL + auth for launch, honouring (in order):
* explicit flags β the active context (remote mode) β localhost:<port>.
* @param {{port?:string, remote?:string, baseUrl?:string, token?:string, apiKey?:string, context?:string}} opts
* @returns {{ baseUrl:string, authToken:string|undefined }}
*/
export function resolveLaunchTarget(opts = {}) {
const explicit = opts.remote ?? opts.baseUrl;
let baseUrl;
if (explicit) {
baseUrl = stripTrailingSlash(explicit).replace(/\/v1$/, "");
} else {
let fromCtx;
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
fromCtx = ctx?.baseUrl;
} catch {
/* no context */
}
baseUrl = fromCtx
? stripTrailingSlash(fromCtx).replace(/\/v1$/, "")
: `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let authToken = opts.token ?? opts.apiKey ?? opts["api-key"];
if (!authToken) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
authToken = ctx?.accessToken || ctx?.apiKey || undefined;
} catch {
/* no context auth */
}
}
if (!authToken) authToken = process.env.ANTHROPIC_AUTH_TOKEN ?? process.env.OMNIROUTE_API_KEY;
return { baseUrl, authToken };
}
/**
* @param {{port?:string, remote?:string, token?:string, apiKey?:string, profile?:string, claudeHome?:string}} opts
* @param {string[]} claudeArgs pass-through args for the claude binary
* @returns {Promise<number>} exit code
*/
export async function runLaunchCommand(opts = {}, claudeArgs = []) {
const { baseUrl, authToken } = resolveLaunchTarget(opts);
// Health check the (possibly remote) proxy before launching.
try {
const res = await fetch(`${baseUrl}/api/monitoring/health`, {
signal: AbortSignal.timeout(3000),
});
if (!res.ok) throw new Error(`status ${res.status}`);
} catch {
console.error(
(t("launch.notRunning") || "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.").replace(
"{port}",
baseUrl
)
);
return 1;
}
const configDir = opts.profile
? join(opts.claudeHome || join(os.homedir(), ".claude"), "profiles", opts.profile)
: undefined;
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
return await new Promise((resolve) => {
const child = spawn("claude", claudeArgs, { env, stdio: "inherit" });
child.on("error", (err) => {
if (err && err.code === "ENOENT") {
console.error(t("launch.notFound") || "The 'claude' CLI was not found in PATH.");
resolve(127);
} else {
console.error(String(err?.message || err));
resolve(1);
}
});
child.on("exit", (code) => resolve(code ?? 0));
});
}
export function registerLaunch(program) {
program
.command("launch")
.description(
t("launch.description") || "Launch Claude Code pointed at OmniRoute (local or remote)"
)
.option("--port <port>", t("serve.port") || "Proxy port", "20128")
.option("--remote <url>", "Remote OmniRoute base URL (overrides --port and the active context)")
.option("--profile <name>", "Claude Code profile to use (CLAUDE_CONFIG_DIR ~/.claude/profiles/<name>)")
.option("--token <token>", t("launch.token") || "Token Claude sends (ANTHROPIC_AUTH_TOKEN)")
.option("--api-key <key>", "Alias for --token (OmniRoute access token / API key)")
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[claudeArgs...]", "arguments passed through to the claude binary")
.action(async (claudeArgs, opts) => {
const exitCode = await runLaunchCommand(opts, claudeArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}
|