paseo-acp-agy 1.1.9 → 1.2.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.
@@ -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) {
@@ -329,11 +338,19 @@ export class ACPServer {
329
338
  this.sendSuccess(id, { stopReason: "end_turn" });
330
339
  break;
331
340
  }
341
+ let hasSentNonNarration = false;
332
342
  const onStepUpdate = (event) => {
333
343
  const step = event.step_update;
334
344
  if (!step)
335
345
  return;
336
346
  if (step.step_type === "agent_response" && step.text_delta) {
347
+ if (this.skipNarration && !hasSentNonNarration) {
348
+ if (isNarrationText(step.text_delta)) {
349
+ logger.debug("Skipping pure narration text chunk", { delta: step.text_delta });
350
+ return;
351
+ }
352
+ hasSentNonNarration = true;
353
+ }
337
354
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
338
355
  sessionId: session.id,
339
356
  update: {
@@ -343,6 +360,7 @@ export class ACPServer {
343
360
  });
344
361
  }
345
362
  else if (step.step_type === "thought" && step.text_delta) {
363
+ hasSentNonNarration = true;
346
364
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
347
365
  sessionId: session.id,
348
366
  update: {
@@ -352,6 +370,7 @@ export class ACPServer {
352
370
  });
353
371
  }
354
372
  else if (step.step_type === "tool" && step.tool_info) {
373
+ hasSentNonNarration = true;
355
374
  const toolCallId = `tool_${step.step_index}`;
356
375
  const toolName = step.tool_name || step.tool_info.name || "tool";
357
376
  const toolKind = mapToolNameToKind(toolName);
@@ -377,6 +396,16 @@ export class ACPServer {
377
396
  rawOutput: step.tool_info.output ?? "",
378
397
  },
379
398
  });
399
+ const imageMarkdown = extractImageMarkdownLink(toolName, step.tool_info.output, step.tool_info.parameters);
400
+ if (imageMarkdown) {
401
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
402
+ sessionId: session.id,
403
+ update: {
404
+ sessionUpdate: "agent_message_chunk",
405
+ content: { type: "text", text: `\n\n${imageMarkdown}\n\n` },
406
+ },
407
+ });
408
+ }
380
409
  }
