@minhspark/codex-mcp-bridge 1.23.0 → 1.24.1

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.
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { resolveCodexSenderContext } from "./codex-sender-context.mjs";
8
+ import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspectors } from "./claude-sender-context.mjs";
9
+ import { readClaudeAccountContext } from "./desktop-account-context.mjs";
10
+ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
11
+ import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
12
+ import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
13
+ import { AntigravityCliSession } from "./antigravity-cli.mjs";
14
+ import { exitForVersionRequest } from "./cli-version.mjs";
15
+ import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
16
+ import { requireBridgeAccounts, readBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
17
+
18
+ exitForVersionRequest(import.meta.url);
19
+
20
+ const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
21
+ const rootPolicy = createHardenedRootPolicy();
22
+ const directory = antigravityBridgeDirectory();
23
+ const deliveries = new AntigravityDeliveries(path.join(directory, "cli-deliveries"));
24
+ const sessions = new Map();
25
+ let startingSessions = 0;
26
+ const caller = process.env.ANTIGRAVITY_BRIDGE_CALLER;
27
+ const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() });
28
+ const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
29
+ const schema = { requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(65536) };
30
+
31
+ async function sender(extra) {
32
+ if (caller === "codex") {
33
+ const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
34
+ const accounts = requireBridgeAccounts(readBridgeAccounts());
35
+ const { result } = await senderRelay.requestDesktop("read_thread", { threadId, hostId: "local", turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 1 }, { accountContext: accounts, beforeSend: () => assertAccountIdentity(accounts) });
36
+ assertAccountIdentity(accounts);
37
+ return { thread: result?.thread, latestTurnId: result?.turns?.[0]?.id ?? null };
38
+ } });
39
+ if (context.status !== "verified") throw new Error(context.reason ?? "Unverified Codex sender");
40
+ return { provider: "codex", id: context.threadId, cwd: context.cwd };
41
+ }
42
+ if (caller === "claude") {
43
+ const context = requireClaudeSenderContext(await readClaudeSenderContext({ account: readClaudeAccountContext() }));
44
+ return { provider: "claude", id: context.sessionId, cwd: context.cwd };
45
+ }
46
+ throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
47
+ }
48
+
49
+ function sameCwd(expected, actual) {
50
+ if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
51
+ const left = fs.realpathSync.native(expected);
52
+ const right = fs.realpathSync.native(actual);
53
+ if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
54
+ rootPolicy.assert(left, "Antigravity project");
55
+ return left;
56
+ }
57
+
58
+ function owns(record, identity) {
59
+ if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw new Error("Delivery does not belong to this sending conversation");
60
+ sameCwd(identity.cwd, record.cwd);
61
+ return record;
62
+ }
63
+
64
+ function handoff(prompt, identity) {
65
+ return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
66
+ }
67
+
68
+ const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force. Do not claim support for Antigravity-origin handoffs into Codex or Claude; this server receives from those hosts." });
69
+
70
+ function register(name, description, inputSchema, readOnly, handler) {
71
+ server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
72
+ try { return result(await handler(args, extra)); }
73
+ catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
74
+ });
75
+ }
76
+
77
+ register("antigravity_bridge_status", "Inspect connection capabilities without sending prompts.", {}, true, async () => {
78
+ let desktop;
79
+ try { desktop = await callAntigravityDesktop({ operation: "status" }, { timeoutMs: 5000 }); }
80
+ catch { desktop = { available: false, reason: "Open Antigravity with the installed codex-bridge sidecar enabled" }; }
81
+ return { version, caller: caller ?? "unconfigured", desktop, cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false } };
82
+ });
83
+
84
+ register("list_antigravity_conversations", "List native Desktop conversations and bridge-owned CLI sessions in the sending project.", { surface: z.enum(["desktop", "cli"]) }, true, async ({ surface }, extra) => {
85
+ const identity = await sender(extra);
86
+ const records = surface === "desktop" ? await callAntigravityDesktop({ operation: "list" }) : [...sessions.values()].map((entry) => ({ ...entry.session.read(), sender: entry.sender }));
87
+ return records.filter((record) => {
88
+ try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || record.sender.id === identity.id && record.sender.provider === identity.provider; }
89
+ catch { return false; }
90
+ });
91
+ });
92
+
93
+ register("send_to_antigravity_conversation", "Send one authorized prompt to an exact existing Desktop conversation or a bridge-owned CLI session. Retain requestId and poll the delivery receipt.", { ...schema, surface: z.enum(["desktop", "cli"]), conversationId: z.string().uuid() }, false, async (args, extra) => {
94
+ const identity = await sender(extra);
95
+ const cwd = sameCwd(identity.cwd, args.cwd);
96
+ const prompt = handoff(args.prompt, identity);
97
+ if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw new Error("Desktop handoff must fit within 30000 characters and contain no NUL");
98
+ if (args.surface === "desktop") return callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
99
+ const entry = sessions.get(args.conversationId);
100
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender and bridge process");
101
+ sameCwd(cwd, entry.session.read().cwd);
102
+ return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
103
+ const current = await sender(extra);
104
+ sameCwd(cwd, current.cwd);
105
+ if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
106
+ return entry.session.send(prompt);
107
+ });
108
+ });
109
+
110
+ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity CLI streaming session for explicitly authorized new work; sends the initial prompt and returns a durable delivery receipt.", schema, false, async (args, extra) => {
111
+ const identity = await sender(extra);
112
+ const cwd = sameCwd(identity.cwd, args.cwd);
113
+ if (sessions.size + startingSessions >= 4) throw new Error("At most four CLI sessions may be open in one bridge");
114
+ return deliveries.dispatch({ ...args, cwd, sender: identity, surface: "cli" }, async () => {
115
+ if (sessions.size + startingSessions >= 4) throw Object.assign(new Error("CLI session capacity reached"), { deliveryState: "not_sent" });
116
+ startingSessions++;
117
+ let reserved = true;
118
+ let session;
119
+ try {
120
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_)/i.test(name)));
121
+ session = new AntigravityCliSession({ cwd, env });
122
+ const started = await session.start();
123
+ deliveries.identify(args.requestId, started.conversationId);
124
+ sessions.set(started.conversationId, { session, sender: identity });
125
+ startingSessions--;
126
+ reserved = false;
127
+ const current = await sender(extra);
128
+ sameCwd(cwd, current.cwd);
129
+ if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
130
+ return await session.send(handoff(args.prompt, identity));
131
+ } catch (error) {
132
+ if (error.deliveryState !== "unknown") {
133
+ for (const [id, entry] of sessions) if (entry.session === session) sessions.delete(id);
134
+ await session?.close();
135
+ }
136
+ throw error;
137
+ } finally {
138
+ if (reserved) startingSessions--;
139
+ }
140
+ });
141
+ });
142
+
143
+ register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means the native API accepted it, not that the agent completed a turn.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
144
+ const identity = await sender(extra);
145
+ const record = args.surface === "desktop" ? await callAntigravityDesktop({ operation: "receipt", requestId: args.requestId }) : deliveries.read(args.requestId);
146
+ return owns(record, identity);
147
+ });
148
+
149
+ register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
150
+ const identity = await sender(extra);
151
+ const entry = sessions.get(conversationId);
152
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender");
153
+ sameCwd(identity.cwd, entry.session.read().cwd);
154
+ if (["running", "starting"].includes(entry.session.read().status)) throw new Error("Wait for the active CLI delivery before closing");
155
+ const closed = await entry.session.close();
156
+ if (closed.status !== "closing") sessions.delete(conversationId);
157
+ return { closed: closed.status !== "closing", conversationId, status: closed.status };
158
+ });
159
+
160
+ let stopping = false;
161
+ async function stop() {
162
+ if (stopping) return;
163
+ stopping = true;
164
+ await Promise.allSettled([...sessions.values()].map(({ session }) => session.close()));
165
+ await stopProcessInspectors();
166
+ await server.close();
167
+ }
168
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
169
+ process.stdin.once("end", () => { void stop(); });
170
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,248 @@
1
+ import { spawn as spawnProcess } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { StringDecoder } from "node:string_decoder";
6
+
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+
9
+ function failure(message, code, deliveryState = "not-sent") {
10
+ return Object.assign(new Error(message), { code, deliveryState });
11
+ }
12
+
13
+ function directory(value) {
14
+ if (typeof value !== "string" || !path.isAbsolute(value)) throw new Error("Antigravity CLI cwd must be an absolute directory");
15
+ const resolved = fs.realpathSync.native(value);
16
+ if (!fs.statSync(resolved).isDirectory()) throw new Error("Antigravity CLI cwd must be a directory");
17
+ return resolved;
18
+ }
19
+
20
+ function positive(value, fallback, name) {
21
+ const result = value ?? fallback;
22
+ if (!Number.isSafeInteger(result) || result < 1) throw new Error(`${name} must be a positive integer`);
23
+ return result;
24
+ }
25
+
26
+ export function resolveAntigravityCliBinary({ binary, env = process.env, platform = process.platform } = {}) {
27
+ const selected = binary ?? env.ANTIGRAVITY_CLI_BIN;
28
+ const windows = platform === "win32";
29
+ const name = windows ? "agy.exe" : "agy";
30
+ const home = env.HOME ?? env.USERPROFILE ?? os.homedir();
31
+ const candidates = selected !== undefined ? [selected] : [
32
+ ...(windows ? [path.join(env.LOCALAPPDATA ?? path.join(home, "AppData", "Local"), "agy", "bin", name)] : [path.join(home, ".local", "bin", name)]),
33
+ ...(env.PATH ?? env.Path ?? "").split(path.delimiter).filter((item) => path.isAbsolute(item)).map((item) => path.join(item, name)),
34
+ ];
35
+ if (selected !== undefined && (typeof selected !== "string" || !path.isAbsolute(selected))) throw new Error("Antigravity CLI binary must be an absolute executable path");
36
+ for (const candidate of candidates) {
37
+ try {
38
+ const resolved = fs.realpathSync.native(candidate);
39
+ if (!fs.statSync(resolved).isFile() || windows && !/\.exe$/i.test(resolved)) continue;
40
+ fs.accessSync(resolved, windows ? fs.constants.R_OK : fs.constants.X_OK);
41
+ return resolved;
42
+ } catch {}
43
+ }
44
+ throw new Error("Antigravity CLI executable was not found; set ANTIGRAVITY_CLI_BIN to its absolute path");
45
+ }
46
+
47
+ export class AntigravityCliSession {
48
+ constructor(options = {}) {
49
+ this.cwd = directory(options.cwd);
50
+ this.env = { ...(options.env ?? process.env) };
51
+ this.binary = resolveAntigravityCliBinary({ binary: options.binary, env: this.env });
52
+ this.spawn = options.spawn ?? spawnProcess;
53
+ this.requestedConversationId = options.conversationId ?? null;
54
+ if (this.requestedConversationId !== null && !UUID.test(this.requestedConversationId)) throw new Error("Invalid Antigravity conversation ID");
55
+ this.startupTimeoutMs = positive(options.startupTimeoutMs, 30000, "startupTimeoutMs");
56
+ this.turnTimeoutMs = positive(options.turnTimeoutMs, 600000, "turnTimeoutMs");
57
+ this.closeTimeoutMs = positive(options.closeTimeoutMs, 3000, "closeTimeoutMs");
58
+ this.maxFrameBytes = positive(options.maxFrameBytes, 2 * 1024 * 1024, "maxFrameBytes");
59
+ this.maxTurnBytes = positive(options.maxTurnBytes, 16 * 1024 * 1024, "maxTurnBytes");
60
+ this.status = "new";
61
+ this.conversationId = null;
62
+ this.permissionMode = null;
63
+ this.child = null;
64
+ this.pending = null;
65
+ this.awaitingResult = false;
66
+ this.lastResult = null;
67
+ this.lastError = null;
68
+ this.deliveryState = "not-sent";
69
+ this.turnBytes = 0;
70
+ this.buffer = "";
71
+ this.decoder = new StringDecoder("utf8");
72
+ this.exited = false;
73
+ }
74
+
75
+ read() {
76
+ return {
77
+ conversationId: this.conversationId,
78
+ cwd: this.cwd,
79
+ permissionMode: this.permissionMode,
80
+ pid: this.child?.pid ?? null,
81
+ status: this.status,
82
+ deliveryState: this.deliveryState,
83
+ result: this.lastResult ? structuredClone(this.lastResult) : null,
84
+ error: this.lastError ? { code: this.lastError.code, message: this.lastError.message } : null,
85
+ };
86
+ }
87
+
88
+ start() {
89
+ if (this.startPromise) return this.startPromise;
90
+ if (this.status !== "new") return Promise.reject(failure("Antigravity CLI session cannot be started", "SESSION_CLOSED"));
91
+ this.status = "starting";
92
+ this.startPromise = new Promise((resolve, reject) => {
93
+ this.startResolve = resolve;
94
+ this.startReject = reject;
95
+ });
96
+ this.startTimer = setTimeout(() => this.fail(failure("Antigravity CLI did not emit a valid init event before the deadline", "STARTUP_TIMEOUT")), this.startupTimeoutMs);
97
+ const args = ["--input-format", "stream-json", "--output-format", "stream-json"];
98
+ if (this.requestedConversationId) args.push("--conversation", this.requestedConversationId);
99
+ try {
100
+ this.child = this.spawn(this.binary, args, { cwd: this.cwd, env: this.env, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
101
+ this.child.on("error", () => this.fail(failure("Antigravity CLI process failed", "PROCESS_ERROR", this.pending ? "unknown" : this.deliveryState)));
102
+ this.child.stdin.on("error", () => this.fail(failure("Antigravity CLI input pipe failed", "STDIN_ERROR", this.pending ? "unknown" : this.deliveryState)));
103
+ this.child.stdout.on("error", () => this.fail(failure("Antigravity CLI output pipe failed", "STDOUT_ERROR", this.pending ? "unknown" : this.deliveryState)));
104
+ this.child.stderr.on("error", () => this.fail(failure("Antigravity CLI diagnostic pipe failed", "STDERR_ERROR", this.pending ? "unknown" : this.deliveryState)));
105
+ this.child.stderr.resume();
106
+ this.child.stdout.on("data", (chunk) => this.consume(chunk));
107
+ this.child.on("close", (code, signal) => {
108
+ this.exited = true;
109
+ this.exitCode = code;
110
+ this.exitSignal = signal;
111
+ if (this.status === "starting" || this.pending) this.fail(failure("Antigravity CLI exited before completing the operation", "PROCESS_EXIT", this.pending ? "unknown" : this.deliveryState));
112
+ else if (!["failed", "unknown"].includes(this.status)) this.status = "closed";
113
+ this.closeResolve?.();
114
+ });
115
+ } catch {
116
+ this.fail(failure("Antigravity CLI could not be launched", "SPAWN_FAILED"));
117
+ }
118
+ return this.startPromise;
119
+ }
120
+
121
+ consume(chunk) {
122
+ if (["failed", "closed"].includes(this.status)) return;
123
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
124
+ this.turnBytes += bytes.length;
125
+ if (this.turnBytes > this.maxTurnBytes) return this.fail(failure("Antigravity CLI output exceeds its per-turn limit", "OUTPUT_LIMIT", this.pending ? "unknown" : this.deliveryState));
126
+ this.buffer += this.decoder.write(bytes);
127
+ let newline;
128
+ while ((newline = this.buffer.indexOf("\n")) >= 0) {
129
+ const line = this.buffer.slice(0, newline).replace(/\r$/, "");
130
+ this.buffer = this.buffer.slice(newline + 1);
131
+ if (Buffer.byteLength(line) > this.maxFrameBytes) return this.fail(failure("Antigravity CLI frame exceeds its limit", "FRAME_LIMIT", this.pending ? "unknown" : this.deliveryState));
132
+ if (!line.trim()) continue;
133
+ try { this.event(JSON.parse(line)); }
134
+ catch (error) { return this.fail(failure(error.message === "Invalid Antigravity event" ? error.message : "Antigravity CLI emitted invalid protocol data", "INVALID_PROTOCOL", this.pending ? "unknown" : this.deliveryState)); }
135
+ if (["failed", "closed"].includes(this.status)) return;
136
+ }
137
+ if (Buffer.byteLength(this.buffer) > this.maxFrameBytes) this.fail(failure("Antigravity CLI frame exceeds its limit", "FRAME_LIMIT", this.pending ? "unknown" : this.deliveryState));
138
+ }
139
+
140
+ event(event) {
141
+ if (!event || typeof event !== "object" || Array.isArray(event) || typeof event.event !== "string") throw new Error("Invalid Antigravity event");
142
+ if (event.event === "init") {
143
+ if (this.status !== "starting" || !UUID.test(event.conversation_id ?? "")
144
+ || this.requestedConversationId && event.conversation_id.toLowerCase() !== this.requestedConversationId.toLowerCase()
145
+ || directory(event.init?.cwd) !== this.cwd
146
+ || typeof event.init?.permission_mode !== "string" || !event.init.permission_mode || event.init.permission_mode.length > 128) throw new Error("Invalid Antigravity event");
147
+ this.conversationId = event.conversation_id;
148
+ this.permissionMode = event.init.permission_mode;
149
+ this.status = "idle";
150
+ clearTimeout(this.startTimer);
151
+ this.startResolve(this.read());
152
+ this.startResolve = null;
153
+ this.startReject = null;
154
+ return;
155
+ }
156
+ if (!this.conversationId) throw new Error("Invalid Antigravity event");
157
+ if (event.event !== "result") return;
158
+ const result = event.result;
159
+ if (!this.awaitingResult || !result || result.conversation_id !== this.conversationId
160
+ || typeof result.status !== "string" || !/^[A-Z][A-Z_]{0,127}$/.test(result.status)
161
+ || typeof result.response !== "string" || result.error !== undefined && typeof result.error !== "string") throw new Error("Invalid Antigravity event");
162
+ this.lastResult = { conversation_id: result.conversation_id, status: result.status, response: result.response, ...(result.error === undefined ? {} : { error: result.error }) };
163
+ this.awaitingResult = false;
164
+ this.deliveryState = "completed";
165
+ if (this.pending) {
166
+ const pending = this.pending;
167
+ this.pending = null;
168
+ clearTimeout(pending.timer);
169
+ this.status = "idle";
170
+ pending.resolve(structuredClone(this.lastResult));
171
+ }
172
+ }
173
+
174
+ async send(prompt) {
175
+ if (typeof prompt !== "string" || !prompt.trim()) throw failure("Antigravity prompt must be non-empty text", "INVALID_PROMPT");
176
+ const frame = `${JSON.stringify({ event: "user", message: { content: prompt } })}\n`;
177
+ if (Buffer.byteLength(frame) > this.maxFrameBytes) throw failure("Antigravity prompt exceeds its frame limit", "PROMPT_LIMIT");
178
+ await this.start();
179
+ if (this.status !== "idle" || this.pending) throw failure("Antigravity CLI session is busy, closed, or has an uncertain delivery; inspect its receipt instead of resending", "SESSION_UNAVAILABLE", this.deliveryState);
180
+ if (directory(this.cwd) !== this.cwd) throw failure("Antigravity CLI working directory changed", "CWD_CHANGED");
181
+ this.status = "running";
182
+ this.awaitingResult = true;
183
+ this.deliveryState = "unknown";
184
+ this.turnBytes = 0;
185
+ this.lastResult = null;
186
+ return new Promise((resolve, reject) => {
187
+ const timer = setTimeout(() => {
188
+ const pending = this.pending;
189
+ if (!pending) return;
190
+ this.pending = null;
191
+ this.status = "unknown";
192
+ this.lastError = failure("Antigravity CLI turn exceeded the observation deadline; it may still be running. Do not resend.", "TURN_TIMEOUT", "unknown");
193
+ pending.reject(this.lastError);
194
+ }, this.turnTimeoutMs);
195
+ this.pending = { resolve, reject, timer };
196
+ try {
197
+ this.child.stdin.write(frame, (error) => {
198
+ if (error) this.fail(failure("Antigravity CLI input write failed; delivery may have started", "STDIN_ERROR", "unknown"));
199
+ });
200
+ } catch {
201
+ this.fail(failure("Antigravity CLI input write failed; delivery may have started", "STDIN_ERROR", "unknown"));
202
+ }
203
+ });
204
+ }
205
+
206
+ fail(error) {
207
+ this.lastError = error;
208
+ this.deliveryState = error.deliveryState;
209
+ this.status = error.deliveryState === "unknown" ? "unknown" : "failed";
210
+ clearTimeout(this.startTimer);
211
+ this.startReject?.(error);
212
+ this.startResolve = null;
213
+ this.startReject = null;
214
+ if (this.pending) {
215
+ const pending = this.pending;
216
+ this.pending = null;
217
+ clearTimeout(pending.timer);
218
+ pending.reject(error);
219
+ }
220
+ try { this.child?.kill(); } catch {}
221
+ }
222
+
223
+ close() {
224
+ if (this.closePromise) return this.closePromise;
225
+ this.closePromise = this.finishClose();
226
+ return this.closePromise;
227
+ }
228
+
229
+ async finishClose() {
230
+ if (!this.child || this.exited) {
231
+ this.status = "closed";
232
+ return this.read();
233
+ }
234
+ const closed = new Promise((resolve) => { this.closeResolve = resolve; });
235
+ try { this.child.stdin.end(); } catch {}
236
+ let timer;
237
+ await Promise.race([closed, new Promise((resolve) => { timer = setTimeout(resolve, this.closeTimeoutMs); })]);
238
+ clearTimeout(timer);
239
+ if (!this.exited) {
240
+ if (this.pending || this.status === "starting") this.fail(failure("Antigravity CLI session was closed before its operation completed", "SESSION_CLOSED", this.pending ? "unknown" : this.deliveryState));
241
+ else try { this.child.kill(); } catch {}
242
+ await Promise.race([closed, new Promise((resolve) => { timer = setTimeout(resolve, this.closeTimeoutMs); })]);
243
+ clearTimeout(timer);
244
+ }
245
+ this.status = this.exited ? "closed" : "closing";
246
+ return this.read();
247
+ }
248
+ }
@@ -0,0 +1,93 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { ensureBridgeDirectory } from "./antigravity-ipc.mjs";
5
+
6
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
+
8
+ export class AntigravityDeliveries {
9
+ constructor(directory) {
10
+ this.directory = ensureBridgeDirectory(directory);
11
+ this.active = new Set();
12
+ }
13
+
14
+ file(requestId) {
15
+ if (!UUID.test(requestId)) throw new Error("requestId must be a UUID");
16
+ return path.join(this.directory, `${requestId}.json`);
17
+ }
18
+
19
+ read(requestId) {
20
+ const file = this.file(requestId);
21
+ let descriptor;
22
+ try { descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)); }
23
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
24
+ let record;
25
+ try {
26
+ const stat = fs.lstatSync(file, { bigint: true });
27
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n || stat.size > 4n * 1024n * 1024n) throw new Error("Invalid delivery record");
28
+ const opened = fs.fstatSync(descriptor, { bigint: true });
29
+ if (!opened.isFile() || opened.nlink !== 1n || opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size !== stat.size || opened.mtimeNs !== stat.mtimeNs) throw new Error("Delivery record changed before reading");
30
+ const buffer = Buffer.alloc(Number(opened.size) + 1);
31
+ let bytes = 0;
32
+ while (bytes < buffer.length) {
33
+ const count = fs.readSync(descriptor, buffer, bytes, buffer.length - bytes, bytes);
34
+ if (!count) break;
35
+ bytes += count;
36
+ }
37
+ const after = fs.fstatSync(descriptor, { bigint: true });
38
+ if (bytes !== Number(opened.size) || after.size !== opened.size || after.mtimeNs !== opened.mtimeNs || after.ctimeNs !== opened.ctimeNs) throw new Error("Delivery record changed while reading");
39
+ record = JSON.parse(buffer.subarray(0, bytes).toString("utf8"));
40
+ } finally { fs.closeSync(descriptor); }
41
+ if (record.requestId !== requestId) throw new Error("Delivery identity mismatch");
42
+ if (record.status === "pending" && !this.active.has(requestId)) return { ...record, status: "unknown", reason: "This process cannot observe the original worker. Inspect the exact destination; do not resend." };
43
+ return record;
44
+ }
45
+
46
+ identify(requestId, conversationId) {
47
+ if (!UUID.test(conversationId) || !this.active.has(requestId)) throw new Error("Cannot bind inactive delivery");
48
+ const record = this.read(requestId);
49
+ record.conversationId = conversationId;
50
+ this.write(record);
51
+ }
52
+
53
+ write(record) {
54
+ const temporary = `${this.file(record.requestId)}.${process.pid}.tmp`;
55
+ fs.writeFileSync(temporary, JSON.stringify(record), { flag: "wx", mode: 0o600 });
56
+ fs.renameSync(temporary, this.file(record.requestId));
57
+ }
58
+
59
+ async dispatch(request, operation) {
60
+ const { requestId, ...identity } = request;
61
+ const digest = crypto.createHash("sha256").update(JSON.stringify(identity)).digest("hex");
62
+ const existing = this.read(requestId);
63
+ if (existing) {
64
+ if (existing.digest !== digest) throw new Error("requestId was already used for a different delivery");
65
+ return existing;
66
+ }
67
+ const record = { requestId, digest, status: "pending", cwd: request.cwd, sender: request.sender, conversationId: request.conversationId ?? null, surface: request.surface, createdAt: new Date().toISOString() };
68
+ try { fs.writeFileSync(this.file(requestId), JSON.stringify(record), { flag: "wx", mode: 0o600 }); }
69
+ catch (error) {
70
+ if (error.code !== "EEXIST") throw error;
71
+ throw new Error("Delivery is owned by another process. Inspect requestId; do not resend.");
72
+ }
73
+ this.active.add(requestId);
74
+ void Promise.resolve().then(operation).then((result) => {
75
+ record.status = ["accepted", "sent_unconfirmed", "held", "refused"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
76
+ record.result = result;
77
+ record.conversationId = result?.conversationId ?? result?.conversation_id ?? record.conversationId;
78
+ }, (error) => {
79
+ record.status = ["not_sent", "not-sent"].includes(error.deliveryState) ? "failed" : "unknown";
80
+ record.reason = error.message;
81
+ if (typeof error.msgId === "string" && UUID.test(error.msgId)) record.result = { msgId: error.msgId, completed: false };
82
+ if (error.accountContext) record.result = { ...record.result, accountContext: error.accountContext };
83
+ }).finally(() => {
84
+ record.finishedAt = new Date().toISOString();
85
+ try {
86
+ record.conversationId ??= this.read(requestId)?.conversationId;
87
+ this.write(record);
88
+ } catch (error) { process.stderr.write(`Could not persist Antigravity delivery outcome: ${error.code ?? "WRITE_FAILED"}\n`); }
89
+ this.active.delete(requestId);
90
+ });
91
+ return record;
92
+ }
93
+ }