paseo-acp-agy 1.1.4 → 1.1.5

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.
package/README.md CHANGED
@@ -90,6 +90,28 @@ To include `paseo-acp-agy` in Paseo's built-in provider store (`ACP_PROVIDER_CAT
90
90
 
91
91
  ---
92
92
 
93
+ ## Plan Usage & Context Window Meter Setup
94
+
95
+ To display the **Google Antigravity** quota breakdown in Paseo's `Settings -> Usage` (Plan usage) and activate the real-time context-window meter ("bolinha") in the chat composer:
96
+
97
+ ```bash
98
+ npx -y paseo-acp-agy setup
99
+ ```
100
+
101
+ Or if installed globally:
102
+ ```bash
103
+ paseo-acp-agy setup
104
+ ```
105
+
106
+ Then restart your Paseo app or daemon:
107
+ ```bash
108
+ paseo daemon restart
109
+ ```
110
+
111
+ > **Note**: `paseo-acp-agy` also checks and auto-configures your local Paseo server automatically whenever it launches via `--acp`. Running `setup` manually ensures your existing daemon instance picks up the telemetry hooks immediately upon restart.
112
+
113
+ ---
114
+
93
115
  ## Requirements
94
116
 
95
117
  - **Node.js**: >= 20.0.0 (Node.js 22 recommended)
@@ -19,6 +19,7 @@ export declare class ACPServer {
19
19
  private sendSuccess;
20
20
  private sendError;
21
21
  private publishCommands;
22
+ private publishUsageUpdate;
22
23
  private sessionState;
23
24
  private requireSession;
24
25
  private validateModel;
@@ -80,6 +80,26 @@ export class ACPServer {
80
80
  },
81
81
  });
82
82
  }