381
410
  else if (step.state === "ERROR") {
382
411
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
@@ -496,6 +525,7 @@ export class ACPServer {
496
525
  break;
497
526
  }
498
527
  case ACP_METHODS.SESSION_SET_MODE:
528
+ case ACP_METHODS.SESSION_SET_MODE_CAMEL:
499
529
  case ACP_METHODS.SESSION_SET_MODE_ALIAS: {
500
530
  const sessionId = String(params.sessionId || "");
501
531
  const modeId = String(params.modeId || params.mode || "default");
@@ -516,6 +546,7 @@ export class ACPServer {
516
546
  break;
517
547
  }
518
548
  case ACP_METHODS.SESSION_SET_MODEL:
549
+ case ACP_METHODS.SESSION_SET_MODEL_CAMEL:
519
550
  case ACP_METHODS.SESSION_SET_MODEL_ALIAS: {
520
551
  const sessionId = String(params.sessionId || "");
521
552
  const session = this.requireSession(sessionId);
@@ -546,12 +577,19 @@ export class ACPServer {
546
577
  if (parsedModel.effort)
547
578
  session.setEffort(parsedModel.effort);
548
579
  session.setModel(model.modelId);
549
- const configOptions = buildConfigOptionsForModel(session.model, session.effort, models);
580
+ const configOptions = buildConfigOptionsForSession({
581
+ modelId: session.model,
582
+ currentEffort: session.effort,
583
+ currentMode: session.mode,
584
+ currentPermission: session.permission,
585
+ availableModels: models,
586
+ });
550
587
  if (!isNotification)
551
588
  this.sendSuccess(id, { configOptions });
552
589
  break;
553
590
  }
554
591
  case ACP_METHODS.SESSION_SET_CONFIG_OPTION:
592
+ case ACP_METHODS.SESSION_SET_CONFIG_OPTION_CAMEL:
555
593
  case ACP_METHODS.SESSION_SET_CONFIG_OPTION_ALIAS: {
556
594
  const sessionId = String(params.sessionId || "");
557
595
  const configId = String(params.configId || "");
@@ -584,12 +622,36 @@ export class ACPServer {
584
622
  session.setModel(value);
585
623
  logger.info("Updated model for session via config option", { sessionId, model: value });
586
624
  }
625
+ else if (configId === "mode") {
626
+ if (!AVAILABLE_MODES.some((candidate) => candidate.id === value)) {
627
+ if (!isNotification)
628
+ this.sendError(id, -32602, `Unsupported mode: ${value}`);
629
+ break;
630
+ }
631
+ session.setMode(value);
632
+ logger.info("Updated mode for session via config option", { sessionId, mode: value });
633
+ }
634
+ else if (configId === "permission") {
635
+ if (!AVAILABLE_PERMISSIONS.some((candidate) => candidate.id === value)) {
636
+ if (!isNotification)
637
+ this.sendError(id, -32602, `Unsupported permission: ${value}`);
638
+ break;
639
+ }
640
+ session.setPermission(value);
641
+ logger.info("Updated permission for session via config option", { sessionId, permission: value });
642
+ }
587
643
  else {
588
644
  if (!isNotification)
589
645
  this.sendError(id, -32602, `Unsupported config option: ${configId}`);
590
646
  break;
591
647
  }
592
- const configOptions = buildConfigOptionsForModel(session.model, session.effort, models);
648
+ const configOptions = buildConfigOptionsForSession({
649
+ modelId: session.model,
650
+ currentEffort: session.effort,
651
+ currentMode: session.mode,
652
+ currentPermission: session.permission,
653
+ availableModels: models,
654
+ });
593
655
  if (!isNotification)
594
656
  this.sendSuccess(id, { configOptions });
595
657
  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;
@@ -189,6 +189,13 @@ export class AntigravityProcess extends EventEmitter {
189
189
  this.scheduleRestart();
190
190
  }
191
191
  }
192
+ get currentPermissions() {
193
+ return this.permissions;
194
+ }
195
+ setPermissions(permissions) {
196
+ this.permissions = { ...this.permissions, ...permissions };
197
+ this.scheduleRestart();
198
+ }
192
199
  setConversationId(conversationId) {
193
200
  if (this.conversationId !== conversationId) {
194
201
  this.conversationId = conversationId;
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
@@ -239,7 +243,10 @@ if (args.includes("setup") ||
239
243
  }
240
244
  // Auto-run integration in background when starting ACP server
241
245
  void ensurePaseoIntegration().catch(() => { });
242
- const server = new ACPServer();
246
+ const skipNarration = args.includes("--skip-naration") ||
247
+ args.includes("--skip-narration") ||
248
+ process.env.AGY_ACP_SKIP_NARRATION === "true";
249
+ const server = new ACPServer({ skipNarration });
243
250
  const cleanup = async () => {
244
251
  try {
245
252
  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
  }
@@ -649,7 +655,16 @@ export function findPaseoAsarPaths() {
649
655
  candidates.add(path.resolve(loc));
650
656
  }
651
657
  }
652
- return Array.from(candidates);
658
+ const result = [];
659
+ const seen = new Set();
660
+ for (const loc of candidates) {
661
+ const key = process.platform === "win32" ? loc.toLowerCase() : loc;
662
+ if (!seen.has(key)) {
663
+ seen.add(key);
664
+ result.push(loc);
665
+ }
666
+ }
667
+ return result;
653
668
  }
654
669
  /**
655
670
  * Extracts, patches, and repacks a Paseo app.asar archive to integrate Antigravity
@@ -663,11 +678,8 @@ export async function patchPaseoAsar(asarPath) {
663
678
  if (!fs.existsSync(asarPath)) {
664
679
  return { success: false, changes: [], error: `Asar archive not found: ${asarPath}` };
665
680
  }
666
- // Dynamic import of @electron/asar
667
- const asarModule = await import("@electron/asar");
668
- const asar = asarModule.default || asarModule;
669
681
  tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-asar-extract-"));
670
- asar.extractAll(asarPath, tempDir);
682
+ extractAll(asarPath, tempDir);
671
683
  // Look for server directory in extracted files
672
684
  const serverCandidates = [
673
685
  path.join(tempDir, "node_modules", "@getpaseo", "server"),
@@ -722,7 +734,7 @@ export async function patchPaseoAsar(asarPath) {
722
734
  }
723
735
  }
724
736
  tempAsar = path.join(os.tmpdir(), `app-${Date.now()}.asar`);
725
- await asar.createPackage(tempDir, tempAsar);
737
+ await createPackage(tempDir, tempAsar);
726
738
  // Replace original archive with locked file handling for Windows
727
739
  try {
728
740
  fs.copyFileSync(tempAsar, asarPath);
@@ -871,9 +883,7 @@ export async function isPaseoAsarPatched(asarPath) {
871
883
  try {
872
884
  if (!fs.existsSync(asarPath))
873
885
  return false;
874
- const asarModule = await import("@electron/asar");
875
- const asar = asarModule.default || asarModule;
876
- const files = asar.listPackage(asarPath);
886
+ const files = listPackage(asarPath);
877
887
  return files.some((f) => f.includes("antigravity.js"));
878
888
  }
879
889
  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
  };
@@ -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,
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.0";
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.0",
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",