paseo-acp-agy 1.1.2 → 1.1.4

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
@@ -98,11 +98,43 @@ To include `paseo-acp-agy` in Paseo's built-in provider store (`ACP_PROVIDER_CAT
98
98
 
99
99
  ---
100
100
 
101
+ ## Security, Permissions & Antigravity CLI Limitations
102
+
103
+ 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).
104
+
105
+ ### Why `--dangerously-skip-permissions` is enabled by default
106
+
107
+ The official Google Antigravity CLI (`agy`) communicates with this adapter via `--input-format stream-json --output-format stream-json`:
108
+ 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.
109
+ 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:
110
+ ```text
111
+ permission check failed for read_file: user denied permission for read_file(...)
112
+ ```
113
+ 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.
114
+
115
+ To prevent unrecoverable `user denied permission` errors on fresh workspaces (especially on new Windows or Linux installations), `paseo-acp-agy`:
116
+ - Passes `--dangerously-skip-permissions` by default so tool authorization is delegated to the host application.
117
+ - Passes `--add-dir <cwd>` on process startup to demarcate the current project workspace directory.
118
+ - Streams tool invocations (`step_update` → `tool_call`) in real time to the Paseo UI so the user retains full visibility over all actions.
119
+
120
+ ### Controlling Permissions & Sandbox Mode
121
+
122
+ You can adjust this behavior using environment variables in your Paseo configuration or shell:
123
+
124
+ | Variable | Description | Default |
125
+ | :--- | :--- | :--- |
126
+ | `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` |
127
+ | `AGY_ACP_SANDBOX` | Set to `true` or `1` to launch `agy` with terminal restrictions enabled (`--sandbox`) | `false` |
128
+
129
+ ---
130
+
101
131
  ## Environment Variables
102
132
 
103
133
  | Variable | Description | Default |
104
134
  | :--- | :--- | :--- |
105
135
  | `AGY_BIN_PATH` | Path to the Google Antigravity binary | Auto-detected from `PATH` or `~/.local/bin/agy` |
136
+ | `AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS` | Auto-approve tool permissions in headless `agy` | `true` |
137
+ | `AGY_ACP_SANDBOX` | Enable terminal sandbox mode (`--sandbox`) | `false` |
106
138
  | `AGY_ACP_LOG_FILE` | Enable debug file logging | Disabled |
107
139
 
108
140
  ---
@@ -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
  }
@@ -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,17 +1,40 @@
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
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { logger } from "./logger.js";
6
7
  import { getEffectiveEffortForModel, } from "./protocol.js";
