Skip to content

feat(clink): add Antigravity (agy) CLI client + prompt_to_arg - #466

Open
vishnujayvel wants to merge 3 commits into
BeehiveInnovations:mainfrom
vishnujayvel:feat/clink-agy-prompt-to-arg
Open

feat(clink): add Antigravity (agy) CLI client + prompt_to_arg#466
vishnujayvel wants to merge 3 commits into
BeehiveInnovations:mainfrom
vishnujayvel:feat/clink-agy-prompt-to-arg

Conversation

@vishnujayvel

Copy link
Copy Markdown

What

Adds Google's Antigravity CLI (agy) as a first-class clink client (proposed in #465), filling the gap left by the now-defunct gemini client for individual accounts.

Why a new primitive (prompt_to_arg)

agy needs the prompt as its --print flag's own value (not stdin), and has no JSON output. Rather than special-case it, this adds a general prompt_to_arg client-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 returned sanitized_command/debug logs, so large prompts aren't duplicated into every log line.

What changed

  • clink/models.pyPromptArgConfig + prompt_to_arg on the client config
  • clink/registry.py — thread it through resolution
  • clink/agents/base.py — prompt-as-argument delivery with redacted sanitized_command; empty stdin sent (matching the file-based mechanism)
  • clink/constants.py — register agy
  • clink/parsers/agy.py (+ __init__) — plain-text parser (no JSON)
  • conf/cli_clients/agy.jsonagy --sandbox (sandbox over skip-permissions) + prompt_to_arg: "--print {prompt}"
  • docs + tests (agy parser; prompt_to_arg redaction + real-prompt delivery; a manual e2e run against the real agy binary)

Safety / correctness notes

  • Prompt substitution is shlex.split(template) then .format(prompt=…), and the process is launched via create_subprocess_exec (no shell) — so quotes, newlines, $, backticks, and braces in the prompt are passed literally, not shell-expanded.
  • Two honest trade-offs of delivering the prompt as an argv value (inherent to agy's CLI interface, not this change): it's subject to OS ARG_MAX limits for very large prompts, and the prompt is visible in the process list (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

  • pytest for the agy parser + prompt_to_arg tests — 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 files
  • Manual end-to-end run against real agy v1.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.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread clink/agents/base.py Outdated
Comment on lines +114 to +118
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)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread conf/cli_clients/agy.json
"command": "agy",
"additional_args": ["--sandbox"],
"env": {},
"prompt_to_arg": {"flag_template": "--print {prompt}"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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.
@vishnujayvel

Copy link
Copy Markdown
Author

Done — switched to an upfront {prompt} presence check plus .replace() for both the real and the
redacted arg rendering, matching your suggested diff. A flag template containing any other {...} no
longer raises.

@vishnujayvel

Copy link
Copy Markdown
Author

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
2026-07-12
, fixed in Antigravity CLI 1.1.1. I verified against the real binary rather than
relying on the tracker: spawning agy through clink's exact execution model
(asyncio.create_subprocess_exec with stdin/stdout/stderr piped, then communicate() — i.e. a
non-TTY subprocess) returns real content reliably on 1.1.1 (3/3 runs; also confirmed end-to-end
through clink's own create_agent()BaseCLIAgent.run() path, not a mock). So the empty-stdout
failure you describe does not occur on supported versions.

I considered allocating a PTY (the community workaround) and decided against it: it would add a
per-CLI process model to a shared spawn path to work around a bug that's already fixed upstream, and
it measurably changes output (\r\n instead of \n), which the parser would then have to undo.

What I did ship in response to your review:

  • stdin=DEVNULL when the prompt is passed via argv (prompt_to_arg). clink was opening
    stdin=PIPE and writing zero bytes — an open-but-empty stdin pipe is precisely the hang class
    documented in Python mcp not working #76. Closing it is correct regardless of agy version and protects anyone still on an
    older binary.
  • A documented version floor (agy ≥ 1.1.1) in the clink docs, linking Python mcp not working #76, so a user on an old
    binary understands an empty result instead of filing it against pal.

Tests: agy suite 9/9 green; -k clink unchanged vs baseline (same 2 pre-existing environmental
failures, no regressions); ruff/black/isort clean. Added a regression guard asserting that clients
without prompt_to_arg still get stdin=PIPE with the prompt written to stdin.

@vishnujayvel

Copy link
Copy Markdown
Author

The failing lint job looks unrelated to this PR: black --check fails on 10 files this PR doesn't touch, and the same 10 files fail identically on unmodified main (the workflow installs unpinned black>=23.0.0, which now resolves to 26.x with newer formatting rules — version drift, not a regression from this branch). The files changed here pass black/ruff, and the unit suite is green locally (875 passed, 4 skipped). Happy to open a separate small PR reformatting those 10 files against main if that's welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant