@anhlp/alp-runtime 0.0.0-beta.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.
Files changed (94) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +7 -0
  3. package/README.md +7 -0
  4. package/dist/acp-transport.d.ts +114 -0
  5. package/dist/acp-transport.js +478 -0
  6. package/dist/antigravity-transport.d.ts +80 -0
  7. package/dist/antigravity-transport.js +609 -0
  8. package/dist/board-domain.d.ts +56 -0
  9. package/dist/board-domain.js +157 -0
  10. package/dist/board.d.ts +21 -0
  11. package/dist/board.js +69 -0
  12. package/dist/catalog.d.ts +31 -0
  13. package/dist/catalog.js +38 -0
  14. package/dist/claude-transport.d.ts +157 -0
  15. package/dist/claude-transport.js +671 -0
  16. package/dist/contracts.d.ts +163 -0
  17. package/dist/contracts.js +5 -0
  18. package/dist/events.d.ts +1 -0
  19. package/dist/events.js +0 -0
  20. package/dist/github.d.ts +10 -0
  21. package/dist/github.js +39 -0
  22. package/dist/handoff.d.ts +36 -0
  23. package/dist/handoff.js +73 -0
  24. package/dist/index.d.ts +24 -0
  25. package/dist/index.js +64 -0
  26. package/dist/language.d.ts +41 -0
  27. package/dist/language.js +68 -0
  28. package/dist/live.d.ts +48 -0
  29. package/dist/live.js +45 -0
  30. package/dist/mailbox.d.ts +24 -0
  31. package/dist/mailbox.js +36 -0
  32. package/dist/mcp-tools.d.ts +115 -0
  33. package/dist/mcp-tools.js +71 -0
  34. package/dist/permission-path.d.ts +2 -0
  35. package/dist/permission-path.js +44 -0
  36. package/dist/process-info.d.ts +14 -0
  37. package/dist/process-info.js +32 -0
  38. package/dist/providers.d.ts +28 -0
  39. package/dist/providers.js +71 -0
  40. package/dist/recall.d.ts +42 -0
  41. package/dist/recall.js +60 -0
  42. package/dist/resolve.d.ts +121 -0
  43. package/dist/resolve.js +238 -0
  44. package/dist/runtime-context.d.ts +43 -0
  45. package/dist/runtime-context.js +61 -0
  46. package/dist/runtime.d.ts +4 -0
  47. package/dist/runtime.js +2979 -0
  48. package/dist/session-config.d.ts +1973 -0
  49. package/dist/session-config.js +205 -0
  50. package/dist/session-state.d.ts +140 -0
  51. package/dist/session-state.js +0 -0
  52. package/dist/tool-results.d.ts +12 -0
  53. package/dist/tool-results.js +25 -0
  54. package/dist/tools.d.ts +674 -0
  55. package/dist/tools.js +375 -0
  56. package/dist/transport.d.ts +47 -0
  57. package/dist/transport.js +169 -0
  58. package/dist/usage.d.ts +15 -0
  59. package/dist/usage.js +35 -0
  60. package/dist/workspace.d.ts +101 -0
  61. package/dist/workspace.js +259 -0
  62. package/dist/worktree-domain.d.ts +88 -0
  63. package/dist/worktree-domain.js +196 -0
  64. package/package.json +256 -0
  65. package/src/acp-transport.ts +473 -0
  66. package/src/antigravity-transport.ts +658 -0
  67. package/src/board-domain.ts +185 -0
  68. package/src/board.ts +80 -0
  69. package/src/catalog.ts +32 -0
  70. package/src/claude-transport.ts +771 -0
  71. package/src/contracts.ts +166 -0
  72. package/src/events.ts +1 -0
  73. package/src/github.ts +37 -0
  74. package/src/handoff.ts +86 -0
  75. package/src/index.ts +26 -0
  76. package/src/language.ts +102 -0
  77. package/src/live.ts +86 -0
  78. package/src/mailbox.ts +46 -0
  79. package/src/mcp-tools.ts +73 -0
  80. package/src/permission-path.ts +37 -0
  81. package/src/process-info.ts +40 -0
  82. package/src/providers.ts +84 -0
  83. package/src/recall.ts +85 -0
  84. package/src/resolve.ts +230 -0
  85. package/src/runtime-context.ts +49 -0
  86. package/src/runtime.ts +3579 -0
  87. package/src/session-config.ts +301 -0
  88. package/src/session-state.ts +141 -0
  89. package/src/tool-results.ts +26 -0
  90. package/src/tools.ts +379 -0
  91. package/src/transport.ts +116 -0
  92. package/src/usage.ts +42 -0
  93. package/src/workspace.ts +318 -0
  94. package/src/worktree-domain.ts +216 -0
