paseo-acp-agy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,535 @@
1
+ import { spawn } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { logger } from "./logger.js";
6
+ import { getEffectiveEffortForModel, } from "./protocol.js";
7
+ import { buildAgyArgs } from "./permissions.js";
8
+ function resolveDefaultAgyBinary() {
9
+ if (process.env.AGY_BIN_PATH)
10
+ return process.env.AGY_BIN_PATH;
11
+ if (process.env.HOME) {
12
+ const localPath = path.join(process.env.HOME, ".local", "bin", "agy");
13
+ if (fs.existsSync(localPath))
14
+ return localPath;
15
+ }
16
+ return "agy";
17
+ }
18
+ export class AntigravityProcess extends EventEmitter {
19
+ child = null;
20
+ binaryPath;
21
+ cwd;
22
+ model;
23
+ effort;
24
+ mode;
25
+ conversationId;
26
+ permissions;
27
+ env;
28
+ isClosed = false;
29
+ needsRestart = false;
30
+ transitionPromise = null;
31
+ currentTurnPromise = null;
32
+ initialInfo = null;
33
+ constructor(options = {}) {
34
+ super();
35
+ this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
36
+ this.cwd = options.cwd || process.cwd();
37
+ this.model = options.model;
38
+ this.effort = options.effort;
39
+ this.mode = options.mode;
40
+ this.conversationId = options.conversationId;
41
+ this.permissions = options.permissions || { sandbox: false, dangerouslySkipPermissions: false };
42
+ this.env = { ...process.env, ...options.env };
43
+ // EventEmitter treats "error" specially. Keep a defensive listener for
44
+ // callers/tests that emit it directly; child failures use process_error.
45
+ this.on("error", (err) => {
46
+ logger.error("AntigravityProcess error", { error: err.message });
47
+ });
48
+ }
49
+ get isRunning() {
50
+ const child = this.child;
51
+ return child !== null && this.isProcessTreeAlive(child);
52
+ }
53
+ get isExecutingTurn() {
54
+ return this.currentTurnPromise !== null;
55
+ }
56
+ get currentConversationId() {
57
+ return this.conversationId;
58
+ }
59
+ get currentModel() {
60
+ return this.model;
61
+ }
62
+ get currentEffort() {
63
+ return this.effort;
64
+ }
65
+ get currentMode() {
66
+ return this.mode;
67
+ }
68
+ setModel(model) {
69
+ if (this.model !== model) {
70
+ this.model = model;
71
+ this.scheduleRestart();
72
+ }
73
+ }
74
+ setEffort(effort) {
75
+ if (this.effort !== effort) {
76
+ this.effort = effort;
77
+ this.scheduleRestart();
78
+ }
79
+ }
80
+ setMode(mode) {
81
+ if (this.mode !== mode) {
82
+ this.mode = mode;
83
+ this.scheduleRestart();
84
+ }
85
+ }
86
+ setConversationId(conversationId) {
87
+ if (this.conversationId !== conversationId) {
88
+ this.conversationId = conversationId;
89
+ this.scheduleRestart();
90
+ }
91
+ }
92
+ scheduleRestart() {
93
+ if (!this.child)
94
+ return;
95
+ this.needsRestart = true;
96
+ logger.info("Scheduled agy restart for updated session configuration", {
97
+ model: this.model,
98
+ effort: this.effort,
99
+ mode: this.mode,
100
+ conversationId: this.conversationId,
101
+ turnActive: this.isExecutingTurn,
102
+ });
103
+ }
104
+ isProcessTreeAlive(child) {
105
+ const pid = child.pid;
106
+ if (pid && process.platform !== "win32") {
107
+ try {
108
+ process.kill(-pid, 0);
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ return child.exitCode === null && child.signalCode === null;
116
+ }
117
+ signalProcessTree(child, signal) {
118
+ const pid = child.pid;
119
+ if (pid && process.platform !== "win32") {
120
+ try {
121
+ process.kill(-pid, signal);
122
+ return true;
123
+ }
124
+ catch (err) {
125
+ if (err.code === "ESRCH")
126
+ return true;
127
+ logger.warn("Failed to signal agy process group; falling back to child PID", {
128
+ pid,
129
+ signal,
130
+ error: err.message,
131
+ });
132
+ }
133
+ }
134
+ try {
135
+ return child.kill(signal);
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ async waitForProcessTreeExit(child, timeoutMs) {
142
+ const deadline = Date.now() + timeoutMs;
143
+ while (Date.now() < deadline) {
144
+ if (!this.isProcessTreeAlive(child))
145
+ return true;
146
+ await new Promise((resolve) => setTimeout(resolve, 25));
147
+ }
148
+ return !this.isProcessTreeAlive(child);
149
+ }
150
+ async terminateChild(child, gracefulTimeoutMs = 2_000) {
151
+ if (!this.isProcessTreeAlive(child)) {
152
+ if (this.child === child)
153
+ this.child = null;
154
+ return;
155
+ }
156
+ this.signalProcessTree(child, "SIGTERM");
157
+ if (!(await this.waitForProcessTreeExit(child, gracefulTimeoutMs))) {
158
+ logger.warn("agy process tree did not exit after SIGTERM; sending SIGKILL", {
159
+ pid: child.pid,
160
+ });
161
+ this.signalProcessTree(child, "SIGKILL");
162
+ if (!(await this.waitForProcessTreeExit(child, 1_000))) {
163
+ logger.error("agy process tree remained alive after SIGKILL", { pid: child.pid });
164
+ }
165
+ }
166
+ if (this.child === child)
167
+ this.child = null;
168
+ }
169
+ async applyPendingRestart(owner) {
170
+ if (!this.needsRestart)
171
+ return;
172
+ if (this.currentTurnPromise && this.currentTurnPromise !== owner) {
173
+ throw new Error("Cannot restart agy while another prompt turn is active");
174
+ }
175
+ if (owner?.child) {
176
+ throw new Error("Cannot restart agy after the active turn has started executing");
177
+ }
178
+ if (!this.child) {
179
+ this.needsRestart = false;
180
+ return;
181
+ }
182
+ if (!this.transitionPromise) {
183
+ const oldChild = this.child;
184
+ this.transitionPromise = (async () => {
185
+ logger.info("Restarting agy process with updated session configuration", {
186
+ model: this.model,
187
+ effort: this.effort,
188
+ mode: this.mode,
189
+ conversationId: this.conversationId,
190
+ });
191
+ await this.terminateChild(oldChild);
192
+ this.initialInfo = null;
193
+ this.needsRestart = false;
194
+ })().finally(() => {
195
+ this.transitionPromise = null;
196
+ });
197
+ }
198
+ await this.transitionPromise;
199
+ }
200
+ async start() {
201
+ if (this.isClosed)
202
+ throw new Error("Cannot start a closed Antigravity process");
203
+ if (this.transitionPromise)
204
+ await this.transitionPromise;
205
+ if (this.isRunning)
206
+ return;
207
+ const extraArgs = [];
208
+ if (this.model)
209
+ extraArgs.push("--model", this.model);
210
+ const effectiveEffort = getEffectiveEffortForModel(this.model, this.effort);
211
+ if (effectiveEffort)
212
+ extraArgs.push("--effort", effectiveEffort);
213
+ if (this.conversationId)
214
+ extraArgs.push("--conversation", this.conversationId);
215
+ const args = buildAgyArgs({
216
+ sandbox: this.permissions.sandbox,
217
+ dangerouslySkipPermissions: this.permissions.dangerouslySkipPermissions,
218
+ mode: this.mode,
219
+ }, extraArgs);
220
+ logger.info("Spawning agy process", {
221
+ binary: this.binaryPath,
222
+ args,
223
+ cwd: this.cwd,
224
+ effectiveEffort,
225
+ });
226
+ const child = spawn(this.binaryPath, args, {
227
+ cwd: this.cwd,
228
+ env: this.env,
229
+ stdio: ["pipe", "pipe", "pipe"],
230
+ detached: process.platform !== "win32",
231
+ });
232
+ this.child = child;
233
+ if (!child.stdin || !child.stdout || !child.stderr) {
234
+ if (this.child === child)
235
+ this.child = null;
236
+ this.signalProcessTree(child, "SIGKILL");
237
+ throw new Error("Failed to create stdio pipes for agy process");
238
+ }
239
+ child.stdout.setEncoding("utf-8");
240
+ child.stderr.setEncoding("utf-8");
241
+ let stdoutBuffer = "";
242
+ let stderrBuffer = "";
243
+ child.stdout.on("data", (chunk) => {
244
+ stdoutBuffer += chunk;
245
+ const lines = stdoutBuffer.split("\n");
246
+ stdoutBuffer = lines.pop() || "";
247
+ for (const line of lines) {
248
+ const trimmed = line.trim();
249
+ if (!trimmed)
250
+ continue;
251
+ try {
252
+ this.handleEvent(JSON.parse(trimmed), child);
253
+ }
254
+ catch (err) {
255
+ logger.warn("Failed to parse line from agy stdout", {
256
+ line: trimmed,
257
+ error: err.message,
258
+ });
259
+ }
260
+ }
261
+ });
262
+ child.stderr.on("data", (chunk) => {
263
+ stderrBuffer += chunk;
264
+ const lines = stderrBuffer.split("\n");
265
+ stderrBuffer = lines.pop() || "";
266
+ for (const line of lines) {
267
+ if (line.trim())
268
+ logger.warn("agy stderr: " + line.trim());
269
+ }
270
+ });
271
+ child.on("error", (err) => {
272
+ logger.error("agy child process emitted error", { error: err.message });
273
+ if (this.currentTurnPromise?.child === child) {
274
+ this.currentTurnPromise.reject(err);
275
+ this.currentTurnPromise = null;
276
+ }
277
+ this.emit("process_error", err);
278
+ });
279
+ child.on("close", (code, signal) => {
280
+ logger.info("agy process exited", { code, signal, pid: child.pid });
281
+ if (this.currentTurnPromise?.child === child) {
282
+ this.currentTurnPromise.reject(new Error(`agy process exited prematurely with code ${code} signal ${signal}`));
283
+ this.currentTurnPromise = null;
284
+ }
285
+ // The leader can die while a tool subprocess remains in its process
286
+ // group. Keep the group identity long enough to reap those descendants.
287
+ if (this.isProcessTreeAlive(child)) {
288
+ void this.terminateChild(child).catch((err) => {
289
+ logger.error("Failed to clean descendant processes after agy exit", {
290
+ pid: child.pid,
291
+ error: err.message,
292
+ });
293
+ });
294
+ }
295
+ else if (this.child === child) {
296
+ this.child = null;
297
+ }
298
+ this.emit("exit", { code, signal, pid: child.pid });
299
+ });
300
+ }
301
+ handleEvent(event, child) {
302
+ const ownsProcess = this.child === child;
303
+ const ownsTurn = this.currentTurnPromise?.child === child;
304
+ if (!ownsProcess && !ownsTurn) {
305
+ logger.debug("Ignoring event from stale agy child", { event: event.event, pid: child.pid });
306
+ return;
307
+ }
308
+ logger.debug("Received agy event", { event: event.event, pid: child.pid });
309
+ switch (event.event) {
310
+ case "init": {
311
+ const initEvent = event;
312
+ if (ownsProcess && initEvent.conversation_id) {
313
+ this.conversationId = initEvent.conversation_id;
314
+ this.initialInfo = initEvent.init;
315
+ this.emit("init", initEvent);
316
+ }
317
+ break;
318
+ }
319
+ case "step_update": {
320
+ const stepEvent = event;
321
+ if (ownsProcess && stepEvent.step_update?.conversation_id) {
322
+ this.conversationId = stepEvent.step_update.conversation_id;
323
+ }
324
+ if (ownsTurn)
325
+ this.emit("step_update", stepEvent);
326
+ break;
327
+ }
328
+ case "result": {
329
+ if (!ownsTurn || !this.currentTurnPromise)
330
+ break;
331
+ const resultEvent = event;
332
+ if (resultEvent.result?.conversation_id) {
333
+ this.conversationId = resultEvent.result.conversation_id;
334
+ }
335
+ this.emit("result", resultEvent);
336
+ const active = this.currentTurnPromise;
337
+ this.currentTurnPromise = null;
338
+ active.resolve(resultEvent);
339
+ break;
340
+ }
341
+ default:
342
+ logger.warn("Unknown agy event received", { raw: event });
343
+ }
344
+ }
345
+ async ensureReady(expectedConversationId, timeoutMs = 10_000) {
346
+ if (this.currentTurnPromise) {
347
+ throw new Error("Cannot validate an Antigravity session while a prompt turn is active");
348
+ }
349
+ await this.applyPendingRestart();
350
+ if (!this.isRunning)
351
+ await this.start();
352
+ const child = this.child;
353
+ if (!child)
354
+ throw new Error("Antigravity process failed to start");
355
+ if (this.initialInfo !== null) {
356
+ if (expectedConversationId && this.conversationId !== expectedConversationId) {
357
+ const actual = this.conversationId;
358
+ await this.terminateChild(child);
359
+ this.conversationId = expectedConversationId;
360
+ throw new Error(`Antigravity resumed unexpected conversation ${actual || "<none>"}; expected ${expectedConversationId}`);
361
+ }
362
+ return;
363
+ }
364
+ await new Promise((resolve, reject) => {
365
+ let settled = false;
366
+ const cleanup = () => {
367
+ this.off("init", onInit);
368
+ child.off("error", onError);
369
+ child.off("close", onClose);
370
+ clearTimeout(timer);
371
+ };
372
+ const finishReject = (err) => {
373
+ if (settled)
374
+ return;
375
+ settled = true;
376
+ cleanup();
377
+ reject(err);
378
+ };
379
+ const finishResolve = () => {
380
+ if (settled)
381
+ return;
382
+ settled = true;
383
+ cleanup();
384
+ resolve();
385
+ };
386
+ const onInit = (event) => {
387
+ if (this.child !== child)
388
+ return;
389
+ if (expectedConversationId && event.conversation_id !== expectedConversationId) {
390
+ const actual = event.conversation_id;
391
+ cleanup();
392
+ settled = true;
393
+ void this.terminateChild(child).finally(() => {
394
+ this.conversationId = expectedConversationId;
395
+ reject(new Error(`Antigravity resumed unexpected conversation ${actual}; expected ${expectedConversationId}`));
396
+ });
397
+ return;
398
+ }
399
+ finishResolve();
400
+ };
401
+ const onError = (err) => finishReject(err);
402
+ const onClose = (code, signal) => finishReject(new Error(`Antigravity exited before initialization: code=${code} signal=${signal}`));
403
+ const timer = setTimeout(() => {
404
+ if (settled)
405
+ return;
406
+ cleanup();
407
+ settled = true;
408
+ void this.terminateChild(child).finally(() => {
409
+ if (expectedConversationId)
410
+ this.conversationId = expectedConversationId;
411
+ reject(new Error(`Timed out waiting ${timeoutMs}ms for Antigravity initialization`));
412
+ });
413
+ }, timeoutMs);
414
+ this.on("init", onInit);
415
+ child.once("error", onError);
416
+ child.once("close", onClose);
417
+ if (this.initialInfo !== null) {
418
+ onInit({
419
+ event: "init",
420
+ conversation_id: this.conversationId || "",
421
+ init: this.initialInfo,
422
+ });
423
+ }
424
+ });
425
+ }
426
+ async sendPrompt(text, onStepUpdate) {
427
+ if (this.currentTurnPromise) {
428
+ throw new Error("A prompt turn is already in progress on this session");
429
+ }
430
+ let resolveTurn;
431
+ let rejectTurn;
432
+ const turnPromise = new Promise((resolve, reject) => {
433
+ resolveTurn = resolve;
434
+ rejectTurn = reject;
435
+ });
436
+ const active = {
437
+ child: null,
438
+ cancelled: false,
439
+ resolve: resolveTurn,
440
+ reject: rejectTurn,
441
+ };
442
+ // Reserve before the first await so concurrent prompt/cancel calls see the
443
+ // turn while the child is still starting or restarting.
444
+ this.currentTurnPromise = active;
445
+ let stepListener = null;
446
+ const failBeforeWrite = (err) => {
447
+ if (stepListener)
448
+ this.off("step_update", stepListener);
449
+ if (this.currentTurnPromise === active)
450
+ this.currentTurnPromise = null;
451
+ active.reject(err);
452
+ };
453
+ try {
454
+ await this.applyPendingRestart(active);
455
+ if (active.cancelled) {
456
+ failBeforeWrite(new Error("Prompt turn cancelled before execution"));
457
+ return turnPromise;
458
+ }
459
+ if (!this.isRunning)
460
+ await this.start();
461
+ if (active.cancelled) {
462
+ failBeforeWrite(new Error("Prompt turn cancelled before execution"));
463
+ return turnPromise;
464
+ }
465
+ const child = this.child;
466
+ if (!child || !child.stdin || !child.stdin.writable) {
467
+ failBeforeWrite(new Error("Cannot send prompt: agy process stdin is not writable"));
468
+ return turnPromise;
469
+ }
470
+ active.child = child;
471
+ if (onStepUpdate) {
472
+ stepListener = onStepUpdate;
473
+ this.on("step_update", stepListener);
474
+ const originalResolve = active.resolve;
475
+ const originalReject = active.reject;
476
+ active.resolve = (result) => {
477
+ if (stepListener)
478
+ this.off("step_update", stepListener);
479
+ originalResolve(result);
480
+ };
481
+ active.reject = (err) => {
482
+ if (stepListener)
483
+ this.off("step_update", stepListener);
484
+ originalReject(err);
485
+ };
486
+ }
487
+ if (active.cancelled) {
488
+ failBeforeWrite(new Error("Prompt turn cancelled before execution"));
489
+ return turnPromise;
490
+ }
491
+ const payload = {
492
+ event: "user",
493
+ message: { content: text },
494
+ };
495
+ logger.debug("Writing prompt to agy stdin", { promptPreview: text.slice(0, 100) });
496
+ child.stdin.write(JSON.stringify(payload) + "\n");
497
+ }
498
+ catch (err) {
499
+ failBeforeWrite(err);
500
+ }
501
+ return turnPromise;
502
+ }
503
+ cancelCurrentTurn() {
504
+ const active = this.currentTurnPromise;
505
+ if (!active)
506
+ return false;
507
+ active.cancelled = true;
508
+ logger.info("Cancelling active agy turn", { hasChild: Boolean(active.child) });
509
+ if (active.child)
510
+ this.signalProcessTree(active.child, "SIGINT");
511
+ return true;
512
+ }
513
+ async close() {
514
+ if (this.isClosed)
515
+ return;
516
+ this.isClosed = true;
517
+ if (this.transitionPromise)
518
+ await this.transitionPromise;
519
+ const active = this.currentTurnPromise;
520
+ if (active) {
521
+ active.cancelled = true;
522
+ this.currentTurnPromise = null;
523
+ active.reject(new Error("Session closed while turn was in progress"));
524
+ }
525
+ const child = this.child;
526
+ if (child) {
527
+ try {
528
+ await this.terminateChild(child);
529
+ }
530
+ catch (err) {
531
+ logger.error("Error during agy process cleanup", { error: err.message });
532
+ }
533
+ }
534
+ }
535
+ }
@@ -0,0 +1 @@
1
+ export declare function saveBase64Image(data: string, mimeType?: string): string | null;
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import crypto from "node:crypto";
5
+ import { logger } from "./logger.js";
6
+ const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
7
+ function getStateRoot() {
8
+ if (process.env.AGY_ACP_STATE_DIR)
9
+ return process.env.AGY_ACP_STATE_DIR;
10
+ const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
11
+ return path.join(stateHome, "agy-acp");
12
+ }
13
+ function getAttachmentsDir() {
14
+ const attachmentsDir = path.join(getStateRoot(), "attachments");
15
+ try {
16
+ fs.mkdirSync(attachmentsDir, { recursive: true, mode: 0o700 });
17
+ fs.chmodSync(attachmentsDir, 0o700);
18
+ }
19
+ catch { }
20
+ return attachmentsDir;
21
+ }
22
+ const MIME_TO_EXT = {
23
+ "image/png": ".png",
24
+ "image/jpeg": ".jpg",
25
+ "image/jpg": ".jpg",
26
+ "image/webp": ".webp",
27
+ "image/gif": ".gif",
28
+ "image/svg+xml": ".svg",
29
+ "image/bmp": ".bmp",
30
+ "image/tiff": ".tiff",
31
+ };
32
+ function maxAttachmentBytes() {
33
+ const configured = Number.parseInt(process.env.AGY_ACP_MAX_ATTACHMENT_BYTES || "", 10);
34
+ return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_MAX_ATTACHMENT_BYTES;
35
+ }
36
+ function normalizeBase64(data) {
37
+ const comma = data.indexOf(",");
38
+ const stripped = data.startsWith("data:") && comma >= 0 ? data.slice(comma + 1) : data;
39
+ return stripped.replace(/\s+/g, "");
40
+ }
41
+ function decodeValidatedBase64(data) {
42
+ if (!data || !/^[A-Za-z0-9+/]*={0,2}$/.test(data))
43
+ return null;
44
+ const unpadded = data.replace(/=+$/, "");
45
+ if (unpadded.length % 4 === 1)
46
+ return null;
47
+ try {
48
+ const buffer = Buffer.from(data, "base64");
49
+ if (buffer.toString("base64").replace(/=+$/, "") !== unpadded)
50
+ return null;
51
+ return buffer;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export function saveBase64Image(data, mimeType = "image/png") {
58
+ const normalizedMime = mimeType.toLowerCase();
59
+ const ext = MIME_TO_EXT[normalizedMime];
60
+ if (!ext) {
61
+ logger.warn("Rejected attachment with unsupported MIME type", { mimeType: normalizedMime });
62
+ return null;
63
+ }
64
+ const normalized = normalizeBase64(data);
65
+ const maxBytes = maxAttachmentBytes();
66
+ if (normalized.length > Math.ceil(maxBytes / 3) * 4 + 4) {
67
+ logger.warn("Rejected attachment exceeding configured size limit", {
68
+ encodedLength: normalized.length,
69
+ maxBytes,
70
+ });
71
+ return null;
72
+ }
73
+ const buffer = decodeValidatedBase64(normalized);
74
+ if (!buffer) {
75
+ logger.warn("Rejected attachment with invalid base64 payload");
76
+ return null;
77
+ }
78
+ if (buffer.length === 0 || buffer.length > maxBytes) {
79
+ logger.warn("Rejected attachment outside configured decoded size limit", {
80
+ sizeBytes: buffer.length,
81
+ maxBytes,
82
+ });
83
+ return null;
84
+ }
85
+ const dir = getAttachmentsDir();
86
+ const hash = crypto.createHash("sha256").update(buffer).digest("hex").slice(0, 16);
87
+ const filePath = path.join(dir, `attachment_${hash}${ext}`);
88
+ if (!fs.existsSync(filePath)) {
89
+ try {
90
+ fs.writeFileSync(filePath, buffer, { mode: 0o600, flag: "wx" });
91
+ fs.chmodSync(filePath, 0o600);
92
+ logger.info("Saved attached image for session", { filePath, sizeBytes: buffer.length });
93
+ }
94
+ catch (err) {
95
+ if (!fs.existsSync(filePath)) {
96
+ logger.error("Failed to save attached image", { error: err.message });
97
+ return null;
98
+ }
99
+ }
100
+ }
101
+ return filePath;
102
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { ACPServer } from "./acp-server.js";
3
+ import { logger } from "./logger.js";
4
+ import { formatDiagnosticVersion, resolveBuildMetadata } from "./version.js";
5
+ const args = process.argv.slice(2);
6
+ if (args.includes("--version") || args.includes("-v")) {
7
+ if (args.includes("--json")) {
8
+ process.stdout.write(JSON.stringify(resolveBuildMetadata(), null, 2) + "\n");
9
+ }
10
+ else {
11
+ process.stdout.write(`${formatDiagnosticVersion()}\n`);
12
+ }
13
+ process.exit(0);
14
+ }
15
+ if (args.includes("--help") || args.includes("-h")) {
16
+ process.stdout.write(`paseo-acp-agy - ACP adapter for Google Antigravity CLI
17
+
18
+ Usage:
19
+ paseo-acp-agy [options]
20
+ agy-acp [options]
21
+ npx -y paseo-acp-agy [options]
22
+
23
+ Options:
24
+ --acp Start ACP server over stdio (default)
25
+ -v, --version Show version
26
+ --json Show version in JSON format (with --version)
27
+ -h, --help Show help
28
+
29
+ Environment Variables:
30
+ AGY_ACP_LOG_LEVEL debug | info | warn | error (default: info)
31
+ AGY_ACP_LOG_DIR Directory for log files
32
+ AGY_ACP_SANDBOX Set to 'true' to run agy in sandbox mode
33
+ AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS Set to 'true' to auto-approve tool permissions
34
+ AGY_BIN_PATH Path to agy binary (default: agy in PATH or ~/.local/bin/agy)
35
+ `);
36
+ process.exit(0);
37
+ }
38
+ const server = new ACPServer();
39
+ const cleanup = async () => {
40
+ try {
41
+ await server.stop();
42
+ logger.close();
43
+ }
44
+ catch (err) {
45
+ // ignore
46
+ }
47
+ process.exit(0);
48
+ };
49
+ process.on("SIGINT", cleanup);
50
+ process.on("SIGTERM", cleanup);
51
+ server.start();