paseo-acp-agy 1.1.4 → 1.1.6

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 });
@@ -23,7 +23,11 @@ export function resolveDefaultAgyBinary() {
23
23
  return cand;
24
24
  }
25
25
  try {
26
- const out = execFileSync("where.exe", ["agy"], { encoding: "utf-8", timeout: 1000 }).trim();
26
+ const out = execFileSync("where.exe", ["agy"], {
27
+ encoding: "utf-8",
28
+ timeout: 2000,
29
+ windowsHide: true,
30
+ }).trim();
27
31
  const first = out.split(/\r?\n/)[0]?.trim();
28
32
  if (first && fs.existsSync(first))
29
33
  return first;
@@ -156,7 +160,10 @@ export class AntigravityProcess extends EventEmitter {
156
160
  }
157
161
  else if (pid && process.platform === "win32") {
158
162
  try {
159
- execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
163
+ execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], {
164
+ stdio: "ignore",
165
+ windowsHide: true,
166
+ });
160
167
  return true;
161
168
  }
162
169
  catch {
@@ -260,12 +267,14 @@ export class AntigravityProcess extends EventEmitter {
260
267
  cwd: this.cwd,
261
268
  effectiveEffort,
262
269
  });
270
+ const isWin = process.platform === "win32";
263
271
  const child = spawn(this.binaryPath, args, {
264
272
  cwd: this.cwd,
265
273
  env: this.env,
266
274
  stdio: ["pipe", "pipe", "pipe"],
267
- detached: process.platform !== "win32",
268
- shell: process.platform === "win32",
275
+ detached: !isWin,
276
+ shell: isWin,
277
+ windowsHide: true,
269
278
  });
270
279
  this.child = child;