7
- import { buildAgyArgs } from "./permissions.js";
8
- function resolveDefaultAgyBinary() {
8
+ import { buildAgyArgs, resolvePermissionSettings } from "./permissions.js";
9
+ export function resolveDefaultAgyBinary() {
9
10
  if (process.env.AGY_BIN_PATH)
10
11
  return process.env.AGY_BIN_PATH;
11
- if (process.env.HOME) {
12
- const localPath = path.join(process.env.HOME, ".local", "bin", "agy");
13
- if (fs.existsSync(localPath))
14
- return localPath;
12
+ const home = os.homedir();
13
+ if (home) {
14
+ if (process.platform === "win32") {
15
+ const candidates = [
16
+ path.join(home, ".local", "bin", "agy.exe"),
17
+ path.join(home, "AppData", "Local", "Programs", "antigravity", "agy.exe"),
18
+ path.join(home, ".local", "bin", "agy.cmd"),
19
+ path.join(home, ".local", "bin", "agy.bat"),
20
+ ];
21
+ for (const cand of candidates) {
22
+ if (fs.existsSync(cand))
23
+ return cand;
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 { }
32
+ }
33
+ else {
34
+ const localPath = path.join(home, ".local", "bin", "agy");
35
+ if (fs.existsSync(localPath))
36
+ return localPath;
37
+ }
15
38
  }
16
39
  return "agy";
17
40
  }
@@ -38,7 +61,7 @@ export class AntigravityProcess extends EventEmitter {
38
61
  this.effort = options.effort;
39
62
  this.mode = options.mode;
40
63
  this.conversationId = options.conversationId;
41
- this.permissions = options.permissions || { sandbox: false, dangerouslySkipPermissions: false };
64
+ this.permissions = options.permissions || resolvePermissionSettings();
42
65
  this.env = { ...process.env, ...options.env };
43
66
  // EventEmitter treats "error" specially. Keep a defensive listener for
44
67
  // callers/tests that emit it directly; child failures use process_error.
@@ -131,6 +154,15 @@ export class AntigravityProcess extends EventEmitter {
131
154
  });
132
155
  }
133
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
+ }
134
166
  try {
135
167
  return child.kill(signal);
136
168
  }
@@ -212,10 +244,15 @@ export class AntigravityProcess extends EventEmitter {
212
244
  extraArgs.push("--effort", effectiveEffort);
213
245
  if (this.conversationId)
214
246
  extraArgs.push("--conversation", this.conversationId);
247
+ const addDirs = [...(this.permissions.addDirs || [])];
248
+ if (this.cwd && !addDirs.includes(this.cwd)) {
249
+ addDirs.push(this.cwd);
250
+ }
215
251
  const args = buildAgyArgs({
216
252
  sandbox: this.permissions.sandbox,
217
253
  dangerouslySkipPermissions: this.permissions.dangerouslySkipPermissions,
218
254
  mode: this.mode,
255
+ addDirs,
219
256
  }, extraArgs);
220
257
  logger.info("Spawning agy process", {
221
258
  binary: this.binaryPath,
@@ -228,6 +265,7 @@ export class AntigravityProcess extends EventEmitter {
228
265
  env: this.env,
229
266
  stdio: ["pipe", "pipe", "pipe"],
230
267
  detached: process.platform !== "win32",
268
+ shell: process.platform === "win32",
231
269
  });
232
270
  this.child = child;
233
271
  if (!child.stdin || !child.stdout || !child.stderr) {
@@ -14,7 +14,9 @@ function getAttachmentsDir() {
14
14
  const attachmentsDir = path.join(getStateRoot(), "attachments");
15
15
  try {
16
16
  fs.mkdirSync(attachmentsDir, { recursive: true, mode: 0o700 });
17
- fs.chmodSync(attachmentsDir, 0o700);
17
+ if (process.platform !== "win32") {
18
+ fs.chmodSync(attachmentsDir, 0o700);
19
+ }
18
20
  }
19
21
  catch { }
20
22
  return attachmentsDir;
@@ -88,7 +90,9 @@ export function saveBase64Image(data, mimeType = "image/png") {
88
90
  if (!fs.existsSync(filePath)) {
89
91
  try {
90
92
  fs.writeFileSync(filePath, buffer, { mode: 0o600, flag: "wx" });
91
- fs.chmodSync(filePath, 0o600);
93
+ if (process.platform !== "win32") {
94
+ fs.chmodSync(filePath, 0o600);
95
+ }
92
96
  logger.info("Saved attached image for session", { filePath, sizeBytes: buffer.length });
93
97
  }
94
98
  catch (err) {
@@ -2,10 +2,12 @@ export interface PermissionSettings {
2
2
  sandbox: boolean;
3
3
  dangerouslySkipPermissions: boolean;
4
4
  mode?: string;
5
+ addDirs?: string[];
5
6
  }
6
7
  export declare function resolvePermissionSettings(options?: {
7
8
  sandbox?: boolean;
8
9
  dangerouslySkipPermissions?: boolean;
9
10
  mode?: string;
11
+ addDirs?: string[];
10
12
  }): PermissionSettings;
11
13
  export declare function buildAgyArgs(settings: PermissionSettings, extraArgs?: string[]): string[];
@@ -3,12 +3,17 @@ export function resolvePermissionSettings(options) {
3
3
  const envSandbox = process.env.AGY_ACP_SANDBOX;
4
4
  const envSkipPerms = process.env.AGY_ACP_DANGEROUSLY_SKIP_PERMISSIONS;
5
5
  const sandbox = options?.sandbox ?? (envSandbox === "true" || envSandbox === "1");
6
- // Never default to skip permissions; must be explicit
7
- const dangerouslySkipPermissions = options?.dangerouslySkipPermissions ?? (envSkipPerms === "true" || envSkipPerms === "1");
6
+ // In ACP mode, permissions and tool approvals are governed by the host application (e.g. Paseo).
7
+ // Because agy runs non-interactively over stream-json pipes without an interactive TTY, agy's
8
+ // internal terminal prompts cannot query the user and will fail with "user denied permission".
9
+ // Therefore, dangerouslySkipPermissions defaults to true unless explicitly disabled.
10
+ const dangerouslySkipPermissions = options?.dangerouslySkipPermissions ??
11
+ (envSkipPerms !== undefined ? envSkipPerms === "true" || envSkipPerms === "1" : true);
8
12
  return {
9
13
  sandbox,
10
14
  dangerouslySkipPermissions,
11
15
  mode: options?.mode,
16
+ addDirs: options?.addDirs,
12
17
  };
13
18
  }
14
19
  export function buildAgyArgs(settings, extraArgs) {
@@ -22,6 +27,13 @@ export function buildAgyArgs(settings, extraArgs) {
22
27
  if (settings.mode && settings.mode !== "default") {
23
28
  args.push("--mode", settings.mode);
24
29
  }
30
+ if (settings.addDirs && settings.addDirs.length > 0) {
31
+ for (const dir of settings.addDirs) {
32
+ if (dir) {
33
+ args.push("--add-dir", dir);
34
+ }
35
+ }
36
+ }
25
37
  if (extraArgs && extraArgs.length > 0) {
26
38
  args.push(...extraArgs);
27
39
  }
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
  }
@@ -279,11 +292,13 @@ export async function fetchAntigravityUsage(binaryPath = "agy", force = false) {
279
292
  timeout: 8_000,
280
293
  maxBuffer: 1024 * 1024,
281
294
  env: process.env,
295
+ shell: process.platform === "win32",
282
296
  }),
283
297
  execFileAsync(binaryPath, ["--print", "/credits"], {
284
298
  timeout: 8_000,
285
299
  maxBuffer: 1024 * 1024,
286
300
  env: process.env,
301
+ shell: process.platform === "win32",
287
302
  }),
288
303
  ]);
289
304
  const balances = [];
@@ -408,6 +423,7 @@ export async function fetchAvailableModels(binaryPath = "agy", force = false) {
408
423
  timeout: 5_000,
409
424
  env: process.env,
410
425
  maxBuffer: 4 * 1024 * 1024,
426
+ shell: process.platform === "win32",
411
427
  });
412
428
  const parsed = parseAgyModelsOutput(stdout);
413
429
  cachedModels = parsed;
@@ -19,7 +19,9 @@ export class SessionStore {
19
19
  this.dir = dir || path.join(getStateRoot(), "sessions");
20
20
  try {
21
21
  fs.mkdirSync(this.dir, { recursive: true, mode: 0o700 });
22
- fs.chmodSync(this.dir, 0o700);
22
+ if (process.platform !== "win32") {
23
+ fs.chmodSync(this.dir, 0o700);
24
+ }
23
25
  }
24
26
  catch { }
25
27
  }
@@ -56,8 +58,13 @@ export class SessionStore {
56
58
  mode: 0o600,
57
59
  flag: "wx",
58
60
  });
61
+ if (process.platform === "win32" && fs.existsSync(filePath)) {
62
+ fs.unlinkSync(filePath);
63
+ }
59
64
  fs.renameSync(tempPath, filePath);
60
- fs.chmodSync(filePath, 0o600);
65
+ if (process.platform !== "win32") {
66
+ fs.chmodSync(filePath, 0o600);
67
+ }
61
68
  }
62
69
  catch (err) {
63
70
  try {
@@ -31,7 +31,7 @@ function transcriptPath(conversationId) {
31
31
  }
32
32
  function summarizeConversation(cid, logFile, mtime) {
33
33
  try {
34
- const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
34
+ const lines = fs.readFileSync(logFile, "utf8").split(/\r?\n/).filter(Boolean);
35
35
  let firstPrompt = "";
36
36
  let userTurns = 0;
37
37
  for (const line of lines) {
@@ -111,7 +111,7 @@ export function getConversationSteps(cid) {
111
111
  if (!logFile || !fs.existsSync(logFile))
112
112
  return null;
113
113
  try {
114
- const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
114
+ const lines = fs.readFileSync(logFile, "utf8").split(/\r?\n/).filter(Boolean);
115
115
  const steps = [];
116
116
  let userTurns = 0;
117
117
  for (let i = 0; i < lines.length; i++) {
@@ -272,11 +272,30 @@ function formatCountdown(isoStr) {
272
272
  }
273
273
  export function formatUsageOutput(rawText) {
274
274
  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)
275
+ for (const line of rawText.trim().split(/\r?\n/)) {
276
+ const trimmedLine = line.trim();
277
+ if (!trimmedLine || trimmedLine.toLowerCase().startsWith("quota:"))
278
278
  continue;
279
- let fam = parts[0].trim();
279
+ let fam = "";
280
+ let rawWindow = "";
281
+ let pctStr = "";
282
+ let reset = "";
283
+ const m = trimmedLine.match(/^(.*?)\s{2,}(.*?Remaining)\s+(\d+%)(?:\s+(.*))?$/i);
284
+ if (m) {
285
+ fam = m[1].trim();
286
+ rawWindow = m[2].trim();
287
+ pctStr = m[3].trim();
288
+ reset = m[4]?.trim() || "";
289
+ }
290
+ else {
291
+ const parts = trimmedLine.includes("\t") ? trimmedLine.split("\t") : trimmedLine.split(/\s{2,}/);
292
+ if (parts.length < 3)
293
+ continue;
294
+ fam = parts[0].trim();
295
+ rawWindow = parts[1].trim();
296
+ pctStr = parts[2].trim();
297
+ reset = parts[3]?.trim() || "";
298
+ }
280
299
  let icon = "🤖";
281
300
  if (fam.toLowerCase().includes("gemini")) {
282
301
  fam = "Google Gemini";
@@ -286,7 +305,7 @@ export function formatUsageOutput(rawText) {
286
305
  fam = "Claude & GPT";
287
306
  icon = "🔶";
288
307
  }
289
- const rawWindow = parts[1].replace(" Remaining", "").trim();
308
+ rawWindow = rawWindow.replace(/\s+Remaining$/i, "").trim();
290
309
  const lowerWindow = rawWindow.toLowerCase();
291
310
  const isFiveHour = lowerWindow.includes("five hour") || /\b5\s*hour/.test(lowerWindow);
292
311
  const win = isFiveHour
@@ -294,10 +313,8 @@ export function formatUsageOutput(rawText) {
294
313
  : lowerWindow.includes("weekly")
295
314
  ? "Cota Semanal"
296
315
  : rawWindow;
297
- const pctStr = parts[2].trim();
298
316
  const parsedPct = Number.parseInt(pctStr.replace("%", ""), 10);
299
317
  const pct = Number.isFinite(parsedPct) ? parsedPct : 0;
300
- const reset = parts[3]?.trim() || "";
301
318
  if (!groups.has(fam))
302
319
  groups.set(fam, { icon, items: [] });
303
320
  groups.get(fam).items.push({ win, isFiveHour, pct, pctStr, reset });
@@ -324,6 +341,7 @@ async function runAgySlash(binaryPath, cwd, slashCommand) {
324
341
  env: process.env,
325
342
  timeout: AGY_COMMAND_TIMEOUT_MS,
326
343
  maxBuffer: AGY_COMMAND_MAX_BUFFER,
344
+ shell: process.platform === "win32",
327
345
  });
328
346
  return stdout.trim() || stderr.trim();
329
347
  }
package/dist/version.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -31,16 +32,35 @@ function resolveAgyInfo() {
31
32
  let binaryPath = process.env.AGY_BIN_PATH;
32
33
  if (!binaryPath) {
33
34
  try {
34
- binaryPath = execSync("which agy 2>/dev/null || command -v agy 2>/dev/null", { encoding: "utf8" }).trim();
35
+ const lookupCmd = process.platform === "win32"
36
+ ? "where.exe agy"
37
+ : "which agy 2>/dev/null || command -v agy 2>/dev/null";
38
+ const output = execSync(lookupCmd, { encoding: "utf8" }).trim();
39
+ if (output) {
40
+ binaryPath = output.split(/\r?\n/)[0].trim();
41
+ }
35
42
  }
36
43
  catch {
37
44
  // ignore
38
45
  }
39
46
  }
40
- if (!binaryPath && process.env.HOME) {
41
- const defaultLocalPath = path.join(process.env.HOME, ".local", "bin", "agy");
42
- if (fs.existsSync(defaultLocalPath)) {
43
- binaryPath = defaultLocalPath;
47
+ if (!binaryPath) {
48
+ const homeDir = os.homedir();
49
+ if (homeDir) {
50
+ const candidates = process.platform === "win32"
51
+ ? [
52
+ path.join(homeDir, ".local", "bin", "agy.exe"),
53
+ path.join(homeDir, "AppData", "Local", "Programs", "antigravity", "agy.exe"),
54
+ path.join(homeDir, ".local", "bin", "agy.cmd"),
55
+ path.join(homeDir, ".local", "bin", "agy.bat"),
56
+ ]
57
+ : [path.join(homeDir, ".local", "bin", "agy")];
58
+ for (const cand of candidates) {
59
+ if (fs.existsSync(cand)) {
60
+ binaryPath = cand;
61
+ break;
62
+ }
63
+ }
44
64
  }
45
65
  }
46
66
  if (!binaryPath)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
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",