@cavi-ai/antigravity 0.2.0 → 0.2.3

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3
4
+
5
+ - Use OpenClaw's provider-owned guided discovery for Control UI reconnect, the
6
+ same local-CLI path as Ollama and LM Studio. Models → Providers shows
7
+ **Reconnect** and persists the non-secret connection without an OpenClaw
8
+ credential. This replaces the 0.2.2 credential-only Connect routing, which
9
+ is not a host contract for profileless CLI sessions.
10
+
11
+ ## 0.2.2
12
+
13
+ - Route Control UI login through the credential-only provider connection flow.
14
+ This avoids inference-gated setup, which cannot safely use `agy` because the
15
+ CLI does not expose a hard tool-free mode.
16
+
17
+ ## 0.2.1
18
+
19
+ - Register `before_prompt_build` with `api.on` so the plugin loads on current
20
+ OpenClaw hosts. `0.2.0` called `registerHook` without a name and aborted
21
+ registration with `hook registration missing name`.
22
+
3
23
  ## 0.2.0
4
24
 
5
25
  - Add provider-owned guided setup that validates the external `agy` session
package/README.md CHANGED
@@ -37,7 +37,10 @@ Restart the OpenClaw Gateway after installation.
37
37
 
38
38
  ## Connect
39
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.
40
+ In the Control UI, open **Models → Providers** and choose **Reconnect** on the
41
+ Antigravity CLI card. The plugin validates the existing `agy` session and
42
+ refreshes the provider connection without creating a credential or changing the
43
+ default model.
41
44
 
42
45
  ## Use
43
46
 
@@ -116,8 +119,17 @@ openclaw config set plugins.entries.antigravity.config.command /absolute/path/to
116
119
  ```bash
117
120
  npm test # plugin behaviour
118
121
  npm run docs:test # documentation build/verify/release tooling
122
+ npm run check:host-integration # live agy + isolated installed-host Reconnect check
119
123
  ```
120
124
 
125
+ The host-integration check requires a signed-in `agy` session and a compatible
126
+ installed `openclaw` command. It runs `agy models`, starts the working-tree
127
+ plugin in a temporary loopback-only OpenClaw state, verifies that the host
128
+ projects **Reconnect** for Antigravity, and verifies that credential-only
129
+ **Connect** is absent. The temporary gateway and state are removed afterward;
130
+ the installed gateway, OpenClaw config, credentials, and default model are not
131
+ changed. Set `AGY_BIN` or `OPENCLAW_BIN` to use a non-default executable.
132
+
121
133
  Releasing docs is automated: publish a GitHub Release `vX.Y.Z` (matching
122
134
  `package.json`) and the `Publish release documentation` workflow builds the
123
135
  versioned artifact, attaches it to the release, and dispatches cavi-home to
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "antigravity",
3
3
  "name": "Antigravity CLI",
4
- "version": "0.2.0",
4
+ "version": "0.2.3",
5
5
  "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
6
6
  "enabledByDefault": false,
7
7
  "activation": {
@@ -31,8 +31,7 @@
31
31
  "choiceHint": "Use an existing Antigravity CLI (agy) login on this host. Run `agy models` to confirm.",
32
32
  "groupId": "antigravity",
33
33
  "groupLabel": "Antigravity",
34
- "groupHint": "Subscription-backed models via agy",
35
- "onboardingFeatured": true
34
+ "groupHint": "Subscription-backed models via agy"
36
35
  }
37
36
  ],
38
37
  "uiHints": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavi-ai/antigravity",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Runs Google's Antigravity CLI (agy) as a subscription-backed OpenClaw model provider.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "src",
32
+ "scripts/check-host-integration.mjs",
32
33
  "setup-api.js",
33
34
  "doctor-contract-api.js",
34
35
  "openclaw.plugin.json",
@@ -65,6 +66,7 @@
65
66
  },