271
280
  if (!child.stdin || !child.stdout || !child.stderr) {
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,504 @@
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
+ path.join(home, "AppData", "Roaming", "npm", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
38
+ path.join(home, "AppData", "Roaming", "npm", "node_modules", "@getpaseo", "server"),
39
+ path.join(home, ".npm-global", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
40
+ path.join(home, ".npm-global", "node_modules", "@getpaseo", "server"),
41
+ ];
42
+ for (const loc of winLocations) {
43
+ if (loc && fs.existsSync(loc))
44
+ candidates.add(path.resolve(loc));
45
+ }
46
+ // Try detecting global npm root via npm.cmd
47
+ try {
48
+ const npmRoot = execFileSync("cmd.exe", ["/c", "npm.cmd", "root", "-g"], {
49
+ encoding: "utf-8",
50
+ timeout: 8000,
51
+ windowsHide: true,
52
+ }).trim();
53
+ if (npmRoot && fs.existsSync(npmRoot)) {
54
+ const p1 = path.join(npmRoot, "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
55
+ const p2 = path.join(npmRoot, "@getpaseo", "server");
56
+ if (fs.existsSync(p1))
57
+ candidates.add(path.resolve(p1));
58
+ if (fs.existsSync(p2))
59
+ candidates.add(path.resolve(p2));
60
+ }
61
+ }
62
+ catch { }
63
+ // Try detecting npm global prefix
64
+ try {
65
+ const npmPrefix = execFileSync("cmd.exe", ["/c", "npm.cmd", "config", "get", "prefix"], {
66
+ encoding: "utf-8",
67
+ timeout: 8000,
68
+ windowsHide: true,
69
+ }).trim();
70
+ if (npmPrefix && fs.existsSync(npmPrefix)) {
71
+ const p1 = path.join(npmPrefix, "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
72
+ const p2 = path.join(npmPrefix, "node_modules", "@getpaseo", "server");
73
+ if (fs.existsSync(p1))
74
+ candidates.add(path.resolve(p1));
75
+ if (fs.existsSync(p2))
76
+ candidates.add(path.resolve(p2));
77
+ }
78
+ }
79
+ catch { }
80
+ // Check where.exe paseo across all output lines
81
+ try {
82
+ const whereOut = execFileSync("where.exe", ["paseo"], {
83
+ encoding: "utf-8",
84
+ timeout: 4000,
85
+ windowsHide: true,
86
+ }).trim();
87
+ for (const line of whereOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)) {
88
+ const paseoDir = path.dirname(line);
89
+ const checks = [
90
+ path.join(paseoDir, "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"),
91
+ path.join(paseoDir, "node_modules", "@getpaseo", "server"),
92
+ path.join(path.dirname(paseoDir), "node_modules", "@getpaseo", "server"),
93
+ path.join(paseoDir, "resources", "app.asar.unpacked", "node_modules", "@getpaseo", "server"),
94
+ path.join(paseoDir, "resources", "app", "node_modules", "@getpaseo", "server"),
95
+ ];
96
+ for (const c of checks) {
97
+ if (fs.existsSync(c))
98
+ candidates.add(path.resolve(c));
99
+ }
100
+ }
101
+ }
102
+ catch { }
103
+ // Search common PATH dirs for npm/node_modules
104
+ if (process.env.PATH) {
105
+ for (const p of process.env.PATH.split(";")) {
106
+ const trimmed = p.trim();
107
+ if (!trimmed || !fs.existsSync(trimmed))
108
+ continue;
109
+ const p1 = path.join(trimmed, "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
110
+ const p2 = path.join(trimmed, "node_modules", "@getpaseo", "server");
111
+ if (fs.existsSync(p1))
112
+ candidates.add(path.resolve(p1));
113
+ if (fs.existsSync(p2))
114
+ candidates.add(path.resolve(p2));
115
+ }
116
+ }
117
+ }
118
+ else {
119
+ // POSIX locations (Linux / macOS)
120
+ const posixLocations = [
121
+ "/usr/lib/node_modules/@getpaseo/cli/node_modules/@getpaseo/server",
122
+ "/usr/lib/node_modules/@getpaseo/server",
123
+ "/usr/local/lib/node_modules/@getpaseo/cli/node_modules/@getpaseo/server",
124
+ "/usr/local/lib/node_modules/@getpaseo/server",
125
+ "/Applications/Paseo.app/Contents/Resources/app.asar.unpacked/node_modules/@getpaseo/server",
126
+ "/Applications/Paseo.app/Contents/Resources/app/node_modules/@getpaseo/server",
127
+ ];
128
+ if (home) {
129
+ 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"));
130
+ // Check nvm paths if available
131
+ const nvmDir = path.join(home, ".nvm", "versions", "node");
132
+ if (fs.existsSync(nvmDir)) {
133
+ try {
134
+ const versions = fs.readdirSync(nvmDir);
135
+ for (const ver of versions) {
136
+ posixLocations.push(path.join(nvmDir, ver, "lib", "node_modules", "@getpaseo", "cli", "node_modules", "@getpaseo", "server"), path.join(nvmDir, ver, "lib", "node_modules", "@getpaseo", "server"));
137
+ }
138
+ }
139
+ catch { }
140
+ }
141
+ }
142
+ for (const loc of posixLocations) {
143
+ if (fs.existsSync(loc))
144
+ candidates.add(path.resolve(loc));
145
+ }
146
+ try {
147
+ const npmRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf-8", timeout: 2000 }).trim();
148
+ if (npmRoot && fs.existsSync(npmRoot)) {
149
+ const p1 = path.join(npmRoot, "@getpaseo", "cli", "node_modules", "@getpaseo", "server");
150
+ const p2 = path.join(npmRoot, "@getpaseo", "server");
151
+ if (fs.existsSync(p1))
152
+ candidates.add(path.resolve(p1));
153
+ if (fs.existsSync(p2))
154
+ candidates.add(path.resolve(p2));
155
+ }
156
+ }
157
+ catch { }
158
+ }
159
+ // Filter out any paths that do not actually have a dist directory or package.json
160
+ const verified = [];
161
+ for (const dir of candidates) {
162
+ if (fs.existsSync(path.join(dir, "dist")) || fs.existsSync(path.join(dir, "package.json"))) {
163
+ verified.push(dir);
164
+ }
165
+ }
166
+ return verified;
167
+ }
168
+ /**
169
+ * Returns the JavaScript source for the Antigravity quota provider to be
170
+ * injected into Paseo server's quota-fetcher providers.
171
+ */
172
+ export function generateAntigravityQuotaProviderJs() {
173
+ return `import { execFile, execFileSync } from "node:child_process";
174
+ import { promisify } from "node:util";
175
+ import fs from "node:fs";
176
+ import os from "node:os";
177
+ import path from "node:path";
178
+ import { toneFromUsedPct, windowFromUsedPct, unavailableUsage } from "../usage.js";
179
+
180
+ const execFileAsync = promisify(execFile);
181
+
182
+ function resolveAgyBinary() {
183
+ if (process.env.AGY_BIN_PATH) return process.env.AGY_BIN_PATH;
184
+ const home = os.homedir();
185
+ if (home) {
186
+ if (process.platform === "win32") {
187
+ const candidates = [
188
+ 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"),
192
+ ];
193
+ for (const cand of candidates) {
194
+ if (fs.existsSync(cand)) return cand;
195
+ }
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
+ } else {
202
+ const localPath = path.join(home, ".local", "bin", "agy");
203
+ if (fs.existsSync(localPath)) return localPath;
204
+ }
205
+ }
206
+ return "agy";
207
+ }
208
+
209
+ export class AntigravityQuotaProvider {
210
+ constructor(options) {
211
+ this.providerId = "antigravity";
212
+ this.displayName = "Antigravity";
213
+ this.logger = typeof options?.logger?.child === "function" ? options.logger.child({ module: "antigravity-quota-provider" }) : options?.logger;
214
+ this.binaryPath = resolveAgyBinary();
215
+ }
216
+
217
+ async fetchUsage() {
218
+ try {
219
+ const isWin = process.platform === "win32";
220
+ let bin = this.binaryPath;
221
+ if (isWin && bin.includes(" ") && !bin.startsWith('"')) {
222
+ bin = \`"\${bin}"\`;
223
+ }
224
+ 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 }),
227
+ ]);
228
+
229
+ const usageOut = usageRes.status === "fulfilled" ? usageRes.value.stdout || usageRes.value.stderr : "";
230
+ const creditsOut = creditsRes.status === "fulfilled" ? creditsRes.value.stdout || creditsRes.value.stderr : "";
231
+
232
+ const rawWindows = [];
233
+ for (const line of usageOut.split(/[\\r\\n]+/)) {
234
+ const trimmed = line.trim();
235
+ if (!trimmed || trimmed.toLowerCase().startsWith("quota:")) continue;
236
+
237
+ let scope = "";
238
+ let limitType = "";
239
+ let remainingPct = null;
240
+ let resetsAt = null;
241
+
242
+ const m = trimmed.match(/^(.*?)\\s{2,}(.*?Remaining)\\s+(\\d+)%(?:\\s+(.*))?$/i);
243
+ if (m) {
244
+ scope = m[1].trim();
245
+ limitType = m[2].trim();
246
+ remainingPct = parseInt(m[3], 10);
247
+ resetsAt = m[4] ? new Date(m[4].trim()).toISOString() : null;
248
+ } else {
249
+ const parts = trimmed.split(/\\t+|\\s{2,}/).map(p => p.trim());
250
+ if (parts.length >= 3) {
251
+ scope = parts[0];
252
+ limitType = parts[1];
253
+ const remMatch = parts[2].match(/(\\d+)%/);
254
+ if (remMatch) remainingPct = parseInt(remMatch[1], 10);
255
+ resetsAt = parts[3] ? new Date(parts[3]).toISOString() : null;
256
+ }
257
+ }
258
+
259
+ if (remainingPct !== null && !isNaN(remainingPct)) {
260
+ const usedPct = Math.max(0, Math.min(100, 100 - remainingPct));
261
+ const isFiveHour = /five\\s*hour/i.test(limitType);
262
+ const isWeekly = /weekly/i.test(limitType);
263
+ const isGemini = /gemini/i.test(scope);
264
+
265
+ let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
266
+ let label = isFiveHour ? "Session" : isWeekly ? "Weekly" : limitType.replace(/\\s+Remaining$/i, "");
267
+ if (!isGemini) {
268
+ id = \`claude_\${id}\`;
269
+ label = \`Claude \${label}\`;
270
+ }
271
+
272
+ rawWindows.push({
273
+ id,
274
+ label,
275
+ utilizationPct: usedPct,
276
+ resetsAt,
277
+ tone: toneFromUsedPct(usedPct),
278
+ isFiveHour,
279
+ isGemini,
280
+ });
281
+ }
282
+ }
283
+
284
+ rawWindows.sort((a, b) => {
285
+ if (a.isGemini && !b.isGemini) return -1;
286
+ if (!a.isGemini && b.isGemini) return 1;
287
+ if (a.isFiveHour && !b.isFiveHour) return -1;
288
+ if (!a.isFiveHour && b.isFiveHour) return 1;
289
+ return 0;
290
+ });
291
+
292
+ const windows = rawWindows.map(w => windowFromUsedPct(w));
293
+
294
+ const balances = [];
295
+ const credMatch = creditsOut.match(/Remaining\\s+credits\\s+([\\d.]+)/i);
296
+ const remainingCredits = credMatch ? parseFloat(credMatch[1]) : 0;
297
+ balances.push({
298
+ id: "credits",
299
+ label: "Credits",
300
+ remaining: remainingCredits,
301
+ unit: "usd",
302
+ tone: remainingCredits > 0 ? "ok" : "default",
303
+ });
304
+
305
+ return {
306
+ providerId: this.providerId,
307
+ displayName: "Antigravity",
308
+ status: "available",
309
+ planLabel: "Google Gemini",
310
+ windows,
311
+ balances,
312
+ details: [],
313
+ error: null,
314
+ };
315
+ } catch (err) {
316
+ return unavailableUsage({
317
+ providerId: this.providerId,
318
+ displayName: "Antigravity",
319
+ error: err.message,
320
+ });
321
+ }
322
+ }
323
+ }
324
+ `;
325
+ }
326
+ /**
327
+ * Patches a Paseo server installation directory to enable Antigravity:
328
+ * 1. Patches quota-fetcher/manifest.js to register Antigravity
329
+ * 2. Writes quota-fetcher/providers/antigravity.js
330
+ * 3. Patches acp-agent.js to map context window tokens and emit usage updates
331
+ */
332
+ export function patchPaseoServer(serverDir) {
333
+ const changes = [];
334
+ try {
335
+ // 1. Locate manifest.js in quota-fetcher
336
+ const manifestCandidates = [
337
+ path.join(serverDir, "dist", "server", "services", "quota-fetcher", "manifest.js"),
338
+ path.join(serverDir, "dist", "services", "quota-fetcher", "manifest.js"),
339
+ ];
340
+ let manifestFile = manifestCandidates.find((f) => fs.existsSync(f));
341
+ if (!manifestFile && fs.existsSync(path.join(serverDir, "dist"))) {
342
+ const findManifest = (dir) => {
343
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
344
+ for (const e of entries) {
345
+ const full = path.join(dir, e.name);
346
+ if (e.isDirectory() && e.name !== "node_modules") {
347
+ const found = findManifest(full);
348
+ if (found)
349
+ return found;
350
+ }
351
+ else if (e.isFile() && e.name === "manifest.js" && dir.includes("quota-fetcher")) {
352
+ return full;
353
+ }
354
+ }
355
+ return null;
356
+ };
357
+ manifestFile = findManifest(path.join(serverDir, "dist")) || undefined;
358
+ }
359
+ if (manifestFile) {
360
+ const quotaDir = path.dirname(manifestFile);
361
+ const providersDir = path.join(quotaDir, "providers");
362
+ if (!fs.existsSync(providersDir)) {
363
+ fs.mkdirSync(providersDir, { recursive: true });
364
+ }
365
+ // Write / update providers/antigravity.js
366
+ const antigravityJsPath = path.join(providersDir, "antigravity.js");
367
+ fs.writeFileSync(antigravityJsPath, generateAntigravityQuotaProviderJs(), "utf-8");
368
+ changes.push(`Created/Updated ${antigravityJsPath}`);
369
+ // Patch manifest.js
370
+ let manifestCode = fs.readFileSync(manifestFile, "utf-8");
371
+ let manifestModified = false;
372
+ if (!manifestCode.includes('from "./providers/antigravity.js"') && !manifestCode.includes("AntigravityQuotaProvider")) {
373
+ manifestCode = `import { AntigravityQuotaProvider } from "./providers/antigravity.js";\n` + manifestCode;
374
+ manifestModified = true;
375
+ }
376
+ if (!manifestCode.includes('providerId: "antigravity"')) {
377
+ 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}`);
381
+ manifestModified = true;
382
+ }
383
+ }
384
+ if (manifestModified) {
385
+ fs.writeFileSync(manifestFile, manifestCode, "utf-8");
386
+ changes.push(`Patched ${manifestFile} with AntigravityQuotaProvider`);
387
+ }
388
+ }
389
+ // 2. Locate acp-agent.js
390
+ const acpCandidates = [
391
+ path.join(serverDir, "dist", "server", "server", "agent", "providers", "acp-agent.js"),
392
+ path.join(serverDir, "dist", "server", "agent", "providers", "acp-agent.js"),
393
+ path.join(serverDir, "dist", "agent", "providers", "acp-agent.js"),
394
+ ];
395
+ let acpFile = acpCandidates.find((f) => fs.existsSync(f));
396
+ if (!acpFile && fs.existsSync(path.join(serverDir, "dist"))) {
397
+ const findAcp = (dir) => {
398
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
399
+ for (const e of entries) {
400
+ const full = path.join(dir, e.name);
401
+ if (e.isDirectory() && e.name !== "node_modules") {
402
+ const found = findAcp(full);
403
+ if (found)
404
+ return found;
405
+ }
406
+ else if (e.isFile() && e.name === "acp-agent.js") {
407
+ return full;
408
+ }
409
+ }
410
+ return null;
411
+ };
412
+ acpFile = findAcp(path.join(serverDir, "dist")) || undefined;
413
+ }
414
+ if (acpFile) {
415
+ let acpCode = fs.readFileSync(acpFile, "utf-8");
416
+ let acpModified = false;
417
+ // Patch mapACPUsage
418
+ if (!acpCode.includes("contextWindowMaxTokens: usage.contextWindowMaxTokens")) {
419
+ const oldMapRegex = /export\s+function\s+mapACPUsage\s*\([^)]*\)\s*\{[\s\S]*?return\s*\{[\s\S]*?\};\s*\}/m;
420
+ const newMap = `export function mapACPUsage(usage) {
421
+ if (!usage) {
422
+ return undefined;
423
+ }
424
+ return {
425
+ inputTokens: usage.inputTokens ?? undefined,
426
+ outputTokens: usage.outputTokens ?? undefined,
427
+ cachedInputTokens: usage.cachedReadTokens ?? usage.cachedInputTokens ?? undefined,
428
+ totalCostUsd: usage.totalCostUsd ?? (usage.cost?.amount !== undefined ? Number(usage.cost.amount) : undefined),
429
+ contextWindowMaxTokens: usage.contextWindowMaxTokens ?? usage.size ?? undefined,
430
+ contextWindowUsedTokens: usage.contextWindowUsedTokens ?? usage.used ?? undefined,
431
+ };
432
+ }`;
433
+ if (oldMapRegex.test(acpCode)) {
434
+ acpCode = acpCode.replace(oldMapRegex, newMap);
435
+ acpModified = true;
436
+ }
437
+ }
438
+ // 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) {
442
+ if (!update) return;
443
+ const usage = mapACPUsage(update);
444
+ if (usage) {
445
+ this.currentTurnUsage = { ...this.currentTurnUsage, ...usage };
446
+ const event = {
447
+ type: "usage_updated",
448
+ provider: this.provider,
449
+ usage: this.currentTurnUsage,
450
+ ...(this.activeForegroundTurnId ? { turnId: this.activeForegroundTurnId } : {}),
451
+ };
452
+ if (typeof this.deliverTranslatedEvents === "function") {
453
+ this.deliverTranslatedEvents([event]);
454
+ } else if (typeof this.pushEvent === "function") {
455
+ this.pushEvent(event);
456
+ }
457
+ }
458
+ }`;
459
+ if (handlerRegex.test(acpCode)) {
460
+ acpCode = acpCode.replace(handlerRegex, newHandler);
461
+ acpModified = true;
462
+ }
463
+ }
464
+ if (acpModified) {
465
+ fs.writeFileSync(acpFile, acpCode, "utf-8");
466
+ changes.push(`Patched ${acpFile} for context-window token telemetry and usage updates`);
467
+ }
468
+ }
469
+ return { success: true, changes };
470
+ }
471
+ catch (err) {
472
+ const errorMsg = err instanceof Error ? err.message : String(err);
473
+ logger.warn("Failed to patch Paseo server", { serverDir, error: errorMsg });
474
+ return { success: false, changes, error: errorMsg };
475
+ }
476
+ }
477
+ /**
478
+ * Discovers and patches all accessible Paseo installations.
479
+ */
480
+ export function ensurePaseoIntegration(options) {
481
+ const serverPaths = options?.targetPaths || findPaseoServerInstallations();
482
+ const patchedPaths = [];
483
+ const errors = [];
484
+ for (const sPath of serverPaths) {
485
+ const res = patchPaseoServer(sPath);
486
+ if (res.success) {
487
+ if (res.changes.length > 0) {
488
+ patchedPaths.push(sPath);
489
+ if (options?.verbose) {
490
+ logger.info(`Integrated with Paseo server at ${sPath}`, { changes: res.changes });
491
+ }
492
+ }
493
+ }
494
+ else if (res.error) {
495
+ errors.push(`${sPath}: ${res.error}`);
496
+ }
497
+ }
498
+ return {
499
+ found: serverPaths.length > 0,
500
+ serverPaths,
501
+ patchedPaths,
502
+ errors,
503
+ };
504
+ }
@@ -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,19 +292,22 @@ 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",
303
+ windowsHide: true,
296
304
  }),
