Call the Google Antigravity CLI (agy) headlessly — from a subprocess, an MCP server, CI, or another coding agent — and actually get its output back. Codename PtyGravity.
agy -p "<prompt>" gates its stdout on isatty(). The moment stdout is not a real terminal — any automation — it prints nothing and exits 0. No error, no output, just emptiness.
$ agy -p "say hi" | cat $ # empty. exit 0. no error.
The popular winpty agy -p workaround needs a terminal that already exists — so it still fails from a subprocess, MCP server, or CI runner.
$ python -m agy_headless_bridge "say hi" Hi! How can I help?
A fresh pseudo-terminal is allocated and agy attached to it. It sees a real tty, emits normally; the bridge strips ANSI/TUI noise and returns clean text.
flowchart TB
subgraph BEF["❌ BEFORE — agy -p from any non-TTY caller"]
direction TB
a1["subprocess · MCP · CI · agent"] --> a2["agy -p prompt"]
a2 --> a3["stdout gated by isatty()"]
a3 --> a4["(empty string)
exit 0 · no error · no output"]
end
subgraph AFT["✅ AFTER — through agy-headless-bridge"]
direction TB
b1["subprocess · MCP · CI · agent"] --> b2["run(prompt)"]
b2 --> b3["allocate fresh pseudo-terminal"]
b3 --> b4["agy -p prompt
isatty() == True"]
b4 --> b5["clean() strips ANSI/TUI"]
b5 --> b6["clean text ✓"]
end
classDef bad fill:#2a1313,stroke:#f87171,color:#ffd9d9;
classDef good fill:#0f2a1e,stroke:#34d399,color:#d7ffe9;
class a1,a2,a3,a4 bad;
class b1,b2,b3,b4,b5,b6 good;
The bridge spawns agy attached to a brand-new pseudo-terminal that requires no pre-existing parent tty — so it works from anywhere.
flowchart TD
A["Caller — non-TTY
Claude Code · MCP · subprocess · CI"] -->|"prompt"| B{{"run(prompt)"}}
B --> C["find_agy()
$AGY_PATH → PATH → OS defaults"]
C --> D{"sys.platform?"}
D -->|"win32"| E["pywinpty
PtyProcess.spawn"]
D -->|"posix"| F["stdlib pty
os.openpty + Popen"]
E --> G(["fresh pseudo-terminal"])
F --> G
G --> H["agy -p prompt
isatty == True → emits"]
H -->|"raw bytes + ANSI/TUI chrome"| I["clean()
strip CSI/OSC · collapse \\r repaints · drop spinner glyphs"]
I -->|"clean text"| A
classDef br fill:#1b1740,stroke:#7c5cff,color:#e7e9ef;
classDef pty fill:#0c2730,stroke:#22d3ee,color:#e7e9ef;
classDef agy fill:#0f2a1e,stroke:#34d399,color:#e7e9ef;
class B,C,I br;
class E,F,G pty;
class H agy;
sequenceDiagram
autonumber
participant C as Caller (non-TTY)
participant B as bridge.run()
participant P as fresh PTY
participant A as agy -p
C->>B: run("prompt")
B->>B: find_agy() + pick allocator
B->>P: allocate pseudo-terminal
B->>A: spawn, stdout→PTY master
A->>A: isatty(stdout)==True ✓
A-->>P: streams answer + TUI chrome
loop until EOF / timeout
B->>P: read(4096)
end
B->>B: clean() — strip ANSI, collapse \r, drop glyphs
B-->>C: clean text
isatty() gate. Give agy a tty and it behaves.| Platform | Backend | Why | Status |
|---|---|---|---|
| Windows | pywinpty · PtyProcess | ConPTY allocates a new pty with no parent-tty requirement | verified |
| Linux / macOS | stdlib pty · os.openpty + Popen | No third-party dep on POSIX | pty CI ✓ · agy beta |
agy round-trip on macOS/Linux hasn't run on hardware yet — try it and report back. PRs welcome.One core function, three entry points around it. Everything routes through run().
run(prompt: str, timeout: float = 180, agy_path: str | None = None) -> str
| Raises / returns | When |
|---|---|
AgyNotFoundError | binary not found via $AGY_PATH → PATH → defaults |
TimeoutError | agy exceeds AGY_BRIDGE_TIMEOUT — the process is killed, not left hanging |
ValueError | empty / whitespace prompt |
"" (empty str) | agy genuinely produced no output |
clean str | success |
clean() stripsagy's pty output is a TUI stream, not plain text. clean() turns it back into the answer:
CSI & OSC sequences — colors, cursor moves, window-title sets.
A spinner overwrites one line many times; only the final paint survives.
Box-drawing & spinner glyphs ╭─╮ │ ⠋⠙⠹ are dropped.
⠋ thinking…\r⠙ thinking…\r\x1b[2K \x1b[32m╭─────────────╮\x1b[0m \x1b[32m│\x1b[0m A closure is a function that captures variables from the scope where it was defined. \x1b[32m╰─────────────╯\x1b[0m
A closure is a function that captures variables from the scope where it was defined.
{
"jsonrpc": "2.0", "id": 2,
"result": { "content": [ { "type": "text", "text": "<agy's cleaned answer>" } ] }
}
Both tools take one required string arg — agy_ask(prompt), agy_research(query). On failure the text is an [agy-mcp] ERROR: … string, not a JSON-RPC error, so the agent always gets a readable reply.
| Symptom | Fix |
|---|---|
pip install fails building pywinpty (Windows) | Install MS C++ Build Tools, or use a CPython with a prebuilt wheel; python -m pip install -U pip first. |
AgyNotFoundError | Set AGY_PATH, or ensure agy --version works in your shell. |
| Empty string returned | Confirm agy -p "say hi" works in a real terminal — if not, it's agy/auth, not the bridge. |
TimeoutError | Raise AGY_BRIDGE_TIMEOUT (e.g. 600) or pass timeout= to run(). |
| pty allocation fails | Windows: reinstall pywinpty. POSIX: needs a real /dev/pts (some minimal containers lack it). |
| Garbled output | Open an issue with OS + versions + raw output. |
| Entry point | Invoke | Use for |
|---|---|---|
| Library | from agy_headless_bridge import run | embedding agy in Python |
| CLI | agy-bridge "prompt" | shell scripts, quick calls |
| MCP server | python -m agy_headless_bridge.mcp_server | agents calling agy as a tool (agy_ask / agy_research) |
This package only spawns the agy already on your machine — it ships no credentials and does not install or authenticate anything.
| Need | Detail |
|---|---|
| Python 3.9+ | python --version |
Antigravity CLI (agy) | installed + authenticated — antigravity.google/cli. Auth once interactively (browser OAuth) or set ANTIGRAVITY_API_KEY. |
| Sanity check | In a real terminal, agy -p "say hi" prints a reply. From a pipe it won't — that's the bug this fixes. |
| Windows only | pywinpty (auto-installed). POSIX uses stdlib pty — nothing extra. |
Let one agent delegate work to Gemini via Antigravity, headlessly.
| Use case | How |
|---|---|
| Claude Code asks Gemini for a second opinion / diff review | MCP server → agy_ask |
| CI step runs an agy prompt and captures the answer | agy-bridge "..." in the workflow |
| Python pipeline fans work out to agy | from agy_headless_bridge import run |
| Codex / any MCP-capable agent delegates to agy | register the same MCP server |
| Cron job summarizes logs via agy | agy-bridge in the script |
claude mcp add --transport stdio antigravity -- \
python -m agy_headless_bridge.mcp_server
agy_ask tool to ask Antigravity to review this function for edge cases, then summarize its findings."Want /agy:* slash commands + model selection too? Pair with the community antigravity-cc plugin — it handles triggering & model swap; this handles headless I/O.
{
"mcpServers": {
"antigravity": {
"command": "python",
"args": ["-m", "agy_headless_bridge.mcp_server"]
}
}
}
agy_research with the query 'idiomatic error handling in Rust' and turn the result into a checklist."ANSWER="$(agy-bridge 'Summarize the key risk in this diff in one sentence.')" echo "$ANSWER"
You still need the Antigravity CLI itself installed and authenticated. The bridge locates it via $AGY_PATH → PATH → OS defaults.
pip install agy-headless-bridge # Windows pulls in pywinpty automatically; POSIX uses the stdlib pty module.
from agy_headless_bridge import run print(run("Explain a closure in one line."))
python -m agy_headless_bridge "reply with exactly: OK" # or the console script: agy-bridge "reply with exactly: OK"
claude mcp add --transport stdio antigravity -- \
python -m agy_headless_bridge.mcp_server
Exposes two tools — agy_ask and agy_research — so your agent can delegate work to Antigravity / Gemini.
run() API, an agy-bridge console script, and a JSON-RPC stdio MCP server — pick your entry point.
Speaks JSON-RPC framing directly. Only dependency is pywinpty, and only on Windows.
Strips CSI/OSC escapes, collapses \r spinner repaints, drops box-drawing chrome — you get just the answer.
Configurable AGY_BRIDGE_TIMEOUT; kills a runaway agy and raises instead of hanging.
$AGY_PATH → PATH → OS default install locations. No hardcoded paths.
Unit tests for cleaning & discovery always run; a live agy round-trip auto-skips when agy is absent.
| Env var | Default | Meaning |
|---|---|---|
AGY_PATH | auto-detect | Absolute path to the agy binary |
AGY_BRIDGE_TIMEOUT | 180 | Seconds before a call is killed |
Model selection (swapping Gemini Pro / Flash / Claude inside agy) is not handled here — that's an agy settings concern, already covered by the antigravity-cc Claude Code plugin (it patches settings.json). Pair the two. This bridge fixes the I/O layer they're missing; it does not install or authenticate agy.