66
67
  "scripts": {
67
68
  "test": "node --test test/*.test.mjs",
69
+ "check:host-integration": "node scripts/check-host-integration.mjs",
68
70
  "docs:test": "node --test scripts/docs/*.test.mjs",
69
71
  "docs:build": "node scripts/docs/build.mjs",
70
72
  "docs:verify": "node scripts/docs/verify.mjs",
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync, spawn } from "node:child_process";
4
+ import { randomBytes } from "node:crypto";
5
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { createServer } from "node:net";
7
+ import { tmpdir } from "node:os";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parseAntigravityModelIds } from "../src/provider.js";
11
+
12
+ const PROVIDER_ID = "antigravity-cli";
13
+ const CHOICE_ID = "antigravity-cli";
14
+ const ACTION_LABEL = "Reconnect";
15
+ const MIN_HOST_VERSION = [2026, 7, 0];
16
+ const COMMAND_TIMEOUT_MS = 30_000;
17
+ const STARTUP_TIMEOUT_MS = 20_000;
18
+
19
+ function fail(message) {
20
+ throw new Error(`Host integration check failed: ${message}`);
21
+ }
22
+
23
+ export function assertAgySession(output) {
24
+ const modelIds = parseAntigravityModelIds(output);
25
+ if (modelIds.length === 0) {
26
+ fail("no models were returned by a live `agy` session; run `agy models` and sign in if needed");
27
+ }
28
+ return modelIds;
29
+ }
30
+
31
+ export function assertReconnectProjection(status) {
32
+ const capability = status?.providerCapabilities?.find(
33
+ (candidate) => candidate?.provider === PROVIDER_ID,
34
+ );
35
+ if (!capability) {
36
+ fail(`the OpenClaw host did not publish capabilities for ${PROVIDER_ID}`);
37
+ }
38
+ if (capability.loginOptions?.length) {
39
+ fail("credential-only Connect is exposed for Antigravity");
40
+ }
41
+ const reconnect = capability.setupActions?.find(
42
+ (action) => action?.choiceId === CHOICE_ID && action?.actionLabel === ACTION_LABEL,
43
+ );
44
+ if (!reconnect) {
45
+ fail('the Antigravity guided-discovery action is not labeled "Reconnect"');
46
+ }
47
+ return { choiceId: reconnect.choiceId, actionLabel: reconnect.actionLabel };
48
+ }
49
+
50
+ function parseHostVersion(output) {
51
+ const match = String(output).match(/OpenClaw\s+(\d+)\.(\d+)\.(\d+)/u);
52
+ if (!match) {
53
+ fail(`could not parse the installed OpenClaw version from ${JSON.stringify(String(output).trim())}`);
54
+ }
55
+ return match.slice(1, 4).map(Number);
56
+ }
57
+
58
+ function compareVersions(left, right) {
59
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
60
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
61
+ if (difference !== 0) return Math.sign(difference);
62
+ }
63
+ return 0;
64
+ }
65
+
66
+ export function commandOutput(command, args, options = {}) {
67
+ const timeout = options.timeout ?? COMMAND_TIMEOUT_MS;
68
+ try {
69
+ return execFileSync(command, args, {
70
+ encoding: "utf8",
71
+ maxBuffer: 16 * 1024 * 1024,
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ ...options,
74
+ timeout,
75
+ });
76
+ } catch (error) {
77
+ if (error?.code === "ETIMEDOUT") {
78
+ fail(`${command} timed out after ${timeout}ms`);
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ function parseJsonOutput(command, args, options) {
85
+ const output = commandOutput(command, args, options);
86
+ try {
87
+ return JSON.parse(output);
88
+ } catch {
89
+ fail(`${command} returned non-JSON output`);
90
+ }
91
+ }
92
+
93
+ export function buildGatewayCallArgs(method, gatewayUrl, token, timeout = 10_000) {
94
+ return [
95
+ "gateway",
96
+ "call",
97
+ method,
98
+ "--url",
99
+ gatewayUrl,
100
+ "--token",
101
+ token,
102
+ "--json",
103
+ "--timeout",
104
+ String(timeout),
105
+ ];
106
+ }
107
+
108
+ async function reserveLoopbackPort() {
109
+ const server = createServer();
110
+ await new Promise((resolve, reject) => {
111
+ server.once("error", reject);
112
+ server.listen(0, "127.0.0.1", resolve);
113
+ });
114
+ const address = server.address();
115
+ const port = typeof address === "object" && address ? address.port : null;
116
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
117
+ if (!port) fail("could not reserve a loopback port for the isolated host");
118
+ return port;
119
+ }
120
+
121
+ async function delay(milliseconds) {
122
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
123
+ }
124
+
125
+ async function waitForGateway(openclaw, gatewayUrl, token, env, child, diagnostics) {
126
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
127
+ while (Date.now() < deadline) {
128
+ if (child.exitCode !== null) {
129
+ fail(`the isolated OpenClaw gateway exited early${diagnostics()}`);
130
+ }
131
+ try {
132
+ parseJsonOutput(openclaw, buildGatewayCallArgs("health", gatewayUrl, token, 1_000), {
133
+ env,
134
+ timeout: 2_000,
135
+ });
136
+ return;
137
+ } catch {
138
+ await delay(250);
139
+ }
140
+ }
141
+ fail(`the isolated OpenClaw gateway did not become ready within ${STARTUP_TIMEOUT_MS}ms${diagnostics()}`);
142
+ }
143
+
144
+ async function stopGateway(child) {
145
+ if (child.exitCode !== null) return;
146
+ child.kill("SIGTERM");
147
+ await Promise.race([
148
+ new Promise((resolve) => child.once("exit", resolve)),
149
+ delay(3_000).then(() => child.kill("SIGKILL")),
150
+ ]);
151
+ }
152
+
153
+ function boundedDiagnostics(chunks) {
154
+ const text = chunks.join("").trim();
155
+ return text ? `\nGateway diagnostics:\n${text.slice(-4_000)}` : "";
156
+ }
157
+
158
+ export async function withTemporaryRoot(run) {
159
+ const tempRoot = await mkdtemp(join(tmpdir(), "antigravity-host-check-"));
160
+ try {
161
+ return await run(tempRoot);
162
+ } finally {
163
+ await rm(tempRoot, { recursive: true, force: true });
164
+ }
165
+ }
166
+
167
+ export async function runHostIntegrationCheck({
168
+ openclaw = process.env.OPENCLAW_BIN?.trim() || "openclaw",
169
+ agy = process.env.AGY_BIN?.trim() || "agy",
170
+ } = {}) {
171
+ const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
172
+ const packageJson = JSON.parse(await readFile(join(pluginRoot, "package.json"), "utf8"));
173
+ const hostVersion = parseHostVersion(commandOutput(openclaw, ["--version"]));
174
+ if (compareVersions(hostVersion, MIN_HOST_VERSION) < 0) {
175
+ fail(`OpenClaw ${hostVersion.join(".")} is older than the required ${MIN_HOST_VERSION.join(".")}`);
176
+ }
177
+
178
+ const modelIds = assertAgySession(commandOutput(agy, ["models"]));
179
+ return await withTemporaryRoot(async (tempRoot) => {
180
+ const stateDir = join(tempRoot, "state");
181
+ const configPath = join(tempRoot, "openclaw.json");
182
+ const port = await reserveLoopbackPort();
183
+ const gatewayUrl = `ws://127.0.0.1:${port}`;
184
+ const gatewayToken = randomBytes(24).toString("hex");
185
+ const env = {
186
+ ...process.env,
187
+ OPENCLAW_CONFIG_PATH: configPath,
188
+ OPENCLAW_CONFIG_READONLY: "1",
189
+ OPENCLAW_STATE_DIR: stateDir,
190
+ };
191
+ const gatewayOutput = [];
192
+ let gateway;
193
+
194
+ try {
195
+ await mkdir(stateDir, { recursive: true });
196
+ await writeFile(
197
+ configPath,
198
+ `${JSON.stringify(
199
+ {
200
+ gateway: { mode: "local", bind: "loopback", auth: { mode: "token" } },
201
+ plugins: {
202
+ allow: ["antigravity"],
203
+ load: { paths: [pluginRoot] },
204
+ entries: { antigravity: { enabled: true, config: { command: agy } } },
205
+ },
206
+ },
207
+ null,
208
+ 2,
209
+ )}\n`,
210
+ { mode: 0o600 },
211
+ );
212
+
213
+ gateway = spawn(
214
+ openclaw,
215
+ [
216
+ "gateway",
217
+ "run",
218
+ "--port",
219
+ String(port),
220
+ "--bind",
221
+ "loopback",
222
+ "--auth",
223
+ "token",
224
+ "--token",
225
+ gatewayToken,
226
+ "--allow-unconfigured",
227
+ ],
228
+ { env, stdio: ["ignore", "pipe", "pipe"] },
229
+ );
230
+ gateway.stdout.on("data", (chunk) => gatewayOutput.push(String(chunk)));
231
+ gateway.stderr.on("data", (chunk) => gatewayOutput.push(String(chunk)));
232
+ const diagnostics = () => boundedDiagnostics(gatewayOutput);
233
+
234
+ await waitForGateway(openclaw, gatewayUrl, gatewayToken, env, gateway, diagnostics);
235
+ const status = parseJsonOutput(
236
+ openclaw,
237
+ buildGatewayCallArgs("models.authStatus", gatewayUrl, gatewayToken),
238
+ { env, timeout: 15_000 },
239
+ );
240
+ const action = assertReconnectProjection(status);
241
+ return {
242
+ pluginVersion: packageJson.version,
243
+ hostVersion: hostVersion.join("."),
244
+ agyModelCount: modelIds.length,
245
+ action,
246
+ };
247
+ } finally {
248
+ if (gateway) await stopGateway(gateway);
249
+ }
250
+ });
251
+ }
252
+
253
+ const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
254
+ if (isMain) {
255
+ runHostIntegrationCheck()
256
+ .then((result) => {
257
+ process.stdout.write(
258
+ [
259
+ `PASS @cavi-ai/antigravity ${result.pluginVersion}`,
260
+ `OpenClaw ${result.hostVersion}`,
261
+ `live agy models: ${result.agyModelCount}`,
262
+ `host action: ${result.action.actionLabel}`,
263
+ "credential-only Connect: absent",
264
+ ].join("\n") + "\n",
265
+ );
266
+ })
267
+ .catch((error) => {
268
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
269
+ process.exitCode = 1;
270
+ });
271
+ }
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ export const PLUGIN_ID = "antigravity";
7
7
  export const plugin = {
8
8
  id: PLUGIN_ID,
9
9
  name: "Antigravity CLI",
10
- version: "0.2.0",
10
+ version: "0.2.3",
11
11
  description: "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
12
12
  register: registerAntigravity,
13
13
  };
package/src/provider.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Provider registration for Antigravity (`agy`).
2
2
  //
3
- // agy owns the user's Antigravity OAuth session. The custom auth method below
3
+ // agy owns the user's Antigravity OAuth session. Guided discovery/reconnect
4
4
  // validates that CLI-owned session and records the provider's non-secret
5
5
  // connection (models, endpoint) in config; OpenClaw stores no key for this provider.
6
6
  import { execFile } from "node:child_process";
package/src/register.js CHANGED
@@ -8,17 +8,18 @@ const ANTIGRAVITY_OPENCLAW_CONTEXT = [
8
8
  "Do not change global OpenClaw or mcporter configuration unless the user explicitly asks.",
9
9
  ].join(" ");
10
10
 
11
+ function injectOpenClawGuidance(_event, context) {
12
+ return context?.modelProviderId === "antigravity-cli"
13
+ ? { prependContext: ANTIGRAVITY_OPENCLAW_CONTEXT }
14
+ : undefined;
15
+ }
16
+
11
17
  /** Shared by runtime entry and package-root setup-api. */
12
18
  export function registerAntigravity(api) {
13
19
  const config = api?.pluginConfig ?? {};
14
20
  api.registerProvider(buildAntigravityProvider(config));
15
21
  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
- );
22
+ // Typed lifecycle hooks only fire through api.on. registerHook("before_prompt_build")
23
+ // is ignored, and without opts.name it throws and aborts the whole plugin.
24
+ api.on?.("before_prompt_build", injectOpenClawGuidance);
24
25
  }