@cavi-ai/antigravity 0.2.2 → 0.2.4
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 +19 -0
- package/README.md +19 -5
- package/agy-plugin/agents/openclaw-antigravity-setup/agent.md +11 -0
- package/agy-plugin/plugin.json +9 -0
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -1
- package/scripts/check-host-integration.mjs +336 -0
- package/src/cli-backend.js +12 -0
- package/src/index.js +1 -1
- package/src/provider.js +44 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
- Make the guided-discovery **Reconnect** verification tool-free by installing
|
|
6
|
+
a bundled, isolated `agy` custom agent for setup probes. Normal provider
|
|
7
|
+
turns retain the configured Antigravity mode and tool behavior.
|
|
8
|
+
- Declare the native `agy` executable as the CLI backend runtime artifact so
|
|
9
|
+
current OpenClaw hosts can bind the setup probe to a durable owner.
|
|
10
|
+
- Extend the installed-host integration check through Reconnect activation and
|
|
11
|
+
isolate its temporary `agy` plugin config from the user's existing plugins.
|
|
12
|
+
- Require Antigravity CLI 1.2.1 or newer for hard tool-free setup probes.
|
|
13
|
+
|
|
14
|
+
## 0.2.3
|
|
15
|
+
|
|
16
|
+
- Use OpenClaw's provider-owned guided discovery for Control UI reconnect, the
|
|
17
|
+
same local-CLI path as Ollama and LM Studio. Models → Providers shows
|
|
18
|
+
**Reconnect** and persists the non-secret connection without an OpenClaw
|
|
19
|
+
credential. This replaces the 0.2.2 credential-only Connect routing, which
|
|
20
|
+
is not a host contract for profileless CLI sessions.
|
|
21
|
+
|
|
3
22
|
## 0.2.2
|
|
4
23
|
|
|
5
24
|
- Route Control UI login through the credential-only provider connection flow.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ ingests and serves at [cavi-ai.xyz/docs/antigravity](https://cavi-ai.xyz/docs/an
|
|
|
18
18
|
|
|
19
19
|
- OpenClaw 2026.7 or newer
|
|
20
20
|
- Node.js 20 or newer
|
|
21
|
-
- Antigravity CLI installed and signed in
|
|
21
|
+
- Antigravity CLI 1.2.1 or newer, installed and signed in
|
|
22
22
|
|
|
23
23
|
Confirm that Antigravity is ready:
|
|
24
24
|
|
|
@@ -37,10 +37,13 @@ Restart the OpenClaw Gateway after installation.
|
|
|
37
37
|
|
|
38
38
|
## Connect
|
|
39
39
|
|
|
40
|
-
In the Control UI, open **Models → Providers
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
default model.
|
|
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. Reconnect installs or refreshes the bundled
|
|
44
|
+
`openclaw-antigravity-tool-free` custom agent, which OpenClaw uses only for its
|
|
45
|
+
connection probe. Normal provider turns keep the configured `agy` mode and tool
|
|
46
|
+
behavior.
|
|
44
47
|
|
|
45
48
|
## Use
|
|
46
49
|
|
|
@@ -119,8 +122,19 @@ openclaw config set plugins.entries.antigravity.config.command /absolute/path/to
|
|
|
119
122
|
```bash
|
|
120
123
|
npm test # plugin behaviour
|
|
121
124
|
npm run docs:test # documentation build/verify/release tooling
|
|
125
|
+
npm run check:host-integration # live agy + isolated installed-host Reconnect check
|
|
122
126
|
```
|
|
123
127
|
|
|
128
|
+
The host-integration check requires a signed-in `agy` session and a compatible
|
|
129
|
+
installed `openclaw` command. It runs `agy models`, starts the working-tree
|
|
130
|
+
plugin in a temporary loopback-only OpenClaw state, activates the projected
|
|
131
|
+
**Reconnect** action, and verifies that credential-only **Connect** is absent.
|
|
132
|
+
The check links the live `agy` session into a temporary home while keeping its
|
|
133
|
+
plugin configuration isolated. The temporary gateway, home, and state are
|
|
134
|
+
removed afterward; the installed gateway, OpenClaw config, credentials,
|
|
135
|
+
default model, and pre-existing `agy` plugins are not changed. Set `AGY_BIN` or
|
|
136
|
+
`OPENCLAW_BIN` to use a non-default executable.
|
|
137
|
+
|
|
124
138
|
Releasing docs is automated: publish a GitHub Release `vX.Y.Z` (matching
|
|
125
139
|
`package.json`) and the `Publish release documentation` workflow builds the
|
|
126
140
|
versioned artifact, attaches it to the release, and dispatches cavi-home to
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: openclaw-antigravity-setup
|
|
3
|
+
description: Tool-free OpenClaw Antigravity connection probe
|
|
4
|
+
mainAgent: true
|
|
5
|
+
excludeDefaultComponents: true
|
|
6
|
+
inheritCustomizations: false
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Connection probe
|
|
10
|
+
|
|
11
|
+
Reply directly to the user's message. You have no tools.
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "antigravity",
|
|
3
3
|
"name": "Antigravity CLI",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
5
5
|
"description": "Runs Google's Antigravity CLI (agy) as a subscription-backed model provider.",
|
|
6
6
|
"enabledByDefault": false,
|
|
7
7
|
"activation": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"provider": "antigravity-cli",
|
|
26
26
|
"method": "cli",
|
|
27
27
|
"choiceId": "antigravity-cli",
|
|
28
|
-
"
|
|
28
|
+
"appGuidedDiscovery": true,
|
|
29
|
+
"appGuidedActionLabel": "Reconnect",
|
|
29
30
|
"choiceLabel": "Antigravity CLI",
|
|
30
31
|
"choiceHint": "Use an existing Antigravity CLI (agy) login on this host. Run `agy models` to confirm.",
|
|
31
32
|
"groupId": "antigravity",
|
|
32
33
|
"groupLabel": "Antigravity",
|
|
33
|
-
"groupHint": "Subscription-backed models via agy"
|
|
34
|
-
"onboardingFeatured": true
|
|
34
|
+
"groupHint": "Subscription-backed models via agy"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
37
|
"uiHints": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavi-ai/antigravity",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"src",
|
|
32
|
+
"agy-plugin",
|
|
33
|
+
"scripts/check-host-integration.mjs",
|
|
32
34
|
"setup-api.js",
|
|
33
35
|
"doctor-contract-api.js",
|
|
34
36
|
"openclaw.plugin.json",
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
69
|
"test": "node --test test/*.test.mjs",
|
|
70
|
+
"check:host-integration": "node scripts/check-host-integration.mjs",
|
|
68
71
|
"docs:test": "node --test scripts/docs/*.test.mjs",
|
|
69
72
|
"docs:build": "node scripts/docs/build.mjs",
|
|
70
73
|
"docs:verify": "node scripts/docs/verify.mjs",
|
|
@@ -0,0 +1,336 @@
|
|
|
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, readdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { createServer } from "node:net";
|
|
7
|
+
import { homedir, 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, params) {
|
|
94
|
+
const args = [
|
|
95
|
+
"gateway",
|
|
96
|
+
"call",
|
|
97
|
+
method,
|
|
98
|
+
"--url",
|
|
99
|
+
gatewayUrl,
|
|
100
|
+
"--token",
|
|
101
|
+
token,
|
|
102
|
+
"--json",
|
|
103
|
+
"--timeout",
|
|
104
|
+
String(timeout),
|
|
105
|
+
];
|
|
106
|
+
if (params !== undefined) {
|
|
107
|
+
args.push("--params", JSON.stringify(params));
|
|
108
|
+
}
|
|
109
|
+
return args;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findReconnectCandidate(detection) {
|
|
113
|
+
const candidate = detection?.candidates?.find(
|
|
114
|
+
(entry) =>
|
|
115
|
+
entry?.kind === `provider-auto:${CHOICE_ID}` &&
|
|
116
|
+
typeof entry?.modelRef === "string" &&
|
|
117
|
+
entry.modelRef.startsWith(`${PROVIDER_ID}/`),
|
|
118
|
+
);
|
|
119
|
+
if (!candidate) {
|
|
120
|
+
fail("guided discovery did not return an Antigravity Reconnect candidate");
|
|
121
|
+
}
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertReconnectActivation(result, candidate) {
|
|
126
|
+
if (result?.ok !== true || result?.modelRef !== candidate.modelRef) {
|
|
127
|
+
fail(`Reconnect activation failed: ${result?.error ?? "unexpected host response"}`);
|
|
128
|
+
}
|
|
129
|
+
return { modelRef: result.modelRef, latencyMs: result.latencyMs };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function createIsolatedAgyHome(tempRoot, sourceHome = homedir()) {
|
|
133
|
+
const sourceGemini = join(sourceHome, ".gemini");
|
|
134
|
+
const isolatedHome = join(tempRoot, "home");
|
|
135
|
+
const isolatedGemini = join(isolatedHome, ".gemini");
|
|
136
|
+
await mkdir(isolatedGemini, { recursive: true });
|
|
137
|
+
for (const entry of await readdir(sourceGemini, { withFileTypes: true })) {
|
|
138
|
+
if (entry.name === "config") continue;
|
|
139
|
+
await symlink(
|
|
140
|
+
join(sourceGemini, entry.name),
|
|
141
|
+
join(isolatedGemini, entry.name),
|
|
142
|
+
entry.isDirectory() ? "dir" : "file",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
await mkdir(join(isolatedGemini, "config"), { recursive: true });
|
|
146
|
+
return isolatedHome;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function reserveLoopbackPort() {
|
|
150
|
+
const server = createServer();
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
server.once("error", reject);
|
|
153
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
154
|
+
});
|
|
155
|
+
const address = server.address();
|
|
156
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
157
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
158
|
+
if (!port) fail("could not reserve a loopback port for the isolated host");
|
|
159
|
+
return port;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function delay(milliseconds) {
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function waitForGateway(openclaw, gatewayUrl, token, env, child, diagnostics) {
|
|
167
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
168
|
+
while (Date.now() < deadline) {
|
|
169
|
+
if (child.exitCode !== null) {
|
|
170
|
+
fail(`the isolated OpenClaw gateway exited early${diagnostics()}`);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
parseJsonOutput(openclaw, buildGatewayCallArgs("health", gatewayUrl, token, 1_000), {
|
|
174
|
+
env,
|
|
175
|
+
timeout: 2_000,
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
} catch {
|
|
179
|
+
await delay(250);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
fail(`the isolated OpenClaw gateway did not become ready within ${STARTUP_TIMEOUT_MS}ms${diagnostics()}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function stopGateway(child) {
|
|
186
|
+
if (child.exitCode !== null) return;
|
|
187
|
+
child.kill("SIGTERM");
|
|
188
|
+
await Promise.race([
|
|
189
|
+
new Promise((resolve) => child.once("exit", resolve)),
|
|
190
|
+
delay(3_000).then(() => child.kill("SIGKILL")),
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function boundedDiagnostics(chunks) {
|
|
195
|
+
const text = chunks.join("").trim();
|
|
196
|
+
return text ? `\nGateway diagnostics:\n${text.slice(-4_000)}` : "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function withTemporaryRoot(run) {
|
|
200
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "antigravity-host-check-"));
|
|
201
|
+
try {
|
|
202
|
+
return await run(tempRoot);
|
|
203
|
+
} finally {
|
|
204
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function runHostIntegrationCheck({
|
|
209
|
+
openclaw = process.env.OPENCLAW_BIN?.trim() || "openclaw",
|
|
210
|
+
agy = process.env.AGY_BIN?.trim() || "agy",
|
|
211
|
+
} = {}) {
|
|
212
|
+
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
213
|
+
const packageJson = JSON.parse(await readFile(join(pluginRoot, "package.json"), "utf8"));
|
|
214
|
+
const hostVersion = parseHostVersion(commandOutput(openclaw, ["--version"]));
|
|
215
|
+
if (compareVersions(hostVersion, MIN_HOST_VERSION) < 0) {
|
|
216
|
+
fail(`OpenClaw ${hostVersion.join(".")} is older than the required ${MIN_HOST_VERSION.join(".")}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const modelIds = assertAgySession(commandOutput(agy, ["models"]));
|
|
220
|
+
return await withTemporaryRoot(async (tempRoot) => {
|
|
221
|
+
const isolatedHome = await createIsolatedAgyHome(tempRoot);
|
|
222
|
+
const stateDir = join(tempRoot, "state");
|
|
223
|
+
const configPath = join(tempRoot, "openclaw.json");
|
|
224
|
+
const port = await reserveLoopbackPort();
|
|
225
|
+
const gatewayUrl = `ws://127.0.0.1:${port}`;
|
|
226
|
+
const gatewayToken = randomBytes(24).toString("hex");
|
|
227
|
+
const env = {
|
|
228
|
+
...process.env,
|
|
229
|
+
HOME: isolatedHome,
|
|
230
|
+
USERPROFILE: isolatedHome,
|
|
231
|
+
OPENCLAW_CONFIG_PATH: configPath,
|
|
232
|
+
OPENCLAW_STATE_DIR: stateDir,
|
|
233
|
+
};
|
|
234
|
+
const gatewayOutput = [];
|
|
235
|
+
let gateway;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await mkdir(stateDir, { recursive: true });
|
|
239
|
+
await writeFile(
|
|
240
|
+
configPath,
|
|
241
|
+
`${JSON.stringify(
|
|
242
|
+
{
|
|
243
|
+
gateway: { mode: "local", bind: "loopback", auth: { mode: "token" } },
|
|
244
|
+
plugins: {
|
|
245
|
+
allow: ["antigravity"],
|
|
246
|
+
load: { paths: [pluginRoot] },
|
|
247
|
+
entries: { antigravity: { enabled: true, config: { command: agy } } },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
null,
|
|
251
|
+
2,
|
|
252
|
+
)}\n`,
|
|
253
|
+
{ mode: 0o600 },
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
gateway = spawn(
|
|
257
|
+
openclaw,
|
|
258
|
+
[
|
|
259
|
+
"gateway",
|
|
260
|
+
"run",
|
|
261
|
+
"--port",
|
|
262
|
+
String(port),
|
|
263
|
+
"--bind",
|
|
264
|
+
"loopback",
|
|
265
|
+
"--auth",
|
|
266
|
+
"token",
|
|
267
|
+
"--token",
|
|
268
|
+
gatewayToken,
|
|
269
|
+
"--allow-unconfigured",
|
|
270
|
+
],
|
|
271
|
+
{ env, stdio: ["ignore", "pipe", "pipe"] },
|
|
272
|
+
);
|
|
273
|
+
gateway.stdout.on("data", (chunk) => gatewayOutput.push(String(chunk)));
|
|
274
|
+
gateway.stderr.on("data", (chunk) => gatewayOutput.push(String(chunk)));
|
|
275
|
+
const diagnostics = () => boundedDiagnostics(gatewayOutput);
|
|
276
|
+
|
|
277
|
+
await waitForGateway(openclaw, gatewayUrl, gatewayToken, env, gateway, diagnostics);
|
|
278
|
+
const status = parseJsonOutput(
|
|
279
|
+
openclaw,
|
|
280
|
+
buildGatewayCallArgs("models.authStatus", gatewayUrl, gatewayToken),
|
|
281
|
+
{ env, timeout: 15_000 },
|
|
282
|
+
);
|
|
283
|
+
const action = assertReconnectProjection(status);
|
|
284
|
+
const detection = parseJsonOutput(
|
|
285
|
+
openclaw,
|
|
286
|
+
buildGatewayCallArgs("openclaw.setup.detect", gatewayUrl, gatewayToken, 30_000),
|
|
287
|
+
{ env, timeout: 35_000 },
|
|
288
|
+
);
|
|
289
|
+
const candidate = findReconnectCandidate(detection);
|
|
290
|
+
const activation = assertReconnectActivation(
|
|
291
|
+
parseJsonOutput(
|
|
292
|
+
openclaw,
|
|
293
|
+
buildGatewayCallArgs(
|
|
294
|
+
"openclaw.setup.activate",
|
|
295
|
+
gatewayUrl,
|
|
296
|
+
gatewayToken,
|
|
297
|
+
120_000,
|
|
298
|
+
{ kind: candidate.kind, modelRef: candidate.modelRef },
|
|
299
|
+
),
|
|
300
|
+
{ env, timeout: 125_000 },
|
|
301
|
+
),
|
|
302
|
+
candidate,
|
|
303
|
+
);
|
|
304
|
+
return {
|
|
305
|
+
pluginVersion: packageJson.version,
|
|
306
|
+
hostVersion: hostVersion.join("."),
|
|
307
|
+
agyModelCount: modelIds.length,
|
|
308
|
+
action,
|
|
309
|
+
activation,
|
|
310
|
+
};
|
|
311
|
+
} finally {
|
|
312
|
+
if (gateway) await stopGateway(gateway);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
318
|
+
if (isMain) {
|
|
319
|
+
runHostIntegrationCheck()
|
|
320
|
+
.then((result) => {
|
|
321
|
+
process.stdout.write(
|
|
322
|
+
[
|
|
323
|
+
`PASS @cavi-ai/antigravity ${result.pluginVersion}`,
|
|
324
|
+
`OpenClaw ${result.hostVersion}`,
|
|
325
|
+
`live agy models: ${result.agyModelCount}`,
|
|
326
|
+
`host action: ${result.action.actionLabel}`,
|
|
327
|
+
`Reconnect activation: ${result.activation.modelRef}`,
|
|
328
|
+
"credential-only Connect: absent",
|
|
329
|
+
].join("\n") + "\n",
|
|
330
|
+
);
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => {
|
|
333
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
334
|
+
process.exitCode = 1;
|
|
335
|
+
});
|
|
336
|
+
}
|
package/src/cli-backend.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { ANTIGRAVITY_MODEL_ALIASES } from "./models.js";
|
|
16
16
|
|
|
17
17
|
export const ANTIGRAVITY_BACKEND_ID = "antigravity-cli";
|
|
18
|
+
export const TOOL_FREE_SETUP_AGENT_ID = "openclaw-antigravity-setup";
|
|
18
19
|
|
|
19
20
|
/** agy print mode cannot prompt a human, so tool calls need a non-review mode. */
|
|
20
21
|
export const DEFAULT_MODE = "accept-edits";
|
|
@@ -44,6 +45,17 @@ export function buildAntigravityCliBackend(options = {}) {
|
|
|
44
45
|
const base = buildBaseArgs(options);
|
|
45
46
|
return {
|
|
46
47
|
id: ANTIGRAVITY_BACKEND_ID,
|
|
48
|
+
runtimeArtifact: {
|
|
49
|
+
kind: "bundled-package-tree",
|
|
50
|
+
packageName: "agy",
|
|
51
|
+
entrypoint: "command",
|
|
52
|
+
nativeExecutableNames: ["agy", "agy.exe"],
|
|
53
|
+
},
|
|
54
|
+
sideQuestionToolMode: "disabled",
|
|
55
|
+
resolveExecutionArgs: ({ baseArgs, executionMode }) =>
|
|
56
|
+
executionMode === "side-question"
|
|
57
|
+
? [...baseArgs, "--agent", TOOL_FREE_SETUP_AGENT_ID]
|
|
58
|
+
: baseArgs,
|
|
47
59
|
// Standalone backend: agy's catalog (Gemini + Claude + GPT-OSS behind one
|
|
48
60
|
// subscription) belongs to no existing provider, so it owns direct
|
|
49
61
|
// `antigravity-cli/<model>` refs rather than aliasing a canonical provider.
|
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.
|
|
10
|
+
version: "0.2.4",
|
|
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,9 +1,11 @@
|
|
|
1
1
|
// Provider registration for Antigravity (`agy`).
|
|
2
2
|
//
|
|
3
|
-
// agy owns the user's Antigravity OAuth session.
|
|
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";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import { promisify } from "node:util";
|
|
8
10
|
import { ANTIGRAVITY_BACKEND_ID } from "./cli-backend.js";
|
|
9
11
|
import {
|
|
@@ -18,6 +20,12 @@ export const ANTIGRAVITY_PROVIDER_ID = ANTIGRAVITY_BACKEND_ID;
|
|
|
18
20
|
const execFileAsync = promisify(execFile);
|
|
19
21
|
const COMMAND_TIMEOUT_MS = 15_000;
|
|
20
22
|
const COMMAND_MAX_BUFFER_BYTES = 1_048_576;
|
|
23
|
+
const MINIMUM_TOOL_FREE_AGY_VERSION = [1, 2, 1];
|
|
24
|
+
const TOOL_FREE_SETUP_PLUGIN_ROOT = join(
|
|
25
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
26
|
+
"..",
|
|
27
|
+
"agy-plugin",
|
|
28
|
+
);
|
|
21
29
|
|
|
22
30
|
const MISSING_AUTH_MESSAGE =
|
|
23
31
|
"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.";
|
|
@@ -42,6 +50,20 @@ function isAbortError(error, signal) {
|
|
|
42
50
|
return signal?.aborted === true || error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
function supportsToolFreeSetupAgent(output) {
|
|
54
|
+
const match = /(\d+)\.(\d+)\.(\d+)/u.exec(String(output ?? ""));
|
|
55
|
+
if (!match) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const version = match.slice(1).map(Number);
|
|
59
|
+
for (let index = 0; index < version.length; index += 1) {
|
|
60
|
+
if (version[index] !== MINIMUM_TOOL_FREE_AGY_VERSION[index]) {
|
|
61
|
+
return version[index] > MINIMUM_TOOL_FREE_AGY_VERSION[index];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
/** Extracts model ids from the human-readable `agy models` table. */
|
|
46
68
|
export function parseAntigravityModelIds(output) {
|
|
47
69
|
const ids = [];
|
|
@@ -77,6 +99,16 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
|
|
|
77
99
|
return parseAntigravityModelIds(output);
|
|
78
100
|
};
|
|
79
101
|
|
|
102
|
+
const installToolFreeSetupAgent = async (context) => {
|
|
103
|
+
throwIfAborted(context.signal);
|
|
104
|
+
const version = await execute(command, ["--version"], context);
|
|
105
|
+
if (!supportsToolFreeSetupAgent(version)) {
|
|
106
|
+
throw new Error("Antigravity CLI 1.2.1 or newer is required for tool-free setup checks.");
|
|
107
|
+
}
|
|
108
|
+
await execute(command, ["plugin", "install", TOOL_FREE_SETUP_PLUGIN_ROOT], context);
|
|
109
|
+
throwIfAborted(context.signal);
|
|
110
|
+
};
|
|
111
|
+
|
|
80
112
|
const detect = async (context) => {
|
|
81
113
|
try {
|
|
82
114
|
const modelId = chooseDetectedModel(await listModels(context));
|
|
@@ -138,9 +170,9 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
|
|
|
138
170
|
detectAvailability: async (context) => {
|
|
139
171
|
try {
|
|
140
172
|
throwIfAborted(context.signal);
|
|
141
|
-
await execute(command, ["--version"], context);
|
|
173
|
+
const version = await execute(command, ["--version"], context);
|
|
142
174
|
throwIfAborted(context.signal);
|
|
143
|
-
return
|
|
175
|
+
return supportsToolFreeSetupAgent(version);
|
|
144
176
|
} catch (error) {
|
|
145
177
|
if (isAbortError(error, context.signal)) {
|
|
146
178
|
throw error;
|
|
@@ -156,6 +188,7 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
|
|
|
156
188
|
}
|
|
157
189
|
const modelId = context.modelRef.slice(prefix.length);
|
|
158
190
|
try {
|
|
191
|
+
await installToolFreeSetupAgent(context);
|
|
159
192
|
const available = await listModels(context);
|
|
160
193
|
return available.includes(modelId)
|
|
161
194
|
? validatedResult(context.modelRef, context.config)
|
|
@@ -169,6 +202,14 @@ export function buildAntigravityProvider(options = {}, dependencies = {}) {
|
|
|
169
202
|
},
|
|
170
203
|
},
|
|
171
204
|
run: async (context) => {
|
|
205
|
+
try {
|
|
206
|
+
await installToolFreeSetupAgent(context);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (isAbortError(error, context.signal)) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
throw new Error(RECONNECT_ERROR_MESSAGE);
|
|
212
|
+
}
|
|
172
213
|
const detected = await detect(context);
|
|
173
214
|
if (!detected) {
|
|
174
215
|
throw new Error(RECONNECT_ERROR_MESSAGE);
|