Search by

develate / antigravity-cli-php

thoasty-dev

A resilient PHP SDK for controlling the Antigravity (agy) CLI.

Package info

github.com/develate/antigravity-cli-php

pkg:composer/develate/antigravity-cli-php

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.2.0 2026-09-08 13:48 UTC

This package is auto-updated.

Last update: 2026-09-08 13:48:55 UTC


README

A PHP SDK for driving the Antigravity CLI (agy) headlessly.

It speaks the CLI's documented print-mode interface and nothing else: no private endpoint, no undocumented protocol, and no shared-configuration workaround. What the CLI cannot be asked to do, this SDK reports as unsupported rather than approximating.

use Develate\AntigravityCli\Antigravity;

$agy = new Antigravity();

echo $agy->query('Summarise this repository.')->text;

Requirements

  • PHP 8.2+
  • The agy binary on PATH, already signed in (agy manages its own Google credentials in the operating system keyring)

Installation

composer require develate/antigravity-cli-php

Runs

A run is one turn. query() waits for the answer; stream() hands back a Run you can iterate as it happens.

use Develate\AntigravityCli\Antigravity;
use Develate\AntigravityCli\RunOptions;
use Develate\AntigravityCli\SessionOptions;
use Develate\AntigravityCli\Event\StepUpdateEvent;
use Develate\AntigravityCli\Value\Effort;

$agy = (new Antigravity('agy'))->in('/srv/project');

$options = new SessionOptions(
    model: 'gemini-3.1-pro',
    effort: Effort::High,
);

$run = $agy->session($options)->stream('Explain the build pipeline.', new RunOptions(timeout: 300.0));

foreach ($run as $event) {
    if ($event instanceof StepUpdateEvent && $event->textDelta !== null) {
        echo $event->textDelta;
    }
}

$result = $run->result();

Result carries the answer text, the turn's status, its usage, the tool calls it made, any actions the CLI's permission rules refused, and every event verbatim.

Cancelling

Run::cancel() stops the process at the next poll. The child is stopped when a run is cancelled, throws, or is simply abandoned mid-iteration.

$run->cancel();

Conversations

Antigravity allocates the conversation id; this SDK never invents one. It appears on the result and on the session once the CLI has answered.

$session = $agy->session($options);
$session->query('Add a health check endpoint.');

$conversationId = $session->id();

Resuming needs that id explicitly:

$resumed = $agy->resume($conversationId, $options);
$resumed->query('Now add a test for it.');

agy --continue resumes whatever conversation the CLI touched last, which on a shared machine is not necessarily yours. The SDK does not offer it.

One process per turn, or one per conversation

session() starts a process per turn, which is what you want when turns are queued independently and cancelled on their own. persistentSession() keeps one process for the whole conversation, which is cheaper per turn:

$session = $agy->persistentSession($options);
$session->query('One');
$session->query('Two');
$session->close();

Turns run one at a time either way. A second turn started while the first is in flight throws, because two answers on one pipe cannot be told apart.

Token accounting

The CLI's result event reports usage for the whole conversation, not the turn. A second turn that reported it as its own would charge the first turn twice.

Result::$usage is therefore built from the steps that completed during the turn, and Result::$cumulativeUsage carries the CLI's running total unchanged:

$result->usage?->inputTokens;           // this turn
$result->cumulativeUsage?->inputTokens; // the conversation so far

When neither the steps nor a trustworthy baseline can supply a figure, $usage is null. Unknown is reported as unknown.

Permissions

Antigravity's own permission rules apply, and they keep their native meaning.

use Develate\AntigravityCli\Value\ExecutionMode;

// Edits are accepted; commands needing approval are denied, not prompted.
new SessionOptions(mode: ExecutionMode::AcceptEdits, sandbox: true);

// Everything is auto-approved.
new SessionOptions(dangerouslyBypassPermissions: true);

A headless run cannot answer a permission prompt, so a sandboxed run denies what it cannot approve and says so on the result:

if ($result->hasDenials()) {
    foreach ($result->deniedActions as $denied) {
        echo $denied->displayName;
    }
}

A turn can therefore succeed having quietly skipped work you asked for. Check hasDenials() when that matters.

ExecutionMode::Plan is Antigravity's own planning mode. It prefixes the conversation with a planning instruction; it is not a read-only enforcement boundary, and this SDK does not present it as one.

Capabilities

Some things the CLI's headless interface simply does not offer. They are named rather than emulated:

use Develate\AntigravityCli\Value\Capability;

$agy->supports(Capability::ResumeByConversationId); // true
$agy->supports(Capability::ImageInput);             // false

$agy->capabilities()->require(Capability::InteractiveApproval); // throws UnsupportedCapability

Unsupported: interactive approval, image input, forking, rewinding, and managed authentication.

Model-specific reasoning effort

Use the pure helper for a selected slug, including a custom model or one that has not been fetched. It does not instantiate a client or start a process:

use Develate\AntigravityCli\Value\ModelCapabilities;