@@ -0,0 +1,658 @@
1
+ import { spawn, execFile, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { rmSync } from 'node:fs';
4
+ import { mkdir, open, readFile, rename, writeFile, unlink } from 'node:fs/promises';
5
+ import net from 'node:net';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { createInterface, type Interface } from 'node:readline';
9
+ import { openMcpTools } from './mcp-tools.js';
10
+
11
+ /**
12
+ * Antigravity provider definition for ALP.
13
+ */
14
+ export type AntigravityProvider = {
15
+ id: string;
16
+ label: string;
17
+ command: string;
18
+ args?: string[];
19
+ env?: Record<string, string>;
20
+ models?: Array<{ id: string; label?: string; description?: string }>;
21
+ additionalModels?: Array<{ id: string; label?: string; description?: string }>;
22
+ enabled?: boolean;
23
+ /** Explicit opt-in: read-only is a prompt constraint, never a native sandbox. */
24
+ readOnlyMode?: 'instructions' | 'reject';
25
+ };
26
+
27
+ type Listener = (method: string, params: any) => void;
28
+ type RequestHandler = (method: string, params: any) => Promise<unknown>;
29
+
30
+ /** The MCP server ALP's tools reach an Antigravity agent through via local socket. */
31
+ const BRIDGE = `// ALP Antigravity bridge v1
32
+
33
+ const net = require('node:net');
34
+ const readline = require('node:readline');
35
+ const socket = process.env.ALP_BRIDGE_SOCKET ? net.connect(process.env.ALP_BRIDGE_SOCKET) : null;
36
+ const waiting = new Map();
37
+ let sequence = 0;
38
+ let buffer = '';
39
+ socket?.setEncoding('utf8');
40
+ socket?.write(JSON.stringify({ token: process.env.ALP_BRIDGE_TOKEN }) + '\\n');
41
+ socket?.on('data', data => {
42
+ buffer += data;
43
+ for (let index; (index = buffer.indexOf('\\n')) >= 0;) {
44
+ const line = buffer.slice(0, index); buffer = buffer.slice(index + 1);
45
+ try { const message = JSON.parse(line); const waiter = waiting.get(message.id); if (waiter) { waiting.delete(message.id); waiter(message); } } catch {}
46
+ }
47
+ });
48
+ socket?.on('close', () => process.exit(0));
49
+ socket?.on('error', () => process.exit(1));
50
+ const ask = message => !socket ? Promise.resolve({ tools: [], result: { content: [{ type: 'text', text: 'No ALP session' }], isError: true } }) : new Promise(resolve => { const id = ++sequence; waiting.set(id, resolve); socket.write(JSON.stringify({ ...message, id }) + '\\n'); });
51
+ const send = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\\n');
52
+ readline.createInterface({ input: process.stdin }).on('line', async line => {
53
+ let message; try { message = JSON.parse(line); } catch { return; }
54
+ if (message.id === undefined || message.id === null) return;
55
+ try {
56
+ if (message.method === 'initialize') return send({ id: message.id, result: { protocolVersion: (message.params && message.params.protocolVersion) || '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: 'alp', version: '1' } } });
57
+ if (message.method === 'ping') return send({ id: message.id, result: {} });
58
+ if (message.method === 'tools/list') { const reply = await ask({ op: 'list' }); return send({ id: message.id, result: { tools: reply.tools } }); }
59
+ if (message.method === 'tools/call') { const reply = await ask({ op: 'call', name: message.params.name, arguments: message.params.arguments || {} }); return send({ id: message.id, result: reply.result }); }
60
+ send({ id: message.id, error: { code: -32601, message: 'Method not found' } });
61
+ } catch (error) { send({ id: message.id, error: { code: -32603, message: String((error && error.message) || error) } }); }
62
+ });
63
+ process.stdin.on('end', () => process.exit(0));
64
+ `;
65
+
66
+ export function encodePrompt(text: string): string {
67
+ return `${JSON.stringify({ event: 'user', message: { content: text } })}\n`;
68
+ }
69
+
70
+ /**
71
+ * AntigravityTransport runs the Antigravity CLI (agy) over its stream-json protocol.
72
+ * Emits line-delimited JSON events and parses stdout line by line.
73
+ */
74
+ export class AntigravityTransport {
75
+ private child?: ChildProcessWithoutNullStreams;
76
+ private lines?: Interface;
77
+ private exit: Promise<void> = Promise.resolve();
78
+ private listeners = new Set<Listener>();
79
+ private failures = new Set<(error: Error) => void>();
80
+ private requestHandler?: RequestHandler;
81
+ private closed = false;
82
+ private conversationId = '';
83
+ private config: any;
84
+ private activeTurn?: string;
85
+ private messageText = new Map<string, string>();
86
+ private messages = 0;
87
+ private instructionsSent = false;
88
+ private tools: Array<{ name: string; description: string; inputSchema?: unknown }> = [];
89
+ private bridge?: { server: net.Server; socket: string; token: string; connections: Set<net.Socket> };
90
+ private readyPromise?: Promise<void>;
91
+ private resolveReady?: () => void;
92
+ private rejectReady?: (error: Error) => void;
93
+ private external?: Awaited<ReturnType<typeof openMcpTools>>;
94
+ private externalStarting?: ReturnType<typeof openMcpTools>;
95
+ private readonly startupAbort = new AbortController();
96
+
97
+ constructor(
98
+ private readonly command: string = 'agy',
99
+ private readonly cwd: string = process.cwd(),
100
+ private readonly env: NodeJS.ProcessEnv = process.env,
101
+ private readonly provider?: AntigravityProvider,
102
+ ) {}
103
+
104
+ async initialize(): Promise<void> {
105
+ if (/\.(cmd|bat|ps1)$/i.test(this.command)) {
106
+ throw new Error('Antigravity command must be a native executable, not a shell launcher');
107
+ }
108
+ }
109
+
110
+ onNotification(listener: Listener): () => void {
111
+ this.listeners.add(listener);
112
+ return () => this.listeners.delete(listener);
113
+ }
114
+
115
+ onFailure(listener: (error: Error) => void): () => void {
116
+ this.failures.add(listener);
117
+ return () => this.failures.delete(listener);
118
+ }
119
+
120
+ onRequest(handler: RequestHandler): void {
121
+ this.requestHandler = handler;
122
+ }
123
+
124
+ async request(method: string, params: any): Promise<any> {
125
+ if (this.closed) throw new Error('Runtime is closed');
126
+
127
+ if (method === 'thread/start' || method === 'thread/resume') {
128
+ if (params.sandbox !== 'full-access' && params.sandbox !== 'danger-full-access') throw new Error('Antigravity currently supports only full-access');
129
+ this.config = params;
130
+ this.tools = params.dynamicTools ?? [];
131
+
132
+ try {
133
+ if (Object.keys(params.mcpServers ?? {}).length) {
134
+ this.externalStarting = openMcpTools(params.mcpServers, params.cwd || this.cwd, { ...this.env, ...this.provider?.env, ...params.env }, this.startupAbort.signal);
135
+ const external = await this.externalStarting;
136
+ if (this.closed) { await external.close(); throw new Error('Runtime is closed'); }
137
+ this.external = external;
138
+ }
139
+ if (this.tools.length > 0 || this.external?.tools.length) await this.openBridge();
140
+ if (this.closed) throw new Error('Runtime is closed');
141
+ await this.startProcess(params);
142
+ } catch (error) { await this.close(); throw error; }
143
+
144
+ return {
145
+ thread: { id: this.conversationId, turns: [] },
146
+ cwd: params.cwd,
147
+ model: params.model,
148
+ reasoningEffort: params.thinking,
149
+ };
150
+ }
151
+
152
+ if (method === 'turn/start') {
153
+ if (!this.conversationId) throw new Error('Antigravity session is not open');
154
+ if (!this.child) await this.startProcess({ ...this.config, threadId: this.conversationId });
155
+ if (this.activeTurn) throw new Error('Antigravity already has an active turn');
156
+
157
+ const turnId = randomUUID();
158
+ this.activeTurn = turnId;
159
+ this.messageText.clear();
160
+
161
+ const parts = (params.input ?? [])
162
+ .filter((part: any) => typeof part?.text === 'string')
163
+ .map((part: any) => part.text);
164
+ let promptText = parts.join('\n\n');
165
+
166
+ if (!this.instructionsSent && this.config?.developerInstructions) {
167
+ promptText = `Instructions from ALP, which runs you. Follow them for this whole session.\n\n${this.config.developerInstructions}\n\n${promptText}`;
168
+ this.instructionsSent = true;
169
+ }
170
+
171
+ setImmediate(() => {
172
+ try {
173
+ this.child?.stdin.write(encodePrompt(promptText));
174
+ } catch (error) {
175
+ this.endTurn(turnId, 'failed', error instanceof Error ? error.message : String(error));
176
+ }
177
+ });
178
+
179
+ return { turn: { id: turnId } };
180
+ }
181
+
182
+ if (method === 'turn/steer') {
183
+ throw new Error('Antigravity takes no messages during a turn; ALP delivers them when it ends');
184
+ }
185
+
186
+ if (method === 'turn/interrupt') {
187
+ if (this.child && this.activeTurn) {
188
+ const turnId = this.activeTurn;
189
+ const child = this.child;
190
+ // agy exits on SIGINT. Ignore that expected exit, then resume its
191
+ // conversation in a fresh process when the next prompt arrives.
192
+ this.child = undefined;
193
+ this.lines?.close();
194
+ const kill = (signal: NodeJS.Signals) => {
195
+ if (process.platform !== 'win32' && child.pid) {
196
+ try { process.kill(-child.pid, signal); } catch { child.kill(signal); }
197
+ } else child.kill(signal);
198
+ };
199
+ kill('SIGINT');
200
+ const timer = setTimeout(() => kill('SIGKILL'), 1500);
201
+ try { await this.exit; } finally { clearTimeout(timer); }
202
+ this.endTurn(turnId, 'interrupted');
203
+ }
204
+ return {};
205
+ }
206
+
207
+ if (method === 'session/configure' || method === 'thread/delete') {
208
+ if (method === 'session/configure' && params.sandbox && params.sandbox !== 'full-access') throw new Error('Antigravity currently supports only full-access');
209
+ return {};
210
+ }
211
+
212
+ throw new Error(`Antigravity does not support '${method}'`);
213
+ }
214
+
215
+ private startProcess(params: any): Promise<void> {
216
+ if (this.closed) return Promise.reject(new Error('Runtime is closed'));
217
+ this.readyPromise = new Promise((resolve, reject) => {
218
+ this.resolveReady = resolve;
219
+ this.rejectReady = reject;
220
+ });
221
+
222
+ const args = [
223
+ ...(this.provider?.args ?? []),
224
+ '--input-format',
225
+ 'stream-json',
226
+ '--output-format',
227
+ 'stream-json',
228
+ '--add-dir',
229
+ params.cwd || this.cwd,
230
+ '--disable-slash-commands',
231
+ '--dangerously-skip-permissions',
232
+ ];
233
+
234
+ if (params.model) {
235
+ args.push('--model', params.model);
236
+ }
237
+
238
+ if (params.threadId) {
239
+ args.push('--conversation', params.threadId);
240
+ }
241
+
242
+ const env: NodeJS.ProcessEnv = {
243
+ ...this.env,
244
+ ...(this.provider?.env ?? {}),
245
+ ...(params.env ?? {}),
246
+ // Session credentials are inherited by this agy's MCP child only, never persisted.
247
+ ALP_BRIDGE_SOCKET: this.bridge?.socket ?? '',
248
+ ALP_BRIDGE_TOKEN: this.bridge?.token ?? '',
249
+ };
250
+
251
+ const child = spawn(this.command, args, {
252
+ cwd: params.cwd || this.cwd,
253
+ env,
254
+ windowsHide: true,
255
+ detached: process.platform !== 'win32',
256
+ stdio: ['pipe', 'pipe', 'pipe'],
257
+ });
258
+
259
+ this.child = child;
260
+ this.exit = new Promise(resolve => {
261
+ child.once('close', () => resolve());
262
+ child.once('error', () => resolve());
263
+ });
264
+
265
+ let stderr = '';
266
+ child.stderr.on('data', chunk => {
267
+ stderr = (stderr + chunk.toString()).slice(-8192);
268
+ });
269
+
270
+ child.stdin.on('error', error => this.fail(error));
271
+ child.on('error', error => {
272
+ const err = new Error(`Antigravity did not start: ${error.message}`);
273
+ if (this.rejectReady) this.rejectReady(err);
274
+ this.fail(err);
275
+ });
276
+
277
+ child.on('close', code => {
278
+ const msg = stderr.trim() || `Antigravity exited (${code})`;
279
+ if (!this.closed && this.child === child) {
280
+ const error = new Error(msg);
281
+ if (this.rejectReady) this.rejectReady(error);
282
+ if (this.activeTurn) {
283
+ this.endTurn(this.activeTurn, 'failed', msg);
284
+ }
285
+ this.fail(error);
286
+ }
287
+ });
288
+
289
+ const lines = createInterface({ input: child.stdout, terminal: false });
290
+ this.lines = lines;
291
+ lines.on('line', line => this.receiveLine(line));
292
+
293
+ const startupTimeout = setTimeout(() => {
294
+ if (this.rejectReady) {
295
+ const error = new Error('Antigravity did not initialize within 30 seconds');
296
+ this.rejectReady(error);
297
+ this.fail(error);
298
+ }
299
+ }, 30_000);
300
+
301
+ return this.readyPromise.finally(() => clearTimeout(startupTimeout));
302
+ }
303
+
304
+ private receiveLine(line: string): void {
305
+ if (!line.trim() || this.closed) return;
306
+ if (line.length > 10 * 1024 * 1024) {
307
+ this.fail(new Error('Antigravity frame exceeds 10MB limit'));
308
+ return;
309
+ }
310
+
311
+ let frame: any;
312
+ try {
313
+ frame = JSON.parse(line);
314
+ } catch {
315
+ return;
316
+ }
317
+
318
+ if (frame.event === 'init') {
319
+ this.conversationId = frame.conversation_id || frame.init?.conversation_id || randomUUID();
320
+ if (this.resolveReady) {
321
+ this.resolveReady();
322
+ this.resolveReady = undefined;
323
+ this.rejectReady = undefined;
324
+ }
325
+ return;
326
+ }
327
+
328
+ if (frame.event === 'step_update') {
329
+ this.handleStepUpdate(frame.step_update);
330
+ return;
331
+ }
332
+
333
+ if (frame.event === 'result') {
334
+ this.handleResult(frame.result);
335
+ return;
336
+ }
337
+ }
338
+
339
+ private handleStepUpdate(step: any): void {
340
+ if (!step || !this.activeTurn) return;
341
+
342
+ if (step.step_type === 'agent_response') {
343
+ const itemId = `agy-msg-${this.activeTurn}-${step.step_index ?? this.messages}`;
344
+ const delta = typeof step.text_delta === 'string' ? step.text_delta : '';
345
+ if (delta) {
346
+ this.messageText.set(itemId, (this.messageText.get(itemId) ?? '') + delta);
347
+ this.emit('item/agentMessage/delta', {
348
+ threadId: this.conversationId,
349
+ itemId,
350
+ delta,
351
+ });
352
+ }
353
+
354
+ if (step.state === 'DONE' && this.messageText.has(itemId)) {
355
+ this.emit('item/completed', {
356
+ threadId: this.conversationId,
357
+ item: { type: 'agentMessage', id: itemId, text: this.messageText.get(itemId) },
358
+ });
359
+ }
360
+
361
+ if (step.state === 'DONE') { this.messageText.delete(itemId); this.messages++; }
362
+
363
+ if (step.usage && typeof step.usage === 'object') {
364
+ const inTokens = step.usage.input_tokens || 0;
365
+ const outTokens = step.usage.output_tokens || 0;
366
+ const cachedTokens = step.usage.cache_read_tokens || 0;
367
+ const total = inTokens + outTokens;
368
+ const last = { totalTokens: total, inputTokens: inTokens, outputTokens: outTokens };
369
+ this.emit('thread/tokenUsage/updated', {
370
+ threadId: this.conversationId,
371
+ turnId: this.activeTurn,
372
+ tokenUsage: {
373
+ last,
374
+ total: last,
375
+ ...(cachedTokens ? { cachedInputTokens: cachedTokens } : {}),
376
+ },
377
+ });
378
+ }
379
+ return;
380
+ }
381
+
382
+ if (step.step_type === 'tool') {
383
+ const toolName = step.tool_name || 'tool';
384
+ const callId = `agy-tool-${this.activeTurn}-${step.step_index ?? ++this.messages}`;
385
+
386
+ if (step.state === 'ACTIVE') {
387
+ this.emit('item/started', {
388
+ threadId: this.conversationId,
389
+ item: {
390
+ type: 'commandExecution',
391
+ id: callId,
392
+ command: toolName,
393
+ cwd: this.cwd,
394
+ status: 'inProgress',
395
+ aggregatedOutput: '',
396
+ },
397
+ });
398
+ } else if (step.state === 'DONE' || step.state === 'ERROR') {
399
+ const output = typeof step.tool_info?.output === 'string'
400
+ ? step.tool_info.output
401
+ : step.tool_info?.error?.message || '';
402
+ this.emit('item/completed', {
403
+ threadId: this.conversationId,
404
+ item: {
405
+ type: 'commandExecution',
406
+ id: callId,
407
+ command: toolName,
408
+ cwd: this.cwd,
409
+ status: step.state === 'DONE' ? 'completed' : 'failed',
410
+ exitCode: step.state === 'DONE' ? 0 : 1,
411
+ aggregatedOutput: output,
412
+ },
413
+ });
414
+ }
415
+ }
416
+ }
417
+
418
+ private handleResult(result: any): void {
419
+ if (!this.activeTurn) return;
420
+ const turnId = this.activeTurn;
421
+
422
+ if (!result || typeof result !== 'object') { this.endTurn(turnId, 'failed', 'Invalid Antigravity result'); return; }
423
+ if (result.status === 'ERROR') {
424
+ const isInterrupted = result.error === 'interrupted';
425
+ this.endTurn(turnId, isInterrupted ? 'interrupted' : 'failed', result.error);
426
+ } else {
427
+ this.endTurn(turnId, 'completed');
428
+ }
429
+ }
430
+
431
+ private endTurn(turnId: string, status: 'completed' | 'interrupted' | 'failed', error?: string): void {
432
+ if (this.activeTurn !== turnId) return;
433
+ this.activeTurn = undefined;
434
+
435
+ setImmediate(() => {
436
+ this.emit('turn/completed', {
437
+ threadId: this.conversationId,
438
+ turn: {
439
+ id: turnId,
440
+ status,
441
+ ...(status === 'failed' ? { error: { message: error || 'Antigravity turn failed' } } : {}),
442
+ },
443
+ });
444
+
445
+ });
446
+ }
447
+
448
+ private async openBridge(): Promise<void> {
449
+ if (this.bridge) return;
450
+
451
+ const token = randomBytes(16).toString('hex');
452
+ const name = `alp-agy-${randomBytes(6).toString('hex')}`;
453
+ const socket = process.platform === 'win32'
454
+ ? `\\\\.\\pipe\\${name}`
455
+ : path.join(os.tmpdir(), `${name}.sock`);
456
+ const connections = new Set<net.Socket>();
457
+
458
+ const server = net.createServer(connection => {
459
+ connections.add(connection);
460
+ connection.once('close', () => connections.delete(connection));
461
+ this.serveBridge(connection, token);
462
+ });
463
+
464
+ await new Promise<void>((resolve, reject) => {
465
+ server.once('error', reject);
466
+ server.listen(socket, () => resolve());
467
+ });
468
+
469
+ this.bridge = { server, socket, token, connections };
470
+
471
+ // Register into ~/.gemini/config/mcp_config.json
472
+ await this.registerMcpServer();
473
+ }
474
+
475
+ /** One stable launcher; each child inherits its own socket/token from agy.
476
+ * No session endpoint or credential is written to the global configuration.
477
+ * Leave the launcher installed: removing it would break concurrent sessions.
478
+ */
479
+ private async registerMcpServer(): Promise<void> {
480
+ const configFile = this.env.ALP_AGY_MCP_CONFIG ?? path.join(os.homedir(), '.gemini', 'config', 'mcp_config.json');
481
+ await mkdir(path.dirname(configFile), { recursive: true });
482
+ const lockFile = `${configFile}.alp-lock`;
483
+ let lock;
484
+ for (let attempt = 0; attempt < 100; attempt++) {
485
+ try { lock = await open(lockFile, 'wx', 0o600); break; }
486
+ catch (error: any) {
487
+ if (error.code !== 'EEXIST') throw error;
488
+ await new Promise(resolve => setTimeout(resolve, 25));
489
+ }
490
+ }
491
+ if (!lock) throw new Error(`Antigravity MCP configuration is locked: ${lockFile}`);
492
+ const temporary = `${configFile}.${randomUUID()}.tmp`;
493
+ try {
494
+ let text = '{}';
495
+ try { text = await readFile(configFile, 'utf8'); } catch (error: any) { if (error.code !== 'ENOENT') throw error; }
496
+ const config = JSON.parse(text);
497
+ if (!config || typeof config !== 'object' || Array.isArray(config) ||
498
+ (config.mcpServers !== undefined && (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)))) {
499
+ throw new Error('Invalid Antigravity MCP configuration; existing file was preserved');
500
+ }
501
+ config.mcpServers ??= {};
502
+ const existing = config.mcpServers.alp;
503
+ if (existing && !existing.args?.some((arg: unknown) => typeof arg === 'string' && arg.startsWith('// ALP Antigravity bridge v1'))) {
504
+ throw new Error('Antigravity already has an MCP server named alp; rename it before using ALP');
505
+ }
506
+ const launcher = { command: process.execPath, args: ['-e', BRIDGE], ...(process.versions.electron ? { env: { ELECTRON_RUN_AS_NODE: '1' } } : {}) };
507
+ if (JSON.stringify(existing) === JSON.stringify(launcher)) return;
508
+ config.mcpServers.alp = launcher;
509
+ await writeFile(temporary, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
510
+ await rename(temporary, configFile);
511
+ } finally {
512
+ await unlink(temporary).catch(() => {});
513
+ await lock.close();
514
+ await unlink(lockFile);
515
+ }
516
+ }
517
+
518
+ private serveBridge(connection: net.Socket, token: string): void {
519
+ let trusted = false;
520
+ connection.setEncoding('utf8');
521
+ connection.on('error', () => {});
522
+ const reply = (value: unknown) => {
523
+ if (!connection.destroyed) connection.write(`${JSON.stringify(value)}\n`);
524
+ };
525
+
526
+ createInterface({ input: connection }).on('line', async line => {
527
+ let message: any;
528
+ try { message = JSON.parse(line); } catch { connection.destroy(); return; }
529
+ if (!trusted) {
530
+ if (message.token !== token) { connection.destroy(); return; }
531
+ trusted = true;
532
+ return;
533
+ }
534
+ if (message.op === 'list') {
535
+ reply({
536
+ id: message.id,
537
+ tools: [...this.tools.map(tool => ({
538
+ name: tool.name,
539
+ description: tool.description,
540
+ inputSchema: tool.inputSchema ?? { type: 'object' },
541
+ })), ...(this.external?.tools ?? [])],
542
+ });
543
+ return;
544
+ }
545
+ if (message.op === 'call') {
546
+ reply({
547
+ id: message.id,
548
+ result: await this.toolCall(String(message.name), message.arguments ?? {}),
549
+ });
550
+ }
551
+ });
552
+ }
553
+
554
+ private async toolCall(tool: string, args: Record<string, unknown>): Promise<any> {
555
+ if (this.activeTurn && this.external?.has(tool)) {
556
+ const callId = `agy-mcp-tool-${randomUUID()}`;
557
+ const item = { type: 'dynamicToolCall', id: callId, callId, tool, arguments: args };
558
+ this.emit('item/started', { threadId: this.conversationId, item: { ...item, status: 'inProgress' } });
559
+ const result = await this.external.call(tool, args).catch(() => ({ content: [{ type: 'text', text: `External MCP tool '${tool}' failed` }], isError: true }));
560
+ const ok = result.isError !== true;
561
+ this.emit('item/completed', {
562
+ threadId: this.conversationId,
563
+ item: { ...item, status: ok ? 'completed' : 'failed', success: ok, contentItems: (Array.isArray(result.content) ? result.content : []).map(part => ({ type: 'inputText', text: part.type === 'text' ? part.text : JSON.stringify(part) })) },
564
+ });
565
+ return result;
566
+ }
567
+ if (!this.requestHandler || !this.activeTurn || !this.tools.some(t => t.name === tool)) {
568
+ return {
569
+ content: [{ type: 'text', text: this.activeTurn ? `Unknown ALP tool '${tool}'` : 'ALP tools are unavailable between turns' }],
570
+ isError: true,
571
+ };
572
+ }
573
+
574
+ const callId = `agy-alp-tool-${randomUUID()}`;
575
+ const item = { type: 'dynamicToolCall', id: callId, callId, tool, arguments: args };
576
+ this.emit('item/started', { threadId: this.conversationId, item: { ...item, status: 'inProgress' } });
577
+
578
+ const result: any = await this.requestHandler('item/tool/call', {
579
+ threadId: this.conversationId,
580
+ turnId: this.activeTurn,
581
+ callId,
582
+ namespace: null,
583
+ tool,
584
+ arguments: args,
585
+ }).catch(error => ({
586
+ success: false,
587
+ contentItems: [{ type: 'inputText', text: error instanceof Error ? error.message : String(error) }],
588
+ }));
589
+
590
+ const ok = result?.success !== false;
591
+ this.emit('item/completed', {
592
+ threadId: this.conversationId,
593
+ item: { ...item, status: ok ? 'completed' : 'failed', success: ok, contentItems: result?.contentItems ?? [] },
594
+ });
595
+
596
+ return {
597
+ content: (result?.contentItems ?? []).map((part: any) => ({ type: 'text', text: part.text ?? JSON.stringify(part) })),
598
+ isError: !ok,
599
+ };
600
+ }
601
+
602
+ private emit(method: string, params: any): void {
603
+ for (const listener of this.listeners) listener(method, params);
604
+ }
605
+
606
+ private fail(error: Error): void {
607
+ if (this.closed) return;
608
+ for (const listener of this.failures) listener(error);
609
+ void this.close();
610
+ }
611
+
612
+ async close(): Promise<void> {
613
+ if (this.closed) return this.exit;
614
+ this.closed = true;
615
+
616
+ this.listeners.clear();
617
+ this.failures.clear();
618
+ this.startupAbort.abort();
619
+ await this.externalStarting?.then(external => external.close(), () => {});
620
+ await this.external?.close();
621
+
622
+ if (this.bridge) {
623
+ this.bridge.server.close();
624
+ for (const connection of this.bridge.connections) connection.destroy();
625
+ if (process.platform !== 'win32') {
626
+ try { rmSync(this.bridge.socket, { force: true }); } catch {}
627
+ }
628
+ }
629
+
630
+ if (this.lines) {
631
+ this.lines.close();
632
+ }
633
+
634
+ const child = this.child;
635
+ if (!child) return;
636
+
637
+ try { child.stdin.end(); } catch {}
638
+
639
+ const timer = setTimeout(() => {
640
+ if (process.platform === 'win32' && child.pid) {
641
+ execFile('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true }, () => {});
642
+ } else if (child.pid) {
643
+ try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
644
+ }
645
+ }, 1500);
646
+
647
+ let deadline: NodeJS.Timeout | undefined;
648
+ try {
649
+ await Promise.race([
650
+ this.exit,
651
+ new Promise<void>(resolve => { deadline = setTimeout(resolve, 4000); }),
652
+ ]);
653
+ } finally {
654
+ clearTimeout(timer);
655
+ clearTimeout(deadline);
656
+ }
657
+ }
658
+ }