paseo-acp-agy 1.1.9 → 1.2.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.
@@ -5,6 +5,7 @@ export declare class ACPServer {
5
5
  private input;
6
6
  private output;
7
7
  private binaryPath;
8
+ private skipNarration;
8
9
  private rl;
9
10
  private isRunning;
10
11
  constructor(options?: {
@@ -12,6 +13,7 @@ export declare class ACPServer {
12
13
  output?: Writable;
13
14
  sessionManager?: SessionManager;
14
15
  binaryPath?: string;
16
+ skipNarration?: boolean;
15
17
  });
16
18
  start(): void;
17
19
  private send;
@@ -1,6 +1,6 @@
1
1
  import readline from "node:readline";
2
2
  import { logger } from "./logger.js";
3
- import { ACP_METHODS, AVAILABLE_MODES, fetchAvailableModels, buildConfigOptionsForModel, extractPromptText, mapToolNameToKind, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
3
+ import { ACP_METHODS, AVAILABLE_MODES, AVAILABLE_PERMISSIONS, fetchAvailableModels, buildConfigOptionsForSession, extractPromptText, mapToolNameToKind, extractImageMarkdownLink, isNarrationText, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
4
4
  import { SessionManager } from "./session.js";
5
5
  import { executeSlashCommand, AVAILABLE_SLASH_COMMANDS } from "./slash-commands.js";
6
6
  import { getShortVersion } from "./version.js";
@@ -21,12 +21,15 @@ export class ACPServer {
21
21
  input;
22
22
  output;
23
23
  binaryPath;
24
+ skipNarration;
24
25
  rl = null;
25
26
  isRunning = false;
26
27
  constructor(options = {}) {
27
28
  this.input = options.input || process.stdin;
28
29
  this.output = options.output || process.stdout;
29
30
  this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
31
+ this.skipNarration =
32
+ options.skipNarration ?? (process.env.AGY_ACP_SKIP_NARRATION === "true");
30
33
  this.sessionManager =
31
34
  options.sessionManager || new SessionManager({ defaultBinaryPath: this.binaryPath });
32
35
  }
@@ -112,7 +115,13 @@ export class ACPServer {
112
115
  availableModels,
113
116
  currentModelId: session.model,
114
117
  },
115
- configOptions: buildConfigOptionsForModel(session.model, session.effort, availableModels),
118
+ configOptions: buildConfigOptionsForSession({
119
+ modelId: session.model,
120
+ currentEffort: session.effort,
121
+ currentMode: session.mode,
122
+ currentPermission: session.permission,
123
+ availableModels,
124
+ }),
116
125
  };
117
126
  }
118
127
  requireSession(sessionId) {
@@ -199,6 +208,17 @@ export class ACPServer {
199
208
  this.sendSuccess(id, await this.sessionState(session, true));
200
209
  this.publishCommands(session.id);
201
210
  this.publishUsageUpdate(session);
211
+ // Re-publish usage after ACPAgent completes awaiting newSession and stores this.sessionId
212
+ setTimeout(() => {
213
+ if (this.sessionManager.getSession(session.id)) {
214
+ this.publishUsageUpdate(session);
215
+ }
216
+ }, 150);
217
+ setTimeout(() => {
218
+ if (this.sessionManager.getSession(session.id)) {
219
+ this.publishUsageUpdate(session);
220
+ }
221
+ }, 500);
202
222
  }
203
223
  break;
204
224
  }
@@ -229,6 +249,12 @@ export class ACPServer {
229
249
  this.sendSuccess(id, await this.sessionState(session));
230
250
  this.publishCommands(session.id);
231
251
  this.publishUsageUpdate(session);
252
+ const activeSession = session;
253
+ setTimeout(() => {
254
+ if (this.sessionManager.getSession(activeSession.id)) {
255
+ this.publishUsageUpdate(activeSession);
256
+ }
257
+ }, 150);
232
258
  }
233
259
  }
234
260
  catch (err) {
@@ -329,11 +355,19 @@ export class ACPServer {
329
355
  this.sendSuccess(id, { stopReason: "end_turn" });
330
356
  break;
331
357
  }
358
+ let hasSentNonNarration = false;
332
359
  const onStepUpdate = (event) => {
333
360
  const step = event.step_update;
334
361
  if (!step)
335
362
  return;
336
363
  if (step.step_type === "agent_response" && step.text_delta) {
364
+ if (this.skipNarration && !hasSentNonNarration) {
365
+ if (isNarrationText(step.text_delta)) {
366
+ logger.debug("Skipping pure narration text chunk", { delta: step.text_delta });
367
+ return;
368
+ }
369
+ hasSentNonNarration = true;
370
+ }
337
371
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
338
372
  sessionId: session.id,
339
373
  update: {
@@ -343,6 +377,7 @@ export class ACPServer {
343
377
  });
344
378
  }
345
379
  else if (step.step_type === "thought" && step.text_delta) {
380
+ hasSentNonNarration = true;
346
381
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
347
382
  sessionId: session.id,
348
383
  update: {
@@ -352,6 +387,7 @@ export class ACPServer {
352
387
  });
353
388
  }
354
389
  else if (step.step_type === "tool" && step.tool_info) {
390
+ hasSentNonNarration = true;
355
391
  const toolCallId = `tool_${step.step_index}`;
356
392
  const toolName = step.tool_name || step.tool_info.name || "tool";
357
393
  const toolKind = mapToolNameToKind(toolName);
@@ -377,6 +413,16 @@ export class ACPServer {
377
413
  rawOutput: step.tool_info.output ?? "",
378
414
  },
379
415
  });
416
+ const imageMarkdown = extractImageMarkdownLink(toolName, step.tool_info.output, step.tool_info.parameters);
417
+ if (imageMarkdown) {
418
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
419
+ sessionId: session.id,
420
+ update: {
421
+ sessionUpdate: "agent_message_chunk",
422
+ content: { type: "text", text: `\n\n${imageMarkdown}\n\n` },
423
+ },
424
+ });
425
+ }
380
426
  }