$showReasoningDropdown = ModelCapabilities::supportsReasoningEffort($selectedModel);
// false for 'claude-opus-4-6-thinking' or 'gemini-3.8-flash-high'
// true for 'gemini-3.8-flash', unknown/custom slugs, null and ''

Models returned by $agy->models() also expose $model->supportsReasoningEffort(). The helper means separately adjustable effort, not whether the model reasons.

agy models in CLI 1.1.27 returns only slug/label pairs, with no capability metadata. ModelCapabilities centralizes the fallback rules, verified on that version using agy --print=/usage --output-format=json --model=SLUG --effort=LEVEL (no model turn):

  • claude-opus-4-6-thinking and claude-sonnet-4-6 reject --effort.
  • Explicit gemini-…-low, -medium and -high variants reject a conflicting effort and accept a matching one. They return false because their effort is already fixed by the slug.
  • Base gemini-3.8-flash accepts --effort=high.

Matching is case-insensitive. Other Claude or “thinking” names are not excluded. Unknown/custom names return true to preserve pass-through behavior; this is not a guarantee that the CLI recognizes a model. Null and empty selections also return true: the SDK cannot resolve the account's default without a process, so an explicitly selected effort is still sent and the CLI validates it.

Command construction uses this same helper to omit --effort for unsupported models, including saved effort values on resumed sessions. The selected model, other options, and the immutable session options are preserved. Null effort always omits the flag, for every model.

Reports and commands

/usage is answered by the CLI itself, without asking a model:

$quota = $agy->quota();

if ($quota->available) {
    foreach ($quota->entries as $entry) {
        echo "{$entry->group}: {$entry->remainingPercent}% until {$entry->resetsAt?->format('c')}\n";
    }
}

When the output is not recognised, available is false and the raw text and stderr are kept. No percentage, reset time or account detail is ever invented.

The supported command families are wrapped too. Each one runs only when you call it; none is a side effect of a run:

$agy->models();
$agy->agents();
$agy->mcp()->list();
$agy->plugins()->list();
$agy->remoteControl()->status();
$agy->changelog();
$agy->update();   // updates the binary in place
$agy->install();  // writes to the shell profile

mcp() and plugins() edit configuration shared by every Antigravity session on the machine.

MCP servers

Register local stdio servers with arguments and environment variables, or remote HTTP servers with request headers:

$agy->mcp()->addStdio('filesystem', 'npx', [
    '-y', '@modelcontextprotocol/server-filesystem', '/srv/project',
]);
$agy->mcp()->addStdio('tools', 'my-mcp-server', env: ['API_KEY' => $apiKey]);
$agy->mcp()->addHttp('api', 'https://example.com/mcp', headers: [
    'Authorization' => 'Bearer '.$token,
]);

$agy->mcp()->list(); // configured server names
$agy->mcp()->disable('api');
$agy->mcp()->enable('api');
$agy->mcp()->remove('api');

Adding an existing name updates its configuration. Mutation methods return CommandOutput and throw ProcessFailed when the CLI exits unsuccessfully. The existing add('name', ['command', 'argument']) API also accepts named env, headers, and type (stdio or http) arguments. Values are passed as individual process arguments, without shell interpolation.

These operations use the CLI's shared user configuration. Configure servers before starting sessions; the SDK does not inject temporary MCP settings into individual turns. Credentials supplied here are persisted by the CLI along with the server configuration. This API follows agy mcp add --help in CLI 1.1.27 and the official MCP command release notes.

Compatibility

$agy->isAvailable();  // the binary exists and is executable
$agy->isCompatible(); // it answers a command this SDK relies on
$agy->version();      // null when it cannot be read

A binary whose version cannot be read is unknown, not incompatible. Compatibility is decided by the interface being present.

Environment and identity

The environment belongs to the client, not to a single call, so a listing and a run cannot disagree about which configuration they are using. false removes an inherited variable:

$agy = new Antigravity('agy', env: [
    'GEMINI_DIR' => '/srv/accounts/one',
    'SOME_INHERITED_KEY' => false,
]);

Antigravity adopts the local configuration it finds. It has no identity-isolation variable to set, and signing in and out is done through the CLI itself.

Errors

Exception Meaning
AntigravityNotFound the binary is missing or not executable
ProcessFailed the process ended without answering the turn
ProcessTimedOut the deadline passed
ProcessCancelled the run was cancelled
RunFailed the turn itself ended unsuccessfully
InvalidOptions options the CLI cannot honour, rejected before starting
UnsupportedCapability asked for something the CLI does not offer
InvalidStreamJson malformed output, in strict parsing mode

An unsuccessful turn is reported on the result rather than thrown, because the CLI signals it in the result event and can still exit 0. Use Run::resultOrFail() when you would rather have an exception.

Antigravity writes a lot of diagnostic chatter to stderr on healthy runs, so stderr alone is never read as failure.

Testing

composer test

The suite runs against a stand-in binary that speaks the real flag and NDJSON protocol, so no network access or Antigravity account is needed.

License

MIT