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