UNPKG

@jovan158/vantage

Version:

See and control what your coding agent does

128 lines (123 loc) • 5.13 kB
// pi talks to many providers, each at its own address, from inside one Node // process. Vantage's extension (loaded with -e for the session) sends the // model requests to known provider hosts through the proxy — by rewriting // the address inside pi's own fetch — and judges each tool call through // `vantage hook pi`: a "deny" blocks, an "ask" asks in pi's UI. When a run is // done, waiting alerts show as a notification. Memory goes in through // --append-system-prompt. import fs from "node:fs"; import path from "node:path"; // Model API hosts pi's built-in providers use. export const PI_HOSTS = [ "api.anthropic.com", "api.openai.com", "chatgpt.com", "generativelanguage.googleapis.com", "cloudcode-pa.googleapis.com", "openrouter.ai", "api.githubcopilot.com", "api.individual.githubcopilot.com", "api.business.githubcopilot.com", "api.enterprise.githubcopilot.com", "opencode.ai", "ai-gateway.vercel.sh", "router.huggingface.co", "api.mistral.ai", "api.groq.com", "api.x.ai", "api.deepseek.com", "api.cerebras.ai", "api.together.ai", "api.fireworks.ai", "integrate.api.nvidia.com", "inference.baseten.co", "api.z.ai", "api.moonshot.ai", "api.minimax.io", "api.kimi.com", ]; export function piExtension(hook, proxy, hosts, events) { return `// Written by Vantage for one pi session (see https://github.com/Jovan158/vantage.ai). import { spawn } from "node:child_process"; const HOOK = ${JSON.stringify(hook)}; const PROXY = ${JSON.stringify(proxy)}; const HOSTS = new Set(${JSON.stringify(hosts)}); // Model requests go through Vantage's proxy: https://<host>/<path> becomes // <proxy>/h/<host>/<path>. Everything else is left alone. const direct = globalThis.fetch; globalThis.fetch = function (input, init) { try { const url = new URL(typeof input === "string" || input instanceof URL ? String(input) : input.url); if (url.protocol === "https:" && HOSTS.has(url.hostname)) { const target = PROXY + "/h/" + url.hostname + url.pathname + url.search; if (typeof input === "string" || input instanceof URL) return direct.call(this, target, init); return direct.call(this, new Request(target, input), init); } } catch {} return direct.call(this, input, init); }; function vantage(payload) { return new Promise((resolve) => { if (!HOOK) return resolve({}); let out = ""; const child = spawn(HOOK.command, HOOK.args, { stdio: ["pipe", "pipe", "ignore"], windowsHide: true }); child.stdout.on("data", (d) => (out += d)); child.on("error", () => resolve({})); child.on("close", () => { try { resolve(out ? JSON.parse(out) : {}); } catch { resolve({}); } }); child.stdin.end(JSON.stringify(payload)); }); } export default function (pi) {${events.tool ? ` pi.on("tool_call", async (event, ctx) => { const out = await vantage({ hook_event_name: "PreToolUse", tool_name: event.toolName, tool_input: event.input, cwd: ctx.cwd || process.cwd() }); if (out.systemMessage && ctx.hasUI) ctx.ui.notify(out.systemMessage, "warning"); const d = out.hookSpecificOutput; if (!d) return undefined; const reason = d.permissionDecisionReason || "Vantage"; if (d.permissionDecision === "deny") return { block: true, reason }; if (d.permissionDecision === "ask") { if (!ctx.hasUI) return { block: true, reason: reason + " There is no one to ask in this mode, so it was not run." }; const ok = await ctx.ui.confirm("Vantage", reason); if (!ok) return { block: true, reason: "The user declined. " + reason }; } return undefined; });` : ""}${events.stop ? ` pi.on("agent_end", async (_event, ctx) => { const out = await vantage({ hook_event_name: "Stop", cwd: ctx.cwd || process.cwd() }); if (out.systemMessage && ctx.hasUI) ctx.ui.notify(out.systemMessage, "warning"); });` : ""} } `; } export const pi = { key: "pi", id: "pi", name: "pi", short: "pi", command: "pi", install: "npm install -g @earendil-works/pi-coding-agent", capabilities: { enforce: "ask", chatAlerts: true }, routes: (env) => PI_HOSTS.map((host) => ({ prefix: `/h/${host}`, upstream: env.VANTAGE_UPSTREAM ?? `https://${host}` })), prepare(ctx) { const file = path.join(ctx.sessionDir, "vantage-pi.mjs"); fs.mkdirSync(ctx.sessionDir, { recursive: true }); const hook = ctx.enforce || ctx.chatAlerts ? ctx.hook : null; fs.writeFileSync(file, piExtension(hook, ctx.proxyUrl, PI_HOSTS, { tool: ctx.enforce, stop: ctx.chatAlerts })); const args = ["-e", file]; if (ctx.memory) args.push("--append-system-prompt", ctx.memory); return { args, env: {}, notes: [], memory: Boolean(ctx.memory) }; }, // -p / --print answers once; --mode json and rpc are for programs. isInteractive: (args) => !args.some((a, i) => a === "-p" || a === "--print" || ((a === "--mode" || a.startsWith("--mode=")) && (a.split("=")[1] ?? args[i + 1]) !== "text")), };