paseo-acp-agy 1.1.8 → 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.
@@ -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,54 +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 appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
16
- const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
17
- const programFiles = process.env.ProgramFiles || "C:\\Program Files";
18
- const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
19
- const candidates = [
20
- path.join(home, ".local", "bin", "agy.exe"),
21
- path.join(home, ".local", "bin", "agy.cmd"),
22
- path.join(home, ".local", "bin", "agy.bat"),
23
- path.join(appData, "npm", "agy.cmd"),
24
- path.join(appData, "npm", "agy.exe"),
25
- path.join(appData, "npm", "agy.bat"),
26
- path.join(localAppData, "npm", "agy.cmd"),
27
- path.join(localAppData, "npm", "agy.exe"),
28
- path.join(localAppData, "Programs", "antigravity", "agy.exe"),
29
- path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
30
- path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
31
- path.join(programFiles, "Antigravity", "bin", "agy.exe"),
32
- path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
33
- ];
34
- for (const cand of candidates) {
35
- if (fs.existsSync(cand))
36
- 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
+ }
37
112
  }
38
- try {
39
- const out = execFileSync("where.exe", ["agy"], {
40
- encoding: "utf-8",
41
- timeout: 2000,
42
- windowsHide: true,
43
- }).trim();
44
- const first = out.split(/\r?\n/)[0]?.trim();
45
- if (first && fs.existsSync(first))
46
- return first;
113
+ else {
114
+ const localPath = path.join(home, ".local", "bin", "agy");
115
+ if (fs.existsSync(localPath))
116
+ resolved = localPath;
47
117
  }
48
- catch { }
49
- }
50
- else {
51
- const localPath = path.join(home, ".local", "bin", "agy");
52
- if (fs.existsSync(localPath))
53
- return localPath;
54
118
  }
55
119
  }
56
- return "agy";
120
+ cachedBinaryPath = resolved;
121
+ lastBinaryResolveTime = now;
122
+ return resolved;
57
123
  }
58
124
  export class AntigravityProcess extends EventEmitter {
59
125
  child = null;
@@ -282,12 +348,13 @@ export class AntigravityProcess extends EventEmitter {
282
348
  effectiveEffort,
283
349
  });
284
350
  const isWin = process.platform === "win32";
351
+ const isBatch = isWin && isWindowsBatchScript(this.binaryPath);
285
352
  const child = spawn(this.binaryPath, args, {
286
353
  cwd: this.cwd,
287
354
  env: this.env,
288
355
  stdio: ["pipe", "pipe", "pipe"],
289
356
  detached: !isWin,
290
- shell: isWin,
357
+ shell: isBatch,
291
358
  windowsHide: true,
292
359
  });
293
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,13 +46,168 @@ 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
213
  const res = await ensurePaseoIntegration({ verbose: true });
