feat(headroom): add proxy lifecycle management + dashboard UI

Build on the optional Headroom Token Saver from Carmelo Campos
(PR: feat: add optional Headroom token saver). Add managed start/stop
of the local headroom proxy from the dashboard, install detection,
status probing, and a simplified Token Saver UI.

- detect headroom CLI + python>=3.10, probe proxy /health
- spawn/stop proxy as a detached, pid-tracked process
- /api/headroom/{status,start,stop} routes, gated local-only in dashboardGuard
- one-click Start/Stop Headroom modal, no manual config needed
- claude<->openai shape conversion for /v1/compress via 9router translators

Thanks to Carmelo Campos (@carmelogunsroses) for the original Headroom integration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
decolua
2026-06-20 10:09:50 +07:00
co-authored by Cursor
parent efd20be8d8
commit b55cf36d2e
33 changed files with 1019 additions and 130 deletions
+5
View File
@@ -34,8 +34,13 @@ const DEFAULT_SETTINGS = {
mitmRouterBaseUrl: DEFAULT_MITM_ROUTER_BASE,
dnsToolEnabled: {},
rtkEnabled: true,
headroomEnabled: false,
headroomUrl: "http://localhost:8787",
headroomCompressUserMessages: false,
cavemanEnabled: false,
cavemanLevel: "full",
ponytailEnabled: false,
ponytailLevel: "full",
};
async function readRaw() {
+63
View File
@@ -0,0 +1,63 @@
import { execSync } from "child_process";
const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`;
const PYTHON_CANDIDATES = ["python3.13", "python3.12", "python3.11", "python3.10", "python3"];
const MIN_VERSION = [3, 10];
const HEADROOM_HEALTH_TIMEOUT_MS = 1500;
// Detect whether the headroom CLI is installed and where its binary lives.
export function findHeadroomBinary() {
try {
const path = execSync("which headroom", {
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH },
}).toString().trim();
return path || null;
} catch {
return null;
}
}
// Find a Python interpreter >= 3.10 (headroom-ai requires it). Returns null if none.
export function findPython310() {
for (const candidate of PYTHON_CANDIDATES) {
try {
const ver = execSync(`${candidate} --version`, {
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
env: { ...process.env, PATH: EXTENDED_PATH },
}).toString().trim();
const match = ver.match(/(\d+)\.(\d+)/);
if (!match) continue;
const [major, minor] = [parseInt(match[1], 10), parseInt(match[2], 10)];
if (major > MIN_VERSION[0] || (major === MIN_VERSION[0] && minor >= MIN_VERSION[1])) {
return candidate;
}
} catch {
// candidate not present, try next
}
}
return null;
}
// Probe whether a Headroom proxy is reachable at the given URL by hitting /health.
export async function probeProxyRunning(url) {
if (!url) return false;
const base = String(url).replace(/\/$/, "");
try {
const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(HEADROOM_HEALTH_TIMEOUT_MS) });
return res.ok;
} catch {
return false;
}
}
// Aggregate status for the dashboard: installed, running, python interpreter.
export async function getHeadroomStatus(url) {
const path = findHeadroomBinary();
const python = findPython310();
const installed = Boolean(path);
const running = installed ? await probeProxyRunning(url) : false;
return { installed, path, running, python };
}
+128
View File
@@ -0,0 +1,128 @@
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
import { DATA_DIR } from "@/lib/dataDir.js";
import { findHeadroomBinary } from "./detect.js";
const HEADROOM_DIR = path.join(DATA_DIR, "headroom");
const PID_FILE = path.join(HEADROOM_DIR, "proxy.pid");
const LOG_FILE = path.join(HEADROOM_DIR, "proxy.log");
const DEFAULT_PORT = 8787;
const STARTUP_TIMEOUT_MS = 8000;
function ensureDir() {
if (!fs.existsSync(HEADROOM_DIR)) fs.mkdirSync(HEADROOM_DIR, { recursive: true });
}
function readPid() {
try {
if (fs.existsSync(PID_FILE)) return parseInt(fs.readFileSync(PID_FILE, "utf8"), 10);
} catch { /* ignore */ }
return null;
}
function writePid(pid) {
ensureDir();
fs.writeFileSync(PID_FILE, String(pid));
}
function clearPid() {
try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
}
// process.kill throws if pid is dead — use this to probe.
export function isPidAlive(pid) {
if (!pid || typeof pid !== "number") return false;
try { process.kill(pid, 0); return true; } catch { return false; }
}
export function getManagedPid() {
const pid = readPid();
return pid && isPidAlive(pid) ? pid : null;
}
export async function startHeadroomProxy({ port = DEFAULT_PORT } = {}) {
const safePort = Number(port) > 0 && Number(port) < 65536 ? Number(port) : DEFAULT_PORT;
const binary = findHeadroomBinary();
if (!binary) {
const err = new Error("Headroom CLI not installed");
err.code = "NOT_INSTALLED";
throw err;
}
const existing = getManagedPid();
if (existing) return { pid: existing, alreadyRunning: true };
ensureDir();
// spawn stdio requires fd numbers, not WriteStream objects.
const outFd = fs.openSync(LOG_FILE, "a");
const child = spawn(binary, ["proxy", "--port", String(safePort)], {
stdio: ["ignore", outFd, outFd],
detached: true,
windowsHide: true,
env: { ...process.env },
});
if (!child.pid) {
fs.closeSync(outFd);
const err = new Error("Failed to spawn headroom proxy");
err.code = "SPAWN_FAILED";
throw err;
}
child.unref();
writePid(child.pid);
// Wait until the process either stays alive briefly (success) or exits fast (failure).
await new Promise((resolve, reject) => {
const startupTimer = setTimeout(() => {
if (isPidAlive(child.pid)) resolve();
else reject(new Error("headroom proxy exited during startup — see proxy.log"));
}, STARTUP_TIMEOUT_MS);
child.once("exit", (code) => {
clearTimeout(startupTimer);
clearPid();
fs.closeSync(outFd);
const e = new Error(`headroom proxy exited early (code=${code}) — see proxy.log`);
e.code = "EARLY_EXIT";
reject(e);
});
});
// Close parent's copy of the fd; child retains its own after unref.
fs.closeSync(outFd);
return { pid: child.pid, alreadyRunning: false };
}
export function stopHeadroomProxy() {
const pid = getManagedPid();
if (!pid) return { stopped: false, reason: "not_running" };
try {
process.kill(pid, "SIGTERM");
// Give it a moment, then force if still alive.
setTimeout(() => {
if (isPidAlive(pid)) {
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
}
}, 2000);
clearPid();
return { stopped: true, pid };
} catch (e) {
clearPid();
const err = new Error(`Failed to stop headroom proxy: ${e.message}`);
err.code = "STOP_FAILED";
throw err;
}
}
export function getHeadroomLogTail(maxLines = 200) {
try {
if (!fs.existsSync(LOG_FILE)) return "";
const content = fs.readFileSync(LOG_FILE, "utf8");
const lines = content.split(/\r?\n/).filter(Boolean);
return lines.slice(-maxLines).join("\n");
} catch { return ""; }
}