#!/usr/bin/env node // Antigravity (agy) companion — minimal runtime for Claude Code plugin. // Mirrors the codex-companion pattern but limited to `task` and `setup`. // Headless execution: `agy --print --dangerously-skip-permissions --print-timeout `. import { spawn, spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 min foreground default const DEFAULT_BG_TIMEOUT_MS = 30 * 60 * 1000; // 30 min background default const STATE_DIR = path.join(os.homedir(), ".cache", "antigravity-plugin"); const JOBS_DIR = path.join(STATE_DIR, "jobs"); const LAST_THREAD_FILE = path.join(STATE_DIR, "last-thread.json"); function ensureDir(p) { try { fs.mkdirSync(p, { recursive: true }); } catch {} } // ---- which agy binary ---------------------------------------------------- function findAgyBinary() { const candidates = [ process.env.AGY_BINARY, path.join(os.homedir(), ".local", "bin", "agy"), "/usr/local/bin/agy", "/opt/homebrew/bin/agy" ].filter(Boolean); for (const c of candidates) { try { fs.accessSync(c, fs.constants.X_OK); return c; } catch {} } // PATH lookup const res = spawnSync("which", ["agy"], { encoding: "utf8" }); if (res.status === 0) return res.stdout.trim(); return null; } function getAgyVersion(bin) { try { const res = spawnSync(bin, ["--help"], { encoding: "utf8", timeout: 5000 }); if (res.status === 0) return "available"; } catch {} return null; } // ---- CLI args ------------------------------------------------------------- const BOOL_FLAGS = new Set([ "background", "sandbox", "resume", "resume-last", "fresh", "json", "wait" ]); const VALUE_FLAGS = new Set([ "add-dir", "timeout", "model", "effort" ]); function parseArgs(argv) { const out = { _: [] }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith("--")) { const eq = a.indexOf("="); const key = eq > -1 ? a.slice(2, eq) : a.slice(2); if (eq > -1) { out[key] = a.slice(eq + 1); } else if (BOOL_FLAGS.has(key)) { out[key] = true; } else if (VALUE_FLAGS.has(key) && argv[i + 1] !== undefined) { out[key] = argv[++i]; } else { out[key] = true; } } else { out._.push(a); } } return out; } // ---- setup command ------------------------------------------------------- function setup(args) { const bin = findAgyBinary(); const installed = !!bin; const result = { cli: "agy", installed, binary_path: bin || null, headless_mode: installed ? "agy --print --dangerously-skip-permissions" : null, install_command: installed ? null : "curl -fsSL https://antigravity.google/cli/install.sh | bash", notes: installed ? "Antigravity CLI is installed. Use `/antigravity:rescue` to delegate tasks." : "Antigravity CLI not found. Install via the official one-liner, then re-run /antigravity:setup." }; if (args.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log("Antigravity CLI status\n----------------------"); console.log(`Installed : ${result.installed ? "✓" : "✗"}`); if (result.binary_path) console.log(`Binary path : ${result.binary_path}`); if (result.headless_mode) console.log(`Headless cmd : ${result.headless_mode}`); if (result.install_command) console.log(`Install command : ${result.install_command}`); console.log(`\n${result.notes}`); } process.exit(installed ? 0 : 1); } // ---- task command -------------------------------------------------------- function buildPromptText(args) { return (args._.join(" ") || "").trim(); } function runTaskForeground(prompt, opts) { const bin = opts.bin; const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS; const timeoutStr = Math.floor(timeoutMs / 1000) + "s"; const cliArgs = [ "--print", "--dangerously-skip-permissions", "--print-timeout", timeoutStr ]; if (opts.continueLast) cliArgs.push("--continue"); if (opts.addDir) cliArgs.push("--add-dir", opts.addDir); if (opts.sandbox) cliArgs.push("--sandbox"); cliArgs.push(prompt); // Use spawn to stream output to stdout in real-time const child = spawn(bin, cliArgs, { stdio: ["ignore", "pipe", "pipe"] }); let stdoutBuf = ""; let stderrBuf = ""; child.stdout.on("data", (d) => { const s = d.toString(); stdoutBuf += s; process.stdout.write(s); }); child.stderr.on("data", (d) => { const s = d.toString(); stderrBuf += s; process.stderr.write(s); }); return new Promise((resolve) => { const timer = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} process.stderr.write("\n[antigravity] timeout reached (" + timeoutStr + ")\n"); resolve({ code: 124, stdout: stdoutBuf, stderr: stderrBuf, timedOut: true }); }, timeoutMs); child.on("close", (code) => { clearTimeout(timer); resolve({ code, stdout: stdoutBuf, stderr: stderrBuf, timedOut: false }); }); }); } function runTaskBackground(prompt, opts) { ensureDir(JOBS_DIR); const jobId = "agy-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 7); const jobFile = path.join(JOBS_DIR, jobId + ".json"); const logFile = path.join(JOBS_DIR, jobId + ".log"); const bin = opts.bin; const timeoutMs = opts.timeoutMs || DEFAULT_BG_TIMEOUT_MS; const timeoutStr = Math.floor(timeoutMs / 1000) + "s"; const cliArgs = [ "--print", "--dangerously-skip-permissions", "--print-timeout", timeoutStr ]; if (opts.continueLast) cliArgs.push("--continue"); if (opts.addDir) cliArgs.push("--add-dir", opts.addDir); if (opts.sandbox) cliArgs.push("--sandbox"); cliArgs.push(prompt); const logFd = fs.openSync(logFile, "a"); const child = spawn(bin, cliArgs, { detached: true, stdio: ["ignore", logFd, logFd] }); const meta = { jobId, pid: child.pid, startedAt: new Date().toISOString(), cmd: bin + " " + cliArgs.join(" "), prompt, logFile, timeoutMs }; fs.writeFileSync(jobFile, JSON.stringify(meta, null, 2)); child.unref(); fs.closeSync(logFd); console.log(`[antigravity] Job started in background.`); console.log(` job id : ${jobId}`); console.log(` pid : ${child.pid}`); console.log(` log file : ${logFile}`); console.log(` status : node "${process.argv[1]}" status ${jobId}`); console.log(` result : node "${process.argv[1]}" result ${jobId}`); process.exit(0); } async function task(args) { const bin = findAgyBinary(); if (!bin) { console.error("[antigravity] agy not found in PATH. Run /antigravity:setup first."); process.exit(2); } const prompt = buildPromptText(args); if (!prompt) { console.error("[antigravity] empty task prompt."); process.exit(2); } const opts = { bin, continueLast: !!args["resume-last"] || !!args["resume"], addDir: args["add-dir"] || null, sandbox: !!args.sandbox, timeoutMs: args.timeout ? parseTimeout(args.timeout) : null }; // Save last-thread marker so future --resume can be smart ensureDir(STATE_DIR); fs.writeFileSync(LAST_THREAD_FILE, JSON.stringify({ ts: new Date().toISOString(), prompt: prompt.slice(0, 200), bin }, null, 2)); if (args.background) { runTaskBackground(prompt, opts); return; } const r = await runTaskForeground(prompt, opts); process.exit(r.code || 0); } function parseTimeout(v) { if (typeof v === "number") return v; const m = String(v).match(/^(\d+)([sm]?)$/i); if (!m) return DEFAULT_TIMEOUT_MS; const n = parseInt(m[1], 10); const unit = (m[2] || "s").toLowerCase(); return unit === "m" ? n * 60_000 : n * 1000; } // ---- status / result ----------------------------------------------------- function status(args) { ensureDir(JOBS_DIR); const jobs = fs.readdirSync(JOBS_DIR).filter(f => f.endsWith(".json")).map(f => { try { return JSON.parse(fs.readFileSync(path.join(JOBS_DIR, f), "utf8")); } catch { return null; } }).filter(Boolean); const wanted = args._[0]; const target = wanted ? jobs.find(j => j.jobId === wanted) : jobs.sort((a,b) => (b.startedAt||'').localeCompare(a.startedAt||''))[0]; if (!target) { console.log("[antigravity] no background jobs found."); process.exit(0); } const alive = isPidAlive(target.pid); const out = { jobId: target.jobId, pid: target.pid, startedAt: target.startedAt, alive, logFile: target.logFile, promptPreview: (target.prompt || "").slice(0, 200) }; if (args.json) console.log(JSON.stringify(out, null, 2)); else { console.log(`Job ${target.jobId}`); console.log(` pid : ${target.pid} (${alive ? "running" : "exited"})`); console.log(` started : ${target.startedAt}`); console.log(` log : ${target.logFile}`); console.log(` prompt : ${out.promptPreview}`); } } function isPidAlive(pid) { try { process.kill(pid, 0); return true; } catch { return false; } } function result(args) { ensureDir(JOBS_DIR); const wanted = args._[0]; let job; if (wanted) { const f = path.join(JOBS_DIR, wanted + ".json"); try { job = JSON.parse(fs.readFileSync(f, "utf8")); } catch {} } else { const list = fs.readdirSync(JOBS_DIR).filter(f => f.endsWith(".json")).map(f => { try { return JSON.parse(fs.readFileSync(path.join(JOBS_DIR, f), "utf8")); } catch { return null; } }).filter(Boolean).sort((a,b) => (b.startedAt||'').localeCompare(a.startedAt||'')); job = list[0]; } if (!job) { console.error("[antigravity] no job found."); process.exit(1); } try { const log = fs.readFileSync(job.logFile, "utf8"); process.stdout.write(log); process.exit(0); } catch (e) { console.error("[antigravity] log file not readable: " + (e?.message || e)); process.exit(1); } } function cancel(args) { ensureDir(JOBS_DIR); const wanted = args._[0]; if (!wanted) { console.error("[antigravity] cancel requires a job id."); process.exit(2); } const f = path.join(JOBS_DIR, wanted + ".json"); let job; try { job = JSON.parse(fs.readFileSync(f, "utf8")); } catch { console.error("[antigravity] job not found."); process.exit(1); } try { process.kill(job.pid, "SIGTERM"); console.log("[antigravity] sent SIGTERM to " + job.pid); } catch (e) { console.error("[antigravity] could not signal pid " + job.pid + ": " + (e?.message||e)); process.exit(1); } } function taskResumeCandidate(args) { let available = false, thread = null; try { const data = JSON.parse(fs.readFileSync(LAST_THREAD_FILE, "utf8")); if (data && data.ts) { const ageMs = Date.now() - Date.parse(data.ts); if (ageMs < 24 * 3600 * 1000) { available = true; thread = data; } } } catch {} const out = { available, thread }; if (args.json) console.log(JSON.stringify(out, null, 2)); else console.log(available ? "Resumable thread available." : "No resumable thread."); } // ---- entry --------------------------------------------------------------- function printUsage() { console.log(`Antigravity companion — usage: setup [--json] check agy availability task [--background] [--resume] [--add-dir

] run a task (foreground default) [--sandbox] [--timeout 10m] status [] [--json] show background job status result [] print background job log cancel SIGTERM a background job task-resume-candidate [--json] check resumable thread `); } (async function main() { const argv = process.argv.slice(2); const sub = argv.shift(); const args = parseArgs(argv); switch (sub) { case "setup": return setup(args); case "task": return task(args); case "status": return status(args); case "result": return result(args); case "cancel": return cancel(args); case "task-resume-candidate": return taskResumeCandidate(args); case "-h": case "--help": case undefined: return printUsage(); default: console.error("[antigravity] unknown subcommand: " + sub); printUsage(); process.exit(2); } })();