@@ -49,3 +49,16 @@ export declare function ensurePaseoIntegration(options?: {
49
49
  targetPaths?: string[];
50
50
  targetAsarPaths?: string[];
51
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,8 +181,18 @@ 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") {
@@ -189,35 +201,80 @@ function resolveAgyBinary() {
189
201
  const programFiles = process.env.ProgramFiles || "C:\\\\Program Files";
190
202
  const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\\\Program Files (x86)";
191
203
 
192
- const candidates = [
193
- path.join(home, ".local", "bin", "agy.exe"),
194
- path.join(home, ".local", "bin", "agy.cmd"),
195
- path.join(home, ".local", "bin", "agy.bat"),
196
- path.join(appData, "npm", "agy.cmd"),
197
- path.join(appData, "npm", "agy.exe"),
198
- path.join(appData, "npm", "agy.bat"),
199
- path.join(localAppData, "npm", "agy.cmd"),
200
- path.join(localAppData, "npm", "agy.exe"),
201
- path.join(localAppData, "Programs", "antigravity", "agy.exe"),
204
+ // Prioritize .exe candidates over .cmd / .bat
205
+ const exeCandidates = [
202
206
  path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
203
- path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
207
+ path.join(localAppData, "Programs", "antigravity", "agy.exe"),
208
+ path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
204
209
  path.join(programFiles, "Antigravity", "bin", "agy.exe"),
205
210
  path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
211
+ path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
212
+ path.join(home, ".local", "bin", "agy.exe"),
213
+ path.join(appData, "npm", "agy.exe"),
214
+ path.join(localAppData, "npm", "agy.exe"),
206
215
  ];
207
- for (const cand of candidates) {
208
- 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
+ }
209
268
  }
210
- try {
211
- const out = execFileSync("where.exe", ["agy"], { encoding: "utf-8", timeout: 2000, windowsHide: true }).trim();
212
- const first = out.split(/\\r?\\n/)[0]?.trim();
213
- if (first && fs.existsSync(first)) return first;
214
- } catch {}
215
269
  } else {
216
270
  const localPath = path.join(home, ".local", "bin", "agy");
217
- if (fs.existsSync(localPath)) return localPath;
271
+ if (fs.existsSync(localPath)) resolved = localPath;
218
272
  }
219
273
  }
220
- return "agy";
274
+
275
+ cachedAgyBin = resolved;
276
+ cachedAgyBinTime = now;
277
+ return resolved;
221
278
  }
222
279
 