381
427
  else if (step.state === "ERROR") {
382
428
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
@@ -496,6 +542,7 @@ export class ACPServer {
496
542
  break;
497
543
  }
498
544
  case ACP_METHODS.SESSION_SET_MODE:
545
+ case ACP_METHODS.SESSION_SET_MODE_CAMEL:
499
546
  case ACP_METHODS.SESSION_SET_MODE_ALIAS: {
500
547
  const sessionId = String(params.sessionId || "");
501
548
  const modeId = String(params.modeId || params.mode || "default");
@@ -516,6 +563,7 @@ export class ACPServer {
516
563
  break;
517
564
  }
518
565
  case ACP_METHODS.SESSION_SET_MODEL:
566
+ case ACP_METHODS.SESSION_SET_MODEL_CAMEL:
519
567
  case ACP_METHODS.SESSION_SET_MODEL_ALIAS: {
520
568
  const sessionId = String(params.sessionId || "");
521
569
  const session = this.requireSession(sessionId);
@@ -546,12 +594,19 @@ export class ACPServer {
546
594
  if (parsedModel.effort)
547
595
  session.setEffort(parsedModel.effort);
548
596
  session.setModel(model.modelId);
549
- const configOptions = buildConfigOptionsForModel(session.model, session.effort, models);
597
+ const configOptions = buildConfigOptionsForSession({
598
+ modelId: session.model,
599
+ currentEffort: session.effort,
600
+ currentMode: session.mode,
601
+ currentPermission: session.permission,
602
+ availableModels: models,
603
+ });
550
604
  if (!isNotification)
551
605
  this.sendSuccess(id, { configOptions });
552
606
  break;
553
607
  }
554
608
  case ACP_METHODS.SESSION_SET_CONFIG_OPTION:
609
+ case ACP_METHODS.SESSION_SET_CONFIG_OPTION_CAMEL:
555
610
  case ACP_METHODS.SESSION_SET_CONFIG_OPTION_ALIAS: {
556
611
  const sessionId = String(params.sessionId || "");
557
612
  const configId = String(params.configId || "");
@@ -584,12 +639,36 @@ export class ACPServer {
584
639
  session.setModel(value);
585
640
  logger.info("Updated model for session via config option", { sessionId, model: value });
586
641
  }
642
+ else if (configId === "mode") {
643
+ if (!AVAILABLE_MODES.some((candidate) => candidate.id === value)) {
644
+ if (!isNotification)
645
+ this.sendError(id, -32602, `Unsupported mode: ${value}`);
646
+ break;
647
+ }
648
+ session.setMode(value);
649
+ logger.info("Updated mode for session via config option", { sessionId, mode: value });
650
+ }
651
+ else if (configId === "permission") {
652
+ if (!AVAILABLE_PERMISSIONS.some((candidate) => candidate.id === value)) {
653
+ if (!isNotification)
654
+ this.sendError(id, -32602, `Unsupported permission: ${value}`);
655
+ break;
656
+ }
657
+ session.setPermission(value);
658
+ logger.info("Updated permission for session via config option", { sessionId, permission: value });
659
+ }
587
660
  else {
588
661
  if (!isNotification)
589
662
  this.sendError(id, -32602, `Unsupported config option: ${configId}`);
590
663
  break;
591
664
  }
592
- const configOptions = buildConfigOptionsForModel(session.model, session.effort, models);
665
+ const configOptions = buildConfigOptionsForSession({
666
+ modelId: session.model,
667
+ currentEffort: session.effort,
668
+ currentMode: session.mode,
669
+ currentPermission: session.permission,
670
+ availableModels: models,
671
+ });
593
672
  if (!isNotification)
594
673
  this.sendSuccess(id, { configOptions });
595
674
  break;
@@ -40,6 +40,8 @@ export declare class AntigravityProcess extends EventEmitter {
40
40
  setModel(model: string): void;
41
41
  setEffort(effort: string): void;
42
42
  setMode(mode: string): void;
43
+ get currentPermissions(): PermissionSettings;
44
+ setPermissions(permissions: Partial<PermissionSettings>): void;
43
45
  setConversationId(conversationId?: string): void;
44
46
  private scheduleRestart;
45
47
  private isProcessTreeAlive;
@@ -42,6 +42,11 @@ export function resolveDefaultAgyBinary(force = false) {
42
42
  path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
43
43
  path.join(programFiles, "Antigravity", "bin", "agy.exe"),
44
44
  path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
45
+ path.join(localAppData, "Google", "Antigravity", "agy.exe"),
46
+ path.join(localAppData, "Google", "Antigravity", "bin", "agy.exe"),
47
+ path.join(home, ".antigravity", "bin", "agy.exe"),
48
+ path.join(home, ".antigravity", "agy.exe"),
49
+ path.join(home, ".gemini", "antigravity-cli", "bin", "agy.exe"),
45
50
  path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
46
51
  path.join(home, ".local", "bin", "agy.exe"),
47
52
  path.join(appData, "npm", "agy.exe"),
@@ -79,7 +84,11 @@ export function resolveDefaultAgyBinary(force = false) {
79
84
  path.join(appData, "npm", "agy.cmd"),
80
85
  path.join(localAppData, "npm", "agy.cmd"),
81
86
  path.join(home, ".local", "bin", "agy.cmd"),
87
+ path.join(home, ".antigravity", "bin", "agy.cmd"),
88
+ path.join(home, ".gemini", "antigravity-cli", "bin", "agy.cmd"),
82
89
  path.join(appData, "npm", "agy.bat"),
90
+ path.join(localAppData, "npm", "agy.bat"),
91
+ path.join(home, ".local", "bin", "agy.bat"),
83
92
  ];
84
93
  for (const cand of batchCandidates) {
85
94
  if (fs.existsSync(cand)) {
@@ -189,6 +198,13 @@ export class AntigravityProcess extends EventEmitter {
189
198
  this.scheduleRestart();
190
199
  }
191
200
  }
201
+ get currentPermissions() {
202
+ return this.permissions;
203
+ }
204
+ setPermissions(permissions) {
205
+ this.permissions = { ...this.permissions, ...permissions };
206
+ this.scheduleRestart();
207
+ }
192
208
  setConversationId(conversationId) {
193
209
  if (this.conversationId !== conversationId) {
194
210
  this.conversationId = conversationId;
@@ -348,7 +364,7 @@ export class AntigravityProcess extends EventEmitter {
348
364
  effectiveEffort,
349
365
  });
350
366
  const isWin = process.platform === "win32";
351
- const isBatch = isWin && isWindowsBatchScript(this.binaryPath);
367
+ const isBatch = isWin && (isWindowsBatchScript(this.binaryPath) || this.binaryPath === "agy");
352
368
  const child = spawn(this.binaryPath, args, {
353
369
  cwd: this.cwd,
354
370
  env: this.env,
package/dist/asar.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface AsarNode {
2
+ files?: Record<string, AsarNode>;
3
+ size?: number;
4
+ offset?: string;
5
+ executable?: boolean;
6
+ unpacked?: boolean;
7
+ }
8
+ /**
9
+ * Reads and parses the ASAR archive header and returns the root node and base offset for data.
10
+ */
11
+ export declare function readArchiveHeader(archivePath: string): {
12
+ header: AsarNode;
13
+ dataBaseOffset: number;
14
+ };
15
+ /**
16
+ * Lists all file paths inside an ASAR archive.
17
+ */
18
+ export declare function listPackage(archivePath: string): string[];
19
+ /**
20
+ * Pure TypeScript, zero-dependency ASAR archive extractor.
21
+ * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
22
+ */
23
+ export declare function extractAll(archivePath: string, destDir: string): void;
24
+ /**
25
+ * Pure TypeScript, zero-dependency ASAR archive builder.
26
+ * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
27
+ */
28
+ export declare function createPackage(srcDir: string, destFile: string): Promise<void>;
package/dist/asar.js ADDED
@@ -0,0 +1,171 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Reads and parses the ASAR archive header and returns the root node and base offset for data.
5
+ */
6
+ export function readArchiveHeader(archivePath) {
7
+ if (!fs.existsSync(archivePath)) {
8
+ throw new Error(`ASAR archive not found: ${archivePath}`);
9
+ }
10
+ const fd = fs.openSync(archivePath, "r");
11
+ try {
12
+ const sizeBuf = Buffer.alloc(8);
13
+ const bytesRead = fs.readSync(fd, sizeBuf, 0, 8, 0);
14
+ if (bytesRead < 8) {
15
+ throw new Error(`Invalid ASAR archive: file too small (${bytesRead} bytes)`);
16
+ }
17
+ const payloadSize = sizeBuf.readUInt32LE(0);
18
+ const headerSize = sizeBuf.readUInt32LE(4);
19
+ if (payloadSize !== 4 || headerSize <= 0) {
20
+ throw new Error(`Invalid ASAR archive header size: payloadSize=${payloadSize}, headerSize=${headerSize}`);
21
+ }
22
+ const headerPickleBuf = Buffer.alloc(headerSize);
23
+ const headerRead = fs.readSync(fd, headerPickleBuf, 0, headerSize, 8);
24
+ if (headerRead < headerSize) {
25
+ throw new Error(`Truncated ASAR header: expected ${headerSize} bytes, got ${headerRead}`);
26
+ }
27
+ const strLen = headerPickleBuf.readUInt32LE(4);
28
+ const jsonStr = headerPickleBuf.subarray(8, 8 + strLen).toString("utf8");
29
+ const header = JSON.parse(jsonStr);
30
+ const dataBaseOffset = 8 + headerSize;
31
+ return { header, dataBaseOffset };
32
+ }
33
+ finally {
34
+ fs.closeSync(fd);
35
+ }
36
+ }
37
+ /**
38
+ * Lists all file paths inside an ASAR archive.
39
+ */
40
+ export function listPackage(archivePath) {
41
+ const { header } = readArchiveHeader(archivePath);
42
+ const result = [];
43
+ function walk(node, currentPrefix) {
44
+ if (node.files) {
45
+ for (const [name, child] of Object.entries(node.files)) {
46
+ const next = currentPrefix ? `${currentPrefix}/${name}` : `/${name}`;
47
+ walk(child, next);
48
+ }
49
+ }
50
+ else {
51
+ result.push(currentPrefix);
52
+ }
53
+ }
54
+ walk(header, "");
55
+ return result;
56
+ }
57
+ /**
58
+ * Pure TypeScript, zero-dependency ASAR archive extractor.
59
+ * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
60
+ */
61
+ export function extractAll(archivePath, destDir) {
62
+ const { header, dataBaseOffset } = readArchiveHeader(archivePath);
63
+ const fd = fs.openSync(archivePath, "r");
64
+ try {
65
+ function extractNode(node, currentPath, relParts = []) {
66
+ if (node.files) {
67
+ fs.mkdirSync(currentPath, { recursive: true });
68
+ for (const [name, child] of Object.entries(node.files)) {
69
+ extractNode(child, path.join(currentPath, name), [...relParts, name]);
70
+ }
71
+ }
72
+ else if (node.size !== undefined && node.offset !== undefined) {
73
+ const fileOffset = dataBaseOffset + parseInt(node.offset, 10);
74
+ const fileBuf = Buffer.alloc(node.size);
75
+ if (node.size > 0) {
76
+ fs.readSync(fd, fileBuf, 0, node.size, fileOffset);
77
+ }
78
+ fs.mkdirSync(path.dirname(currentPath), { recursive: true });
79
+ fs.writeFileSync(currentPath, fileBuf, {
80
+ mode: node.executable ? 0o755 : 0o644,
81
+ });
82
+ }
83
+ else if (node.unpacked) {
84
+ const unpackedSrc = path.join(archivePath + ".unpacked", ...relParts);
85
+ if (fs.existsSync(unpackedSrc)) {
86
+ fs.mkdirSync(path.dirname(currentPath), { recursive: true });
87
+ fs.copyFileSync(unpackedSrc, currentPath);
88
+ }
89
+ }
90
+ }
91
+ extractNode(header, destDir);
92
+ }
93
+ finally {
94
+ fs.closeSync(fd);
95
+ }
96
+ }
97
+ /**
98
+ * Pure TypeScript, zero-dependency ASAR archive builder.
99
+ * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
100
+ */
101
+ export async function createPackage(srcDir, destFile) {
102
+ const files = [];
103
+ function walk(dir, rel = "") {
104
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
105
+ entries.sort((a, b) => a.name.localeCompare(b.name));
106
+ for (const e of entries) {
107
+ const entryRel = rel ? `${rel}/${e.name}` : e.name;
108
+ const full = path.join(dir, e.name);
109
+ if (e.isDirectory()) {
110
+ walk(full, entryRel);
111
+ }
112
+ else if (e.isFile()) {
113
+ const stat = fs.statSync(full);
114
+ const executable = Boolean(stat.mode & 0o111);
115
+ files.push({
116
+ relPath: entryRel,
117
+ fullPath: full,
118
+ size: stat.size,
119
+ executable: executable || undefined,
120
+ });
121
+ }
122
+ }
123
+ }
124
+ walk(srcDir);
125
+ const header = { files: {} };
126
+ let currentOffset = 0;
127
+ for (const f of files) {
128
+ const parts = f.relPath.split("/");
129
+ let curr = header.files;
130
+ for (let i = 0; i < parts.length - 1; i++) {
131
+ if (!curr[parts[i]]) {
132
+ curr[parts[i]] = { files: {} };
133
+ }
134
+ curr = curr[parts[i]].files;
135
+ }
136
+ const leaf = parts[parts.length - 1];
137
+ curr[leaf] = {
138
+ size: f.size,
139
+ offset: String(currentOffset),
140
+ executable: f.executable,
141
+ };
142
+ currentOffset += f.size;
143
+ }
144
+ const jsonBuf = Buffer.from(JSON.stringify(header), "utf8");
145
+ const alignedJsonLen = (jsonBuf.length + 3) & ~3;
146
+ const headerPayloadSize = 4 + alignedJsonLen;
147
+ // Header pickle: payloadSize (uint32LE) + stringLength (uint32LE) + json bytes (padded to multiple of 4)
148
+ const headerPickle = Buffer.alloc(4 + headerPayloadSize, 0);
149
+ headerPickle.writeUInt32LE(headerPayloadSize, 0);
150
+ headerPickle.writeUInt32LE(jsonBuf.length, 4);
151
+ jsonBuf.copy(headerPickle, 8);
152
+ // Size pickle: 4 (uint32LE payload size) + headerPickle.length (uint32LE)
153
+ const sizePickle = Buffer.alloc(8, 0);
154
+ sizePickle.writeUInt32LE(4, 0);
155
+ sizePickle.writeUInt32LE(headerPickle.length, 4);
156
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
157
+ const fd = fs.openSync(destFile, "w");
158
+ try {
159
+ fs.writeSync(fd, sizePickle);
160
+ fs.writeSync(fd, headerPickle);
161
+ for (const f of files) {
162
+ if (f.size > 0) {
163
+ const content = fs.readFileSync(f.fullPath);
164
+ fs.writeSync(fd, content);
165
+ }
166
+ }
167
+ }
168
+ finally {
169
+ fs.closeSync(fd);
170
+ }
171
+ }
@@ -7,6 +7,9 @@ const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
7
7
  function getStateRoot() {
8
8
  if (process.env.AGY_ACP_STATE_DIR)
9
9
  return process.env.AGY_ACP_STATE_DIR;
10
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
11
+ return path.join(process.env.LOCALAPPDATA, "agy-acp");
12
+ }
10
13
  const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
11
14
  return path.join(stateHome, "agy-acp");
12
15
  }
package/dist/index.js CHANGED
@@ -32,18 +32,22 @@ Commands:
32
32
  doctor Diagnose Antigravity binary, quota provider, and Paseo status
33
33
 
34
34
  Options:
35
- --acp Start ACP server over stdio (default)
36
- --setup Integrate Antigravity with local Paseo server installation
37
- --doctor Run environment, binary, and telemetry diagnostics
38
- -v, --version Show version
39
- --json Show version in JSON format (with --version)
40
- -h, --help Show help
35
+ --acp Start ACP server over stdio (default)
36
+ --setup Integrate Antigravity with local Paseo server installation
37
+ --doctor Run environment, binary, and telemetry diagnostics
38
+ --skip-naration Drop leading narration-only assistant chunks (e.g. 'I will...')
39
+ --skip-narration Alias for --skip-naration
40
+ -v, --version Show version
41
+ --json Show version in JSON format (with --version)
42
+ -h, --help Show help
41
43
 
42
44
  Environment Variables:
43
45
  AGY_ACP_LOG_LEVEL debug | info | warn | error (default: info)
44
46
  AGY_ACP_LOG_DIR Directory for log files
45
47
  AGY_ACP_SANDBOX Set to 'true' to run agy in sandbox mode
46
48
  AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS Set to 'true' to auto-approve tool permissions
49
+ AGY_ACP_SKIP_NARRATION Set to 'true' to drop leading narration chunks
50
+ AGY_EXTRA_ARGS Extra space-separated CLI arguments passed to agy
47
51
  AGY_BIN_PATH Path to agy binary (default: agy in PATH or ~/.local/bin/agy)
48
52
  PASEO_SERVER_PATH Path to local @getpaseo/server directory
49
53
  PASEO_ASAR_PATH Path to local Paseo app.asar package
@@ -150,7 +154,14 @@ if (args.includes("doctor") || args.includes("--doctor")) {
150
154
  const paseoRunning = isPaseoRunning();
151
155
  if (paseoRunning) {
152
156
  process.stdout.write(" [!] Paseo process is currently running.\n");
153
- process.stdout.write(" Note: If you run setup/patch, please close Paseo completely (check system tray and Task Manager) to avoid EBUSY file locking.\n");
157
+ if (isWin) {
158
+ process.stdout.write(" Tip: To restart Paseo completely on Windows, run in PowerShell:\n");
159
+ process.stdout.write(" Stop-Process -Name \"Paseo\" -Force -ErrorAction SilentlyContinue\n");
160
+ process.stdout.write(" Then launch Paseo again.\n");
161
+ }
162
+ else {
163
+ process.stdout.write(" Tip: Run 'paseo daemon restart' or close the Paseo app to reload.\n");
164
+ }
154
165
  }
155
166
  else {
156
167
  process.stdout.write(" [OK] Paseo is not currently running (safe to patch/update).\n");
@@ -180,7 +191,7 @@ if (args.includes("doctor") || args.includes("--doctor")) {
180
191
  hasIssue = true;
181
192
  if (paseoRunning) {
182
193
  process.stdout.write(` - Found ${unpatchedCount} unpatched Paseo target(s), but Paseo is currently running.\n`);
183
- process.stdout.write(" Action: Close Paseo.exe completely, then run:\n");
194
+ process.stdout.write(" Action: Close Paseo completely (Stop-Process -Name \"Paseo\" -Force), then run:\n");
184
195
  process.stdout.write(" npx -y paseo-acp-agy setup\n");
185
196
  }
186
197
  else {
@@ -221,12 +232,21 @@ if (args.includes("setup") ||
221
232
  const allPatched = [...res.patchedPaths, ...(res.patchedAsarPaths || [])];
222
233
  if (allPatched.length > 0) {
223
234
  process.stdout.write(`Successfully integrated with: \n${allPatched.map((p) => ` - ${p}`).join("\n")}\n\n` +
224
- `Antigravity quota provider and context-window telemetry are now enabled!\n` +
225
- `Please restart Paseo (or run 'paseo daemon restart') to apply changes.\n`);
235
+ `Antigravity quota provider and context-window telemetry are now enabled!\n`);
226
236
  }
227
237
  else {
228
238
  process.stdout.write("Paseo is already up-to-date and configured for Antigravity telemetry.\n");
229
239
  }
240
+ if (isPaseoRunning()) {
241
+ process.stdout.write(`\n⚠️ IMPORTANT: Paseo is currently running in the background!\n` +
242
+ ` Windows caches running executables in memory, so changes will only take effect after restarting Paseo.\n` +
243
+ ` In PowerShell, run:\n` +
244
+ ` Stop-Process -Name "Paseo" -Force -ErrorAction SilentlyContinue\n` +
245
+ ` Then launch Paseo again.\n\n`);
246
+ }
247
+ else {
248
+ process.stdout.write(`Please start Paseo to apply changes.\n\n`);
249
+ }
230
250
  if (res.errors.length > 0) {
231
251
  process.stderr.write(`Notice: Some paths could not be modified (may require admin/close Paseo):\n${res.errors.map(e => ` - ${e}`).join("\n")}\n`);
232
252
  }
@@ -239,7 +259,10 @@ if (args.includes("setup") ||
239
259
  }
240
260
  // Auto-run integration in background when starting ACP server
241
261
  void ensurePaseoIntegration().catch(() => { });
242
- const server = new ACPServer();
262
+ const skipNarration = args.includes("--skip-naration") ||
263
+ args.includes("--skip-narration") ||
264
+ process.env.AGY_ACP_SKIP_NARRATION === "true";
265
+ const server = new ACPServer({ skipNarration });
243
266
  const cleanup = async () => {
244
267
  try {
245
268
  await server.stop();
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { logger } from "./logger.js";
6
+ import { extractAll, createPackage, listPackage } from "./asar.js";
6
7
  /**
7
8
  * Searches the host machine for @getpaseo/server installations across
8
9
  * Windows, macOS, and Linux.
@@ -160,7 +161,12 @@ export function findPaseoServerInstallations() {
160
161
  }
161
162
  // Filter out any paths that do not actually have a dist directory or package.json
162
163
  const verified = [];
164
+ const seen = new Set();
163
165
  for (const dir of candidates) {
166
+ const key = process.platform === "win32" ? dir.toLowerCase() : dir;
167
+ if (seen.has(key))
168
+ continue;
169
+ seen.add(key);
164
170
  if (fs.existsSync(path.join(dir, "dist")) || fs.existsSync(path.join(dir, "package.json"))) {
165
171
  verified.push(dir);
166
172
  }
@@ -208,6 +214,11 @@ function resolveAgyBinary() {
208
214
  path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
209
215
  path.join(programFiles, "Antigravity", "bin", "agy.exe"),
210
216
  path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
217
+ path.join(localAppData, "Google", "Antigravity", "agy.exe"),
218
+ path.join(localAppData, "Google", "Antigravity", "bin", "agy.exe"),
219
+ path.join(home, ".antigravity", "bin", "agy.exe"),
220
+ path.join(home, ".antigravity", "agy.exe"),
221
+ path.join(home, ".gemini", "antigravity-cli", "bin", "agy.exe"),
211
222
  path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
212
223
  path.join(home, ".local", "bin", "agy.exe"),
213
224
  path.join(appData, "npm", "agy.exe"),
@@ -241,6 +252,8 @@ function resolveAgyBinary() {
241
252
  path.join(appData, "npm", "agy.cmd"),
242
253
  path.join(localAppData, "npm", "agy.cmd"),
243
254
  path.join(home, ".local", "bin", "agy.cmd"),
255
+ path.join(home, ".antigravity", "bin", "agy.cmd"),
256
+ path.join(home, ".gemini", "antigravity-cli", "bin", "agy.cmd"),
244
257
  path.join(appData, "npm", "agy.bat"),
245
258
  path.join(localAppData, "npm", "agy.bat"),
246
259
  path.join(home, ".local", "bin", "agy.bat"),
@@ -290,13 +303,48 @@ export class AntigravityQuotaProvider {
290
303
  const isWin = process.platform === "win32";
291
304
  const bin = resolveAgyBinary();
292
305
  this.binaryPath = bin;
293
- const isBatch = isWin && /\\.(cmd|bat)$/i.test(bin);
306
+ const isBatch = isWin && (!bin.toLowerCase().endsWith(".exe"));
307
+ const home = os.homedir();
308
+ const appData = process.env.APPDATA || (home ? path.join(home, "AppData", "Roaming") : "");
309
+ const localAppData = process.env.LOCALAPPDATA || (home ? path.join(home, "AppData", "Local") : "");
310
+ const programFiles = process.env.ProgramFiles || "C:\\\\Program Files";
311
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\\\Program Files (x86)";
312
+
313
+ const extraPaths = isWin ? [
314
+ path.join(appData, "npm"),
315
+ path.join(localAppData, "npm"),
316
+ path.join(programFiles, "nodejs"),
317
+ path.join(programFilesX86, "nodejs"),
318
+ path.join(localAppData, "Programs", "Antigravity", "bin"),
319
+ path.join(localAppData, "Programs", "antigravity"),
320
+ path.join(home, ".antigravity", "bin"),
321
+ path.join(home, ".local", "bin"),
322
+ ].filter(p => fs.existsSync(p)) : [];
323
+
324
+ const env = { ...process.env };
325
+ if (extraPaths.length > 0) {
326
+ const currentPath = env.PATH || env.Path || "";
327
+ const newPath = (currentPath ? currentPath + ";" : "") + extraPaths.join(";");
328
+ env.PATH = newPath;
329
+ env.Path = newPath;
330
+ }
331
+
294
332
  const [usageRes, creditsRes] = await Promise.allSettled([
295
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/usage"], { timeout: 15000, env: process.env, shell: isBatch, windowsHide: true }),
296
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/credits"], { timeout: 15000, env: process.env, shell: isBatch, windowsHide: true }),
333
+ execFileAsync(bin, ["--print-timeout", "24h", "--print", "/usage"], { timeout: 15000, env, shell: isBatch, windowsHide: true }),
334
+ execFileAsync(bin, ["--print-timeout", "24h", "--print", "/credits"], { timeout: 15000, env, shell: isBatch, windowsHide: true }),
297
335
  ]);
298
336
 
299
- const rawUsageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
337
+ if (usageRes.status === "rejected") {
338
+ const reason = usageRes.reason;
339
+ const msg = reason instanceof Error ? reason.message : String(reason);
340
+ return unavailableUsage({
341
+ providerId: this.providerId,
342
+ displayName: "Antigravity",
343
+ error: \`Quota fetch failed: \${msg}\`,
344
+ });
345
+ }
346
+
347
+ const rawUsageOut = usageRes.value.stdout || usageRes.value.stderr || "";
300
348
  const rawCreditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
301
349
 
302
350
  const usageOut = (rawUsageOut || "").replace(/\\r\\n/g, "\\n");
@@ -317,7 +365,14 @@ export class AntigravityQuotaProvider {
317
365
  scope = m[1].trim();
318
366
  limitType = m[2].trim();
319
367
  remainingPct = parseInt(m[3], 10);
320
- resetsAt = m[4] ? new Date(m[4].trim()).toISOString() : null;
368
+ if (m[4]) {
369
+ try {
370
+ const d = new Date(m[4].trim());
371
+ resetsAt = isNaN(d.getTime()) ? null : d.toISOString();
372
+ } catch {
373
+ resetsAt = null;
374
+ }
375
+ }
321
376
  } else {
322
377
  const parts = trimmed.split(/\\t+|\\s{2,}/).map(p => p.trim());
323
378
  if (parts.length >= 3) {
@@ -325,7 +380,14 @@ export class AntigravityQuotaProvider {
325
380
  limitType = parts[1];
326
381
  const remMatch = parts[2].match(/(\\d+)%/);
327
382
  if (remMatch) remainingPct = parseInt(remMatch[1], 10);
328
- resetsAt = parts[3] ? new Date(parts[3]).toISOString() : null;
383
+ if (parts[3]) {
384
+ try {
385
+ const d = new Date(parts[3]);
386
+ resetsAt = isNaN(d.getTime()) ? null : d.toISOString();
387
+ } catch {
388
+ resetsAt = null;
389
+ }
390
+ }
329
391
  }
330
392
  }
331
393
 
@@ -649,7 +711,16 @@ export function findPaseoAsarPaths() {
649
711
  candidates.add(path.resolve(loc));
650
712
  }
651
713
  }
652
- return Array.from(candidates);
714
+ const result = [];
715
+ const seen = new Set();
716
+ for (const loc of candidates) {
717
+ const key = process.platform === "win32" ? loc.toLowerCase() : loc;
718
+ if (!seen.has(key)) {
719
+ seen.add(key);
720
+ result.push(loc);
721
+ }
722
+ }
723
+ return result;
653
724
  }
654
725
  /**
655
726
  * Extracts, patches, and repacks a Paseo app.asar archive to integrate Antigravity
@@ -663,11 +734,8 @@ export async function patchPaseoAsar(asarPath) {
663
734
  if (!fs.existsSync(asarPath)) {
664
735
  return { success: false, changes: [], error: `Asar archive not found: ${asarPath}` };
665
736
  }
666
- // Dynamic import of @electron/asar
667
- const asarModule = await import("@electron/asar");
668
- const asar = asarModule.default || asarModule;
669
737
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-asar-extract-"));
670
- asar.extractAll(asarPath, tempDir);
738
+ extractAll(asarPath, tempDir);
671
739
  // Look for server directory in extracted files
672
740
  const serverCandidates = [
673
741
  path.join(tempDir, "node_modules", "@getpaseo", "server"),
@@ -722,7 +790,7 @@ export async function patchPaseoAsar(asarPath) {
722
790
  }
723
791
  }
724
792
  tempAsar = path.join(os.tmpdir(), `app-${Date.now()}.asar`);
725
- await asar.createPackage(tempDir, tempAsar);
793
+ await createPackage(tempDir, tempAsar);
726
794
  // Replace original archive with locked file handling for Windows
727
795
  try {
728
796
  fs.copyFileSync(tempAsar, asarPath);
@@ -871,9 +939,7 @@ export async function isPaseoAsarPatched(asarPath) {
871
939
  try {
872
940
  if (!fs.existsSync(asarPath))
873
941
  return false;
874
- const asarModule = await import("@electron/asar");
875
- const asar = asarModule.default || asarModule;
876
- const files = asar.listPackage(asarPath);
942
+ const files = listPackage(asarPath);
877
943
  return files.some((f) => f.includes("antigravity.js"));
878
944
  }
879
945
  catch {
@@ -12,4 +12,5 @@ export declare function resolvePermissionSettings(options?: {
12
12
  addDirs?: string[];
13
13
  printTimeout?: string;
14
14
  }): PermissionSettings;
15
+ export declare function parseExtraArgs(raw?: string): string[];
15
16
  export declare function buildAgyArgs(settings: PermissionSettings, extraArgs?: string[]): string[];
@@ -20,6 +20,14 @@ export function resolvePermissionSettings(options) {
20
20
  printTimeout,
21
21
  };
22
22
  }
23
+ export function parseExtraArgs(raw) {
24
+ if (!raw || !raw.trim())
25
+ return [];
26
+ const matches = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
27
+ if (!matches)
28
+ return [];
29
+ return matches.map((m) => m.replace(/^["']|["']$/g, ""));
30
+ }
23
31
  export function buildAgyArgs(settings, extraArgs) {
24
32
  const args = ["--input-format", "stream-json", "--output-format", "stream-json", "--print="];
25
33
  if (settings.printTimeout) {
@@ -41,6 +49,10 @@ export function buildAgyArgs(settings, extraArgs) {
41
49
  }
42
50
  }
43
51
  }
52
+ const envExtra = parseExtraArgs(process.env.AGY_EXTRA_ARGS);
53
+ if (envExtra.length > 0) {
54
+ args.push(...envExtra);
55
+ }
44
56
  if (extraArgs && extraArgs.length > 0) {
45
57
  args.push(...extraArgs);
46
58
  }
@@ -113,10 +113,13 @@ export declare const ACP_METHODS: {
113
113
  readonly SESSION_CLOSE: "session/close";
114
114
  readonly SESSION_CLOSE_ALIAS: "unstable_closeSession";
115
115
  readonly SESSION_SET_MODE: "session/set_mode";
116
+ readonly SESSION_SET_MODE_CAMEL: "session/setMode";
116
117
  readonly SESSION_SET_MODE_ALIAS: "setSessionMode";
117
118
  readonly SESSION_SET_MODEL: "session/set_model";
119
+ readonly SESSION_SET_MODEL_CAMEL: "session/setModel";
118
120
  readonly SESSION_SET_MODEL_ALIAS: "unstable_setSessionModel";
119
121
  readonly SESSION_SET_CONFIG_OPTION: "session/set_config_option";
122
+ readonly SESSION_SET_CONFIG_OPTION_CAMEL: "session/setConfigOption";
120
123
  readonly SESSION_SET_CONFIG_OPTION_ALIAS: "setSessionConfigOption";
121
124
  readonly SESSION_UPDATE: "session/update";
122
125
  };
@@ -193,6 +196,11 @@ export declare const AVAILABLE_MODES: {
193
196
  name: string;
194
197
  description: string;
195
198
  }[];
199
+ export declare const AVAILABLE_PERMISSIONS: {
200
+ id: string;
201
+ name: string;
202
+ description: string;
203
+ }[];
196
204
  export declare function buildConfigOptionsForModel(modelId: string, currentEffort?: string, availableModels?: ModelDefinition[]): {
197
205
  id: string;
198
206
  name: string;
@@ -205,3 +213,40 @@ export declare function buildConfigOptionsForModel(modelId: string, currentEffor
205
213
  description: string;
206
214
  }[];
207
215
  }[];
216
+ export declare function buildConfigOptionsForSession(options: {
217
+ modelId: string;
218
+ currentEffort?: string;
219
+ currentMode?: string;
220
+ currentPermission?: string;
221
+ availableModels?: ModelDefinition[];
222
+ }): {
223
+ id: string;
224
+ name: string;
225
+ category: string;
226
+ type: "select";
227
+ currentValue: string;
228
+ options: {
229
+ value: string;
230
+ name: string;
231
+ description: string;
232
+ }[];
233
+ }[];
234
+ /**
235
+ * Escapes characters in URLs/paths for Markdown image/link syntax.
236
+ * Escapes backslashes `\` and parentheses `)` to prevent premature link termination,
237
+ * especially crucial for Windows paths (e.g. C:\path\img (1).png).
238
+ */
239
+ export declare function escapeMarkdownUrl(urlOrPath: string): string;
240
+ /**
241
+ * Normalizes an image path or file URL and formats it as an inline Markdown image link.
242
+ */
243
+ export declare function formatImageMarkdown(filePathOrUrl: string): string;
244
+ /**
245
+ * Extracts an image reference from a tool execution output or parameters,
246
+ * returning a formatted Markdown image string if detected.
247
+ */
248
+ export declare function extractImageMarkdownLink(toolName: string, output: unknown, parameters?: Record<string, unknown>): string | null;
249
+ /**
250
+ * Detects whether text consists entirely of conversational narration (e.g. "I will...", "I'll...").
251
+ */
252
+ export declare function isNarrationText(text: string): boolean;
package/dist/protocol.js CHANGED
@@ -23,10 +23,13 @@ export const ACP_METHODS = {
23
23
  SESSION_CLOSE: "session/close",
24
24
  SESSION_CLOSE_ALIAS: "unstable_closeSession",
25
25
  SESSION_SET_MODE: "session/set_mode",
26
+ SESSION_SET_MODE_CAMEL: "session/setMode",
26
27
  SESSION_SET_MODE_ALIAS: "setSessionMode",
27
28
  SESSION_SET_MODEL: "session/set_model",
29
+ SESSION_SET_MODEL_CAMEL: "session/setModel",
28
30
  SESSION_SET_MODEL_ALIAS: "unstable_setSessionModel",
29
31
  SESSION_SET_CONFIG_OPTION: "session/set_config_option",
32
+ SESSION_SET_CONFIG_OPTION_CAMEL: "session/setConfigOption",
30
33
  SESSION_SET_CONFIG_OPTION_ALIAS: "setSessionConfigOption",
31
34
  SESSION_UPDATE: "session/update",
32
35
  };
@@ -300,7 +303,7 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
300
303
  now - lastProviderUsageFetch < PROVIDER_USAGE_CACHE_TTL_MS) {
301
304
  return cachedProviderUsage;
302
305
  }
303
- const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
306
+ const isBatch = process.platform === "win32" && (isWindowsBatchScript(binaryPath) || binaryPath === "agy");
304
307
  const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
305
308
  try {
306
309
  const [usageResult, creditsResult] = await Promise.allSettled([
@@ -438,7 +441,7 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
438
441
  return inFlightModelFetch;
439
442
  }
440
443
  inFlightModelFetch = (async () => {
441
- const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
444
+ const isBatch = process.platform === "win32" && (isWindowsBatchScript(binaryPath) || binaryPath === "agy");
442
445
  const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
443
446
  try {
444
447
  const { stdout } = await execFileAsync(cmd, ["models"], {
@@ -485,6 +488,12 @@ export function getEffectiveEffortForModel(modelId, requestedEffort, models) {
485
488
  export const AVAILABLE_MODES = [
486
489
  { id: "default", name: "Default (Accept Edits)", description: "Standard autonomous execution and coding mode" },
487
490
  { id: "plan", name: "Plan Mode", description: "Planning and read-only analysis without file modifications" },
491
+ { id: "accept-edits", name: "Accept Edits", description: "Run agy with --mode accept-edits" },
492
+ ];
493
+ export const AVAILABLE_PERMISSIONS = [
494
+ { id: "default", name: "Default", description: "Standard permission flow" },
495
+ { id: "sandbox", name: "Sandbox", description: "Run agy with --sandbox" },
496
+ { id: "bypass", name: "Bypass", description: "Run agy with --dangerously-skip-permissions" },
488
497
  ];
489
498
  export function buildConfigOptionsForModel(modelId, currentEffort = "high", availableModels) {
490
499
  const modelList = availableModels || cachedModels || FALLBACK_MODELS;
@@ -513,3 +522,118 @@ export function buildConfigOptionsForModel(modelId, currentEffort = "high", avai
513
522
  },
514
523
  ];
515
524
  }
525
+ export function buildConfigOptionsForSession(options) {
526
+ const modelOptions = buildConfigOptionsForModel(options.modelId, options.currentEffort, options.availableModels);
527
+ const modeOption = {
528
+ id: "mode",
529
+ name: "Execution Mode",
530
+ category: "mode",
531
+ type: "select",
532
+ currentValue: options.currentMode || "default",
533
+ options: AVAILABLE_MODES.map((m) => ({
534
+ value: m.id,
535
+ name: m.name,
536
+ description: m.description,
537
+ })),
538
+ };
539
+ const permissionOption = {
540
+ id: "permission",
541
+ name: "Permission Policy",
542
+ category: "permission",
543
+ type: "select",
544
+ currentValue: options.currentPermission || "default",
545
+ options: AVAILABLE_PERMISSIONS.map((p) => ({
546
+ value: p.id,
547
+ name: p.name,
548
+ description: p.description,
549
+ })),
550
+ };
551
+ return [...modelOptions, modeOption, permissionOption];
552
+ }
553
+ /**
554
+ * Escapes characters in URLs/paths for Markdown image/link syntax.
555
+ * Escapes backslashes `\` and parentheses `)` to prevent premature link termination,
556
+ * especially crucial for Windows paths (e.g. C:\path\img (1).png).
557
+ */
558
+ export function escapeMarkdownUrl(urlOrPath) {
559
+ return urlOrPath.replace(/\\/g, "\\\\").replace(/\)/g, "\\)");
560
+ }
561
+ /**
562
+ * Normalizes an image path or file URL and formats it as an inline Markdown image link.
563
+ */
564
+ export function formatImageMarkdown(filePathOrUrl) {
565
+ let clean = filePathOrUrl.trim();
566
+ if (clean.startsWith("file://")) {
567
+ try {
568
+ clean = fileURLToPath(clean);
569
+ }
570
+ catch {
571
+ clean = clean.replace(/^file:\/\//, "");
572
+ if (process.platform === "win32" && /^\/[a-zA-Z]:/.test(clean)) {
573
+ clean = clean.slice(1);
574
+ }
575
+ }
576
+ }
577
+ return `![Generated image](${escapeMarkdownUrl(clean)})`;
578
+ }
579
+ /**
580
+ * Extracts an image reference from a tool execution output or parameters,
581
+ * returning a formatted Markdown image string if detected.
582
+ */
583
+ export function extractImageMarkdownLink(toolName, output, parameters) {
584
+ const name = toolName.toLowerCase();
585
+ const isImageTool = name.includes("image") || name.includes("generate_image");
586
+ let rawPath = null;
587
+ if (typeof output === "string") {
588
+ const fileUriMatch = output.match(/file:\/\/[^\s)"]+\.(png|jpe?g|webp|gif|svg|bmp)/i);
589
+ if (fileUriMatch) {
590
+ rawPath = fileUriMatch[0];
591
+ }
592
+ else {
593
+ const winPathMatch = output.match(/[a-zA-Z]:\\[^\s)"]+\.(png|jpe?g|webp|gif|svg|bmp)/i);
594
+ if (winPathMatch) {
595
+ rawPath = winPathMatch[0];
596
+ }
597
+ else {
598
+ const posixPathMatch = output.match(/\/[^\s)"]+\.(png|jpe?g|webp|gif|svg|bmp)/i);
599
+ if (posixPathMatch) {
600
+ rawPath = posixPathMatch[0];
601
+ }
602
+ }
603
+ }
604
+ }
605
+ else if (output && typeof output === "object") {
606
+ const obj = output;
607
+ if (typeof obj.path === "string")
608
+ rawPath = obj.path;
609
+ else if (typeof obj.filePath === "string")
610
+ rawPath = obj.filePath;
611
+ else if (typeof obj.uri === "string")
612
+ rawPath = obj.uri;
613
+ }
614
+ if (!rawPath && isImageTool && parameters) {
615
+ if (typeof parameters.TargetFile === "string" &&
616
+ /\.(png|jpe?g|webp|gif|svg|bmp)$/i.test(parameters.TargetFile)) {
617
+ rawPath = parameters.TargetFile;
618
+ }
619
+ }
620
+ if (!rawPath)
621
+ return null;
622
+ return formatImageMarkdown(rawPath);
623
+ }
624
+ /**
625
+ * Detects whether text consists entirely of conversational narration (e.g. "I will...", "I'll...").
626
+ */
627
+ export function isNarrationText(text) {
628
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
629
+ if (lines.length === 0)
630
+ return false;
631
+ return lines.every((l) => {
632
+ const lower = l.toLowerCase();
633
+ return (lower.startsWith("i will") ||
634
+ lower.startsWith("i'll") ||
635
+ lower.startsWith("i’ll") ||
636
+ lower.startsWith("let me") ||
637
+ lower.startsWith("i am going to"));
638
+ });
639
+ }
@@ -14,6 +14,7 @@ export interface PersistedSessionState {
14
14
  model: string;
15
15
  effort: string;
16
16
  mode: string;
17
+ permission?: string;
17
18
  usage?: SessionUsageState;
18
19
  updatedAt: string;
19
20
  }
@@ -7,6 +7,9 @@ function getStateRoot() {
7
7
  if (process.env.AGY_ACP_STATE_DIR) {
8
8
  return process.env.AGY_ACP_STATE_DIR;
9
9
  }
10
+ if (process.platform === "win32" && process.env.LOCALAPPDATA) {
11
+ return path.join(process.env.LOCALAPPDATA, "agy-acp");
12
+ }
10
13
  const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
11
14
  return path.join(stateHome, "agy-acp");
12
15
  }
package/dist/session.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface SessionOptions {
6
6
  model?: string;
7
7
  effort?: string;
8
8
  mode?: string;
9
+ permission?: string;
9
10
  conversationId?: string;
10
11
  usage?: SessionUsageState;
11
12
  sandbox?: boolean;
@@ -20,6 +21,7 @@ export declare class Session {
20
21
  model: string;
21
22
  effort: string;
22
23
  mode: string;
24
+ permission: string;
23
25
  readonly createdAt: Date;
24
26
  lastActivity: Date;
25
27
  isCancelled: boolean;
@@ -36,6 +38,7 @@ export declare class Session {
36
38
  setModel(model: string): void;
37
39
  setEffort(effort: string): void;
38
40
  setMode(mode: string): void;
41
+ setPermission(permission: string): void;
39
42
  setConversationId(conversationId?: string): void;
40
43
  ensureReadyForResume(): Promise<void>;
41
44
  resumeConversation(conversationId: string): Promise<void>;
package/dist/session.js CHANGED
@@ -10,6 +10,7 @@ export class Session {
10
10
  model;
11
11
  effort;
12
12
  mode;
13
+ permission;
13
14
  createdAt;
14
15
  lastActivity;
15
16
  isCancelled = false;
@@ -23,6 +24,9 @@ export class Session {
23
24
  this.model = options.model || "gemini-3.7-flash";
24
25
  this.effort = options.effort || "high";
25
26
  this.mode = options.mode || "default";
27
+ this.permission =
28
+ options.permission ||
29
+ (options.sandbox ? "sandbox" : options.dangerouslySkipPermissions ? "bypass" : "default");
26
30
  this.createdAt = new Date();
27
31
  this.lastActivity = new Date();
28
32
  this.store = options.store || sessionStore;
@@ -35,9 +39,11 @@ export class Session {
35
39
  contextWindowUsedTokens: 0,
36
40
  contextWindowMaxTokens: getModelContextWindow(this.model),
37
41
  };
42
+ const isSandbox = this.permission === "sandbox" || options.sandbox;
43
+ const isBypass = this.permission === "bypass" || options.dangerouslySkipPermissions;
38
44
  const permissions = resolvePermissionSettings({
39
- sandbox: options.sandbox,
40
- dangerouslySkipPermissions: options.dangerouslySkipPermissions,
45
+ sandbox: isSandbox,
46
+ dangerouslySkipPermissions: isBypass,
41
47
  mode: this.mode,
42
48
  });
43
49
  this.process = new AntigravityProcess({
@@ -62,6 +68,7 @@ export class Session {
62
68
  model: this.model,
63
69
  effort: this.effort,
64
70
  mode: this.mode,
71
+ permission: this.permission,
65
72
  usage: this.usage,
66
73
  updatedAt: new Date().toISOString(),
67
74
  };
@@ -98,6 +105,16 @@ export class Session {
98
105
  this.process.setMode(mode);
99
106
  this.persist();
100
107
  }
108
+ setPermission(permission) {
109
+ this.permission = permission;
110
+ const isSandbox = permission === "sandbox";
111
+ const isBypass = permission === "bypass";
112
+ this.process.setPermissions({
113
+ sandbox: isSandbox,
114
+ dangerouslySkipPermissions: isBypass || (!isSandbox && this.process.currentPermissions.dangerouslySkipPermissions),
115
+ });
116
+ this.persist();
117
+ }
101
118
  setConversationId(conversationId) {
102
119
  this.process.setConversationId(conversationId);
103
120
  this.persist();
@@ -185,6 +202,7 @@ export class SessionManager {
185
202
  model: options.model || persisted?.model,
186
203
  effort: options.effort || persisted?.effort,
187
204
  mode: options.mode || persisted?.mode,
205
+ permission: options.permission || persisted?.permission,
188
206
  conversationId: options.conversationId || persisted?.conversationId,
189
207
  usage: options.usage || persisted?.usage,
190
208
  sandbox: options.sandbox,
@@ -337,7 +337,7 @@ export function formatUsageOutput(rawText) {
337
337
  return out;
338
338
  }
339
339
  async function runAgySlash(binaryPath, cwd, slashCommand) {
340
- const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
340
+ const isBatch = process.platform === "win32" && (isWindowsBatchScript(binaryPath) || binaryPath === "agy");
341
341
  const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
342
342
  const { stdout, stderr } = await execFileAsync(cmd, ["--print-timeout", "24h", "--print", slashCommand], {
343
343
  cwd,
package/dist/version.js CHANGED
@@ -16,7 +16,7 @@ function getPackageVersion() {
16
16
  catch {
17
17
  // ignore
18
18
  }
19
- return "1.1.0";
19
+ return "1.2.1";
20
20
  }
21
21
  export const SEMVER_VERSION = getPackageVersion();
22
22
  let cachedBuildMetadata = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "description": "ACP (Agent Client Protocol) provider for Google Antigravity in Paseo and Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,10 +43,11 @@
43
43
  "bugs": {
44
44
  "url": "https://github.com/tucomel/paseo-acp-agy/issues"
45
45
  },
46
- "homepage": "https://github.com/tucomel/paseo-acp-agy#readme",
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
47
49
  "dependencies": {
48
- "@agentclientprotocol/sdk": "^0.17.1",
49
- "@electron/asar": "^4.3.0"
50
+ "@agentclientprotocol/sdk": "^0.17.1"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^22.0.0",