83
+ publishUsageUpdate(session) {
84
+ const costUsd = roundUsageCostUsd(session.usage.totalCostUsd);
85
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
86
+ sessionId: session.id,
87
+ update: {
88
+ sessionUpdate: "usage_update",
89
+ size: session.usage.contextWindowMaxTokens,
90
+ used: session.usage.contextWindowUsedTokens,
91
+ cost: { amount: costUsd, currency: "USD" },
92
+ inputTokens: session.usage.inputTokens,
93
+ outputTokens: session.usage.outputTokens,
94
+ cachedReadTokens: session.usage.cachedInputTokens,
95
+ cachedInputTokens: session.usage.cachedInputTokens,
96
+ totalTokens: session.usage.totalTokens,
97
+ totalCostUsd: costUsd,
98
+ contextWindowMaxTokens: session.usage.contextWindowMaxTokens,
99
+ contextWindowUsedTokens: session.usage.contextWindowUsedTokens,
100
+ },
101
+ });
102
+ }
83
103
  async sessionState(session, forceModels = false) {
84
104
  const availableModels = await fetchAvailableModels(this.binaryPath, forceModels);
85
105
  return {
@@ -178,6 +198,7 @@ export class ACPServer {
178
198
  if (!isNotification) {
179
199
  this.sendSuccess(id, await this.sessionState(session, true));
180
200
  this.publishCommands(session.id);
201
+ this.publishUsageUpdate(session);
181
202
  }
182
203
  break;
183
204
  }
@@ -212,6 +233,7 @@ export class ACPServer {
212
233
  if (!isNotification) {
213
234
  this.sendSuccess(id, await this.sessionState(session));
214
235
  this.publishCommands(session.id);
236
+ this.publishUsageUpdate(session);
215
237
  }
216
238
  }
217
239
  catch (err) {
@@ -375,6 +397,7 @@ export class ACPServer {
375
397
  // after model/tool work. Record it before branching on status.
376
398
  session.recordTurnUsage(turnUsage, executingModel);
377
399
  const usagePayload = this.turnUsagePayload(session, turnUsage, executingModel);
400
+ this.publishUsageUpdate(session);
378
401
  if (session.isCancelled) {
379
402
  if (!isNotification) {
380
403
  this.sendSuccess(id, { stopReason: "cancelled", usage: usagePayload });
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { ACPServer } from "./acp-server.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { formatDiagnosticVersion, resolveBuildMetadata } from "./version.js";
5
+ import { ensurePaseoIntegration } from "./paseo-patcher.js";
5
6
  const args = process.argv.slice(2);
6
7
  if (args.includes("--version") || args.includes("-v")) {
7
8
  if (args.includes("--json")) {
@@ -19,9 +20,14 @@ Usage:
19
20
  paseo-acp-agy [options]
20
21
  agy-acp [options]
21
22
  npx -y paseo-acp-agy [options]
23
+ npx -y paseo-acp-agy setup
24
+
25
+ Commands:
26
+ setup, patch Configure and integrate Antigravity telemetry with Paseo
22
27
 
23
28
  Options:
24
29
  --acp Start ACP server over stdio (default)
30
+ --setup Integrate Antigravity with local Paseo server installation
25
31
  -v, --version Show version
26
32
  --json Show version in JSON format (with --version)
27
33
  -h, --help Show help
@@ -32,9 +38,43 @@ Environment Variables:
32
38
  AGY_ACP_SANDBOX Set to 'true' to run agy in sandbox mode
33
39
  AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS Set to 'true' to auto-approve tool permissions
34
40
  AGY_BIN_PATH Path to agy binary (default: agy in PATH or ~/.local/bin/agy)
41
+ PASEO_SERVER_PATH Path to local @getpaseo/server directory
35
42
  `);
36
43
  process.exit(0);
37
44
  }
45
+ if (args.includes("setup") ||
46
+ args.includes("patch") ||
47
+ args.includes("--setup") ||
48
+ args.includes("--patch")) {
49
+ process.stdout.write("Checking Paseo installation and configuring Antigravity telemetry...\n");
50
+ try {
51
+ const res = ensurePaseoIntegration({ verbose: true });
52
+ 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");
55
+ }
56
+ 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` +
60
+ `Antigravity quota provider and context-window telemetry are now enabled!\n` +
61
+ `Please restart Paseo (or run 'paseo daemon restart') to apply changes.\n`);
62
+ }
63
+ else {
64
+ process.stdout.write("Paseo is already up-to-date and configured for Antigravity telemetry.\n");
65
+ }
66
+ }
67
+ }
68
+ catch (err) {
69
+ process.stderr.write(`Setup encountered an issue: ${err instanceof Error ? err.message : String(err)}\n`);
70
+ }
71
+ process.exit(0);
72
+ }
73
+ // Auto-run integration in background when starting ACP server
74
+ try {
75
+ ensurePaseoIntegration();
76
+ }
77
+ catch { }
38
78
  const server = new ACPServer();
39
79
  const cleanup = async () => {
40
80
  try {
@@ -0,0 +1,34 @@
1
+ export interface PatchResult {
2
+ found: boolean;
3
+ serverPaths: string[];
4
+ patchedPaths: string[];
5
+ errors: string[];
6
+ }
7
+ /**
8
+ * Searches the host machine for @getpaseo/server installations across
9
+ * Windows, macOS, and Linux.
10
+ */
11
+ export declare function findPaseoServerInstallations(): string[];
12
+ /**
13
+ * Returns the JavaScript source for the Antigravity quota provider to be
14
+ * injected into Paseo server's quota-fetcher providers.
15
+ */
16
+ export declare function generateAntigravityQuotaProviderJs(): string;
17
+ /**
18
+ * Patches a Paseo server installation directory to enable Antigravity:
19
+ * 1. Patches quota-fetcher/manifest.js to register Antigravity
20
+ * 2. Writes quota-fetcher/providers/antigravity.js
21
+ * 3. Patches acp-agent.js to map context window tokens and emit usage updates
22
+ */
23
+ export declare function patchPaseoServer(serverDir: string): {
24
+ success: boolean;
25
+ changes: string[];
26
+ error?: string;
27
+ };
28
+ /**
29
+ * Discovers and patches all accessible Paseo installations.
30
+ */
31
+ export declare function ensurePaseoIntegration(options?: {
32
+ verbose?: boolean;
33
+ targetPaths?: string[];
34
+ }): PatchResult;
@@ -0,0 +1,457 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { logger } from "./logger.js";
6
+ /**
7
+ * Searches the host machine for @getpaseo/server installations across
8
+ * Windows, macOS, and Linux.
9
+ */
10
+ export function findPaseoServerInstallations() {
11
+ const candidates = new Set();
12
+ // 1. Explicit environment variable overrides
13
+ if (process.env.PASEO_SERVER_PATH && fs.existsSync(process.env.PASEO_SERVER_PATH)) {
14
+ candidates.add(path.resolve(process.env.PASEO_SERVER_PATH));
15
+ }
16
+ if (process.env.PASEO_INSTALL_DIR && fs.existsSync(process.env.PASEO_INSTALL_DIR)) {
17
+ const direct = path.join(process.env.PASEO_INSTALL_DIR, "node_modules", "@getpaseo", "server");
18
+ if (fs.existsSync(direct))
19
+ candidates.add(path.resolve(direct));
20
+ }
21
+ // 2. Platform-specific default paths
22
+ const home = os.homedir();
23
+ if (process.platform === "win32") {
24
+ const appData = process.env.APPDATA || (home ? path.join(home, "AppData", "Roaming") : "");
25
+ const localAppData = process.env.LOCALAPPDATA || (home ? path.join(home, "AppData", "Local") : "");
26
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
27
+ const winLocations = [
28
+ path.join(appData, "npm", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
29
+ path.join(appData, "npm", "node_modules", "@getpaseo", "server"),
30
+ path.join(localAppData, "npm", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
31
+ path.join(localAppData, "npm", "node_modules", "@getpaseo", "server"),
32
+ path.join(programFiles, "nodejs", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
33
+ path.join(programFiles, "nodejs", "node_modules", "@getpaseo", "server"),
34
+ path.join(localAppData, "Programs", "Paseo", "resources", "app.asar.unpacked", "node_modules", "@getpaseo", "server"),
35
+ path.join(localAppData, "Programs", "Paseo", "resources", "app", "node_modules", "@getpaseo", "server"),
36
+ path.join(programFiles, "Paseo", "resources", "app.asar.unpacked", "node_modules", "@getpaseo", "server"),
37
+ ];
38
+ for (const loc of winLocations) {
39
+ if (loc && fs.existsSync(loc))
40
+ candidates.add(path.resolve(loc));
41
+ }
42
+ // Try detecting global npm root via npm.cmd
43
+ try {
44
+ const npmRoot = execFileSync("cmd.exe", ["/c", "npm.cmd", "root", "-g"], {
45
+ encoding: "utf-8",
46
+ timeout: 2000,
47
+ windowsHide: true,
48
+ }).trim();
49
+ if (npmRoot && fs.existsSync(npmRoot)) {
50
+ const p1 = path.join(npmRoot, "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
51
+ const p2 = path.join(npmRoot, "@getpaseo", "server");
52
+ if (fs.existsSync(p1))
53
+ candidates.add(path.resolve(p1));
54
+ if (fs.existsSync(p2))
55
+ candidates.add(path.resolve(p2));
56
+ }
57
+ }
58
+ catch { }
59
+ // Check where.exe paseo
60
+ try {
61
+ const whereOut = execFileSync("where.exe", ["paseo"], {
62
+ encoding: "utf-8",
63
+ timeout: 2000,
64
+ windowsHide: true,
65
+ }).trim();
66
+ const first = whereOut.split(/\r?\n/)[0]?.trim();
67
+ if (first) {
68
+ const paseoDir = path.dirname(first);
69
+ const p1 = path.join(paseoDir, "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
70
+ const p2 = path.join(paseoDir, "node_modules", "@getpaseo", "server");
71
+ if (fs.existsSync(p1))
72
+ candidates.add(path.resolve(p1));
73
+ if (fs.existsSync(p2))
74
+ candidates.add(path.resolve(p2));
75
+ }
76
+ }
77
+ catch { }
78
+ }
79
+ else {
80
+ // POSIX locations (Linux / macOS)
81
+ const posixLocations = [
82
+ "/usr/lib/node_modules/@getpaseo/cli/node_modules/@getpaseo/server",
83
+ "/usr/lib/node_modules/@getpaseo/server",
84
+ "/usr/local/lib/node_modules/@getpaseo/cli/node_modules/@getpaseo/server",
85
+ "/usr/local/lib/node_modules/@getpaseo/server",
86
+ "/Applications/Paseo.app/Contents/Resources/app.asar.unpacked/node_modules/@getpaseo/server",
87
+ "/Applications/Paseo.app/Contents/Resources/app/node_modules/@getpaseo/server",
88
+ ];
89
+ if (home) {
90
+ posixLocations.push(path.join(home, ".local", "share", "pnpm", "global", "5", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"), path.join(home, ".bun", "install", "global", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"));
91
+ // Check nvm paths if available
92
+ const nvmDir = path.join(home, ".nvm", "versions", "node");
93
+ if (fs.existsSync(nvmDir)) {
94
+ try {
95
+ const versions = fs.readdirSync(nvmDir);
96
+ for (const ver of versions) {
97
+ posixLocations.push(path.join(nvmDir, ver, "lib", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"), path.join(nvmDir, ver, "lib", "node_modules", "@getpaseo", "server"));
98
+ }
99
+ }
100
+ catch { }
101
+ }
102
+ }
103
+ for (const loc of posixLocations) {
104
+ if (fs.existsSync(loc))
105
+ candidates.add(path.resolve(loc));
106
+ }
107
+ try {
108
+ const npmRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8", timeout: 2000 }).trim();
109
+ if (npmRoot && fs.existsSync(npmRoot)) {
110
+ const p1 = path.join(npmRoot, "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
111
+ const p2 = path.join(npmRoot, "@getpaseo", "server");
112
+ if (fs.existsSync(p1))
113
+ candidates.add(path.resolve(p1));
114
+ if (fs.existsSync(p2))
115
+ candidates.add(path.resolve(p2));
116
+ }
117
+ }
118
+ catch { }
119
+ }
120
+ // Filter out any paths that do not actually have a dist directory or package.json
121
+ const verified = [];
122
+ for (const dir of candidates) {
123
+ if (fs.existsSync(path.join(dir, "dist")) || fs.existsSync(path.join(dir, "package.json"))) {
124
+ verified.push(dir);
125
+ }
126
+ }
127
+ return verified;
128
+ }
129
+ /**
130
+ * Returns the JavaScript source for the Antigravity quota provider to be
131
+ * injected into Paseo server's quota-fetcher providers.
132
+ */
133
+ export function generateAntigravityQuotaProviderJs() {
134
+ return `import { execFile } from "node:child_process";
135
+ import { promisify } from "node:util";
136
+ import fs from "node:fs";
137
+ import os from "node:os";
138
+ import path from "node:path";
139
+ import { toneFromUsedPct, windowFromUsedPct, unavailableUsage } from "../usage.js";
140
+
141
+ const execFileAsync = promisify(execFile);
142
+
143
+ function resolveAgyBinary() {
144
+ if (process.env.AGY_BIN_PATH) return process.env.AGY_BIN_PATH;
145
+ const home = os.homedir();
146
+ if (home) {
147
+ if (process.platform === "win32") {
148
+ const candidates = [
149
+ path.join(home, ".local", "bin", "agy.exe"),
150
+ path.join(home, "AppData", "Local", "Programs", "antigravity", "agy.exe"),
151
+ path.join(home, ".local", "bin", "agy.cmd"),
152
+ path.join(home, ".local", "bin", "agy.bat"),
153
+ ];
154
+ for (const cand of candidates) {
155
+ if (fs.existsSync(cand)) return cand;
156
+ }
157
+ try {
158
+ const { execFileSync } = require("node:child_process");
159
+ const out = execFileSync("where.exe", ["agy"], { encoding: "utf-8", timeout: 1000 }).trim();
160
+ const first = out.split(/\\r?\\n/)[0]?.trim();
161
+ if (first && fs.existsSync(first)) return first;
162
+ } catch {}
163
+ } else {
164
+ const localPath = path.join(home, ".local", "bin", "agy");
165
+ if (fs.existsSync(localPath)) return localPath;
166
+ }
167
+ }
168
+ return "agy";
169
+ }
170
+
171
+ export class AntigravityQuotaProvider {
172
+ constructor(options) {
173
+ this.providerId = "antigravity";
174
+ this.displayName = "Antigravity";
175
+ this.logger = typeof options?.logger?.child === "function" ? options.logger.child({ module: "antigravity-quota-provider" }) : options?.logger;
176
+ this.binaryPath = resolveAgyBinary();
177
+ }
178
+
179
+ async fetchUsage() {
180
+ try {
181
+ const isWin = process.platform === "win32";
182
+ const [usageRes, creditsRes] = await Promise.allSettled([
183
+ execFileAsync(this.binaryPath, ["--print", "/usage"], { timeout: 8000, env: process.env, shell: isWin }),
184
+ execFileAsync(this.binaryPath, ["--print", "/credits"], { timeout: 8000, env: process.env, shell: isWin }),
185
+ ]);
186
+
187
+ const usageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
188
+ const creditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
189
+
190
+ const rawWindows = [];
191
+ for (const line of usageOut.split(/[\\r\\n]+/)) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed || trimmed.toLowerCase().startsWith("quota:")) continue;
194
+
195
+ let scope = "";
196
+ let limitType = "";
197
+ let remainingPct = null;
198
+ let resetsAt = null;
199
+
200
+ const m = trimmed.match(/^(.*?)\\s{2,}(.*?Remaining)\\s+(\\d+)%(?:\\s+(.*))?$/i);
201
+ if (m) {
202
+ scope = m[1].trim();
203
+ limitType = m[2].trim();
204
+ remainingPct = parseInt(m[3], 10);
205
+ resetsAt = m[4] ? new Date(m[4].trim()).toISOString() : null;
206
+ } else {
207
+ const parts = trimmed.split(/\\t+|\\s{2,}/).map(p => p.trim());
208
+ if (parts.length >= 3) {
209
+ scope = parts[0];
210
+ limitType = parts[1];
211
+ const remMatch = parts[2].match(/(\\d+)%/);
212
+ if (remMatch) remainingPct = parseInt(remMatch[1], 10);
213
+ resetsAt = parts[3] ? new Date(parts[3]).toISOString() : null;
214
+ }
215
+ }
216
+
217
+ if (remainingPct !== null && !isNaN(remainingPct)) {
218
+ const usedPct = Math.max(0, Math.min(100, 100 - remainingPct));
219
+ const isFiveHour = /five\\s*hour/i.test(limitType);
220
+ const isWeekly = /weekly/i.test(limitType);
221
+ const isGemini = /gemini/i.test(scope);
222
+
223
+ let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
224
+ let label = isFiveHour ? "Session" : isWeekly ? "Weekly" : limitType.replace(/\\s+Remaining$/i, "");
225
+ if (!isGemini) {
226
+ id = \`claude_\${id}\`;
227
+ label = \`Claude \${label}\`;
228
+ }
229
+
230
+ rawWindows.push({
231
+ id,
232
+ label,
233
+ utilizationPct: usedPct,
234
+ resetsAt,
235
+ tone: toneFromUsedPct(usedPct),
236
+ isFiveHour,
237
+ isGemini,
238
+ });
239
+ }
240
+ }
241
+
242
+ rawWindows.sort((a, b) => {
243
+ if (a.isGemini && !b.isGemini) return -1;
244
+ if (!a.isGemini && b.isGemini) return 1;
245
+ if (a.isFiveHour && !b.isFiveHour) return -1;
246
+ if (!a.isFiveHour && b.isFiveHour) return 1;
247
+ return 0;
248
+ });
249
+
250
+ const windows = rawWindows.map(w => windowFromUsedPct(w));
251
+
252
+ const balances = [];
253
+ const credMatch = creditsOut.match(/Remaining\\s+credits\\s+([\\d.]+)/i);
254
+ const remainingCredits = credMatch ? parseFloat(credMatch[1]) : 0;
255
+ balances.push({
256
+ id: "credits",
257
+ label: "Credits",
258
+ remaining: remainingCredits,
259
+ unit: "usd",
260
+ tone: remainingCredits > 0 ? "ok" : "default",
261
+ });
262
+
263
+ return {
264
+ providerId: this.providerId,
265
+ displayName: "Antigravity",
266
+ status: "available",
267
+ planLabel: "Google Gemini",
268
+ windows,
269
+ balances,
270
+ details: [],
271
+ error: null,
272
+ };
273
+ } catch (err) {
274
+ return unavailableUsage({
275
+ providerId: this.providerId,
276
+ displayName: "Antigravity",
277
+ error: err.message,
278
+ });
279
+ }
280
+ }
281
+ }
282
+ `;
283
+ }
284
+ /**
285
+ * Patches a Paseo server installation directory to enable Antigravity:
286
+ * 1. Patches quota-fetcher/manifest.js to register Antigravity
287
+ * 2. Writes quota-fetcher/providers/antigravity.js
288
+ * 3. Patches acp-agent.js to map context window tokens and emit usage updates
289
+ */
290
+ export function patchPaseoServer(serverDir) {
291
+ const changes = [];
292
+ try {
293
+ // 1. Locate manifest.js in quota-fetcher
294
+ const manifestCandidates = [
295
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
296
+ path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
297
+ ];
298
+ let manifestFile = manifestCandidates.find((f) => fs.existsSync(f));
299
+ if (!manifestFile && fs.existsSync(path.join(serverDir, "dist"))) {
300
+ const findManifest = (dir) => {
301
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
302
+ for (const e of entries) {
303
+ const full = path.join(dir, e.name);
304
+ if (e.isDirectory() && e.name !== "node_modules") {
305
+ const found = findManifest(full);
306
+ if (found)
307
+ return found;
308
+ }
309
+ else if (e.isFile() && e.name === "manifest.js" && dir.includes("quota-fetcher")) {
310
+ return full;
311
+ }
312
+ }
313
+ return null;
314
+ };
315
+ manifestFile = findManifest(path.join(serverDir, "dist")) || undefined;
316
+ }
317
+ if (manifestFile) {
318
+ const quotaDir = path.dirname(manifestFile);
319
+ const providersDir = path.join(quotaDir, "providers");
320
+ if (!fs.existsSync(providersDir)) {
321
+ fs.mkdirSync(providersDir, { recursive: true });
322
+ }
323
+ // Write / update providers/antigravity.js
324
+ const antigravityJsPath = path.join(providersDir, "antigravity.js");
325
+ fs.writeFileSync(antigravityJsPath, generateAntigravityQuotaProviderJs(), "utf-8");
326
+ changes.push(`Created/Updated ${antigravityJsPath}`);
327
+ // Patch manifest.js
328
+ let manifestCode = fs.readFileSync(manifestFile, "utf-8");
329
+ let manifestModified = false;
330
+ if (!manifestCode.includes('from "./providers/antigravity.js"') && !manifestCode.includes("AntigravityQuotaProvider")) {
331
+ manifestCode = `import { AntigravityQuotaProvider } from "./providers/antigravity.js";\n` + manifestCode;
332
+ manifestModified = true;
333
+ }
334
+ if (!manifestCode.includes('providerId: "antigravity"')) {
335
+ const entryToAdd = ` {\n providerId: "antigravity",\n create: (options) => new AntigravityQuotaProvider({\n logger: options.logger,\n fetch: options.fetch,\n }),\n },\n`;
336
+ const marker = "export const PROVIDER_USAGE_FETCHERS = [";
337
+ if (manifestCode.includes(marker)) {
338
+ manifestCode = manifestCode.replace(marker, `${marker}\n${entryToAdd}`);
339
+ manifestModified = true;
340
+ }
341
+ }
342
+ if (manifestModified) {
343
+ fs.writeFileSync(manifestFile, manifestCode, "utf-8");
344
+ changes.push(`Patched ${manifestFile} with AntigravityQuotaProvider`);
345
+ }
346
+ }
347
+ // 2. Locate acp-agent.js
348
+ const acpCandidates = [
349
+ path.join(serverDir, "dist", "server", "server", "agent", "providers", "acp-agent.js"),
350
+ path.join(serverDir, "dist", "server", "agent", "providers", "acp-agent.js"),
351
+ path.join(serverDir, "dist", "agent", "providers", "acp-agent.js"),
352
+ ];
353
+ let acpFile = acpCandidates.find((f) => fs.existsSync(f));
354
+ if (!acpFile && fs.existsSync(path.join(serverDir, "dist"))) {
355
+ const findAcp = (dir) => {
356
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
357
+ for (const e of entries) {
358
+ const full = path.join(dir, e.name);
359
+ if (e.isDirectory() && e.name !== "node_modules") {
360
+ const found = findAcp(full);
361
+ if (found)
362
+ return found;
363
+ }
364
+ else if (e.isFile() && e.name === "acp-agent.js") {
365
+ return full;
366
+ }
367
+ }
368
+ return null;
369
+ };
370
+ acpFile = findAcp(path.join(serverDir, "dist")) || undefined;
371
+ }
372
+ if (acpFile) {
373
+ let acpCode = fs.readFileSync(acpFile, "utf-8");
374
+ let acpModified = false;
375
+ // Patch mapACPUsage
376
+ if (!acpCode.includes("contextWindowMaxTokens: usage.contextWindowMaxTokens")) {
377
+ const oldMapRegex = /export\s+function\s+mapACPUsage\s*\([^)]*\)\s*\{[\s\S]*?return\s*\{[\s\S]*?\};\s*\}/m;
378
+ const newMap = `export function mapACPUsage(usage) {
379
+ if (!usage) {
380
+ return undefined;
381
+ }
382
+ return {
383
+ inputTokens: usage.inputTokens ?? undefined,
384
+ outputTokens: usage.outputTokens ?? undefined,
385
+ cachedInputTokens: usage.cachedReadTokens ?? usage.cachedInputTokens ?? undefined,
386
+ totalCostUsd: usage.totalCostUsd ?? (usage.cost?.amount !== undefined ? Number(usage.cost.amount) : undefined),
387
+ contextWindowMaxTokens: usage.contextWindowMaxTokens ?? usage.size ?? undefined,
388
+ contextWindowUsedTokens: usage.contextWindowUsedTokens ?? usage.used ?? undefined,
389
+ };
390
+ }`;
391
+ if (oldMapRegex.test(acpCode)) {
392
+ acpCode = acpCode.replace(oldMapRegex, newMap);
393
+ acpModified = true;
394
+ }
395
+ }
396
+ // Patch handleUsageUpdate
397
+ if (acpCode.includes("handleUsageUpdate(update) {") && !acpCode.includes("this.notifySubscribers({")) {
398
+ const oldHandlerRegex = /handleUsageUpdate\s*\(\s*update\s*\)\s*\{[\s\S]*?void\s+update;?[\s\S]*?\}/m;
399
+ const newHandler = `handleUsageUpdate(update) {
400
+ if (!update) return;
401
+ const usage = mapACPUsage(update);
402
+ if (usage) {
403
+ this.currentTurnUsage = { ...this.currentTurnUsage, ...usage };
404
+ this.notifySubscribers({
405
+ type: "usage_updated",
406
+ provider: this.provider,
407
+ usage: this.currentTurnUsage,
408
+ ...(this.activeForegroundTurnId ? { turnId: this.activeForegroundTurnId } : {}),
409
+ });
410
+ }
411
+ }`;
412
+ if (oldHandlerRegex.test(acpCode)) {
413
+ acpCode = acpCode.replace(oldHandlerRegex, newHandler);
414
+ acpModified = true;
415
+ }
416
+ }
417
+ if (acpModified) {
418
+ fs.writeFileSync(acpFile, acpCode, "utf-8");
419
+ changes.push(`Patched ${acpFile} for context-window token telemetry and usage updates`);
420
+ }
421
+ }
422
+ return { success: true, changes };
423
+ }
424
+ catch (err) {
425
+ const errorMsg = err instanceof Error ? err.message : String(err);
426
+ logger.warn("Failed to patch Paseo server", { serverDir, error: errorMsg });
427
+ return { success: false, changes, error: errorMsg };
428
+ }
429
+ }
430
+ /**
431
+ * Discovers and patches all accessible Paseo installations.
432
+ */
433
+ export function ensurePaseoIntegration(options) {
434
+ const serverPaths = options?.targetPaths || findPaseoServerInstallations();
435
+ const patchedPaths = [];
436
+ const errors = [];
437
+ for (const sPath of serverPaths) {
438
+ const res = patchPaseoServer(sPath);
439
+ if (res.success) {
440
+ if (res.changes.length > 0) {
441
+ patchedPaths.push(sPath);
442
+ if (options?.verbose) {
443
+ logger.info(`Integrated with Paseo server at ${sPath}`, { changes: res.changes });
444
+ }
445
+ }
446
+ }
447
+ else if (res.error) {
448
+ errors.push(`${sPath}: ${res.error}`);
449
+ }
450
+ }
451
+ return {
452
+ found: serverPaths.length > 0,
453
+ serverPaths,
454
+ patchedPaths,
455
+ errors,
456
+ };
457
+ }
@@ -173,6 +173,7 @@ export interface ProviderUsage {
173
173
  }>;
174
174
  error: string | null;
175
175
  }
176
+ export declare function formatExecBinaryPath(binaryPath: string): string;
176
177
  export declare function fetchAntigravityUsage(binaryPath?: string, force?: boolean): Promise<ProviderUsage>;
177
178
  export declare const ALL_THINKING_LEVELS: Record<string, {
178
179
  name: string;
package/dist/protocol.js CHANGED
@@ -278,6 +278,12 @@ function cacheProviderUsage(binaryPath, result, now) {
278
278
  lastProviderUsageFetch = now;
279
279
  return result;
280
280
  }
281
+ export function formatExecBinaryPath(binaryPath) {
282
+ if (process.platform === "win32" && binaryPath.includes(" ") && !binaryPath.startsWith('"')) {
283
+ return `"${binaryPath}"`;
284
+ }
285
+ return binaryPath;
286
+ }
281
287
  export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
282
288
  const now = Date.now();
283
289
  if (!force &&
@@ -286,15 +292,16 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
286
292
  now - lastProviderUsageFetch < PROVIDER_USAGE_CACHE_TTL_MS) {
287
293
  return cachedProviderUsage;
288
294
  }
295
+ const cmd = formatExecBinaryPath(binaryPath);
289
296
  try {
290
297
  const [usageResult, creditsResult] = await Promise.allSettled([
291
- execFileAsync(binaryPath, ["--print", "/usage"], {
298
+ execFileAsync(cmd, ["--print", "/usage"], {
292
299
  timeout: 8_000,
293
300
  maxBuffer: 1024 * 1024,
294
301
  env: process.env,
295
302
  shell: process.platform === "win32",
296
303
  }),
297
- execFileAsync(binaryPath, ["--print", "/credits"], {
304
+ execFileAsync(cmd, ["--print", "/credits"], {
298
305
  timeout: 8_000,
299
306
  maxBuffer: 1024 * 1024,
300
307
  env: process.env,
@@ -371,6 +378,7 @@ export const FALLBACK_MODELS = [
371
378
  let cachedModels = null;
372
379
  let cachedModelsBinaryPath = null;
373
380
  let lastModelFetch = 0;
381
+ let inFlightModelFetch = null;
374
382
  const MODEL_CACHE_TTL_MS = 60_000;
375
383
  export function parseAgyModelsOutput(rawOutput) {
376
384
  const modelsMap = new Map();
@@ -384,24 +392,21 @@ export function parseAgyModelsOutput(rawOutput) {
384
392
  const modelId = parts[0];
385
393
  const label = parts[1];
386
394
  const effortMatch = modelId.match(/-(high|medium|low)$/);
387
- let baseId = modelId;
388
- let baseLabel = label;
389
- let effort = null;
390
- if (effortMatch) {
391
- effort = effortMatch[1];
392
- baseId = modelId.slice(0, -(effort.length + 1));
393
- baseLabel = label.replace(/\s*\((High|Medium|Low)\)$/, "");
394
- }
395
+ const baseId = effortMatch ? modelId.replace(/-(high|medium|low)$/, "") : modelId;
396
+ const effort = effortMatch ? effortMatch[1] : undefined;
397
+ const cleanLabel = effortMatch
398
+ ? label.replace(/\s*\((High|Medium|Low)\)$/i, "").trim()
399
+ : label;
395
400
  if (!modelsMap.has(baseId)) {
396
401
  modelsMap.set(baseId, {
397
402
  modelId: baseId,
398
- name: baseLabel,
403
+ name: cleanLabel,
399
404
  description: label,
400
- supportedEfforts: [],
405
+ supportedEfforts: effort ? [effort] : [],
401
406
  contextWindowMaxTokens: getModelContextWindow(baseId),
402
407
  });
403
408
  }
404
- if (effort) {
409
+ else if (effort) {
405
410
  const entry = modelsMap.get(baseId);
406
411
  if (!entry.supportedEfforts.includes(effort))
407
412
  entry.supportedEfforts.push(effort);
@@ -418,28 +423,38 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
418
423
  now - lastModelFetch < MODEL_CACHE_TTL_MS) {
419
424
  return cachedModels;
420
425
  }
421
- try {
422
- const { stdout } = await execFileAsync(binaryPath, ["models"], {
423
- timeout: 5_000,
424
- env: process.env,
425
- maxBuffer: 4 * 1024 * 1024,
426
- shell: process.platform === "win32",
427
- });
428
- const parsed = parseAgyModelsOutput(stdout);
429
- cachedModels = parsed;
430
- cachedModelsBinaryPath = binaryPath;
431
- lastModelFetch = now;
432
- return parsed;
433
- }
434
- catch (err) {
435
- logger.warn("Failed to fetch models from agy CLI, using fallback models", {
436
- error: err.message,
437
- });
438
- cachedModels = FALLBACK_MODELS;
439
- cachedModelsBinaryPath = binaryPath;
440
- lastModelFetch = now;
441
- return FALLBACK_MODELS;
426
+ if (inFlightModelFetch) {
427
+ return inFlightModelFetch;
442
428
  }
429
+ inFlightModelFetch = (async () => {
430
+ const cmd = formatExecBinaryPath(binaryPath);
431
+ try {
432
+ const { stdout } = await execFileAsync(cmd, ["models"], {
433
+ timeout: 10_000,
434
+ env: process.env,
435
+ maxBuffer: 4 * 1024 * 1024,
436
+ shell: process.platform === "win32",
437
+ });
438
+ const parsed = parseAgyModelsOutput(stdout);
439
+ cachedModels = parsed;
440
+ cachedModelsBinaryPath = binaryPath;
441
+ lastModelFetch = Date.now();
442
+ return parsed;
443
+ }
444
+ catch (err) {
445
+ logger.warn("Failed to fetch models from agy CLI, using fallback models", {
446
+ error: err.message,
447
+ });
448
+ cachedModels = FALLBACK_MODELS;
449
+ cachedModelsBinaryPath = binaryPath;
450
+ lastModelFetch = Date.now();
451
+ return FALLBACK_MODELS;
452
+ }
453
+ finally {
454
+ inFlightModelFetch = null;
455
+ }
456
+ })();
457
+ return inFlightModelFetch;
443
458
  }
444
459
  export function getEffectiveEffortForModel(modelId, requestedEffort, models) {
445
460
  if (!modelId)
@@ -5,6 +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
9
  const execFileAsync = promisify(execFile);
9
10
  const AGY_COMMAND_TIMEOUT_MS = 30_000;
10
11
  const AGY_COMMAND_MAX_BUFFER = 4 * 1024 * 1024;
@@ -336,7 +337,8 @@ export function formatUsageOutput(rawText) {
336
337
  return out;
337
338
  }
338
339
  async function runAgySlash(binaryPath, cwd, slashCommand) {
339
- const { stdout, stderr } = await execFileAsync(binaryPath, ["--print", slashCommand], {
340
+ const cmd = formatExecBinaryPath(binaryPath);
341
+ const { stdout, stderr } = await execFileAsync(cmd, ["--print", slashCommand], {
340
342
  cwd,
341
343
  env: process.env,
342
344
  timeout: AGY_COMMAND_TIMEOUT_MS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
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",