223
280
  export class AntigravityQuotaProvider {
@@ -231,20 +288,22 @@ export class AntigravityQuotaProvider {
231
288
  async fetchUsage() {
232
289
  try {
233
290
  const isWin = process.platform === "win32";
234
- let bin = this.binaryPath;
235
- if (isWin && bin.includes(" ") && !bin.startsWith('"')) {
236
- bin = \`"\${bin}"\`;
237
- }
291
+ const bin = resolveAgyBinary();
292
+ this.binaryPath = bin;
293
+ const isBatch = isWin && /\\.(cmd|bat)$/i.test(bin);
238
294
  const [usageRes, creditsRes] = await Promise.allSettled([
239
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/usage"], { timeout: 15000, env: process.env, shell: isWin, windowsHide: true }),
240
- execFileAsync(bin, ["--print-timeout", "24h", "--print", "/credits"], { timeout: 15000, 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 }),
241
297
  ]);
242
298
 
243
- const usageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
244
- 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");
245
304
 
246
305
  const rawWindows = [];
247
- for (const line of usageOut.split(/[\\r\\n]+/)) {
306
+ for (const line of usageOut.split("\\n")) {
248
307
  const trimmed = line.trim();
249
308
  if (!trimmed || trimmed.toLowerCase().startsWith("quota:")) continue;
250
309
 
@@ -362,7 +421,7 @@ export function patchPaseoServer(serverDir) {
362
421
  if (found)
363
422
  return found;
364
423
  }
365
- 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")) {
366
425
  return full;
367
426
  }
368
427
  }
@@ -389,9 +448,9 @@ export function patchPaseoServer(serverDir) {
389
448
  }
390
449
  if (!manifestCode.includes('providerId: "antigravity"')) {
391
450
  const entryToAdd = ` {\n providerId: "antigravity",\n create: (options) => new AntigravityQuotaProvider({\n logger: options.logger,\n fetch: options.fetch,\n }),\n },\n`;
392
- const marker = "export const PROVIDER_USAGE_FETCHERS = [";
393
- if (manifestCode.includes(marker)) {
394
- 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}`);
395
454
  manifestModified = true;
396
455
  }
397
456
  }
@@ -448,11 +507,44 @@ export function patchPaseoServer(serverDir) {
448
507
  acpCode = acpCode.replace(oldMapRegex, newMap);
449
508
  acpModified = true;
450
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
+ }
451
530
  }
452
531
  // Patch handleUsageUpdate
453
- if (acpCode.includes("handleUsageUpdate(update) {") && (!acpCode.includes("this.deliverTranslatedEvents") || acpCode.includes("this.notifySubscribers"))) {
454
- const handlerRegex = /handleUsageUpdate\s*\(\s*update\s*\)\s*\{[\s\S]*?(?:void\s+update;|this\.notifySubscribers)[\s\S]*?\n\s*\}/m;
455
- 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) {
456
548
  if (!update) return;
457
549
  const usage = mapACPUsage(update);
458
550
  if (usage) {
@@ -470,9 +562,10 @@ export function patchPaseoServer(serverDir) {
470
562
  }
471
563
  }
472
564
  }`;
473
- if (handlerRegex.test(acpCode)) {
474
- acpCode = acpCode.replace(handlerRegex, newHandler);
475
- acpModified = true;
565
+ acpCode = acpCode.slice(0, startIdx) + newHandler + acpCode.slice(i);
566
+ acpModified = true;
567
+ }
568
+ }
476
569
  }
477
570
  }
478
571
  if (acpModified) {
@@ -505,9 +598,12 @@ export function findPaseoAsarPaths() {
505
598
  const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
506
599
  const winAsarLocations = [
507
600
  path.join(localAppData, "Programs", "Paseo", "resources", "app.asar"),
601
+ path.join(localAppData, "Programs", "paseo", "resources", "app.asar"),
508
602
  path.join(localAppData, "Paseo", "resources", "app.asar"),
509
603
  path.join(programFiles, "Paseo", "resources", "app.asar"),
604
+ path.join(programFiles, "paseo", "resources", "app.asar"),
510
605
  path.join(programFilesX86, "Paseo", "resources", "app.asar"),
606
+ path.join(programFilesX86, "paseo", "resources", "app.asar"),
511
607
  path.join(appData, "Paseo", "resources", "app.asar"),
512
608
  ];
513
609
  for (const loc of winAsarLocations) {
@@ -521,9 +617,13 @@ export function findPaseoAsarPaths() {
521
617
  windowsHide: true,
522
618
  }).trim();
523
619
  for (const line of whereOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)) {
524
- const asarCandidate = path.join(path.dirname(line), "resources", "app.asar");
525
- if (fs.existsSync(asarCandidate))
526
- candidates.add(path.resolve(asarCandidate));
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
+ }
527
627
  }
528
628
  }
529
629
  catch { }
@@ -584,7 +684,8 @@ export async function patchPaseoAsar(asarPath) {
584
684
  for (const ent of entries) {
585
685
  if (ent.isDirectory()) {
586
686
  const full = path.join(current, ent.name);
587
- if (ent.name === "server" && full.includes(path.join("@getpaseo", "server"))) {
687
+ const normalizedFull = full.replace(/\\/g, "/");
688
+ if (ent.name === "server" && normalizedFull.includes("@getpaseo/server")) {
588
689
  serverDir = full;
589
690
  break;
590
691
  }
@@ -612,14 +713,37 @@ export async function patchPaseoAsar(asarPath) {
612
713
  // Create backup if not already present
613
714
  const backupPath = `${asarPath}.bak`;
614
715
  if (!fs.existsSync(backupPath)) {
615
- fs.copyFileSync(asarPath, backupPath);
616
- changes.push(`Backed up original asar to ${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
+ }
617
723
  }
618
724
  tempAsar = path.join(os.tmpdir(), `app-${Date.now()}.asar`);
619
725
  await asar.createPackage(tempDir, tempAsar);
620
- // Replace original archive
621
- fs.copyFileSync(tempAsar, asarPath);
622
- changes.push(`Repacked updated asar archive at ${asarPath}`);
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
+ }
623
747
  return { success: true, changes };
624
748
  }
625
749
  catch (err) {
@@ -687,3 +811,101 @@ export async function ensurePaseoIntegration(options) {
687
811
  errors,
688
812
  };
689
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);
340
+ const isBatch = process.platform === "win32" && isWindowsBatchScript(binaryPath);
341
+ const cmd = isBatch && binaryPath.includes(" ") && !binaryPath.startsWith('"') ? `"${binaryPath}"` : binaryPath;
341
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.8",
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",