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 +25 -3
- package/README.md +54 -0
- package/dist/acp-server.d.ts +1 -0
- package/dist/acp-server.js +25 -1
- package/dist/antigravity-process.d.ts +1 -0
- package/dist/antigravity-process.js +19 -2
- package/dist/index.js +40 -0
- package/dist/paseo-patcher.d.ts +34 -0
- package/dist/paseo-patcher.js +457 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +90 -59
- package/dist/slash-commands.js +30 -10
- package/package.json +1 -1
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
|
-
|
|
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
|
-
- `--
|
|
20
|
-
- `--dangerously-skip-permissions`
|
|
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
|
---
|
package/dist/acp-server.d.ts
CHANGED
package/dist/acp-server.js
CHANGED
|
@@ -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 ||
|
|
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
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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:
|
|
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
|
-
|
|
407
|
-
|
|
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)
|
package/dist/slash-commands.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
}
|