297
- execFileAsync(binaryPath, ["--print", "/credits"], {
305
+ execFileAsync(cmd, ["--print", "/credits"], {
298
306
  timeout: 8_000,
299
307
  maxBuffer: 1024 * 1024,
300
308
  env: process.env,
301
309
  shell: process.platform === "win32",
310
+ windowsHide: true,
302
311
  }),
303
312
  ]);
304
313
  const balances = [];
@@ -371,6 +380,7 @@ export const FALLBACK_MODELS = [
371
380
  let cachedModels = null;
372
381
  let cachedModelsBinaryPath = null;
373
382
  let lastModelFetch = 0;
383
+ let inFlightModelFetch = null;
374
384
  const MODEL_CACHE_TTL_MS = 60_000;
375
385
  export function parseAgyModelsOutput(rawOutput) {
376
386
  const modelsMap = new Map();
@@ -384,24 +394,21 @@ export function parseAgyModelsOutput(rawOutput) {
384
394
  const modelId = parts[0];
385
395
  const label = parts[1];
386
396
  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
- }
397
+ const baseId = effortMatch ? modelId.replace(/-(high|medium|low)$/, "") : modelId;
398
+ const effort = effortMatch ? effortMatch[1] : undefined;
399
+ const cleanLabel = effortMatch
400
+ ? label.replace(/\s*\((High|Medium|Low)\)$/i, "").trim()
401
+ : label;
395
402
  if (!modelsMap.has(baseId)) {
396
403
  modelsMap.set(baseId, {
397
404
  modelId: baseId,
398
- name: baseLabel,
405
+ name: cleanLabel,
399
406
  description: label,
400
- supportedEfforts: [],
407
+ supportedEfforts: effort ? [effort] : [],
401
408
  contextWindowMaxTokens: getModelContextWindow(baseId),
402
409
  });
403
410
  }
404
- if (effort) {
411
+ else if (effort) {
405
412
  const entry = modelsMap.get(baseId);
406
413
  if (!entry.supportedEfforts.includes(effort))
407
414
  entry.supportedEfforts.push(effort);
@@ -418,28 +425,39 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
418
425
  now - lastModelFetch < MODEL_CACHE_TTL_MS) {
419
426
  return cachedModels;
420
427
  }
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;
428
+ if (inFlightModelFetch) {
429
+ return inFlightModelFetch;
442
430
  }
431
+ inFlightModelFetch = (async () => {
432
+ const cmd = formatExecBinaryPath(binaryPath);
433
+ try {
434
+ const { stdout } = await execFileAsync(cmd, ["models"], {
435
+ timeout: 10_000,
436
+ env: process.env,
437
+ maxBuffer: 4 * 1024 * 1024,
438
+ shell: process.platform === "win32",
439
+ windowsHide: true,
440
+ });
441
+ const parsed = parseAgyModelsOutput(stdout);
442
+ cachedModels = parsed;
443
+ cachedModelsBinaryPath = binaryPath;
444
+ lastModelFetch = Date.now();
445
+ return parsed;
446
+ }
447
+ catch (err) {
448
+ logger.warn("Failed to fetch models from agy CLI, using fallback models", {
449
+ error: err.message,
450
+ });
451
+ cachedModels = FALLBACK_MODELS;
452
+ cachedModelsBinaryPath = binaryPath;
453
+ lastModelFetch = Date.now();
454
+ return FALLBACK_MODELS;
455
+ }
456
+ finally {
457
+ inFlightModelFetch = null;
458
+ }
459
+ })();
460
+ return inFlightModelFetch;
443
461
  }
444
462
  export function getEffectiveEffortForModel(modelId, requestedEffort, models) {
445
463
  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,12 +337,14 @@ 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,
343
345
  maxBuffer: AGY_COMMAND_MAX_BUFFER,
344
346
  shell: process.platform === "win32",
347
+ windowsHide: true,
345
348
  });
346
349
  return stdout.trim() || stderr.trim();
347
350
  }
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.6",
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",