paseo-acp-agy 1.1.7 → 1.1.9

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.
@@ -204,8 +204,37 @@ export class ACPServer {
204
204
  }
205
205
  case ACP_METHODS.SESSION_LOAD:
206
206
  case ACP_METHODS.SESSION_LOAD_ALIAS: {
207
- if (!isNotification) {
208
- this.sendError(id, -32601, "session/load is not supported because agy-acp does not replay prior history; use session/resume");
207
+ const sessionId = String(params.sessionId || "");
208
+ const cwd = typeof params.cwd === "string" ? params.cwd : undefined;
209
+ if (!sessionId) {
210
+ if (!isNotification)
211
+ this.sendError(id, -32602, "sessionId is required");
212
+ break;
213
+ }
214
+ let session = this.sessionManager.getSession(sessionId);
215
+ let created = false;
216
+ try {
217
+ if (!session) {
218
+ session = this.sessionManager.createSession({
219
+ id: sessionId,
220
+ cwd,
221
+ binaryPath: this.binaryPath,
222
+ });
223
+ created = true;
224
+ }
225
+ if (!created && session.process.currentConversationId) {
226
+ await session.ensureReadyForResume();
227
+ }
228
+ if (!isNotification) {
229
+ this.sendSuccess(id, await this.sessionState(session));
230
+ this.publishCommands(session.id);
231
+ this.publishUsageUpdate(session);
232
+ }
233
+ }
234
+ catch (err) {
235
+ if (!isNotification) {
236
+ this.sendError(id, -32603, `Failed to load session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
237
+ }
209
238
  }
210
239
  break;
211
240
  }
@@ -1,7 +1,10 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { AgyInitEvent, AgyStepUpdateEvent, AgyResultEvent } from "./protocol.js";
3
3
  import { PermissionSettings } from "./permissions.js";
4
- export declare function resolveDefaultAgyBinary(): string;
4
+ export declare const BINARY_RESOLVE_TTL_MS: number;
5
+ export declare function clearBinaryResolutionCache(): void;
6
+ export declare function isWindowsBatchScript(filePath: string): boolean;
7
+ export declare function resolveDefaultAgyBinary(force?: boolean): string;
5
8
  export interface AntigravityProcessOptions {
6
9
  binaryPath?: string;
7
10
  cwd?: string;
@@ -6,41 +6,120 @@ import path from "node:path";
6
6
  import { logger } from "./logger.js";
7
7
  import { getEffectiveEffortForModel, } from "./protocol.js";
8
8
  import { buildAgyArgs, resolvePermissionSettings } from "./permissions.js";
9
- export function resolveDefaultAgyBinary() {
10
- if (process.env.AGY_BIN_PATH)
11
- return process.env.AGY_BIN_PATH;
12
- const home = os.homedir();
13
- if (home) {
14
- if (process.platform === "win32") {
15
- const candidates = [
16
- path.join(home, ".local", "bin", "agy.exe"),
17
- path.join(home, "AppData", "Local", "Programs", "antigravity", "agy.exe"),
18
- path.join(home, ".local", "bin", "agy.cmd"),
19
- path.join(home, ".local", "bin", "agy.bat"),
20
- ];
21
- for (const cand of candidates) {
22
- if (fs.existsSync(cand))
23
- return cand;
9
+ let cachedBinaryPath = null;
10
+ let lastBinaryResolveTime = 0;
11
+ export const BINARY_RESOLVE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours TTL
12
+ export function clearBinaryResolutionCache() {
13
+ cachedBinaryPath = null;
14
+ lastBinaryResolveTime = 0;
15
+ }
16
+ export function isWindowsBatchScript(filePath) {
17
+ return /\.(cmd|bat)$/i.test(filePath);
18
+ }
19
+ export function resolveDefaultAgyBinary(force = false) {
20
+ const now = Date.now();
21
+ if (!force &&
22
+ cachedBinaryPath &&
23
+ now - lastBinaryResolveTime < BINARY_RESOLVE_TTL_MS &&
24
+ (cachedBinaryPath === "agy" || fs.existsSync(cachedBinaryPath))) {
25
+ return cachedBinaryPath;
26
+ }
27
+ let resolved = "agy";
28
+ if (process.env.AGY_BIN_PATH) {
29
+ resolved = process.env.AGY_BIN_PATH;
30
+ }
31
+ else {
32
+ const home = os.homedir();
33
+ if (home) {
34
+ if (process.platform === "win32") {
35
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
36
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
37
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
38
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
39
+ const exeCandidates = [
40
+ path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
41
+ path.join(localAppData, "Programs", "antigravity", "agy.exe"),
42
+ path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
43
+ path.join(programFiles, "Antigravity", "bin", "agy.exe"),
44
+ path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
45
+ path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
46
+ path.join(home, ".local", "bin", "agy.exe"),
47
+ path.join(appData, "npm", "agy.exe"),
48
+ path.join(localAppData, "npm", "agy.exe"),
49
+ ];
50
+ for (const cand of exeCandidates) {
51
+ if (fs.existsSync(cand)) {
52
+ resolved = cand;
53
+ break;
54
+ }
55
+ }
56
+ if (resolved === "agy") {
57
+ for (const target of ["agy", "agy.exe"]) {
58
+ try {
59
+ const out = execFileSync("where.exe", [target], {
60
+ encoding: "utf-8",
61
+ timeout: 2000,
62
+ windowsHide: true,
63
+ }).trim();
64
+ const lines = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
65
+ for (const line of lines) {
66
+ if (/\.exe$/i.test(line) && fs.existsSync(line)) {
67
+ resolved = line;
68
+ break;
69
+ }
70
+ }
71
+ if (resolved !== "agy")
72
+ break;
73
+ }
74
+ catch { }
75
+ }
76
+ }
77
+ if (resolved === "agy") {
78
+ const batchCandidates = [
79
+ path.join(appData, "npm", "agy.cmd"),
80
+ path.join(localAppData, "npm", "agy.cmd"),
81
+ path.join(home, ".local", "bin", "agy.cmd"),
82
+ path.join(appData, "npm", "agy.bat"),
83
+ ];
84
+ for (const cand of batchCandidates) {
85
+ if (fs.existsSync(cand)) {
86
+ resolved = cand;
87
+ break;
88
+ }
89
+ }
90
+ if (resolved === "agy") {
91
+ for (const target of ["agy", "agy.cmd", "agy.bat"]) {
92
+ try {
93
+ const out = execFileSync("where.exe", [target], {
94
+ encoding: "utf-8",
95
+ timeout: 2000,
96
+ windowsHide: true,
97
+ }).trim();
98
+ const lines = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
99
+ for (const line of lines) {
100
+ if (isWindowsBatchScript(line) && fs.existsSync(line)) {
101
+ resolved = line;
102
+ break;
103
+ }
104
+ }
105
+ if (resolved !== "agy")
106
+ break;
107
+ }
108
+ catch { }
109
+ }
110
+ }
111
+ }
24
112
  }
25
- try {
26
- const out = execFileSync("where.exe", ["agy"], {
27
- encoding: "utf-8",
28
- timeout: 2000,
29
- windowsHide: true,
30
- }).trim();
31
- const first = out.split(/\r?\n/)[0]?.trim();
32
- if (first && fs.existsSync(first))
33
- return first;
113
+ else {
114
+ const localPath = path.join(home, ".local", "bin", "agy");
115
+ if (fs.existsSync(localPath))
116
+ resolved = localPath;
34
117
  }
35
- catch { }
36
- }
37
- else {
38
- const localPath = path.join(home, ".local", "bin", "agy");
39
- if (fs.existsSync(localPath))
40
- return localPath;
41
118
  }
42
119
  }
43
- return "agy";
120
+ cachedBinaryPath = resolved;
121
+ lastBinaryResolveTime = now;
122
+ return resolved;
44
123
  }
45
124
  export class AntigravityProcess extends EventEmitter {
46
125
  child = null;
@@ -269,12 +348,13 @@ export class AntigravityProcess extends EventEmitter {
269
348
  effectiveEffort,
270
349
  });
271
350
  const isWin = process.platform === "win32";
351
+ const isBatch = isWin && isWindowsBatchScript(this.binaryPath);
272
352
  const child = spawn(this.binaryPath, args, {
273
353
  cwd: this.cwd,
274
354
  env: this.env,
275
355
  stdio: ["pipe", "pipe", "pipe"],
276
356
  detached: !isWin,
277
- shell: isWin,
357
+ shell: isBatch,
278
358
  windowsHide: true,
279
359
  });
280
360
  this.child = child;
package/dist/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { execFileSync } from "node:child_process";
2
4
  import { ACPServer } from "./acp-server.js";
3
5
  import { logger } from "./logger.js";
4
6
  import { formatDiagnosticVersion, resolveBuildMetadata } from "./version.js";
5
- import { ensurePaseoIntegration } from "./paseo-patcher.js";
7
+ import { ensurePaseoIntegration, findPaseoServerInstallations, findPaseoAsarPaths, isPaseoServerPatched, isPaseoAsarPatched, isPaseoRunning, } from "./paseo-patcher.js";
8
+ import { resolveDefaultAgyBinary, isWindowsBatchScript } from "./antigravity-process.js";
9
+ import { parseAgyQuotaOutput } from "./protocol.js";
6
10
  const args = process.argv.slice(2);
7
11
  if (args.includes("--version") || args.includes("-v")) {
8
12
  if (args.includes("--json")) {
@@ -21,13 +25,16 @@ Usage:
21
25
  agy-acp [options]
22
26
  npx -y paseo-acp-agy [options]
23
27
  npx -y paseo-acp-agy setup
28
+ npx -y paseo-acp-agy doctor
24
29
 
25
30
  Commands:
26
31
  setup, patch Configure and integrate Antigravity telemetry with Paseo
32
+ doctor Diagnose Antigravity binary, quota provider, and Paseo status
27
33
 
28
34
  Options:
29
35
  --acp Start ACP server over stdio (default)
30
36
  --setup Integrate Antigravity with local Paseo server installation
37
+ --doctor Run environment, binary, and telemetry diagnostics
31
38
  -v, --version Show version
32
39
  --json Show version in JSON format (with --version)
33
40
  -h, --help Show help
@@ -39,30 +46,190 @@ Environment Variables:
39
46
  AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS Set to 'true' to auto-approve tool permissions
40
47
  AGY_BIN_PATH Path to agy binary (default: agy in PATH or ~/.local/bin/agy)
41
48
  PASEO_SERVER_PATH Path to local @getpaseo/server directory
49
+ PASEO_ASAR_PATH Path to local Paseo app.asar package
42
50
  `);
43
51
  process.exit(0);
44
52
  }
53
+ if (args.includes("doctor") || args.includes("--doctor")) {
54
+ process.stdout.write("=== Paseo & Antigravity Doctor ===\n");
55
+ process.stdout.write(`Operating System: ${process.platform} (${process.arch})\n`);
56
+ process.stdout.write(`Node.js Version: ${process.version}\n`);
57
+ process.stdout.write(`Paseo ACP Adapter: ${formatDiagnosticVersion()}\n\n`);
58
+ process.stdout.write("1. Antigravity Binary Resolution:\n");
59
+ const binPath = resolveDefaultAgyBinary(true);
60
+ const exists = fs.existsSync(binPath);
61
+ const isWin = process.platform === "win32";
62
+ let execType = "Executable";
63
+ if (isWin) {
64
+ if (/\.exe$/i.test(binPath))
65
+ execType = "Native Windows Executable (.exe)";
66
+ else if (/\.cmd$/i.test(binPath))
67
+ execType = "Windows Command Script (.cmd)";
68
+ else if (/\.bat$/i.test(binPath))
69
+ execType = "Windows Batch Script (.bat)";
70
+ else
71
+ execType = "CLI command in PATH";
72
+ }
73
+ else {
74
+ execType = "POSIX binary";
75
+ }
76
+ if (exists) {
77
+ process.stdout.write(` [OK] Binary found: ${binPath} (${execType})\n`);
78
+ }
79
+ else if (binPath === "agy") {
80
+ process.stdout.write(` [?] Binary fallback: 'agy' via system PATH (${execType})\n`);
81
+ }
82
+ else {
83
+ process.stdout.write(` [FAIL] Binary not found at: ${binPath}\n`);
84
+ }
85
+ process.stdout.write("\n2. CLI Telemetry & Runtime Probes:\n");
86
+ const isBatch = isWin && isWindowsBatchScript(binPath);
87
+ let modelsSuccess = false;
88
+ let modelsCount = 0;
89
+ const t0 = Date.now();
90
+ try {
91
+ const modelsOut = execFileSync(binPath, ["models"], {
92
+ encoding: "utf-8",
93
+ timeout: 15000,
94
+ windowsHide: true,
95
+ shell: isBatch,
96
+ });
97
+ const latency = Date.now() - t0;
98
+ const modelLines = modelsOut
99
+ .trim()
100
+ .split(/\r?\n/)
101
+ .map((l) => l.trim())
102
+ .filter((l) => Boolean(l) && !l.toLowerCase().startsWith("models:"));
103
+ modelsCount = modelLines.length;
104
+ modelsSuccess = true;
105
+ process.stdout.write(` [OK] 'agy models' probe succeeded in ${latency}ms (${modelsCount} models detected)\n`);
106
+ }
107
+ catch (err) {
108
+ const latency = Date.now() - t0;
109
+ process.stdout.write(` [WARN] 'agy models' probe failed in ${latency}ms: ${err?.message || String(err)}\n`);
110
+ }
111
+ let quotaSuccess = false;
112
+ let quotaCount = 0;
113
+ const t1 = Date.now();
114
+ try {
115
+ const usageOut = execFileSync(binPath, ["--print-timeout", "24h", "--print", "/usage"], {
116
+ encoding: "utf-8",
117
+ timeout: 15000,
118
+ windowsHide: true,
119
+ shell: isBatch,
120
+ });
121
+ const latency = Date.now() - t1;
122
+ const normalized = usageOut.replace(/\r\n/g, "\n");
123
+ const windows = parseAgyQuotaOutput(normalized);
124
+ quotaCount = windows.length;
125
+ quotaSuccess = true;
126
+ process.stdout.write(` [OK] 'agy --print /usage' probe succeeded in ${latency}ms (${quotaCount} quota limit item(s) parsed)\n`);
127
+ }
128
+ catch (err) {
129
+ const latency = Date.now() - t1;
130
+ process.stdout.write(` [WARN] 'agy --print /usage' probe failed in ${latency}ms: ${err?.message || String(err)}\n`);
131
+ }
132
+ process.stdout.write("\n3. Paseo Installation Targets:\n");
133
+ const serverPaths = findPaseoServerInstallations();
134
+ const asarPaths = findPaseoAsarPaths();
135
+ const totalFound = serverPaths.length + asarPaths.length;
136
+ if (totalFound === 0) {
137
+ process.stdout.write(" [!] No Paseo server or desktop installations discovered in standard paths.\n");
138
+ }
139
+ else {
140
+ for (const sPath of serverPaths) {
141
+ const patched = isPaseoServerPatched(sPath);
142
+ process.stdout.write(` - [Server] ${sPath} -> ${patched ? "[PATCHED]" : "[UNPATCHED]"}\n`);
143
+ }
144
+ for (const aPath of asarPaths) {
145
+ const patched = await isPaseoAsarPatched(aPath);
146
+ process.stdout.write(` - [ASAR] ${aPath} -> ${patched ? "[PATCHED]" : "[UNPATCHED]"}\n`);
147
+ }
148
+ }
149
+ process.stdout.write("\n4. Paseo Process Status:\n");
150
+ const paseoRunning = isPaseoRunning();
151
+ if (paseoRunning) {
152
+ 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");
154
+ }
155
+ else {
156
+ process.stdout.write(" [OK] Paseo is not currently running (safe to patch/update).\n");
157
+ }
158
+ process.stdout.write("\n=== Doctor Recommendations ===\n");
159
+ let unpatchedCount = 0;
160
+ for (const sPath of serverPaths) {
161
+ if (!isPaseoServerPatched(sPath))
162
+ unpatchedCount++;
163
+ }
164
+ for (const aPath of asarPaths) {
165
+ if (!(await isPaseoAsarPatched(aPath)))
166
+ unpatchedCount++;
167
+ }
168
+ let hasIssue = false;
169
+ if (!exists && binPath !== "agy") {
170
+ hasIssue = true;
171
+ process.stdout.write(" - Antigravity CLI ('agy') executable was not found.\n");
172
+ process.stdout.write(" Action: Install Antigravity CLI or set AGY_BIN_PATH environment variable.\n");
173
+ }
174
+ if (totalFound === 0) {
175
+ hasIssue = true;
176
+ process.stdout.write(" - No Paseo installations found.\n");
177
+ process.stdout.write(" Action: Set PASEO_SERVER_PATH or PASEO_ASAR_PATH to your Paseo install directory if located elsewhere.\n");
178
+ }
179
+ else if (unpatchedCount > 0) {
180
+ hasIssue = true;
181
+ if (paseoRunning) {
182
+ 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");
184
+ process.stdout.write(" npx -y paseo-acp-agy setup\n");
185
+ }
186
+ else {
187
+ process.stdout.write(` - Found ${unpatchedCount} unpatched Paseo target(s).\n`);
188
+ process.stdout.write(" Action: Run setup to patch Paseo for Antigravity telemetry:\n");
189
+ process.stdout.write(" npx -y paseo-acp-agy setup\n");
190
+ }
191
+ }
192
+ if (!modelsSuccess || !quotaSuccess) {
193
+ if (exists || binPath === "agy") {
194
+ hasIssue = true;
195
+ process.stdout.write(" - Antigravity CLI telemetry probes experienced errors.\n");
196
+ process.stdout.write(" Action: Ensure you are logged in to Antigravity CLI (try running 'agy auth' or 'agy /status').\n");
197
+ }
198
+ }
199
+ if (!hasIssue) {
200
+ process.stdout.write(" [ALL OK] Everything is properly configured! Antigravity quota and telemetry are ready to use in Paseo.\n");
201
+ }
202
+ process.exit(0);
203
+ }
45
204
  if (args.includes("setup") ||
46
205
  args.includes("patch") ||
47
206
  args.includes("--setup") ||
48
207
  args.includes("--patch")) {
208
+ if (isPaseoRunning()) {
209
+ process.stdout.write("Notice: Paseo appears to be running. If setup fails with EBUSY, please close Paseo completely (from system tray / task manager) and re-run setup.\n\n");
210
+ }
49
211
  process.stdout.write("Checking Paseo installation and configuring Antigravity telemetry...\n");
50
212
  try {
51
- const res = ensurePaseoIntegration({ verbose: true });
213
+ const res = await ensurePaseoIntegration({ verbose: true });
52
214
  if (!res.found) {
53
- process.stdout.write("Notice: No active @getpaseo/server installation found in standard paths.\n" +
54
- "If Paseo is installed in a custom directory, set PASEO_SERVER_PATH and run setup again.\n");
215
+ process.stdout.write("Notice: No active @getpaseo/server installation or app.asar found in standard paths.\n" +
216
+ "If Paseo is installed in a custom directory, set PASEO_SERVER_PATH or PASEO_ASAR_PATH and run setup again.\n");
55
217
  }
56
218
  else {
57
- process.stdout.write(`Found ${res.serverPaths.length} Paseo server installation(s).\n`);
58
- if (res.patchedPaths.length > 0) {
59
- process.stdout.write(`Successfully integrated with: \n${res.patchedPaths.map((p) => ` - ${p}`).join("\n")}\n\n` +
219
+ const totalFound = res.serverPaths.length + (res.asarPaths?.length || 0);
220
+ process.stdout.write(`Found ${totalFound} Paseo installation target(s) (${res.serverPaths.length} server dir(s), ${res.asarPaths?.length || 0} asar package(s)).\n`);
221
+ const allPatched = [...res.patchedPaths, ...(res.patchedAsarPaths || [])];
222
+ if (allPatched.length > 0) {
223
+ process.stdout.write(`Successfully integrated with: \n${allPatched.map((p) => ` - ${p}`).join("\n")}\n\n` +
60
224
  `Antigravity quota provider and context-window telemetry are now enabled!\n` +
61
225
  `Please restart Paseo (or run 'paseo daemon restart') to apply changes.\n`);
62
226
  }
63
227
  else {
64
228
  process.stdout.write("Paseo is already up-to-date and configured for Antigravity telemetry.\n");
65
229
  }
230
+ if (res.errors.length > 0) {
231
+ 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
+ }
66
233
  }
67
234
  }
68
235
  catch (err) {
@@ -71,10 +238,7 @@ if (args.includes("setup") ||
71
238
  process.exit(0);
72
239
  }
73
240
  // Auto-run integration in background when starting ACP server
74
- try {
75
- ensurePaseoIntegration();
76
- }
77
- catch { }
241
+ void ensurePaseoIntegration().catch(() => { });
78
242
  const server = new ACPServer();
79
243
  const cleanup = async () => {
80
244
  try {
@@ -2,6 +2,8 @@ export interface PatchResult {
2
2
  found: boolean;
3
3
  serverPaths: string[];
4
4
  patchedPaths: string[];
5
+ asarPaths?: string[];
6
+ patchedAsarPaths?: string[];
5
7
  errors: string[];
6
8
  }
7
9
  /**
@@ -26,9 +28,37 @@ export declare function patchPaseoServer(serverDir: string): {
26
28
  error?: string;
27
29
  };
28
30
  /**
29
- * Discovers and patches all accessible Paseo installations.
31
+ * Searches the host machine for Paseo Desktop app.asar archives across
32
+ * Windows, macOS, and Linux.
33
+ */
34
+ export declare function findPaseoAsarPaths(): string[];
35
+ /**
36
+ * Extracts, patches, and repacks a Paseo app.asar archive to integrate Antigravity
37
+ * quota fetchers and token telemetry.
38
+ */
39
+ export declare function patchPaseoAsar(asarPath: string): Promise<{
40
+ success: boolean;
41
+ changes: string[];
42
+ error?: string;
43
+ }>;
44
+ /**
45
+ * Discovers and patches all accessible Paseo installations (both directory and app.asar).
30
46
  */
31
47
  export declare function ensurePaseoIntegration(options?: {
32
48
  verbose?: boolean;
33
49
  targetPaths?: string[];
34
- }): PatchResult;
50
+ targetAsarPaths?: string[];
51
+ }): Promise<PatchResult>;
52
+ /**
53
+ * Checks whether a @getpaseo/server installation directory is already patched
54
+ * for Antigravity quota and telemetry support.
55
+ */
56
+ export declare function isPaseoServerPatched(serverDir: string): boolean;
57
+ /**
58
+ * Checks whether a Paseo app.asar archive is already patched with Antigravity telemetry.
59
+ */
60
+ export declare function isPaseoAsarPatched(asarPath: string): Promise<boolean>;
61
+ /**
62
+ * Detects if Paseo Desktop is currently running on the host.
63
+ */
64
+ export declare function isPaseoRunning(): boolean;
@@ -92,6 +92,8 @@ export function findPaseoServerInstallations() {
92
92
  path.join(path.dirname(paseoDir), "node_modules", "@getpaseo", "server"),
93
93
  path.join(paseoDir, "resources", "app.asar.unpacked", "node_modules", "@getpaseo", "server"),
94
94
  path.join(paseoDir, "resources", "app", "node_modules", "@getpaseo", "server"),
95
+ path.join(path.dirname(paseoDir), "resources", "app.asar.unpacked", "node_modules", "@getpaseo", "server"),
96
+ path.join(path.dirname(paseoDir), "resources", "app", "node_modules", "@getpaseo", "server"),
95
97
  ];
96
98
  for (const c of checks) {
97
99
  if (fs.existsSync(c))
@@ -179,31 +181,100 @@ import { toneFromUsedPct, windowFromUsedPct, unavailableUsage } from "../usage.j
179
181
 
180
182
  const execFileAsync = promisify(execFile);
181
183
 
184
+ let cachedAgyBin = null;
185
+ let cachedAgyBinTime = 0;
186
+ const BIN_CACHE_TTL_MS = 86400000; // 24 hours
187
+
182
188
  function resolveAgyBinary() {
183
189
  if (process.env.AGY_BIN_PATH) return process.env.AGY_BIN_PATH;
190
+ const now = Date.now();
191
+ if (cachedAgyBin && (now - cachedAgyBinTime < BIN_CACHE_TTL_MS)) {
192
+ return cachedAgyBin;
193
+ }
194
+
195
+ let resolved = "agy";
184
196
  const home = os.homedir();
185
197
  if (home) {
186
198
  if (process.platform === "win32") {
187
- const candidates = [
199
+ const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
200
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
201
+ const programFiles = process.env.ProgramFiles || "C:\\\\Program Files";
202
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\\\Program Files (x86)";
203
+
204
+ // Prioritize .exe candidates over .cmd / .bat
205
+ const exeCandidates = [
206
+ path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
207
+ path.join(localAppData, "Programs", "antigravity", "agy.exe"),
208
+ path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
209
+ path.join(programFiles, "Antigravity", "bin", "agy.exe"),
210
+ path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
211
+ path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
188
212
  path.join(home, ".local", "bin", "agy.exe"),
189
- path.join(home, "AppData", "Local", "Programs", "antigravity", "agy.exe"),
190
- path.join(home, ".local", "bin", "agy.cmd"),
191
- path.join(home, ".local", "bin", "agy.bat"),
213
+ path.join(appData, "npm", "agy.exe"),
214
+ path.join(localAppData, "npm", "agy.exe"),
192
215
  ];
193
- for (const cand of candidates) {
194
- if (fs.existsSync(cand)) return cand;
216
+ for (const cand of exeCandidates) {
217
+ if (fs.existsSync(cand)) {
218
+ resolved = cand;
219
+ break;
220
+ }
221
+ }
222
+
223
+ // Check where.exe agy and select first .exe
224
+ if (resolved === "agy") {
225
+ for (const target of ["agy", "agy.exe"]) {
226
+ try {
227
+ const out = execFileSync("where.exe", [target], { encoding: "utf-8", timeout: 2000, windowsHide: true }).trim();
228
+ const lines = out.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);
229
+ const firstExe = lines.find(l => /\\.exe$/i.test(l) && fs.existsSync(l));
230
+ if (firstExe) {
231
+ resolved = firstExe;
232
+ break;
233
+ }
234
+ } catch {}
235
+ }
236
+ }
237
+
238
+ // Fallback to batch scripts (.cmd / .bat) if no .exe found
239
+ if (resolved === "agy") {
240
+ const batchCandidates = [
241
+ path.join(appData, "npm", "agy.cmd"),
242
+ path.join(localAppData, "npm", "agy.cmd"),
243
+ path.join(home, ".local", "bin", "agy.cmd"),
244
+ path.join(appData, "npm", "agy.bat"),
245
+ path.join(localAppData, "npm", "agy.bat"),
246
+ path.join(home, ".local", "bin", "agy.bat"),
247
+ ];
248
+ for (const cand of batchCandidates) {
249
+ if (fs.existsSync(cand)) {
250
+ resolved = cand;
251
+ break;
252
+ }
253
+ }
254
+ }
255
+
256
+ if (resolved === "agy") {
257
+ for (const target of ["agy", "agy.cmd", "agy.bat"]) {
258
+ try {
259
+ const out = execFileSync("where.exe", [target], { encoding: "utf-8", timeout: 2000, windowsHide: true }).trim();
260
+ const lines = out.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);
261
+ const firstAny = lines.find(l => /\\.(cmd|bat)$/i.test(l) && fs.existsSync(l));
262
+ if (firstAny) {
263
+ resolved = firstAny;
264
+ break;
265
+ }
266
+ } catch {}
267
+ }
195
268
  }
196
- try {
197
- const out = execFileSync("where.exe", ["agy"], { encoding: "utf-8", timeout: 2000, windowsHide: true }).trim();
198
- const first = out.split(/\\r?\\n/)[0]?.trim();
199
- if (first && fs.existsSync(first)) return first;
200
- } catch {}
201
269
  } else {
202
270
  const localPath = path.join(home, ".local", "bin", "agy");
203
- if (fs.existsSync(localPath)) return localPath;
271
+ if (fs.existsSync(localPath)) resolved = localPath;
204
272
  }
205
273
  }
206
- return "agy";
274
+
275
+ cachedAgyBin = resolved;
276
+ cachedAgyBinTime = now;
277
+ return resolved;
207
278
  }
208
279
 
209
280
  export class AntigravityQuotaProvider {
@@ -217,20 +288,22 @@ export class AntigravityQuotaProvider {
217
288
  async fetchUsage() {
218
289
  try {
219
290
  const isWin = process.platform === "win32";
220
- let bin = this.binaryPath;
221
- if (isWin && bin.includes(" ") && !bin.startsWith('"')) {
222
- bin = \`"\${bin}"\`;
223
- }
291
+ const bin = resolveAgyBinary();
292
+ this.binaryPath = bin;
293
+ const isBatch = isWin && /\\.(cmd|bat)$/i.test(bin);
224
294
  const [usageRes, creditsRes] = await Promise.allSettled([
225
- execFileAsync(bin, ["--print", "/usage"], { timeout: 8000, env: process.env, shell: isWin, windowsHide: true }),
226
- execFileAsync(bin, ["--print", "/credits"], { timeout: 8000, env: process.env, shell: isWin, windowsHide: true }),
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 }),
227
297
  ]);
228
298
 
229
- const usageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
230
- const creditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
299
+ const rawUsageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
300
+ const rawCreditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
301
+
302
+ const usageOut = (rawUsageOut || "").replace(/\\r\\n/g, "\\n");
303
+ const creditsOut = (rawCreditsOut || "").replace(/\\r\\n/g, "\\n");
231
304
 
232
305
  const rawWindows = [];
233
- for (const line of usageOut.split(/[\\r\\n]+/)) {
306
+ for (const line of usageOut.split("\\n")) {
234
307
  const trimmed = line.trim();
235
308
  if (!trimmed || trimmed.toLowerCase().startsWith("quota:")) continue;
236
309
 
@@ -348,7 +421,7 @@ export function patchPaseoServer(serverDir) {
348
421
  if (found)
349
422
  return found;
350
423
  }
351
- else if (e.isFile() && e.name === "manifest.js" && dir.includes("quota-fetcher")) {
424
+ else if (e.isFile() && e.name === "manifest.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
352
425
  return full;
353
426
  }
354
427
  }
@@ -375,9 +448,9 @@ export function patchPaseoServer(serverDir) {
375
448
  }
376
449
  if (!manifestCode.includes('providerId: "antigravity"')) {
377
450
  const entryToAdd = ` {\n providerId: "antigravity",\n create: (options) => new AntigravityQuotaProvider({\n logger: options.logger,\n fetch: options.fetch,\n }),\n },\n`;
378
- const marker = "export const PROVIDER_USAGE_FETCHERS = [";
379
- if (manifestCode.includes(marker)) {
380
- manifestCode = manifestCode.replace(marker, `${marker}\n${entryToAdd}`);
451
+ const fetcherArrayRegex = /export\s+const\s+PROVIDER_USAGE_FETCHERS\s*=\s*\[/;
452
+ if (fetcherArrayRegex.test(manifestCode)) {
453
+ manifestCode = manifestCode.replace(fetcherArrayRegex, (match) => `${match}\n${entryToAdd}`);
381
454
  manifestModified = true;
382
455
  }
383
456
  }
@@ -434,11 +507,44 @@ export function patchPaseoServer(serverDir) {
434
507
  acpCode = acpCode.replace(oldMapRegex, newMap);
435
508
  acpModified = true;
436
509
  }
510
+ else {
511
+ const mapMatch = acpCode.match(/export\s+function\s+mapACPUsage\s*\([^)]*\)\s*\{/);
512
+ if (mapMatch && mapMatch.index !== undefined) {
513
+ const mapStart = mapMatch.index;
514
+ const openBrace = acpCode.indexOf("{", mapStart);
515
+ let depth = 1;
516
+ let i = openBrace + 1;
517
+ while (i < acpCode.length && depth > 0) {
518
+ if (acpCode[i] === "{")
519
+ depth++;
520
+ else if (acpCode[i] === "}")
521
+ depth--;
522
+ i++;
523
+ }
524
+ if (depth === 0) {
525
+ acpCode = acpCode.slice(0, mapStart) + newMap + acpCode.slice(i);
526
+ acpModified = true;
527
+ }
528
+ }
529
+ }
437
530
  }
438
531
  // Patch handleUsageUpdate
439
- if (acpCode.includes("handleUsageUpdate(update) {") && (!acpCode.includes("this.deliverTranslatedEvents") || acpCode.includes("this.notifySubscribers"))) {
440
- const handlerRegex = /handleUsageUpdate\s*\(\s*update\s*\)\s*\{[\s\S]*?(?:void\s+update;|this\.notifySubscribers)[\s\S]*?\n\s*\}/m;
441
- const newHandler = `handleUsageUpdate(update) {
532
+ if (acpCode.includes("handleUsageUpdate") && (!acpCode.includes("this.deliverTranslatedEvents") || acpCode.includes("this.notifySubscribers"))) {
533
+ const startIdx = acpCode.search(/\bhandleUsageUpdate\s*\(/);
534
+ if (startIdx !== -1) {
535
+ const openBrace = acpCode.indexOf("{", startIdx);
536
+ if (openBrace !== -1) {
537
+ let depth = 1;
538
+ let i = openBrace + 1;
539
+ while (i < acpCode.length && depth > 0) {
540
+ if (acpCode[i] === "{")
541
+ depth++;
542
+ else if (acpCode[i] === "}")
543
+ depth--;
544
+ i++;
545
+ }
546
+ if (depth === 0) {
547
+ const newHandler = `handleUsageUpdate(update) {
442
548
  if (!update) return;
443
549
  const usage = mapACPUsage(update);
444
550
  if (usage) {
@@ -456,9 +562,10 @@ export function patchPaseoServer(serverDir) {
456
562
  }
457
563
  }
458
564
  }`;
459
- if (handlerRegex.test(acpCode)) {
460
- acpCode = acpCode.replace(handlerRegex, newHandler);
461
- acpModified = true;
565
+ acpCode = acpCode.slice(0, startIdx) + newHandler + acpCode.slice(i);
566
+ acpModified = true;
567
+ }
568
+ }
462
569
  }
463
570
  }
464
571
  if (acpModified) {
@@ -475,11 +582,197 @@ export function patchPaseoServer(serverDir) {
475
582
  }
476
583
  }
477
584
  /**
478
- * Discovers and patches all accessible Paseo installations.
585
+ * Searches the host machine for Paseo Desktop app.asar archives across
586
+ * Windows, macOS, and Linux.
587
+ */
588
+ export function findPaseoAsarPaths() {
589
+ const candidates = new Set();
590
+ const home = os.homedir();
591
+ if (process.env.PASEO_ASAR_PATH && fs.existsSync(process.env.PASEO_ASAR_PATH)) {
592
+ candidates.add(path.resolve(process.env.PASEO_ASAR_PATH));
593
+ }
594
+ if (process.platform === "win32") {
595
+ const localAppData = process.env.LOCALAPPDATA || (home ? path.join(home, "AppData", "Local") : "");
596
+ const appData = process.env.APPDATA || (home ? path.join(home, "AppData", "Roaming") : "");
597
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
598
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
599
+ const winAsarLocations = [
600
+ path.join(localAppData, "Programs", "Paseo", "resources", "app.asar"),
601
+ path.join(localAppData, "Programs", "paseo", "resources", "app.asar"),
602
+ path.join(localAppData, "Paseo", "resources", "app.asar"),
603
+ path.join(programFiles, "Paseo", "resources", "app.asar"),
604
+ path.join(programFiles, "paseo", "resources", "app.asar"),
605
+ path.join(programFilesX86, "Paseo", "resources", "app.asar"),
606
+ path.join(programFilesX86, "paseo", "resources", "app.asar"),
607
+ path.join(appData, "Paseo", "resources", "app.asar"),
608
+ ];
609
+ for (const loc of winAsarLocations) {
610
+ if (loc && fs.existsSync(loc))
611
+ candidates.add(path.resolve(loc));
612
+ }
613
+ try {
614
+ const whereOut = execFileSync("where.exe", ["paseo"], {
615
+ encoding: "utf-8",
616
+ timeout: 2000,
617
+ windowsHide: true,
618
+ }).trim();
619
+ for (const line of whereOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)) {
620
+ const asarCandidate1 = path.join(path.dirname(line), "resources", "app.asar");
621
+ const asarCandidate2 = path.resolve(path.dirname(line), "..", "resources", "app.asar");
622
+ const asarCandidate3 = path.resolve(line, "..", "..", "resources", "app.asar");
623
+ for (const cand of [asarCandidate1, asarCandidate2, asarCandidate3]) {
624
+ if (fs.existsSync(cand))
625
+ candidates.add(path.resolve(cand));
626
+ }
627
+ }
628
+ }
629
+ catch { }
630
+ }
631
+ else if (process.platform === "darwin") {
632
+ const macLocations = [
633
+ "/Applications/Paseo.app/Contents/Resources/app.asar",
634
+ path.join(home, "Applications", "Paseo.app", "Contents", "Resources", "app.asar"),
635
+ ];
636
+ for (const loc of macLocations) {
637
+ if (fs.existsSync(loc))
638
+ candidates.add(path.resolve(loc));
639
+ }
640
+ }
641
+ else {
642
+ const linuxLocations = [
643
+ "/opt/Paseo/resources/app.asar",
644
+ "/usr/lib/paseo/resources/app.asar",
645
+ path.join(home, ".local", "share", "paseo", "resources", "app.asar"),
646
+ ];
647
+ for (const loc of linuxLocations) {
648
+ if (fs.existsSync(loc))
649
+ candidates.add(path.resolve(loc));
650
+ }
651
+ }
652
+ return Array.from(candidates);
653
+ }
654
+ /**
655
+ * Extracts, patches, and repacks a Paseo app.asar archive to integrate Antigravity
656
+ * quota fetchers and token telemetry.
657
+ */
658
+ export async function patchPaseoAsar(asarPath) {
659
+ const changes = [];
660
+ let tempDir = null;
661
+ let tempAsar = null;
662
+ try {
663
+ if (!fs.existsSync(asarPath)) {
664
+ return { success: false, changes: [], error: `Asar archive not found: ${asarPath}` };
665
+ }
666
+ // Dynamic import of @electron/asar
667
+ const asarModule = await import("@electron/asar");
668
+ const asar = asarModule.default || asarModule;
669
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "paseo-asar-extract-"));
670
+ asar.extractAll(asarPath, tempDir);
671
+ // Look for server directory in extracted files
672
+ const serverCandidates = [
673
+ path.join(tempDir, "node_modules", "@getpaseo", "server"),
674
+ path.join(tempDir, "dist", "node_modules", "@getpaseo", "server"),
675
+ ];
676
+ let serverDir = serverCandidates.find((c) => fs.existsSync(c));
677
+ if (!serverDir) {
678
+ // Search recursively within 3 levels
679
+ const searchDirs = [tempDir];
680
+ while (searchDirs.length > 0 && !serverDir) {
681
+ const current = searchDirs.shift();
682
+ try {
683
+ const entries = fs.readdirSync(current, { withFileTypes: true });
684
+ for (const ent of entries) {
685
+ if (ent.isDirectory()) {
686
+ const full = path.join(current, ent.name);
687
+ const normalizedFull = full.replace(/\\/g, "/");
688
+ if (ent.name === "server" && normalizedFull.includes("@getpaseo/server")) {
689
+ serverDir = full;
690
+ break;
691
+ }
692
+ if (full.split(path.sep).length - tempDir.split(path.sep).length < 4) {
693
+ searchDirs.push(full);
694
+ }
695
+ }
696
+ }
697
+ }
698
+ catch { }
699
+ }
700
+ }
701
+ if (!serverDir) {
702
+ return { success: false, changes: [], error: `Could not locate @getpaseo/server inside ${asarPath}` };
703
+ }
704
+ const patchResult = patchPaseoServer(serverDir);
705
+ if (!patchResult.success) {
706
+ return { success: false, changes: [], error: patchResult.error };
707
+ }
708
+ if (patchResult.changes.length === 0) {
709
+ // Already patched!
710
+ return { success: true, changes: [] };
711
+ }
712
+ changes.push(...patchResult.changes);
713
+ // Create backup if not already present
714
+ const backupPath = `${asarPath}.bak`;
715
+ if (!fs.existsSync(backupPath)) {
716
+ try {
717
+ fs.copyFileSync(asarPath, backupPath);
718
+ changes.push(`Backed up original asar to ${backupPath}`);
719
+ }
720
+ catch (backupErr) {
721
+ logger.warn(`Could not create asar backup at ${backupPath}`, { error: String(backupErr) });
722
+ }
723
+ }
724
+ tempAsar = path.join(os.tmpdir(), `app-${Date.now()}.asar`);
725
+ await asar.createPackage(tempDir, tempAsar);
726
+ // Replace original archive with locked file handling for Windows
727
+ try {
728
+ fs.copyFileSync(tempAsar, asarPath);
729
+ changes.push(`Repacked updated asar archive at ${asarPath}`);
730
+ }
731
+ catch (copyErr) {
732
+ if (copyErr && (copyErr.code === "EBUSY" || copyErr.code === "EPERM" || copyErr.code === "EACCES")) {
733
+ const oldPath = `${asarPath}.old-${Date.now()}`;
734
+ try {
735
+ fs.renameSync(asarPath, oldPath);
736
+ fs.copyFileSync(tempAsar, asarPath);
737
+ changes.push(`Repacked updated asar archive at ${asarPath} (safe replaced locked file, moved previous to ${oldPath})`);
738
+ }
739
+ catch (renameErr) {
740
+ throw new Error(`Cannot update ${asarPath}: file is locked by a running Paseo process (${copyErr.code}). Please close Paseo completely (Paseo.exe in system tray / Task Manager) and retry.`);
741
+ }
742
+ }
743
+ else {
744
+ throw copyErr;
745
+ }
746
+ }
747
+ return { success: true, changes };
748
+ }
749
+ catch (err) {
750
+ const msg = err instanceof Error ? err.message : String(err);
751
+ return { success: false, changes, error: msg };
752
+ }
753
+ finally {
754
+ if (tempDir) {
755
+ try {
756
+ fs.rmSync(tempDir, { recursive: true, force: true });
757
+ }
758
+ catch { }
759
+ }
760
+ if (tempAsar) {
761
+ try {
762
+ fs.unlinkSync(tempAsar);
763
+ }
764
+ catch { }
765
+ }
766
+ }
767
+ }
768
+ /**
769
+ * Discovers and patches all accessible Paseo installations (both directory and app.asar).
479
770
  */
480
- export function ensurePaseoIntegration(options) {
771
+ export async function ensurePaseoIntegration(options) {
481
772
  const serverPaths = options?.targetPaths || findPaseoServerInstallations();
773
+ const asarPaths = options?.targetAsarPaths || findPaseoAsarPaths();
482
774
  const patchedPaths = [];
775
+ const patchedAsarPaths = [];
483
776
  const errors = [];
484
777
  for (const sPath of serverPaths) {
485
778
  const res = patchPaseoServer(sPath);
@@ -495,10 +788,124 @@ export function ensurePaseoIntegration(options) {
495
788
  errors.push(`${sPath}: ${res.error}`);
496
789
  }
497
790
  }
791
+ for (const aPath of asarPaths) {
792
+ const res = await patchPaseoAsar(aPath);
793
+ if (res.success) {
794
+ if (res.changes.length > 0) {
795
+ patchedAsarPaths.push(aPath);
796
+ if (options?.verbose) {
797
+ logger.info(`Integrated with Paseo desktop asar at ${aPath}`, { changes: res.changes });
798
+ }
799
+ }
800
+ }
801
+ else if (res.error) {
802
+ errors.push(`${aPath}: ${res.error}`);
803
+ }
804
+ }
498
805
  return {
499
- found: serverPaths.length > 0,
806
+ found: serverPaths.length > 0 || asarPaths.length > 0,
500
807
  serverPaths,
501
808
  patchedPaths,
809
+ asarPaths,
810
+ patchedAsarPaths,
502
811
  errors,
503
812
  };
504
813
  }
814
+ /**
815
+ * Checks whether a @getpaseo/server installation directory is already patched
816
+ * for Antigravity quota and telemetry support.
817
+ */
818
+ export function isPaseoServerPatched(serverDir) {
819
+ try {
820
+ if (!fs.existsSync(serverDir))
821
+ return false;
822
+ const antigravityJsCandidates = [
823
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "providers", "antigravity.js"),
824
+ path.join(serverDir, "dist", "services", "quota-fetcher", "providers", "antigravity.js"),
825
+ ];
826
+ let hasProvider = antigravityJsCandidates.some((p) => fs.existsSync(p));
827
+ if (!hasProvider && fs.existsSync(path.join(serverDir, "dist"))) {
828
+ const checkRecursive = (dir, depth = 0) => {
829
+ if (depth > 5)
830
+ return false;
831
+ try {
832
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
833
+ for (const e of entries) {
834
+ if (e.isDirectory() && e.name !== "node_modules") {
835
+ if (checkRecursive(path.join(dir, e.name), depth + 1))
836
+ return true;
837
+ }
838
+ else if (e.isFile() && e.name === "antigravity.js" && dir.replace(/\\/g, "/").includes("quota-fetcher")) {
839
+ return true;
840
+ }
841
+ }
842
+ }
843
+ catch { }
844
+ return false;
845
+ };
846
+ hasProvider = checkRecursive(path.join(serverDir, "dist"));
847
+ }
848
+ if (hasProvider)
849
+ return true;
850
+ const manifestCandidates = [
851
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
852
+ path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
853
+ ];
854
+ for (const cand of manifestCandidates) {
855
+ if (fs.existsSync(cand)) {
856
+ const content = fs.readFileSync(cand, "utf-8");
857
+ if (content.includes('providerId: "antigravity"'))
858
+ return true;
859
+ }
860
+ }
861
+ return false;
862
+ }
863
+ catch {
864
+ return false;
865
+ }
866
+ }
867
+ /**
868
+ * Checks whether a Paseo app.asar archive is already patched with Antigravity telemetry.
869
+ */
870
+ export async function isPaseoAsarPatched(asarPath) {
871
+ try {
872
+ if (!fs.existsSync(asarPath))
873
+ return false;
874
+ const asarModule = await import("@electron/asar");
875
+ const asar = asarModule.default || asarModule;
876
+ const files = asar.listPackage(asarPath);
877
+ return files.some((f) => f.includes("antigravity.js"));
878
+ }
879
+ catch {
880
+ return false;
881
+ }
882
+ }
883
+ /**
884
+ * Detects if Paseo Desktop is currently running on the host.
885
+ */
886
+ export function isPaseoRunning() {
887
+ try {
888
+ if (process.platform === "win32") {
889
+ const out = execFileSync("tasklist.exe", ["/FI", "IMAGENAME eq Paseo*", "/NH"], {
890
+ encoding: "utf-8",
891
+ timeout: 3000,
892
+ windowsHide: true,
893
+ });
894
+ return /paseo/i.test(out) && !out.includes("INFO:") && !out.includes("No tasks");
895
+ }
896
+ else {
897
+ const out = execFileSync("pgrep", ["-i", "-x", "paseo"], {
898
+ encoding: "utf-8",
899
+ timeout: 2000,
900
+ });
901
+ const pids = out
902
+ .split(/\r?\n/)
903
+ .map((s) => parseInt(s.trim(), 10))
904
+ .filter((p) => !isNaN(p) && p !== process.pid);
905
+ return pids.length > 0;
906
+ }
907
+ }
908
+ catch {
909
+ return false;
910
+ }
911
+ }
@@ -173,7 +173,12 @@ export interface ProviderUsage {
173
173
  }>;
174
174
  error: string | null;
175
175
  }
176
- export declare function formatExecBinaryPath(binaryPath: string): string;
176
+ export declare function formatExecBinaryPath(binaryPath: string, shell?: boolean): string;
177
+ export interface ResolvedCommandExecution {
178
+ cmd: string;
179
+ shell: boolean;
180
+ }
181
+ export declare function resolveCommandExecution(binaryPath: string): ResolvedCommandExecution;
177
182
  export declare function fetchAntigravityUsage(binaryPath?: string, force?: boolean): Promise<ProviderUsage>;
178
183
  export declare const ALL_THINKING_LEVELS: Record<string, {
179
184
  name: string;
package/dist/protocol.js CHANGED
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { logger } from "./logger.js";
5
5
  import { saveBase64Image } from "./attachments.js";
6
+ import { isWindowsBatchScript } from "./antigravity-process.js";
6
7
  const execFileAsync = promisify(execFile);
7
8
  // ACP wire methods use snake_case. The aliases retain the public method names
8
9
  // accepted by the adapter before the hardening work so existing clients do not
@@ -278,12 +279,19 @@ function cacheProviderUsage(binaryPath, result, now) {
278
279
  lastProviderUsageFetch = now;
279
280
  return result;
280
281
  }
281
- export function formatExecBinaryPath(binaryPath) {
282
- if (process.platform === "win32" && binaryPath.includes(" ") && !binaryPath.startsWith('"')) {
282
+ export function formatExecBinaryPath(binaryPath, shell) {
283
+ const isWin = process.platform === "win32";
284
+ const needsShell = shell ?? (isWin && isWindowsBatchScript(binaryPath));
285
+ if (needsShell && isWin && binaryPath.includes(" ") && !binaryPath.startsWith('"')) {
283
286
  return `"${binaryPath}"`;
284
287
  }
285
288
  return binaryPath;
286
289
  }
290
+ export function resolveCommandExecution(binaryPath) {
291
+ const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
292
+ const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
293
+ return { cmd, shell: isBatch };
294
+ }
287
295
  export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
288
296
  const now = Date.now();
289
297
  if (!force &&
@@ -292,21 +300,22 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
292
300
  now - lastProviderUsageFetch < PROVIDER_USAGE_CACHE_TTL_MS) {
293
301
  return cachedProviderUsage;
294
302
  }
295
- const cmd = formatExecBinaryPath(binaryPath);
303
+ const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
304
+ const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
296
305
  try {
297
306
  const [usageResult, creditsResult] = await Promise.allSettled([
298
307
  execFileAsync(cmd, ["--print", "/usage"], {
299
308
  timeout: 8_000,
300
309
  maxBuffer: 1024 * 1024,
301
310
  env: process.env,
302
- shell: process.platform === "win32",
311
+ shell: isBatch,
303
312
  windowsHide: true,
304
313
  }),
305
314
  execFileAsync(cmd, ["--print", "/credits"], {
306
315
  timeout: 8_000,
307
316
  maxBuffer: 1024 * 1024,
308
317
  env: process.env,
309
- shell: process.platform === "win32",
318
+ shell: isBatch,
310
319
  windowsHide: true,
311
320
  }),
312
321
  ]);
@@ -429,13 +438,14 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
429
438
  return inFlightModelFetch;
430
439
  }
431
440
  inFlightModelFetch = (async () => {
432
- const cmd = formatExecBinaryPath(binaryPath);
441
+ const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
442
+ const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
433
443
  try {
434
444
  const { stdout } = await execFileAsync(cmd, ["models"], {
435
445
  timeout: 10_000,
436
446
  env: process.env,
437
447
  maxBuffer: 4 * 1024 * 1024,
438
- shell: process.platform === "win32",
448
+ shell: isBatch,
439
449
  windowsHide: true,
440
450
  });
441
451
  const parsed = parseAgyModelsOutput(stdout);
@@ -5,7 +5,7 @@ import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  import { logger } from "./logger.js";
7
7
  import { sessionStore } from "./session-store.js";
8
- import { formatExecBinaryPath } from "./protocol.js";
8
+ import { isWindowsBatchScript } from "./antigravity-process.js";
9
9
  const execFileAsync = promisify(execFile);
10
10
  const AGY_COMMAND_TIMEOUT_MS = 30_000;
11
11
  const AGY_COMMAND_MAX_BUFFER = 4 * 1024 * 1024;
@@ -337,13 +337,14 @@ export function formatUsageOutput(rawText) {
337
337
  return out;
338
338
  }
339
339
  async function runAgySlash(binaryPath, cwd, slashCommand) {
340
- const cmd = formatExecBinaryPath(binaryPath);
341
- const { stdout, stderr } = await execFileAsync(cmd, ["--print", slashCommand], {
340
+ const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
341
+ const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
342
+ const { stdout, stderr } = await execFileAsync(cmd, ["--print-timeout", "24h", "--print", slashCommand], {
342
343
  cwd,
343
344
  env: process.env,
344
345
  timeout: AGY_COMMAND_TIMEOUT_MS,
345
346
  maxBuffer: AGY_COMMAND_MAX_BUFFER,
346
- shell: process.platform === "win32",
347
+ shell: isBatch,
347
348
  windowsHide: true,
348
349
  });
349
350
  return stdout.trim() || stderr.trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
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",
@@ -45,7 +45,8 @@
45
45
  },
46
46
  "homepage": "https://github.com/tucomel/paseo-acp-agy#readme",
47
47
  "dependencies": {
48
- "@agentclientprotocol/sdk": "^0.17.1"
48
+ "@agentclientprotocol/sdk": "^0.17.1",
49
+ "@electron/asar": "^4.3.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^22.0.0",