feat(clink): add Antigravity (agy) CLI client + prompt_to_arg - #466
feat(clink): add Antigravity (agy) CLI client + prompt_to_arg#466vishnujayvel wants to merge 3 commits into
Conversation
Adds Google's Antigravity CLI (agy) as a first-class clink client,
filling the gap left by gemini (dead: "This client is no longer
supported for Gemini Code Assist for individuals... migrate to the
Antigravity suite of products").
agy differs from claude/codex/gemini in that its `-p`/`--print` flag
requires the prompt as the flag's own value ("flag needs an argument:
-p") rather than reading it from stdin, and it has no structured
output format (no --output-format flag as of v1.0.16) - `--print`
just writes plain text to stdout. Verified directly against the
installed agy v1.0.16 binary.
Rather than special-casing agy, this introduces a general
`prompt_to_arg` client-config option, symmetric with the existing
`output_to_file`/`prompt_to_file`-style hooks: when set, the base
agent renders the real prompt into the flag template for execution
but keeps a redacted copy for the returned sanitized_command / debug
logs, so the (potentially large) prompt text isn't duplicated into
every log line and success response. Empty stdin is sent, matching
the file-based mechanism's behavior for stdin-less CLIs.
- clink/models.py: PromptArgConfig + prompt_to_arg on
CLIClientConfig/ResolvedCLIClient
- clink/registry.py: thread prompt_to_arg through resolution
- clink/agents/base.py: prompt-as-argument delivery with redacted
sanitized_command
- clink/constants.py: register agy (parser=agy_text, runner=None ->
BaseCLIAgent)
- clink/parsers/agy.py + __init__: AgyTextParser (plain-text, no JSON)
- conf/cli_clients/agy.json: command "agy --sandbox" (sandbox over
skip-permissions) + prompt_to_arg "--print {prompt}"
- docs/tools/clink.md: document the agy preset
- tests: agy parser + prompt_to_arg/agy agent coverage, plus a manual
end-to-end run against the real agy binary (--sandbox --print ...
-> "WIRED")
agy authenticates via the local Antigravity session or
ANTIGRAVITY_API_KEY; no key is baked into the config.
There was a problem hiding this comment.
Code Review
This pull request introduces support for the Antigravity (agy) CLI client by adding a new plain-text parser (AgyTextParser) and implementing a mechanism (prompt_to_arg) to pass the prompt as a command-line argument rather than via standard input. Feedback on the changes suggests replacing the use of str.format() with simple string replacement (str.replace()) when rendering the prompt flag template to prevent potential KeyError exceptions if other curly braces are present in the template.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| try: | ||
| rendered_args = [part.format(prompt=prompt) for part in shlex.split(flag_template)] | ||
| except KeyError as exc: # pragma: no cover - defensive | ||
| raise CLIAgentError(f"Invalid prompt flag template '{flag_template}': missing placeholder {exc}") | ||
| redacted_args = [part.format(prompt="<prompt omitted>") for part in shlex.split(flag_template)] |
There was a problem hiding this comment.
Using str.format() on the template parts is fragile because it will raise a KeyError if the template contains any other curly braces (for example, if the user configures additional flags with JSON payloads or other brace-enclosed parameters).
Using simple string replacement with .replace("{prompt}", ...) is much safer, more robust, and avoids the need for a complex try-except block. We can explicitly validate the presence of "{prompt}" beforehand.
| try: | |
| rendered_args = [part.format(prompt=prompt) for part in shlex.split(flag_template)] | |
| except KeyError as exc: # pragma: no cover - defensive | |
| raise CLIAgentError(f"Invalid prompt flag template '{flag_template}': missing placeholder {exc}") | |
| redacted_args = [part.format(prompt="<prompt omitted>") for part in shlex.split(flag_template)] | |
| if "{prompt}" not in flag_template: | |
| raise CLIAgentError(f"Invalid prompt flag template '{flag_template}': missing '{{prompt}}' placeholder") | |
| rendered_args = [part.replace("{prompt}", prompt) for part in shlex.split(flag_template)] | |
| redacted_args = [part.replace("{prompt}", "<prompt omitted>") for part in shlex.split(flag_template)] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f5e027987c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| "command": "agy", | ||
| "additional_args": ["--sandbox"], | ||
| "env": {}, | ||
| "prompt_to_arg": {"flag_template": "--print {prompt}"}, |
There was a problem hiding this comment.
Allocate a TTY for agy print mode
This preset sends every agy request through BaseCLIAgent, which captures stdout/stderr with subprocess pipes; upstream Antigravity reports show agy --print in non-TTY subprocess/pipe contexts can return zero stdout or hang, including with --sandbox (see google-antigravity/antigravity-cli#76 and #318). In that environment, cli_name="agy" either times out or reaches AgyTextParser with empty stdout, so the advertised client is unusable unless the runner allocates a PTY or gates this preset on a headless-safe agy version/flag.
Useful? React with 👍 / 👎.
…emplates
str.format() raises KeyError on any stray curly braces in a client's
flag_template, not just a missing {prompt} placeholder. Validate the
placeholder up front and substitute with str.replace() instead, which
is safe regardless of other braces in the template.
Addresses review feedback from gemini-code-assist on PR BeehiveInnovations#466.
When prompt_to_arg is set, the prompt is delivered via argv and no bytes are ever written to stdin. Spawning with stdin=PIPE in that case leaves an open-but-empty pipe to the child, which is the exact hang class pre-1.1.1 Antigravity hit when run non-interactively (google-antigravity/antigravity-cli#76). Use stdin=DEVNULL instead so the child sees a closed stdin immediately. Behavior for clients that don't set prompt_to_arg (gemini/claude/codex) is unchanged. Also documents an agy version floor (>= 1.1.1) in the clink docs.
|
Done — switched to an upfront |
|
Thanks — this was worth flagging, and I dug into it rather than assuming either way. The upstream issue you cited (google-antigravity/antigravity-cli#76) was closed as completed on I considered allocating a PTY (the community workaround) and decided against it: it would add a What I did ship in response to your review:
Tests: agy suite 9/9 green; |
|
The failing |
What
Adds Google's Antigravity CLI (
agy) as a first-classclinkclient (proposed in #465), filling the gap left by the now-defunctgeminiclient for individual accounts.Why a new primitive (
prompt_to_arg)agyneeds the prompt as its--printflag's own value (not stdin), and has no JSON output. Rather than special-case it, this adds a generalprompt_to_argclient-config option, symmetric with the existing file-based hooks: the real prompt is rendered into the flag template for execution, while a redacted copy (<prompt omitted>) is kept for the returnedsanitized_command/debug logs, so large prompts aren't duplicated into every log line.What changed
clink/models.py—PromptArgConfig+prompt_to_argon the client configclink/registry.py— thread it through resolutionclink/agents/base.py— prompt-as-argument delivery with redactedsanitized_command; empty stdin sent (matching the file-based mechanism)clink/constants.py— registeragyclink/parsers/agy.py(+__init__) — plain-text parser (no JSON)conf/cli_clients/agy.json—agy --sandbox(sandbox over skip-permissions) +prompt_to_arg: "--print {prompt}"prompt_to_argredaction + real-prompt delivery; a manual e2e run against the real agy binary)Safety / correctness notes
shlex.split(template)then.format(prompt=…), and the process is launched viacreate_subprocess_exec(no shell) — so quotes, newlines,$, backticks, and braces in the prompt are passed literally, not shell-expanded.ps) — the redaction only covers app logs /sanitized_command. File/stdin delivery (prompt_to_file) avoids both where a CLI supports it; agy currently doesn't.Verification
pytestfor the agy parser +prompt_to_argtests — 8 passed, 0 failed (pytest, tests/test_clink_agy_agent.py + tests/test_clink_agy_parser.py; Python 3.14.5); ruff clean on all changed filesagyv1.0.16 (--sandbox --print …→WIRED).Opening this alongside #465 — happy to reshape the primitive or narrow it to an agy-specific approach if you'd prefer.