@cavi-ai/antigravity 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Add provider-owned guided setup that validates the external `agy` session
6
+ without storing an OpenClaw credential.
7
+ - Persist the provider connection on reconnect: guided and interactive reconnect
8
+ preserve existing provider options and return a non-secret `configPatch`
9
+ recording the Antigravity endpoint and model catalog.
10
+ - Refresh the static catalog for the models reported by `agy` 1.1.25.
11
+ - Accept model listings written to stdout or stderr and separated by tabs or
12
+ spaces.
13
+ - Give Antigravity provider requests focused guidance for using the local
14
+ `openclaw` CLI and MCP servers managed by `mcporter`, using a stable hook id.
15
+
16
+ ## 0.1.0
17
+
18
+ - First public release under `@cavi-ai/antigravity` (the unscoped npm name
19
+ `openclaw-antigravity` is a different package).
20
+ - OpenClaw provider/cli-backend ownership metadata, package-root setup entry,
21
+ doctor contract, and CLI-owned auth recovery hints.
22
+ - Models via `antigravity-cli/<model>` using Google's Antigravity CLI (`agy`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sasan Sotoodehfar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @cavi-ai/antigravity
2
+
3
+ Use Google's Antigravity CLI (`agy`) as an [OpenClaw](https://github.com/openclaw/openclaw) model provider.
4
+
5
+ The plugin uses the Antigravity login already available to `agy`. It does not ask for or store an Antigravity API key.
6
+
7
+ **Documentation:** [Online docs](https://cavi-ai.xyz/docs/antigravity) ·
8
+ [Overview](docs/antigravity/source/pages/introduction/overview.md) ·
9
+ [Quickstart](docs/antigravity/source/pages/introduction/quickstart.md) ·
10
+ [Configuration](docs/antigravity/source/pages/guides/configuration.md) ·
11
+ [Contributing](CONTRIBUTING.md)
12
+
13
+ The pages under `docs/antigravity/source/` are the release source. A published
14
+ GitHub Release builds them into an immutable versioned artifact that cavi-home
15
+ ingests and serves at [cavi-ai.xyz/docs/antigravity](https://cavi-ai.xyz/docs/antigravity).
16
+
17
+ ## Requirements
18
+
19
+ - OpenClaw 2026.7 or newer
20
+ - Node.js 20 or newer
21
+ - Antigravity CLI installed and signed in
22
+
23
+ Confirm that Antigravity is ready:
24
+
25
+ ```bash
26
+ agy models
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ openclaw plugins install @cavi-ai/antigravity
33
+ openclaw config set plugins.entries.antigravity.enabled true
34
+ ```
35
+
36
+ Restart the OpenClaw Gateway after installation.
37
+
38
+ ## Connect
39
+
40
+ In OpenClaw, select **Antigravity CLI** and choose **Reconnect**. The plugin validates the existing `agy` session and makes its models available without creating a separate credential.
41
+
42
+ ## Use
43
+
44
+ Models use the `antigravity-cli/<model>` format:
45
+
46
+ ```bash
47
+ openclaw agent --model antigravity-cli/gemini-3.1-pro-high -m "hello"
48
+ ```
49
+
50
+ If your OpenClaw configuration uses `agents.defaults.modelPolicy.allow`, add the models you want to use or allow `antigravity-cli/*`.
51
+
52
+ ### Models
53
+
54
+ Run `agy models` for the live list. The plugin includes this catalog from `agy` 1.1.25:
55
+
56
+ | Model | Alias |
57
+ | --- | --- |
58
+ | `gemini-3.8-flash-high` | |
59
+ | `gemini-3.8-flash-medium` | |
60
+ | `gemini-3.8-flash-low` | |
61
+ | `gemini-3.7-flash-high` | |
62
+ | `gemini-3.7-flash-medium` | |
63
+ | `gemini-3.7-flash-low` | |
64
+ | `gemini-3.6-flash-high` | |
65
+ | `gemini-3.6-flash-medium` | `flash` |
66
+ | `gemini-3.6-flash-low` | `flash-lite` |
67
+ | `gemini-3.1-pro-high` | `pro` (default) |
68
+ | `gemini-3.1-pro-low` | |
69
+ | `claude-sonnet-4-6` | `sonnet` |
70
+ | `claude-opus-4-6-thinking` | `opus` |
71
+ | `gpt-oss-120b-medium` | `gpt-oss` |
72
+
73
+ New model IDs that are not yet in the catalog are passed through to `agy`.
74
+
75
+ ## OpenClaw and MCP commands
76
+
77
+ When a request runs through this provider, Antigravity is told to:
78
+
79
+ - use the installed `openclaw` CLI for OpenClaw operations;
80
+ - use `mcporter` for external MCP servers it manages; and
81
+ - check each CLI's help before assuming command syntax.
82
+
83
+ This guidance is limited to Antigravity requests and does not change either tool's global configuration.
84
+
85
+ ## Configuration
86
+
87
+ All settings are optional and live under `plugins.entries.antigravity.config`.
88
+
89
+ | Key | Default | Purpose |
90
+ | --- | --- | --- |
91
+ | `command` | `agy` | Path to the `agy` executable. |
92
+ | `mode` | `accept-edits` | Value passed to `agy --mode`, or `none` to omit it. |
93
+ | `skipPermissions` | `false` | Pass `--dangerously-skip-permissions`. Enable only if you accept tool runs without permission prompts. |
94
+
95
+ Example:
96
+
97
+ ```bash
98
+ openclaw config set plugins.entries.antigravity.config.command /absolute/path/to/agy
99
+ ```
100
+
101
+ ## Troubleshooting
102
+
103
+ - Run `agy models` to confirm that the CLI is installed and signed in.
104
+ - If the provider is missing, confirm the plugin is enabled and restart the Gateway.
105
+ - If OpenClaw rejects a model before it runs, check `agents.defaults.modelPolicy.allow`.
106
+
107
+ ## Limitations
108
+
109
+ - Responses are returned after `agy` finishes; token streaming is not available.
110
+ - Inline image input is not supported by `agy --print`.
111
+ - Per-token cost is reported as zero because Antigravity is subscription-backed.
112
+ - Context-window values are conservative because the CLI does not publish per-model limits.
113
+
114
+ ## Development
115
+
116
+ ```bash
117
+ npm test # plugin behaviour
118
+ npm run docs:test # documentation build/verify/release tooling
119
+ ```
120
+
121
+ Releasing docs is automated: publish a GitHub Release `vX.Y.Z` (matching
122
+ `package.json`) and the `Publish release documentation` workflow builds the
123
+ versioned artifact, attaches it to the release, and dispatches cavi-home to
124
+ ingest it. See [CONTRIBUTING.md](CONTRIBUTING.md).
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,13 @@
1
+ // Doctor contract auto-loaded from the plugin package root (or dist/).
2
+ // See OpenClaw doctor-contract-registry load paths.
3
+
4
+ export const sessionRouteStateOwners = [
5
+ {
6
+ id: "antigravity",
7
+ label: "Antigravity CLI",
8
+ providerIds: ["antigravity-cli"],
9
+ runtimeIds: ["antigravity-cli"],
10
+ cliSessionKeys: ["antigravity-cli", "antigravity", "agy"],
11
+ authProfilePrefixes: ["antigravity-cli:", "antigravity:", "agy:"],
12
+ },
13
+ ];
@@ -0,0 +1,72 @@
1
+ {
2
+ "id": "antigravity",
3
+ "name": "Antigravity CLI",
4
+ "version": "0.2.0",
5
+ "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
6
+ "enabledByDefault": false,
7
+ "activation": {
8
+ "onStartup": true,
9
+ "onProviders": ["antigravity-cli"]
10
+ },
11
+ "providers": ["antigravity-cli"],
12
+ "cliBackends": ["antigravity-cli"],
13
+ "autoEnableWhenConfiguredProviders": ["antigravity-cli"],
14
+ "syntheticAuthRefs": ["antigravity-cli"],
15
+ "setup": {
16
+ "providers": [
17
+ {
18
+ "id": "antigravity-cli"
19
+ }
20
+ ],
21
+ "cliBackends": ["antigravity-cli"]
22
+ },
23
+ "providerAuthChoices": [
24
+ {
25
+ "provider": "antigravity-cli",
26
+ "method": "cli",
27
+ "choiceId": "antigravity-cli",
28
+ "appGuidedDiscovery": true,
29
+ "appGuidedActionLabel": "Reconnect",
30
+ "choiceLabel": "Antigravity CLI",
31
+ "choiceHint": "Use an existing Antigravity CLI (agy) login on this host. Run `agy models` to confirm.",
32
+ "groupId": "antigravity",
33
+ "groupLabel": "Antigravity",
34
+ "groupHint": "Subscription-backed models via agy",
35
+ "onboardingFeatured": true
36
+ }
37
+ ],
38
+ "uiHints": {
39
+ "command": {
40
+ "label": "agy binary",
41
+ "help": "Path to the agy executable. Prefer an absolute path after install; default `agy` resolves via PATH.",
42
+ "placeholder": "agy"
43
+ },
44
+ "mode": {
45
+ "label": "agy mode",
46
+ "help": "Value for `agy --mode` on print-mode runs, or `none` to omit the flag."
47
+ },
48
+ "skipPermissions": {
49
+ "label": "Skip permissions",
50
+ "help": "Passes --dangerously-skip-permissions to agy. Off by default. Only enable if you accept tool runs without agy permission prompts."
51
+ }
52
+ },
53
+ "configSchema": {
54
+ "type": "object",
55
+ "additionalProperties": false,
56
+ "properties": {
57
+ "command": {
58
+ "type": "string",
59
+ "description": "Path to the agy binary. Defaults to `agy` on PATH. Prefer an absolute path; do not pass shell metacharacters."
60
+ },
61
+ "mode": {
62
+ "type": "string",
63
+ "description": "agy --mode value for print-mode runs, or \"none\" to omit the flag.",
64
+ "enum": ["accept-edits", "plan", "none"]
65
+ },
66
+ "skipPermissions": {
67
+ "type": "boolean",
68
+ "description": "Pass --dangerously-skip-permissions. Off by default."
69
+ }
70
+ }
71
+ }
72
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@cavi-ai/antigravity",
3
+ "version": "0.2.0",
4
+ "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed OpenClaw model provider.",
5
+ "keywords": [
6
+ "openclaw",
7
+ "openclaw-plugin",
8
+ "antigravity",
9
+ "agy",
10
+ "gemini",
11
+ "llm"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/cavi-ai/openclaw-antigravity.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/cavi-ai/openclaw-antigravity/issues"
20
+ },
21
+ "homepage": "https://github.com/cavi-ai/openclaw-antigravity#readme",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "main": "./src/index.js",
27
+ "exports": {
28
+ ".": "./src/index.js"
29
+ },
30
+ "files": [
31
+ "src",
32
+ "setup-api.js",
33
+ "doctor-contract-api.js",
34
+ "openclaw.plugin.json",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "peerDependencies": {
43
+ "openclaw": ">=2026.7.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "openclaw": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "openclaw": {
51
+ "extensions": ["./src/index.js"],
52
+ "runtimeExtensions": ["./src/index.js"],
53
+ "install": {
54
+ "npmSpec": "@cavi-ai/antigravity",
55
+ "minHostVersion": ">=2026.7.0"
56
+ },
57
+ "compat": {
58
+ "pluginApi": ">=2026.7",
59
+ "minGatewayVersion": "2026.7.0"
60
+ },
61
+ "build": {
62
+ "openclawVersion": "2026.7.0",
63
+ "pluginSdkVersion": "2026.7.0"
64
+ }
65
+ },
66
+ "scripts": {
67
+ "test": "node --test test/*.test.mjs",
68
+ "docs:test": "node --test scripts/docs/*.test.mjs",
69
+ "docs:build": "node scripts/docs/build.mjs",
70
+ "docs:verify": "node scripts/docs/verify.mjs",
71
+ "docs:release": "node scripts/docs/release-artifact.mjs"
72
+ }
73
+ }
package/setup-api.js ADDED
@@ -0,0 +1,10 @@
1
+ // Lightweight setup entry. OpenClaw discovers package-root `setup-api.js`
2
+ // for descriptor-backed setup without loading the full runtime entry.
3
+ import { registerAntigravity } from "./src/register.js";
4
+
5
+ export default {
6
+ id: "antigravity",
7
+ name: "Antigravity CLI Setup",
8
+ description: "Setup hooks for the Antigravity CLI provider.",
9
+ register: registerAntigravity,
10
+ };
@@ -0,0 +1,71 @@
1
+ // CLI backend that drives Google's Antigravity CLI (`agy`) in print mode.
2
+ //
3
+ // This is the `claude -p` pattern: OpenClaw owns the conversation, agy owns
4
+ // inference and auth. agy authenticates itself against the user's Antigravity
5
+ // subscription, so this backend carries no API key and no auth methods.
6
+ //
7
+ // `agy --print --output-format json` emits a single flat object:
8
+ // {"conversation_id":"…","status":"SUCCESS","response":"…","duration_seconds":…,
9
+ // "num_turns":1,"usage":{input_tokens,output_tokens,…}}
10
+ // Core's generic JSON reader already takes `response` as the assistant text and
11
+ // `sessionIdFields` for the resume handle, so no stream dialect is needed. agy's
12
+ // `--output-format stream-json` is its own dialect (event/step_update/result) and
13
+ // matches neither claude-stream-json nor gemini-stream-json; adopting it would
14
+ // require a core dialect, so print+json is used instead.
15
+ import { ANTIGRAVITY_MODEL_ALIASES } from "./models.js";
16
+
17
+ export const ANTIGRAVITY_BACKEND_ID = "antigravity-cli";
18
+
19
+ /** agy print mode cannot prompt a human, so tool calls need a non-review mode. */
20
+ export const DEFAULT_MODE = "accept-edits";
21
+
22
+ /**
23
+ * Builds the args every invocation shares.
24
+ *
25
+ * @param {{mode?: string, skipPermissions?: boolean}} [options]
26
+ */
27
+ export function buildBaseArgs(options = {}) {
28
+ const args = ["--print", "{prompt}", "--output-format", "json"];
29
+ const mode = options.mode ?? DEFAULT_MODE;
30
+ if (mode !== "none") {
31
+ args.push("--mode", mode);
32
+ }
33
+ if (options.skipPermissions === true) {
34
+ args.push("--dangerously-skip-permissions");
35
+ }
36
+ return args;
37
+ }
38
+
39
+ /**
40
+ * @param {{command?: string, mode?: string, skipPermissions?: boolean}} [options]
41
+ * Plugin config from `plugins.entries.antigravity.config`.
42
+ */
43
+ export function buildAntigravityCliBackend(options = {}) {
44
+ const base = buildBaseArgs(options);
45
+ return {
46
+ id: ANTIGRAVITY_BACKEND_ID,
47
+ // Standalone backend: agy's catalog (Gemini + Claude + GPT-OSS behind one
48
+ // subscription) belongs to no existing provider, so it owns direct
49
+ // `antigravity-cli/<model>` refs rather than aliasing a canonical provider.
50
+ modelProvider: ANTIGRAVITY_BACKEND_ID,
51
+ config: {
52
+ command: options.command?.trim() || "agy",
53
+ args: [...base],
54
+ // agy resumes by conversation id; there is no separate session concept.
55
+ resumeArgs: [...base, "--conversation", "{sessionId}"],
56
+ output: "json",
57
+ resumeOutput: "json",
58
+ input: "arg",
59
+ // agy takes the prompt as an argv value. Very long prompts blow the argv
60
+ // limit, so hand those to stdin instead.
61
+ maxPromptArgChars: 96_000,
62
+ modelArg: "--model",
63
+ modelAliases: { ...ANTIGRAVITY_MODEL_ALIASES },
64
+ sessionMode: "existing",
65
+ sessionIdFields: ["conversation_id"],
66
+ // One agy process at a time: concurrent print-mode runs against the same
67
+ // conversation interleave writes to its history.
68
+ serialize: true,
69
+ },
70
+ };
71
+ }
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ // Antigravity CLI plugin — registers `agy` as an OpenClaw model provider backed
2
+ // by the Antigravity subscription, the slot gemini-cli used to fill.
3
+ import { registerAntigravity } from "./register.js";
4
+
5
+ export const PLUGIN_ID = "antigravity";
6
+
7
+ export const plugin = {
8
+ id: PLUGIN_ID,
9
+ name: "Antigravity CLI",
10
+ version: "0.2.0",
11
+ description: "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
12
+ register: registerAntigravity,
13
+ };
14
+
15
+ export function register(api) {
16
+ plugin.register(api);
17
+ }
18
+
19
+ export default plugin;
package/src/models.js ADDED
@@ -0,0 +1,105 @@
1
+ // Model catalog for Google's Antigravity CLI (`agy`).
2
+ //
3
+ // The list mirrors `agy models` as of agy 1.1.25. agy publishes no machine-readable
4
+ // catalog, so ids are refreshed by hand — run `agy models` after an `agy update`
5
+ // and reconcile. Unknown ids still work: they are passed through to `--model`
6
+ // verbatim, so a new agy model is usable before it is listed here.
7
+
8
+ /** agy's own model ids, from `agy models`. */
9
+ export const ANTIGRAVITY_MODEL_IDS = [
10
+ "gemini-3.8-flash-high",
11
+ "gemini-3.8-flash-medium",
12
+ "gemini-3.8-flash-low",
13
+ "gemini-3.7-flash-high",
14
+ "gemini-3.7-flash-medium",
15
+ "gemini-3.7-flash-low",
16
+ "gemini-3.6-flash-high",
17
+ "gemini-3.6-flash-medium",
18
+ "gemini-3.6-flash-low",
19
+ "gemini-3.1-pro-high",
20
+ "gemini-3.1-pro-low",
21
+ "claude-sonnet-4-6",
22
+ "claude-opus-4-6-thinking",
23
+ "gpt-oss-120b-medium",
24
+ ];
25
+
26
+ export const ANTIGRAVITY_DEFAULT_MODEL = "gemini-3.1-pro-high";
27
+
28
+ /**
29
+ * Placeholder endpoint for the catalog row. `agy` owns the transport, so this is
30
+ * never dialed; it exists because `baseUrl` is a required provider field and a
31
+ * row without it breaks catalog assembly. Loopback keeps it classified as a
32
+ * local endpoint rather than an unresolvable public host.
33
+ */
34
+ export const ANTIGRAVITY_BASE_URL = "http://127.0.0.1/antigravity-cli";
35
+
36
+ /** Adapter recorded on catalog rows. Unused at runtime for the same reason. */
37
+ export const ANTIGRAVITY_MODEL_API = "openai-completions";
38
+
39
+ /**
40
+ * Short names for the ids above. `agy` itself accepts only the full id, so these
41
+ * are expanded by the CLI backend before launch.
42
+ */
43
+ export const ANTIGRAVITY_MODEL_ALIASES = {
44
+ pro: "gemini-3.1-pro-high",
45
+ flash: "gemini-3.6-flash-medium",
46
+ "flash-lite": "gemini-3.6-flash-low",
47
+ sonnet: "claude-sonnet-4-6",
48
+ opus: "claude-opus-4-6-thinking",
49
+ "gpt-oss": "gpt-oss-120b-medium",
50
+ };
51
+
52
+ // Context windows are conservative floors, not published figures — Antigravity
53
+ // documents no per-model limits for the CLI. They exist so OpenClaw budgets and
54
+ // compacts sanely; raise them if agy is observed accepting more.
55
+ const CONTEXT_WINDOW_FLOOR = {
56
+ gemini: 1_000_000,
57
+ claude: 200_000,
58
+ other: 128_000,
59
+ };
60
+
61
+ function contextWindowFor(modelId) {
62
+ if (modelId.startsWith("gemini-")) {
63
+ return CONTEXT_WINDOW_FLOOR.gemini;
64
+ }
65
+ if (modelId.startsWith("claude-")) {
66
+ return CONTEXT_WINDOW_FLOOR.claude;
67
+ }
68
+ return CONTEXT_WINDOW_FLOOR.other;
69
+ }
70
+
71
+ /** Turns an agy model id into a human label, e.g. `gemini-3.1-pro-high` → `Gemini 3.1 Pro (high)`. */
72
+ export function labelForModelId(modelId) {
73
+ const match = /^(.*)-(high|medium|low|thinking)$/.exec(modelId);
74
+ const base = match ? match[1] : modelId;
75
+ const suffix = match ? match[2] : undefined;
76
+ const words = base
77
+ .split("-")
78
+ .map((part) => {
79
+ if (part === "gpt") return "GPT";
80
+ if (part === "oss") return "OSS";
81
+ if (/^\d/.test(part)) return part;
82
+ return part.charAt(0).toUpperCase() + part.slice(1);
83
+ })
84
+ .join(" ");
85
+ return suffix ? `${words} (${suffix})` : words;
86
+ }
87
+
88
+ /** Builds the catalog entries OpenClaw shows in model pickers. */
89
+ export function buildAntigravityModelCatalog() {
90
+ return ANTIGRAVITY_MODEL_IDS.map((id) => ({
91
+ id,
92
+ name: labelForModelId(id),
93
+ api: ANTIGRAVITY_MODEL_API,
94
+ // Thinking-tier ids and the pro tiers reason; the flash-low tiers do not
95
+ // advertise it. Treat everything except the explicit `-low` tiers as reasoning.
96
+ reasoning: !id.endsWith("-low"),
97
+ input: ["text"],
98
+ contextWindow: contextWindowFor(id),
99
+ maxTokens: 64_000,
100
+ // Antigravity is subscription-billed, not metered per token, so there is no
101
+ // per-token price to report. Zero here means "not separately billed", and
102
+ // keeps OpenClaw's cost accounting from inventing charges.
103
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
104
+ }));
105
+ }
@@ -0,0 +1,215 @@
1
+ // Provider registration for Antigravity (`agy`).
2
+ //
3
+ // agy owns the user's Antigravity OAuth session. The custom auth method below
4
+ // validates that CLI-owned session and records the provider's non-secret
5
+ // connection (models, endpoint) in config; OpenClaw stores no key for this provider.
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { ANTIGRAVITY_BACKEND_ID } from "./cli-backend.js";
9
+ import {
10
+ ANTIGRAVITY_BASE_URL,
11
+ ANTIGRAVITY_DEFAULT_MODEL,
12
+ ANTIGRAVITY_MODEL_ALIASES,
13
+ ANTIGRAVITY_MODEL_API,
14
+ buildAntigravityModelCatalog,
15
+ } from "./models.js";
16
+
17
+ export const ANTIGRAVITY_PROVIDER_ID = ANTIGRAVITY_BACKEND_ID;
18
+ const execFileAsync = promisify(execFile);
19
+ const COMMAND_TIMEOUT_MS = 15_000;
20
+ const COMMAND_MAX_BUFFER_BYTES = 1_048_576;
21
+
22
+ const MISSING_AUTH_MESSAGE =
23
+ "Antigravity CLI is not ready. Install Google's Antigravity CLI (`agy`), sign in with your Google subscription, then confirm with `agy models`. This provider stores no API key in OpenClaw.";
24
+ const RECONNECT_ERROR_MESSAGE =
25
+ "Antigravity CLI could not list models. Run `agy` in a terminal to sign in, then choose Reconnect again. OpenClaw stores no Antigravity credential.";
26
+
27
+ async function runCommand(command, args, context = {}) {
28
+ const result = await execFileAsync(command, args, {
29
+ env: context.env,
30
+ signal: context.signal,
31
+ timeout: COMMAND_TIMEOUT_MS,
32
+ maxBuffer: COMMAND_MAX_BUFFER_BYTES,
33
+ });
34
+ return [result.stdout, result.stderr].filter(Boolean).join("\n");
35
+ }
36
+
37
+ function throwIfAborted(signal) {
38
+ signal?.throwIfAborted?.();
39
+ }
40
+
41
+ function isAbortError(error, signal) {
42
+ return signal?.aborted === true || error?.name === "AbortError" || error?.code === "ABORT_ERR";
43
+ }
44
+
45
+ /** Extracts model ids from the human-readable `agy models` table. */
46
+ export function parseAntigravityModelIds(output) {
47
+ const ids = [];
48
+ for (const line of String(output ?? "").split(/\r?\n/)) {
49
+ const match = /^\s*([a-z0-9][a-z0-9._-]*-[a-z0-9][a-z0-9._-]*)\s+\S.*$/u.exec(line);
50
+ const id = match?.[1];
51
+ if (!id || ids.includes(id)) {
52
+ continue;
53
+ }
54
+ ids.push(id);
55
+ }
56
+ return ids;
57
+ }
58
+
59
+ function chooseDetectedModel(modelIds) {
60
+ return modelIds.includes(ANTIGRAVITY_DEFAULT_MODEL)
61
+ ? ANTIGRAVITY_DEFAULT_MODEL
62
+ : modelIds[0];
63
+ }
64
+
65
+ /**
66
+ * @param {{command?: string}} [options]
67
+ * @param {{runCommand?: typeof runCommand}} [dependencies]
68
+ */
69
+ export function buildAntigravityProvider(options = {}, dependencies = {}) {
70
+ const command = options.command?.trim() || "agy";
71
+ const execute = dependencies.runCommand ?? runCommand;
72
+
73
+ const listModels = async (context) => {
74
+ throwIfAborted(context.signal);
75
+ const output = await execute(command, ["models"], context);
76
+ throwIfAborted(context.signal);
77
+ return parseAntigravityModelIds(output);
78
+ };
79
+
80
+ const detect = async (context) => {
81
+ try {
82
+ const modelId = chooseDetectedModel(await listModels(context));
83
+ return modelId
84
+ ? {
85
+ modelRef: `${ANTIGRAVITY_PROVIDER_ID}/${modelId}`,
86
+ detail: `${modelId} via agy`,
87
+ }
88
+ : null;
89
+ } catch (error) {
90
+ if (isAbortError(error, context.signal)) {
91
+ throw error;
92
+ }
93
+ return null;
94
+ }
95
+ };
96
+
97
+ // On a successful reconnect, hand OpenClaw a non-secret config patch so the
98
+ // provider's endpoint and model catalog persist. Shape matches the provider
99
+ // connection contract other CLI backends use (`configPatch.models.providers`).
100
+ const buildConnectionPatch = (config = {}) => {
101
+ const existing = config.models?.providers?.[ANTIGRAVITY_PROVIDER_ID] ?? {};
102
+ return {
103
+ models: {
104
+ mode: config.models?.mode ?? "merge",
105
+ providers: {
106
+ [ANTIGRAVITY_PROVIDER_ID]: {
107
+ ...existing,
108
+ baseUrl: ANTIGRAVITY_BASE_URL,
109
+ api: ANTIGRAVITY_MODEL_API,
110
+ models:
111
+ Array.isArray(existing.models) && existing.models.length > 0
112
+ ? existing.models
113
+ : buildAntigravityModelCatalog(),
114
+ },
115
+ },
116
+ },
117
+ };
118
+ };
119
+
120
+ const validatedResult = (modelRef, config) => ({
121
+ profiles: [],
122
+ defaultModel: modelRef,
123
+ configPatch: buildConnectionPatch(config),
124
+ });
125
+
126
+ return {
127
+ id: ANTIGRAVITY_PROVIDER_ID,
128
+ label: "Antigravity CLI",
129
+ aliases: ["antigravity", "agy"],
130
+ envVars: [],
131
+ auth: [
132
+ {
133
+ id: "cli",
134
+ label: "Antigravity CLI",
135
+ hint: "Reconnect the CLI-owned Antigravity session and refresh available models",
136
+ kind: "custom",
137
+ appGuidedSetup: {
138
+ detectAvailability: async (context) => {
139
+ try {
140
+ throwIfAborted(context.signal);
141
+ await execute(command, ["--version"], context);
142
+ throwIfAborted(context.signal);
143
+ return true;
144
+ } catch (error) {
145
+ if (isAbortError(error, context.signal)) {
146
+ throw error;
147
+ }
148
+ return false;
149
+ }
150
+ },
151
+ detect,
152
+ prepare: async (context) => {
153
+ const prefix = `${ANTIGRAVITY_PROVIDER_ID}/`;
154
+ if (!context.modelRef.startsWith(prefix)) {
155
+ return null;
156
+ }
157
+ const modelId = context.modelRef.slice(prefix.length);
158
+ try {
159
+ const available = await listModels(context);
160
+ return available.includes(modelId)
161
+ ? validatedResult(context.modelRef, context.config)
162
+ : null;
163
+ } catch (error) {
164
+ if (isAbortError(error, context.signal)) {
165
+ throw error;
166
+ }
167
+ return null;
168
+ }
169
+ },
170
+ },
171
+ run: async (context) => {
172
+ const detected = await detect(context);
173
+ if (!detected) {
174
+ throw new Error(RECONNECT_ERROR_MESSAGE);
175
+ }
176
+ return validatedResult(detected.modelRef, context.config);
177
+ },
178
+ },
179
+ ],
180
+ buildMissingAuthMessage: () => MISSING_AUTH_MESSAGE,
181
+ buildAuthDoctorHint: () => MISSING_AUTH_MESSAGE,
182
+ staticCatalog: {
183
+ order: "simple",
184
+ run: async () => ({
185
+ provider: {
186
+ // `agy` is the transport, so nothing here is ever dialed. Both fields
187
+ // are still required shape: a provider row without `baseUrl` fails
188
+ // catalog assembly, which takes down prepared-runtime publication for
189
+ // every agent, not just this provider.
190
+ baseUrl: ANTIGRAVITY_BASE_URL,
191
+ api: ANTIGRAVITY_MODEL_API,
192
+ defaultModel: ANTIGRAVITY_DEFAULT_MODEL,
193
+ models: buildAntigravityModelCatalog(),
194
+ },
195
+ }),
196
+ },
197
+ // agy accepts model ids this catalog has not caught up with. Rather than
198
+ // fail the run, pass an unknown id straight through to `--model`.
199
+ resolveDynamicModel: (ctx) => {
200
+ const modelId = typeof ctx?.modelId === "string" ? ctx.modelId.trim() : "";
201
+ if (!modelId) {
202
+ return null;
203
+ }
204
+ const resolved = ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
205
+ return {
206
+ id: resolved,
207
+ provider: ANTIGRAVITY_PROVIDER_ID,
208
+ name: resolved,
209
+ api: ANTIGRAVITY_MODEL_API,
210
+ baseUrl: ANTIGRAVITY_BASE_URL,
211
+ input: ["text"],
212
+ };
213
+ },
214
+ };
215
+ }
@@ -0,0 +1,24 @@
1
+ import { buildAntigravityCliBackend } from "./cli-backend.js";
2
+ import { buildAntigravityProvider } from "./provider.js";
3
+
4
+ const ANTIGRAVITY_OPENCLAW_CONTEXT = [
5
+ "You are running through OpenClaw's Antigravity CLI model provider.",
6
+ "Use the installed `openclaw` CLI for OpenClaw operations and check its help before assuming command syntax.",
7
+ "Use `mcporter` for external MCP servers it manages and check its help before assuming command syntax.",
8
+ "Do not change global OpenClaw or mcporter configuration unless the user explicitly asks.",
9
+ ].join(" ");
10
+
11
+ /** Shared by runtime entry and package-root setup-api. */
12
+ export function registerAntigravity(api) {
13
+ const config = api?.pluginConfig ?? {};
14
+ api.registerProvider(buildAntigravityProvider(config));
15
+ api.registerCliBackend(buildAntigravityCliBackend(config));
16
+ api.registerHook?.(
17
+ "before_prompt_build",
18
+ (_event, context) =>
19
+ context?.modelProviderId === "antigravity-cli"
20
+ ? { prependContext: ANTIGRAVITY_OPENCLAW_CONTEXT }
21
+ : undefined,
22
+ { registrationId: "antigravity-openclaw-cli-guidance" },
23
+ );
24
+ }