paseo-acp-agy 1.1.3 → 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/PROTOCOL.md CHANGED
@@ -10,14 +10,15 @@ Each Paseo ACP session owns one persistent `agy` process:
10
10
  agy --input-format stream-json --output-format stream-json --print=""
11
11
  ```
12
12
 
13
- Optional session flags are applied when the process starts or is restarted:
13
+ Session flags applied when the process starts or is restarted:
14
14
 
15
15
  - `--model <model-id>`
16
16
  - `--effort <low|medium|high>` when supported by the selected model
17
17
  - `--mode <mode-id>`
18
18
  - `--conversation <conversation-id>` when resuming
19
- - `--sandbox`
20
- - `--dangerously-skip-permissions` only when explicitly configured
19
+ - `--add-dir <cwd>` registering the active workspace directory
20
+ - `--dangerously-skip-permissions` (enabled by default under ACP; see Permission Model)
21
+ - `--sandbox` when configured via `AGY_ACP_SANDBOX=1`
21
22
 
22
23
  Configuration changes schedule a controlled restart before the next prompt. On POSIX systems `agy` is launched as a process-group leader; shutdown, restart and turn cancellation signal the process group so tool subprocesses are not left orphaned.
23
24
 
@@ -69,6 +70,27 @@ For backwards compatibility, the server also accepts the exact pre-hardening ali
69
70
 
70
71
  `agy-acp` currently advertises `loadSession: false`. ACP `session/load` requires the agent to replay prior conversation history through `session/update` notifications. The adapter intentionally does not claim that capability until replay is implemented. Paseo persistence uses `session/resume`, which restores model context without replaying already-rendered history.
71
72
 
73
+ ## Permission Model & CLI Limitations
74
+
75
+ In the standard ACP specification, clients like Paseo or Zed act as supervisors: an agent requests authorization for actions via `session/request_permission`, and the client presents prompts to the user or evaluates auto-accept policies.
76
+
77
+ ### The Antigravity headless stream-json constraint
78
+
79
+ The official Antigravity CLI (`agy`) was created as a terminal-interactive CLI rather than a native ACP server:
80
+ 1. **Piped Stdio vs. Interactive TTY**: `agy` runs headlessly over JSON-RPC stdio pipes. In this mode, `agy`'s internal TTY consent prompts (`Allow <tool>? [y/n]`) cannot query the user.
81
+ 2. **Immediate Denial on Non-Interactive Stdin**: Without `--dangerously-skip-permissions`, whenever `agy` invokes a tool (`read_file`, `write_file`, shell execution) in a workspace path that has not been manually pre-trusted in `~/.gemini/antigravity-cli/settings.json`, it immediately fails with:
82
+ `permission check failed for <tool>: user denied permission for <tool>(<path>)`.
83
+ 3. **Absence of a Protocol-Level Permission Callback**: The `agy stream-json` protocol currently lacks an inbound pause-and-resume event (such as `permission_response`). By the time `agy` outputs a `step_update` event with `step_type: "tool"` and `state: "ACTIVE"`, execution has already been initiated by the binary.
84
+
85
+ ### How `agy-acp` resolves this
86
+
87
+ - **Delegated Trust**: `agy-acp` applies `--dangerously-skip-permissions` by default for ACP sessions so the underlying CLI does not fail on headless stdin prompts.
88
+ - **Workspace Demarcation**: `agy-acp` passes `--add-dir <cwd>` on startup to explicitly register the current working directory in Antigravity's workspace context.
89
+ - **Live Tool Streaming**: Tool calls are mapped and streamed immediately via `session/update` (`tool_call` and `tool_call_update`) so Paseo provides real-time auditability of actions and outputs.
90
+ - **Opt-out & Sandbox**: Users can customize this behavior:
91
+ - Setting `AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS=false` disables `--dangerously-skip-permissions` for environments where `~/.gemini/antigravity-cli/settings.json` is strictly maintained.
92
+ - Setting `AGY_ACP_SANDBOX=true` adds `--sandbox` to enable terminal execution restrictions.
93
+
72
94
  ## Turn concurrency and cancellation
73
95
 
74
96
  A session-level prompt operation is reserved synchronously before any slash-command or process startup/restart `await`. Therefore:
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)
@@ -98,11 +120,43 @@ To include `paseo-acp-agy` in Paseo's built-in provider store (`ACP_PROVIDER_CAT
98
120
 
99
121
  ---
100
122
 
123
+ ## Security, Permissions & Antigravity CLI Limitations
124
+
125
+ In the Agent Client Protocol (ACP) specification, the host client (e.g., [Paseo](https://paseo.sh) or Zed) acts as the security supervisor. Native ACP agents query the host via `session/request_permission` before executing tools (`read_file`, `write_file`, terminal commands).
126
+
127
+ ### Why `--dangerously-skip-permissions` is enabled by default
128
+
129
+ The official Google Antigravity CLI (`agy`) communicates with this adapter via `--input-format stream-json --output-format stream-json`:
130
+ 1. **No Interactive TTY**: `agy` runs headlessly over piped JSON-RPC stdio. In this mode, `agy`'s internal terminal prompt (`Allow <tool>? [y/n]`) cannot reach an interactive user.
131
+ 2. **Headless Permission Denials**: Without `--dangerously-skip-permissions`, whenever `agy` accesses a workspace folder or runs a tool that has not been explicitly pre-approved in `~/.gemini/antigravity-cli/settings.json`, its internal check fails and aborts immediately with:
132
+ ```text
133
+ permission check failed for read_file: user denied permission for read_file(...)
134
+ ```
135
+ 3. **No External Permission Callback**: Unlike native ACP agents or Claude's Agent SDK, `agy`'s `stream-json` interface does not support an external pause-and-confirm handshake. Once a tool step starts in `agy`, it has already been scheduled internally.
136
+
137
+ To prevent unrecoverable `user denied permission` errors on fresh workspaces (especially on new Windows or Linux installations), `paseo-acp-agy`:
138
+ - Passes `--dangerously-skip-permissions` by default so tool authorization is delegated to the host application.
139
+ - Passes `--add-dir <cwd>` on process startup to demarcate the current project workspace directory.
140
+ - Streams tool invocations (`step_update` → `tool_call`) in real time to the Paseo UI so the user retains full visibility over all actions.
141
+
142
+ ### Controlling Permissions & Sandbox Mode
143
+
144
+ You can adjust this behavior using environment variables in your Paseo configuration or shell:
145
+
146
+ | Variable | Description | Default |
147
+ | :--- | :--- | :--- |
148
+ | `AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS` | Set to `false` or `0` to disable automatic skip and strictly require pre-approved permissions in `~/.gemini/antigravity-cli/settings.json` | `true` |
149
+ | `AGY_ACP_SANDBOX` | Set to `true` or `1` to launch `agy` with terminal restrictions enabled (`--sandbox`) | `false` |
150
+
151
+ ---
152
+
101
153
  ## Environment Variables
102
154
 
103
155
  | Variable | Description | Default |
104
156
  | :--- | :--- | :--- |
105
157
  | `AGY_BIN_PATH` | Path to the Google Antigravity binary | Auto-detected from `PATH` or `~/.local/bin/agy` |
158
+ | `AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS` | Auto-approve tool permissions in headless `agy` | `true` |
159
+ | `AGY_ACP_SANDBOX` | Enable terminal sandbox mode (`--sandbox`) | `false` |
106
160
  | `AGY_ACP_LOG_FILE` | Enable debug file logging | Disabled |
107
161
 
108
162
  ---
@@ -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;
@@ -4,6 +4,7 @@ import { ACP_METHODS, AVAILABLE_MODES, fetchAvailableModels, buildConfigOptionsF
4
4
  import { SessionManager } from "./session.js";
5
5
  import { executeSlashCommand, AVAILABLE_SLASH_COMMANDS } from "./slash-commands.js";
6
6
  import { getShortVersion } from "./version.js";
7
+ import { resolveDefaultAgyBinary } from "./antigravity-process.js";
7
8
  function splitModelAndEffort(modelInput) {
8
9
  if (!modelInput)
9
10
  return {};
@@ -25,7 +26,7 @@ export class ACPServer {
25
26
  constructor(options = {}) {
26
27
  this.input = options.input || process.stdin;
27
28
  this.output = options.output || process.stdout;
28
- this.binaryPath = options.binaryPath || process.env.AGY_BIN_PATH || "agy";
29
+ this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
29
30
  this.sessionManager =
30
31
  options.sessionManager || new SessionManager({ defaultBinaryPath: this.binaryPath });
31
32
  }
@@ -79,6 +80,26 @@ export class ACPServer {
79
80
  },
80
81
  });
81
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
+ }
82
103
  async sessionState(session, forceModels = false) {
83
104
  const availableModels = await fetchAvailableModels(this.binaryPath, forceModels);
84
105
  return {
@@ -177,6 +198,7 @@ export class ACPServer {
177
198
  if (!isNotification) {
178
199
  this.sendSuccess(id, await this.sessionState(session, true));
179
200
  this.publishCommands(session.id);
201
+ this.publishUsageUpdate(session);
180
202
  }
181
203
  break;
182
204
  }
@@ -211,6 +233,7 @@ export class ACPServer {
211
233
  if (!isNotification) {
212
234
  this.sendSuccess(id, await this.sessionState(session));
213
235
  this.publishCommands(session.id);
236
+ this.publishUsageUpdate(session);
214
237
  }
215
238
  }
216
239
  catch (err) {
@@ -374,6 +397,7 @@ export class ACPServer {
374
397
  // after model/tool work. Record it before branching on status.
375
398
  session.recordTurnUsage(turnUsage, executingModel);
376
399
  const usagePayload = this.turnUsagePayload(session, turnUsage, executingModel);
400
+ this.publishUsageUpdate(session);
377
401
  if (session.isCancelled) {
378
402
  if (!isNotification) {
379
403
  this.sendSuccess(id, { stopReason: "cancelled", usage: usagePayload });
@@ -1,6 +1,7 @@
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
5
  export interface AntigravityProcessOptions {
5
6
  binaryPath?: string;
6
7
  cwd?: string;
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { spawn, execFileSync } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
@@ -6,7 +6,7 @@ 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
- function resolveDefaultAgyBinary() {
9
+ export function resolveDefaultAgyBinary() {
10
10
  if (process.env.AGY_BIN_PATH)
11
11
  return process.env.AGY_BIN_PATH;
12
12
  const home = os.homedir();
@@ -22,6 +22,13 @@ function resolveDefaultAgyBinary() {
22
22
  if (fs.existsSync(cand))
23
23
  return cand;
24
24
  }
25
+ try {
26
+ const out = execFileSync("where.exe", ["agy"], { encoding: "utf-8", timeout: 1000 }).trim();
27
+ const first = out.split(/\r?\n/)[0]?.trim();
28
+ if (first && fs.existsSync(first))
29
+ return first;
30
+ }
31
+ catch { }
25
32
  }
26
33
  else {
27
34
  const localPath = path.join(home, ".local", "bin", "agy");
@@ -147,6 +154,15 @@ export class AntigravityProcess extends EventEmitter {
147
154
  });
148
155
  }
149
156
  }
157
+ else if (pid && process.platform === "win32") {
158
+ try {
159
+ execFileSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
160
+ return true;
161
+ }
162
+ catch {
163
+ // Fall back to child.kill
164
+ }
165
+ }
150
166
  try {
151
167
  return child.kill(signal);
152
168
  }
@@ -249,6 +265,7 @@ export class AntigravityProcess extends EventEmitter {
249
265
  env: this.env,
250
266
  stdio: ["pipe", "pipe", "pipe"],
251
267
  detached: process.platform !== "win32",
268
+ shell: process.platform === "win32",
252
269
  });
253
270
  this.child = child;
254
271
  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,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
@@ -208,34 +208,47 @@ export function parseAgyQuotaOutput(raw) {
208
208
  const trimmed = line.trim();
209
209
  if (!trimmed || trimmed.toLowerCase().startsWith("quota:"))
210
210
  continue;
211
- const parts = trimmed.split(/\t+|\s{2,}/).map((p) => p.trim());
212
- if (parts.length >= 3) {
213
- const scope = parts[0];
214
- const limitType = parts[1];
215
- const remainingMatch = parts[2].match(/(\d+)%/);
216
- const resetsAt = parts[3] || null;
217
- if (remainingMatch) {
218
- const remainingPct = parseInt(remainingMatch[1], 10);
219
- const usedPct = Math.max(0, Math.min(100, 100 - remainingPct));
220
- const isFiveHour = /five\s*hour/i.test(limitType);
221
- const isWeekly = /weekly/i.test(limitType);
222
- const isGemini = /gemini/i.test(scope);
223
- let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
224
- let label = isFiveHour ? "Session (5h)" : isWeekly ? "Weekly" : limitType;
225
- if (!isGemini) {
226
- id = `claude_${id}`;
227
- label = `Claude ${label}`;
228
- }
229
- windows.push({
230
- id,
231
- label,
232
- usedPct,
233
- remainingPct,
234
- resetsAt,
235
- tone: deriveUsageTone(usedPct),
236
- });
211
+ let scope = "";
212
+ let limitType = "";
213
+ let remainingMatch = null;
214
+ let resetsAt = null;
215
+ const m = trimmed.match(/^(.*?)\s{2,}(.*?Remaining)\s+(\d+%)(?:\s+(.*))?$/i);
216
+ if (m) {
217
+ scope = m[1].trim();
218
+ limitType = m[2].trim();
219
+ remainingMatch = m[3].match(/(\d+)%/);
220
+ resetsAt = m[4]?.trim() || null;
221
+ }
222
+ else {
223
+ const parts = trimmed.split(/\t+|\s{2,}/).map((p) => p.trim());
224
+ if (parts.length >= 3) {
225
+ scope = parts[0];
226
+ limitType = parts[1];
227
+ remainingMatch = parts[2].match(/(\d+)%/);
228
+ resetsAt = parts[3] || null;
237
229
  }
238
230
  }
231
+ if (remainingMatch) {
232
+ const remainingPct = parseInt(remainingMatch[1], 10);
233
+ const usedPct = Math.max(0, Math.min(100, 100 - remainingPct));
234
+ const isFiveHour = /five\s*hour/i.test(limitType);
235
+ const isWeekly = /weekly/i.test(limitType);
236
+ const isGemini = /gemini/i.test(scope);
237
+ let id = isFiveHour ? "session" : isWeekly ? "weekly" : "quota";
238
+ let label = isFiveHour ? "Session (5h)" : isWeekly ? "Weekly" : limitType;
239
+ if (!isGemini) {
240
+ id = `claude_${id}`;
241
+ label = `Claude ${label}`;
242
+ }
243
+ windows.push({
244
+ id,
245
+ label,
246
+ usedPct,
247
+ remainingPct,
248
+ resetsAt,
249
+ tone: deriveUsageTone(usedPct),
250
+ });
251
+ }
239
252
  }
240
253
  return windows;
241
254
  }
@@ -265,6 +278,12 @@ function cacheProviderUsage(binaryPath, result, now) {
265
278
  lastProviderUsageFetch = now;
266
279
  return result;
267
280
  }
281
+ export function formatExecBinaryPath(binaryPath) {
282
+ if (process.platform === "win32" && binaryPath.includes(" ") && !binaryPath.startsWith('"')) {
283
+ return `"${binaryPath}"`;
284
+ }
285
+ return binaryPath;
286
+ }
268
287
  export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
269
288
  const now = Date.now();
270
289
  if (!force &&
@@ -273,17 +292,20 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
273
292
  now - lastProviderUsageFetch < PROVIDER_USAGE_CACHE_TTL_MS) {
274
293
  return cachedProviderUsage;
275
294
  }
295
+ const cmd = formatExecBinaryPath(binaryPath);
276
296
  try {
277
297
  const [usageResult, creditsResult] = await Promise.allSettled([
278
- execFileAsync(binaryPath, ["--print", "/usage"], {
298
+ execFileAsync(cmd, ["--print", "/usage"], {
279
299
  timeout: 8_000,
280
300
  maxBuffer: 1024 * 1024,
281
301
  env: process.env,
302
+ shell: process.platform === "win32",
282
303
  }),
283
- execFileAsync(binaryPath, ["--print", "/credits"], {
304
+ execFileAsync(cmd, ["--print", "/credits"], {
284
305
  timeout: 8_000,
285
306
  maxBuffer: 1024 * 1024,
286
307
  env: process.env,
308
+ shell: process.platform === "win32",
287
309
  }),
288
310
  ]);
289
311
  const balances = [];
@@ -356,6 +378,7 @@ export const FALLBACK_MODELS = [
356
378
  let cachedModels = null;
357
379
  let cachedModelsBinaryPath = null;
358
380
  let lastModelFetch = 0;
381
+ let inFlightModelFetch = null;
359
382
  const MODEL_CACHE_TTL_MS = 60_000;
360
383
  export function parseAgyModelsOutput(rawOutput) {
361
384
  const modelsMap = new Map();
@@ -369,24 +392,21 @@ export function parseAgyModelsOutput(rawOutput) {
369
392
  const modelId = parts[0];
370
393
  const label = parts[1];
371
394
  const effortMatch = modelId.match(/-(high|medium|low)$/);
372
- let baseId = modelId;
373
- let baseLabel = label;
374
- let effort = null;
375
- if (effortMatch) {
376
- effort = effortMatch[1];
377
- baseId = modelId.slice(0, -(effort.length + 1));
378
- baseLabel = label.replace(/\s*\((High|Medium|Low)\)$/, "");
379
- }
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;
380
400
  if (!modelsMap.has(baseId)) {
381
401
  modelsMap.set(baseId, {
382
402
  modelId: baseId,
383
- name: baseLabel,
403
+ name: cleanLabel,
384
404
  description: label,
385
- supportedEfforts: [],
405
+ supportedEfforts: effort ? [effort] : [],
386
406
  contextWindowMaxTokens: getModelContextWindow(baseId),
387
407
  });
388
408
  }
389
- if (effort) {
409
+ else if (effort) {
390
410
  const entry = modelsMap.get(baseId);
391
411
  if (!entry.supportedEfforts.includes(effort))
392
412
  entry.supportedEfforts.push(effort);
@@ -403,27 +423,38 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
403
423
  now - lastModelFetch < MODEL_CACHE_TTL_MS) {
404
424
  return cachedModels;
405
425
  }
406
- try {
407
- const { stdout } = await execFileAsync(binaryPath, ["models"], {
408
- timeout: 5_000,
409
- env: process.env,
410
- maxBuffer: 4 * 1024 * 1024,
411
- });
412
- const parsed = parseAgyModelsOutput(stdout);
413
- cachedModels = parsed;
414
- cachedModelsBinaryPath = binaryPath;
415
- lastModelFetch = now;
416
- return parsed;
417
- }
418
- catch (err) {
419
- logger.warn("Failed to fetch models from agy CLI, using fallback models", {
420
- error: err.message,
421
- });
422
- cachedModels = FALLBACK_MODELS;
423
- cachedModelsBinaryPath = binaryPath;
424
- lastModelFetch = now;
425
- return FALLBACK_MODELS;
426
+ if (inFlightModelFetch) {
427
+ return inFlightModelFetch;
426
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;
427
458
  }
428
459
  export function getEffectiveEffortForModel(modelId, requestedEffort, models) {
429
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;
@@ -31,7 +32,7 @@ function transcriptPath(conversationId) {
31
32
  }
32
33
  function summarizeConversation(cid, logFile, mtime) {
33
34
  try {
34
- const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
35
+ const lines = fs.readFileSync(logFile, "utf8").split(/\r?\n/).filter(Boolean);
35
36
  let firstPrompt = "";
36
37
  let userTurns = 0;
37
38
  for (const line of lines) {
@@ -111,7 +112,7 @@ export function getConversationSteps(cid) {
111
112
  if (!logFile || !fs.existsSync(logFile))
112
113
  return null;
113
114
  try {
114
- const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
115
+ const lines = fs.readFileSync(logFile, "utf8").split(/\r?\n/).filter(Boolean);
115
116
  const steps = [];
116
117
  let userTurns = 0;
117
118
  for (let i = 0; i < lines.length; i++) {
@@ -272,11 +273,30 @@ function formatCountdown(isoStr) {
272
273
  }
273
274
  export function formatUsageOutput(rawText) {
274
275
  const groups = new Map();
275
- for (const line of rawText.trim().split("\n")) {
276
- const parts = line.includes("\t") ? line.split("\t") : line.split(/\s{2,}/);
277
- if (parts.length < 3)
276
+ for (const line of rawText.trim().split(/\r?\n/)) {
277
+ const trimmedLine = line.trim();
278
+ if (!trimmedLine || trimmedLine.toLowerCase().startsWith("quota:"))
278
279
  continue;
279
- let fam = parts[0].trim();
280
+ let fam = "";
281
+ let rawWindow = "";
282
+ let pctStr = "";
283
+ let reset = "";
284
+ const m = trimmedLine.match(/^(.*?)\s{2,}(.*?Remaining)\s+(\d+%)(?:\s+(.*))?$/i);
285
+ if (m) {
286
+ fam = m[1].trim();
287
+ rawWindow = m[2].trim();
288
+ pctStr = m[3].trim();
289
+ reset = m[4]?.trim() || "";
290
+ }
291
+ else {
292
+ const parts = trimmedLine.includes("\t") ? trimmedLine.split("\t") : trimmedLine.split(/\s{2,}/);
293
+ if (parts.length < 3)
294
+ continue;
295
+ fam = parts[0].trim();
296
+ rawWindow = parts[1].trim();
297
+ pctStr = parts[2].trim();
298
+ reset = parts[3]?.trim() || "";
299
+ }
280
300
  let icon = "🤖";
281
301
  if (fam.toLowerCase().includes("gemini")) {
282
302
  fam = "Google Gemini";
@@ -286,7 +306,7 @@ export function formatUsageOutput(rawText) {
286
306
  fam = "Claude & GPT";
287
307
  icon = "🔶";
288
308
  }
289
- const rawWindow = parts[1].replace(" Remaining", "").trim();
309
+ rawWindow = rawWindow.replace(/\s+Remaining$/i, "").trim();
290
310
  const lowerWindow = rawWindow.toLowerCase();
291
311
  const isFiveHour = lowerWindow.includes("five hour") || /\b5\s*hour/.test(lowerWindow);
292
312
  const win = isFiveHour
@@ -294,10 +314,8 @@ export function formatUsageOutput(rawText) {
294
314
  : lowerWindow.includes("weekly")
295
315
  ? "Cota Semanal"
296
316
  : rawWindow;
297
- const pctStr = parts[2].trim();
298
317
  const parsedPct = Number.parseInt(pctStr.replace("%", ""), 10);
299
318
  const pct = Number.isFinite(parsedPct) ? parsedPct : 0;
300
- const reset = parts[3]?.trim() || "";
301
319
  if (!groups.has(fam))
302
320
  groups.set(fam, { icon, items: [] });
303
321
  groups.get(fam).items.push({ win, isFiveHour, pct, pctStr, reset });
@@ -319,11 +337,13 @@ export function formatUsageOutput(rawText) {
319
337
  return out;
320
338
  }
321
339
  async function runAgySlash(binaryPath, cwd, slashCommand) {
322
- const { stdout, stderr } = await execFileAsync(binaryPath, ["--print", slashCommand], {
340
+ const cmd = formatExecBinaryPath(binaryPath);
341
+ const { stdout, stderr } = await execFileAsync(cmd, ["--print", slashCommand], {
323
342
  cwd,
324
343
  env: process.env,
325
344
  timeout: AGY_COMMAND_TIMEOUT_MS,
326
345
  maxBuffer: AGY_COMMAND_MAX_BUFFER,
346
+ shell: process.platform === "win32",
327
347
  });
328
348
  return stdout.trim() || stderr.trim();
329
349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.1.3",
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",