when-does-my-quota-refresh 1.0.0
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/LICENSE +21 -0
- package/README.md +704 -0
- package/dist/auth-S43EEJYA.js +12 -0
- package/dist/auth-S43EEJYA.js.map +1 -0
- package/dist/chunk-YZVX5QIP.js +487 -0
- package/dist/chunk-YZVX5QIP.js.map +1 -0
- package/dist/cli/index.js +2329 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/sdk/index.d.ts +350 -0
- package/dist/sdk/index.js +1804 -0
- package/dist/sdk/index.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,1804 @@
|
|
|
1
|
+
// src/core/logger.ts
|
|
2
|
+
var _verbose = false;
|
|
3
|
+
function setVerbose(verbose) {
|
|
4
|
+
_verbose = verbose;
|
|
5
|
+
}
|
|
6
|
+
function debug(module, msg, data) {
|
|
7
|
+
if (!_verbose) return;
|
|
8
|
+
const prefix = `[${module}]`;
|
|
9
|
+
if (data !== void 0) {
|
|
10
|
+
console.error(`${prefix} ${msg}`, data);
|
|
11
|
+
} else {
|
|
12
|
+
console.error(`${prefix} ${msg}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/core/errors.ts
|
|
17
|
+
var SourceUnavailableError = class extends Error {
|
|
18
|
+
constructor(source, reason) {
|
|
19
|
+
super(`Source "${source}" unavailable${reason ? `: ${reason}` : ""}`);
|
|
20
|
+
this.name = "SourceUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var AuthError = class extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "AuthError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var QuotaFetchError = class extends Error {
|
|
30
|
+
constructor(source, reason) {
|
|
31
|
+
super(`Failed to fetch quota from "${source}"${reason ? `: ${reason}` : ""}`);
|
|
32
|
+
this.name = "QuotaFetchError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/core/platform.ts
|
|
37
|
+
import { exec } from "child_process";
|
|
38
|
+
import { promisify } from "util";
|
|
39
|
+
var execAsync = promisify(exec);
|
|
40
|
+
function isWindows() {
|
|
41
|
+
return process.platform === "win32";
|
|
42
|
+
}
|
|
43
|
+
function isMacOS() {
|
|
44
|
+
return process.platform === "darwin";
|
|
45
|
+
}
|
|
46
|
+
function isLinux() {
|
|
47
|
+
return process.platform === "linux";
|
|
48
|
+
}
|
|
49
|
+
async function runCommand(cmd) {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await execAsync(cmd, { timeout: 1e4 });
|
|
52
|
+
return stdout;
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/sources/local/process-detector.ts
|
|
59
|
+
var ANTIMATTER_MARKERS = [
|
|
60
|
+
"antigravity",
|
|
61
|
+
"language_server",
|
|
62
|
+
"language-server",
|
|
63
|
+
"antigravity-cli",
|
|
64
|
+
"antigravity_cli",
|
|
65
|
+
"agy"
|
|
66
|
+
];
|
|
67
|
+
var ANTIMATTER_PATH_MARKERS = [
|
|
68
|
+
"/antigravity/",
|
|
69
|
+
"Antigravity",
|
|
70
|
+
"antigravity.app",
|
|
71
|
+
"AntigravityStatusProbe",
|
|
72
|
+
"app_data_dir antigravity"
|
|
73
|
+
];
|
|
74
|
+
async function detectAntigravityProcesses() {
|
|
75
|
+
debug("process-detector", `Detecting on platform: ${process.platform}`);
|
|
76
|
+
let processes = [];
|
|
77
|
+
if (isMacOS()) {
|
|
78
|
+
processes = await detectOnMacOS();
|
|
79
|
+
} else if (isLinux()) {
|
|
80
|
+
processes = await detectOnLinux();
|
|
81
|
+
} else if (isWindows()) {
|
|
82
|
+
processes = await detectOnWindows();
|
|
83
|
+
}
|
|
84
|
+
debug("process-detector", `Found ${processes.length} process(es)`);
|
|
85
|
+
return processes;
|
|
86
|
+
}
|
|
87
|
+
async function detectOnMacOS() {
|
|
88
|
+
const output = await runCommand("ps -ax -o pid=,comm=,args=");
|
|
89
|
+
return parsePsOutput(output);
|
|
90
|
+
}
|
|
91
|
+
async function detectOnLinux() {
|
|
92
|
+
const output = await runCommand("ps -eo pid,comm,args --no-headers");
|
|
93
|
+
return parsePsOutput(output);
|
|
94
|
+
}
|
|
95
|
+
async function detectOnWindows() {
|
|
96
|
+
let output = await runCommand(
|
|
97
|
+
`wmic process where "name like '%language%' or commandline like '%antigravity%'" get ProcessId,Name,CommandLine /format:csv`
|
|
98
|
+
);
|
|
99
|
+
let processes = parseWmicOutput(output);
|
|
100
|
+
if (processes.length > 0) return processes;
|
|
101
|
+
output = await runCommand(
|
|
102
|
+
`powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'antigravity|language.server' } | Select-Object ProcessId,Name,CommandLine | Format-Table -AutoSize"`
|
|
103
|
+
);
|
|
104
|
+
processes = parsePowerShellOutput(output);
|
|
105
|
+
if (processes.length > 0) return processes;
|
|
106
|
+
debug("process-detector", "Windows: Falling back to port-based detection");
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
function parsePsOutput(output) {
|
|
110
|
+
const processes = [];
|
|
111
|
+
const lines = output.split("\n").filter(Boolean);
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const trimmed = line.trim();
|
|
114
|
+
if (!trimmed) continue;
|
|
115
|
+
const parts = trimmed.split(/\s+/);
|
|
116
|
+
const pid = parseInt(parts[0], 10);
|
|
117
|
+
if (isNaN(pid)) continue;
|
|
118
|
+
const rest = parts.slice(1).join(" ");
|
|
119
|
+
if (matchesAntigravity(rest)) {
|
|
120
|
+
processes.push({
|
|
121
|
+
pid,
|
|
122
|
+
name: parts[1] || "unknown",
|
|
123
|
+
commandLine: rest,
|
|
124
|
+
scope: classifyProcess(rest)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return processes;
|
|
129
|
+
}
|
|
130
|
+
function parseWmicOutput(output) {
|
|
131
|
+
const processes = [];
|
|
132
|
+
const lines = output.split("\n").filter(Boolean);
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const parts = line.split(",").map((p) => p.trim().replace(/"/g, ""));
|
|
135
|
+
if (parts.length < 4) continue;
|
|
136
|
+
const pid = parseInt(parts[3], 10);
|
|
137
|
+
if (isNaN(pid)) continue;
|
|
138
|
+
const commandLine = parts[1] || "";
|
|
139
|
+
const name = parts[2] || "";
|
|
140
|
+
if (matchesAntigravity(commandLine) || matchesAntigravity(name)) {
|
|
141
|
+
processes.push({
|
|
142
|
+
pid,
|
|
143
|
+
name,
|
|
144
|
+
commandLine,
|
|
145
|
+
scope: classifyProcess(commandLine)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return processes;
|
|
150
|
+
}
|
|
151
|
+
function parsePowerShellOutput(output) {
|
|
152
|
+
const processes = [];
|
|
153
|
+
const lines = output.split("\n").filter(Boolean);
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed || trimmed.startsWith("---") || trimmed.startsWith("ProcessId")) continue;
|
|
157
|
+
const match = trimmed.match(/^\s*(\d+)\s+(\S+)\s+(.*)$/);
|
|
158
|
+
if (!match) continue;
|
|
159
|
+
const pid = parseInt(match[1], 10);
|
|
160
|
+
const name = match[2];
|
|
161
|
+
const commandLine = match[3];
|
|
162
|
+
if (matchesAntigravity(commandLine) || matchesAntigravity(name)) {
|
|
163
|
+
processes.push({
|
|
164
|
+
pid,
|
|
165
|
+
name,
|
|
166
|
+
commandLine,
|
|
167
|
+
scope: classifyProcess(commandLine)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return processes;
|
|
172
|
+
}
|
|
173
|
+
function matchesAntigravity(text) {
|
|
174
|
+
const lower = text.toLowerCase();
|
|
175
|
+
return ANTIMATTER_PATH_MARKERS.some(
|
|
176
|
+
(marker) => lower.includes(marker.toLowerCase())
|
|
177
|
+
) || lower.includes("language") && lower.includes("server") && ANTIMATTER_MARKERS.some((m) => lower.includes(m));
|
|
178
|
+
}
|
|
179
|
+
function classifyProcess(commandLine) {
|
|
180
|
+
const lower = commandLine.toLowerCase();
|
|
181
|
+
if (lower.includes("antigravity-cli") || lower.includes("antigravity_cli") || lower.includes("agy")) {
|
|
182
|
+
return "cli";
|
|
183
|
+
}
|
|
184
|
+
if (lower.includes("antigravity ide") || lower.includes("antigravity-ide") || lower.includes("extensions/antigravity")) {
|
|
185
|
+
return "ide";
|
|
186
|
+
}
|
|
187
|
+
if (lower.includes("app_data_dir antigravity") || lower.includes("antigravity.app")) {
|
|
188
|
+
return "app";
|
|
189
|
+
}
|
|
190
|
+
return "unknown";
|
|
191
|
+
}
|
|
192
|
+
function extractFlags(commandLine) {
|
|
193
|
+
const flags = /* @__PURE__ */ new Map();
|
|
194
|
+
if (!commandLine) return flags;
|
|
195
|
+
const flagRegex = /--(\S+)\s+([^\s-][^\s]*)/g;
|
|
196
|
+
let match;
|
|
197
|
+
while ((match = flagRegex.exec(commandLine)) !== null) {
|
|
198
|
+
flags.set(match[1], match[2]);
|
|
199
|
+
}
|
|
200
|
+
return flags;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/sources/local/port-detective.ts
|
|
204
|
+
async function discoverPorts(pid) {
|
|
205
|
+
debug("port-detective", `Discovering ports for PID ${pid} on ${process.platform}`);
|
|
206
|
+
if (isWindows()) {
|
|
207
|
+
return discoverPortsOnWindows(pid);
|
|
208
|
+
} else if (isMacOS()) {
|
|
209
|
+
return discoverPortsOnMacOS(pid);
|
|
210
|
+
} else {
|
|
211
|
+
return discoverPortsOnLinux(pid);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function discoverPortsOnMacOS(pid) {
|
|
215
|
+
try {
|
|
216
|
+
const output = await runCommand(`lsof -nP -iTCP -sTCP:LISTEN -a -p ${pid}`);
|
|
217
|
+
const ports = [];
|
|
218
|
+
for (const line of output.split("\n")) {
|
|
219
|
+
const match = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
220
|
+
if (match) {
|
|
221
|
+
const port = parseInt(match[1], 10);
|
|
222
|
+
if (!isNaN(port) && !ports.includes(port)) {
|
|
223
|
+
ports.push(port);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
debug("port-detective", `macOS ports: ${ports.join(", ")}`);
|
|
228
|
+
return ports;
|
|
229
|
+
} catch {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function discoverPortsOnLinux(pid) {
|
|
234
|
+
try {
|
|
235
|
+
const output = await runCommand(`ss -tlnp | grep "pid=${pid},"`);
|
|
236
|
+
const ports = parseListenPorts(output);
|
|
237
|
+
if (ports.length > 0) {
|
|
238
|
+
debug("port-detective", `Linux ports (ss): ${ports.join(", ")}`);
|
|
239
|
+
return ports;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const output = await runCommand(`netstat -tlnp 2>/dev/null | grep "${pid}/"`);
|
|
245
|
+
const ports = parseListenPorts(output);
|
|
246
|
+
debug("port-detective", `Linux ports (netstat): ${ports.join(", ")}`);
|
|
247
|
+
return ports;
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function discoverPortsOnWindows(pid) {
|
|
253
|
+
try {
|
|
254
|
+
const output = await runCommand("netstat -ano");
|
|
255
|
+
const ports = [];
|
|
256
|
+
for (const line of output.split("\n")) {
|
|
257
|
+
if (!line.includes("LISTENING")) continue;
|
|
258
|
+
const parts = line.trim().split(/\s+/);
|
|
259
|
+
const linePid = parseInt(parts[parts.length - 1], 10);
|
|
260
|
+
if (linePid === pid) {
|
|
261
|
+
const localAddr = parts[1];
|
|
262
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
263
|
+
if (portMatch) {
|
|
264
|
+
const port = parseInt(portMatch[1], 10);
|
|
265
|
+
if (!isNaN(port) && !ports.includes(port)) {
|
|
266
|
+
ports.push(port);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (ports.length > 0) {
|
|
272
|
+
debug("port-detective", `Windows ports (netstat): ${ports.join(", ")}`);
|
|
273
|
+
return ports;
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const output = await runCommand(
|
|
279
|
+
`powershell -Command "Get-NetTCPConnection -OwningProcess ${pid} -State Listen -ErrorAction SilentlyContinue | Select-Object LocalPort | Format-Table -AutoSize"`
|
|
280
|
+
);
|
|
281
|
+
const ports = [];
|
|
282
|
+
for (const line of output.split("\n")) {
|
|
283
|
+
const match = line.match(/^\s*(\d+)\s*$/);
|
|
284
|
+
if (match) {
|
|
285
|
+
const port = parseInt(match[1], 10);
|
|
286
|
+
if (!isNaN(port) && !ports.includes(port)) {
|
|
287
|
+
ports.push(port);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (ports.length > 0) {
|
|
292
|
+
debug("port-detective", `Windows ports (PowerShell): ${ports.join(", ")}`);
|
|
293
|
+
return ports;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
debug("port-detective", "Windows: Could not find ports via system commands");
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
function parseListenPorts(output) {
|
|
301
|
+
const ports = [];
|
|
302
|
+
for (const line of output.split("\n")) {
|
|
303
|
+
const match = line.match(/:(\d+)\s/);
|
|
304
|
+
if (match) {
|
|
305
|
+
const port = parseInt(match[1], 10);
|
|
306
|
+
if (!isNaN(port) && !ports.includes(port)) {
|
|
307
|
+
ports.push(port);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return ports;
|
|
312
|
+
}
|
|
313
|
+
async function scanPortRange(startPort = 8e3, endPort = 9500) {
|
|
314
|
+
debug("port-detective", `Scanning port range ${startPort}-${endPort}`);
|
|
315
|
+
const foundPorts = [];
|
|
316
|
+
if (isWindows()) {
|
|
317
|
+
const output = await runCommand("netstat -ano");
|
|
318
|
+
for (const line of output.split("\n")) {
|
|
319
|
+
if (!line.includes("LISTENING")) continue;
|
|
320
|
+
const parts = line.trim().split(/\s+/);
|
|
321
|
+
const localAddr = parts[1];
|
|
322
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
323
|
+
if (portMatch) {
|
|
324
|
+
const port = parseInt(portMatch[1], 10);
|
|
325
|
+
if (port >= startPort && port <= endPort) {
|
|
326
|
+
foundPorts.push(port);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
const cmd = isMacOS() ? `lsof -nP -iTCP -sTCP:LISTEN` : `ss -tlnp`;
|
|
332
|
+
const output = await runCommand(cmd);
|
|
333
|
+
for (const line of output.split("\n")) {
|
|
334
|
+
const match = line.match(/:(\d+)\s/);
|
|
335
|
+
if (match) {
|
|
336
|
+
const port = parseInt(match[1], 10);
|
|
337
|
+
if (port >= startPort && port <= endPort && !foundPorts.includes(port)) {
|
|
338
|
+
foundPorts.push(port);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
debug("port-detective", `Found ${foundPorts.length} ports in range`);
|
|
344
|
+
return foundPorts;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/sources/local/connect-client.ts
|
|
348
|
+
import https from "https";
|
|
349
|
+
import http from "http";
|
|
350
|
+
var ENDPOINTS = {
|
|
351
|
+
quotaSummary: "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary",
|
|
352
|
+
userStatus: "/exa.language_server_pb.LanguageServerService/GetUserStatus",
|
|
353
|
+
modelConfigs: "/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs",
|
|
354
|
+
unleash: "/exa.language_server_pb.LanguageServerService/GetUnleashData",
|
|
355
|
+
availableModels: "/exa.language_server_pb.LanguageServerService/GetAvailableModels"
|
|
356
|
+
};
|
|
357
|
+
var ConnectClient = class {
|
|
358
|
+
baseUrl;
|
|
359
|
+
csrfToken;
|
|
360
|
+
isHttps;
|
|
361
|
+
constructor(baseUrl, csrfToken) {
|
|
362
|
+
this.baseUrl = baseUrl;
|
|
363
|
+
this.csrfToken = csrfToken;
|
|
364
|
+
this.isHttps = baseUrl.startsWith("https://");
|
|
365
|
+
debug("connect-client", `Init: ${baseUrl}, hasToken: ${!!csrfToken}`);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Test if this server is reachable and speaks Connect protocol
|
|
369
|
+
*/
|
|
370
|
+
async probe() {
|
|
371
|
+
try {
|
|
372
|
+
await this.request("POST", ENDPOINTS.unleash, {});
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Fetch quota summary (richest data: weekly + session buckets)
|
|
380
|
+
*/
|
|
381
|
+
async fetchQuotaSummary() {
|
|
382
|
+
debug("connect-client", "Fetching RetrieveUserQuotaSummary");
|
|
383
|
+
try {
|
|
384
|
+
const response = await this.request("POST", ENDPOINTS.quotaSummary, {
|
|
385
|
+
metadata: {
|
|
386
|
+
ideName: "antigravity",
|
|
387
|
+
extensionName: "antigravity",
|
|
388
|
+
locale: "en"
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
if (response) {
|
|
392
|
+
return this.parseQuotaSummary(response);
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
debug("connect-client", `QuotaSummary failed: ${err}`);
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Fetch user status (fallback for older servers)
|
|
401
|
+
*/
|
|
402
|
+
async fetchUserStatus() {
|
|
403
|
+
debug("connect-client", "Fetching GetUserStatus");
|
|
404
|
+
try {
|
|
405
|
+
const response = await this.request("POST", ENDPOINTS.userStatus, {
|
|
406
|
+
metadata: {
|
|
407
|
+
ideName: "antigravity",
|
|
408
|
+
extensionName: "antigravity",
|
|
409
|
+
locale: "en"
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
if (response) {
|
|
413
|
+
return this.parseUserStatus(response);
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
debug("connect-client", `UserStatus failed: ${err}`);
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Fetch model configs (last-resort fallback)
|
|
422
|
+
*/
|
|
423
|
+
async fetchModelConfigs() {
|
|
424
|
+
debug("connect-client", "Fetching GetCascadeModelConfigData");
|
|
425
|
+
try {
|
|
426
|
+
return await this.request("POST", ENDPOINTS.modelConfigs, {
|
|
427
|
+
metadata: {
|
|
428
|
+
ideName: "antigravity",
|
|
429
|
+
extensionName: "antigravity",
|
|
430
|
+
locale: "en"
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
} catch (err) {
|
|
434
|
+
debug("connect-client", `ModelConfigs failed: ${err}`);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Make a Connect protocol HTTP(S) request
|
|
440
|
+
*/
|
|
441
|
+
request(method, path, body) {
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
const url = new URL(path, this.baseUrl);
|
|
444
|
+
const headers = {
|
|
445
|
+
"Accept": "application/json",
|
|
446
|
+
"Content-Type": "application/json",
|
|
447
|
+
"Connect-Protocol-Version": "1"
|
|
448
|
+
};
|
|
449
|
+
if (this.csrfToken) {
|
|
450
|
+
headers["X-Codeium-Csrf-Token"] = this.csrfToken;
|
|
451
|
+
}
|
|
452
|
+
const options = {
|
|
453
|
+
hostname: url.hostname,
|
|
454
|
+
port: url.port,
|
|
455
|
+
path: url.pathname,
|
|
456
|
+
method,
|
|
457
|
+
headers,
|
|
458
|
+
timeout: 5e3,
|
|
459
|
+
rejectUnauthorized: false
|
|
460
|
+
};
|
|
461
|
+
const protocol = this.isHttps ? https : http;
|
|
462
|
+
const req = protocol.request(options, (res) => {
|
|
463
|
+
let data = "";
|
|
464
|
+
res.on("data", (chunk) => {
|
|
465
|
+
data += chunk;
|
|
466
|
+
});
|
|
467
|
+
res.on("end", () => {
|
|
468
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
469
|
+
try {
|
|
470
|
+
resolve(JSON.parse(data));
|
|
471
|
+
} catch {
|
|
472
|
+
resolve(data);
|
|
473
|
+
}
|
|
474
|
+
} else if (res.statusCode === 404) {
|
|
475
|
+
reject(new Error(`404: ${path}`));
|
|
476
|
+
} else {
|
|
477
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
req.on("error", reject);
|
|
482
|
+
req.on("timeout", () => {
|
|
483
|
+
req.destroy();
|
|
484
|
+
reject(new Error("Timeout"));
|
|
485
|
+
});
|
|
486
|
+
if (body) req.write(JSON.stringify(body));
|
|
487
|
+
req.end();
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Parse RetrieveUserQuotaSummary response
|
|
492
|
+
*/
|
|
493
|
+
parseQuotaSummary(response) {
|
|
494
|
+
const result = { groups: [], raw: response };
|
|
495
|
+
if (typeof response !== "object" || response === null) return result;
|
|
496
|
+
const data = response;
|
|
497
|
+
const userStatus = data.userStatus;
|
|
498
|
+
const groups = data.groups || userStatus?.groups;
|
|
499
|
+
if (Array.isArray(groups)) {
|
|
500
|
+
result.groups = groups.map((group) => {
|
|
501
|
+
const buckets = Array.isArray(group.buckets) ? group.buckets.map((b) => ({
|
|
502
|
+
bucketId: String(b.bucketId || ""),
|
|
503
|
+
displayName: String(b.displayName || ""),
|
|
504
|
+
description: String(b.description || ""),
|
|
505
|
+
remainingFraction: typeof b.remaining?.remainingFraction === "number" ? b.remaining.remainingFraction : typeof b.remainingFraction === "number" ? b.remainingFraction : 1,
|
|
506
|
+
resetTime: typeof b.resetTime === "string" ? b.resetTime : void 0
|
|
507
|
+
})) : [];
|
|
508
|
+
return {
|
|
509
|
+
displayName: String(group.displayName || ""),
|
|
510
|
+
buckets
|
|
511
|
+
};
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
debug("connect-client", `Parsed ${result.groups.length} groups from quota summary`);
|
|
515
|
+
return result;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Parse GetUserStatus response (legacy fallback)
|
|
519
|
+
*/
|
|
520
|
+
parseUserStatus(response) {
|
|
521
|
+
const status = { raw: response };
|
|
522
|
+
if (typeof response !== "object" || response === null) return status;
|
|
523
|
+
const data = response;
|
|
524
|
+
const userStatus = data.userStatus || data;
|
|
525
|
+
if (typeof userStatus.email === "string") status.email = userStatus.email;
|
|
526
|
+
if (typeof userStatus.isAuthenticated === "boolean") status.isAuthenticated = userStatus.isAuthenticated;
|
|
527
|
+
if (typeof userStatus.planType === "string") status.planType = userStatus.planType;
|
|
528
|
+
const planStatus = userStatus.planStatus;
|
|
529
|
+
if (planStatus) {
|
|
530
|
+
const available = planStatus.availablePromptCredits;
|
|
531
|
+
const planInfo = planStatus.planInfo;
|
|
532
|
+
const monthly = planInfo?.monthlyPromptCredits;
|
|
533
|
+
if (typeof available === "number" && typeof monthly === "number") {
|
|
534
|
+
status.quota = {
|
|
535
|
+
promptCredits: {
|
|
536
|
+
used: monthly - available,
|
|
537
|
+
limit: monthly,
|
|
538
|
+
remaining: available
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const cascadeData = userStatus.cascadeModelConfigData;
|
|
544
|
+
const clientModelConfigs = cascadeData?.clientModelConfigs;
|
|
545
|
+
if (Array.isArray(clientModelConfigs)) {
|
|
546
|
+
status.quota = status.quota || {};
|
|
547
|
+
status.quota.models = clientModelConfigs.map((m) => {
|
|
548
|
+
const modelOrAlias = m.modelOrAlias;
|
|
549
|
+
const modelId = typeof modelOrAlias?.model === "string" ? modelOrAlias.model : "unknown";
|
|
550
|
+
const quotaInfo = m.quotaInfo;
|
|
551
|
+
const remainingFraction = typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : void 0;
|
|
552
|
+
const resetTime = typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0;
|
|
553
|
+
return {
|
|
554
|
+
modelId,
|
|
555
|
+
displayName: typeof m.label === "string" ? m.label : void 0,
|
|
556
|
+
label: typeof m.label === "string" ? m.label : void 0,
|
|
557
|
+
quota: {
|
|
558
|
+
remainingPercentage: remainingFraction,
|
|
559
|
+
resetTime,
|
|
560
|
+
timeUntilResetMs: resetTime ? new Date(resetTime).getTime() - Date.now() : void 0
|
|
561
|
+
},
|
|
562
|
+
isExhausted: remainingFraction === 0
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return status;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/sources/local/parser.ts
|
|
571
|
+
function parseQuotaSummary(summary, email) {
|
|
572
|
+
debug("parser", `Parsing quota summary: ${summary.groups.length} groups`);
|
|
573
|
+
return {
|
|
574
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
575
|
+
method: "local",
|
|
576
|
+
email,
|
|
577
|
+
groups: summary.groups.map((g) => ({
|
|
578
|
+
displayName: g.displayName,
|
|
579
|
+
buckets: g.buckets.map((b) => ({
|
|
580
|
+
bucketId: b.bucketId,
|
|
581
|
+
displayName: b.displayName,
|
|
582
|
+
description: b.description,
|
|
583
|
+
remainingFraction: clampFraction(b.remainingFraction),
|
|
584
|
+
resetTime: b.resetTime,
|
|
585
|
+
timeUntilResetMs: b.resetTime ? Math.max(0, new Date(b.resetTime).getTime() - Date.now()) : void 0
|
|
586
|
+
}))
|
|
587
|
+
}))
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function parseUserStatus(userStatus) {
|
|
591
|
+
debug("parser", "Parsing GetUserStatus fallback");
|
|
592
|
+
const snapshot = {
|
|
593
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
594
|
+
method: "local",
|
|
595
|
+
email: userStatus.email,
|
|
596
|
+
groups: []
|
|
597
|
+
};
|
|
598
|
+
if (userStatus.quota?.promptCredits) {
|
|
599
|
+
snapshot.promptCredits = parsePromptCredits(userStatus.quota.promptCredits);
|
|
600
|
+
}
|
|
601
|
+
if (userStatus.quota?.models) {
|
|
602
|
+
const geminiModels = [];
|
|
603
|
+
const claudeModels = [];
|
|
604
|
+
for (const model of userStatus.quota.models) {
|
|
605
|
+
const info = {
|
|
606
|
+
label: model.label || model.displayName || model.modelId,
|
|
607
|
+
modelId: model.modelId,
|
|
608
|
+
remainingPercentage: model.quota?.remainingPercentage,
|
|
609
|
+
isExhausted: model.isExhausted ?? model.quota?.remainingPercentage === 0,
|
|
610
|
+
resetTime: model.quota?.resetTime,
|
|
611
|
+
timeUntilResetMs: model.quota?.timeUntilResetMs,
|
|
612
|
+
isAutocompleteOnly: detectAutocomplete(model.modelId, model.label)
|
|
613
|
+
};
|
|
614
|
+
if (info.isAutocompleteOnly) continue;
|
|
615
|
+
if (isGeminiModel(model.modelId)) {
|
|
616
|
+
geminiModels.push(info);
|
|
617
|
+
} else {
|
|
618
|
+
claudeModels.push(info);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (geminiModels.length > 0) {
|
|
622
|
+
snapshot.groups.push(createGroupFromModels("Gemini Models", geminiModels));
|
|
623
|
+
}
|
|
624
|
+
if (claudeModels.length > 0) {
|
|
625
|
+
snapshot.groups.push(createGroupFromModels("Claude + GPT Models", claudeModels));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return snapshot;
|
|
629
|
+
}
|
|
630
|
+
function parsePromptCredits(credits) {
|
|
631
|
+
if (!credits) return void 0;
|
|
632
|
+
const limit = credits.limit ?? 0;
|
|
633
|
+
const remaining = credits.remaining ?? limit;
|
|
634
|
+
const used = credits.used ?? limit - remaining;
|
|
635
|
+
if (limit === 0) return void 0;
|
|
636
|
+
return {
|
|
637
|
+
available: remaining,
|
|
638
|
+
monthly: limit,
|
|
639
|
+
usedPercentage: limit > 0 ? used / limit : 0,
|
|
640
|
+
remainingPercentage: limit > 0 ? remaining / limit : 1
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function createGroupFromModels(displayName, models) {
|
|
644
|
+
const bestModel = models.reduce((best, m) => {
|
|
645
|
+
const remaining = m.remainingPercentage ?? 1;
|
|
646
|
+
const bestRemaining = best.remainingPercentage ?? 1;
|
|
647
|
+
return remaining < bestRemaining ? m : best;
|
|
648
|
+
}, models[0]);
|
|
649
|
+
const bucket = {
|
|
650
|
+
bucketId: "session",
|
|
651
|
+
displayName: "Session (5-hour)",
|
|
652
|
+
description: bestModel.resetTime ? `Resets at ${new Date(bestModel.resetTime).toLocaleTimeString()}` : "5-hour session window",
|
|
653
|
+
remainingFraction: bestModel.remainingPercentage ?? 1,
|
|
654
|
+
resetTime: bestModel.resetTime,
|
|
655
|
+
timeUntilResetMs: bestModel.timeUntilResetMs
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
displayName,
|
|
659
|
+
buckets: [bucket]
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function isGeminiModel(modelId) {
|
|
663
|
+
const lower = modelId.toLowerCase();
|
|
664
|
+
return lower.includes("gemini");
|
|
665
|
+
}
|
|
666
|
+
function detectAutocomplete(modelId, label) {
|
|
667
|
+
const text = `${modelId} ${label || ""}`.toLowerCase();
|
|
668
|
+
return text.includes("autocomplete") || text.includes("gemini-2.5") || text.includes("gemini 2.5");
|
|
669
|
+
}
|
|
670
|
+
function clampFraction(value) {
|
|
671
|
+
if (value === void 0 || value === null) return 1;
|
|
672
|
+
return Math.max(0, Math.min(1, value));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/sources/local/index.ts
|
|
676
|
+
var LocalSource = class {
|
|
677
|
+
name = "local";
|
|
678
|
+
priority = 1;
|
|
679
|
+
async isAvailable() {
|
|
680
|
+
try {
|
|
681
|
+
const processes = await detectAntigravityProcesses();
|
|
682
|
+
return processes.length > 0;
|
|
683
|
+
} catch {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async fetchQuota() {
|
|
688
|
+
debug("local", "Starting local fetch");
|
|
689
|
+
const processes = await detectAntigravityProcesses();
|
|
690
|
+
if (processes.length === 0) {
|
|
691
|
+
throw new SourceUnavailableError("local", "No Antigravity process found");
|
|
692
|
+
}
|
|
693
|
+
for (const proc of processes) {
|
|
694
|
+
debug("local", `Trying PID ${proc.pid} (${proc.scope})`);
|
|
695
|
+
const flags = extractFlags(proc.commandLine || "");
|
|
696
|
+
let ports = [];
|
|
697
|
+
const directPort = flags.get("extension_server_port");
|
|
698
|
+
if (directPort) {
|
|
699
|
+
const portNum = parseInt(directPort, 10);
|
|
700
|
+
if (!isNaN(portNum)) {
|
|
701
|
+
ports.push(portNum);
|
|
702
|
+
debug("local", `Found port from command line: ${portNum}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const discoveredPorts = await discoverPorts(proc.pid);
|
|
706
|
+
for (const p of discoveredPorts) {
|
|
707
|
+
if (!ports.includes(p)) ports.push(p);
|
|
708
|
+
}
|
|
709
|
+
const csrfToken = flags.get("csrf_token") || flags.get("extension_server_csrf_token");
|
|
710
|
+
for (const port of ports) {
|
|
711
|
+
const quota = await this.probePort(port, csrfToken);
|
|
712
|
+
if (quota) return quota;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
debug("local", "Direct detection failed, scanning port range");
|
|
716
|
+
const scannedPorts = await scanPortRange(8e3, 9500);
|
|
717
|
+
for (const port of scannedPorts) {
|
|
718
|
+
const quota = await this.probePort(port);
|
|
719
|
+
if (quota) return quota;
|
|
720
|
+
}
|
|
721
|
+
throw new SourceUnavailableError("local", "Could not connect to any local server");
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Probe a single port and try to fetch quota
|
|
725
|
+
*/
|
|
726
|
+
async probePort(port, csrfToken) {
|
|
727
|
+
for (const scheme of ["https", "http"]) {
|
|
728
|
+
const baseUrl = `${scheme}://127.0.0.1:${port}`;
|
|
729
|
+
const client = new ConnectClient(baseUrl, csrfToken);
|
|
730
|
+
const reachable = await client.probe();
|
|
731
|
+
if (!reachable) continue;
|
|
732
|
+
debug("local", `Probing ${baseUrl}`);
|
|
733
|
+
const summary = await client.fetchQuotaSummary();
|
|
734
|
+
if (summary && summary.groups.length > 0) {
|
|
735
|
+
const email = (await client.fetchUserStatus())?.email;
|
|
736
|
+
debug("local", `Got quota summary from ${baseUrl}`);
|
|
737
|
+
return parseQuotaSummary(summary, email);
|
|
738
|
+
}
|
|
739
|
+
const userStatus = await client.fetchUserStatus();
|
|
740
|
+
if (userStatus) {
|
|
741
|
+
debug("local", `Got user status from ${baseUrl}`);
|
|
742
|
+
return parseUserStatus(userStatus);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/sources/cli/index.ts
|
|
750
|
+
import { exec as exec2 } from "child_process";
|
|
751
|
+
import { promisify as promisify2 } from "util";
|
|
752
|
+
var execAsync2 = promisify2(exec2);
|
|
753
|
+
var CliSource = class {
|
|
754
|
+
name = "cli";
|
|
755
|
+
priority = 2;
|
|
756
|
+
agyProcess = null;
|
|
757
|
+
async isAvailable() {
|
|
758
|
+
const path = await findAgyBinary();
|
|
759
|
+
return path !== null;
|
|
760
|
+
}
|
|
761
|
+
async fetchQuota() {
|
|
762
|
+
debug("cli", "Starting CLI source fetch");
|
|
763
|
+
const agyPath = await findAgyBinary();
|
|
764
|
+
if (!agyPath) {
|
|
765
|
+
throw new SourceUnavailableError("cli", "agy binary not found");
|
|
766
|
+
}
|
|
767
|
+
const existingQuota = await this.tryExistingAgy();
|
|
768
|
+
if (existingQuota) return existingQuota;
|
|
769
|
+
return await this.launchAndFetch(agyPath);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Check if there's already a running agy instance we can reuse
|
|
773
|
+
*/
|
|
774
|
+
async tryExistingAgy() {
|
|
775
|
+
const cmd = isWindows() ? `wmic process where "commandline like '%agy%'" get ProcessId,CommandLine /format:csv` : "ps -ax -o pid=,args=";
|
|
776
|
+
const output = await runCommand(cmd);
|
|
777
|
+
const pids = output.split("\n").map((line) => {
|
|
778
|
+
const match = line.match(/^\s*(\d+)/);
|
|
779
|
+
return match ? parseInt(match[1], 10) : NaN;
|
|
780
|
+
}).filter((p) => !isNaN(p));
|
|
781
|
+
for (const pid of pids) {
|
|
782
|
+
const ports = await discoverPorts(pid);
|
|
783
|
+
for (const port of ports) {
|
|
784
|
+
const quota = await this.probePort(port);
|
|
785
|
+
if (quota) return quota;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Launch agy and wait for its server, then fetch quota
|
|
792
|
+
*/
|
|
793
|
+
async launchAndFetch(agyPath) {
|
|
794
|
+
debug("cli", `Launching agy: ${agyPath}`);
|
|
795
|
+
return new Promise((resolve, reject) => {
|
|
796
|
+
const timeout = setTimeout(() => {
|
|
797
|
+
this.agyProcess?.kill();
|
|
798
|
+
reject(new SourceUnavailableError("cli", "agy startup timed out"));
|
|
799
|
+
}, 15e3);
|
|
800
|
+
this.agyProcess = exec2(agyPath, { env: { ...process.env, NO_COLOR: "1" } });
|
|
801
|
+
const pollInterval = setInterval(async () => {
|
|
802
|
+
try {
|
|
803
|
+
if (!this.agyProcess?.pid) return;
|
|
804
|
+
const ports = await discoverPorts(this.agyProcess.pid);
|
|
805
|
+
for (const port of ports) {
|
|
806
|
+
const quota = await this.probePort(port);
|
|
807
|
+
if (quota) {
|
|
808
|
+
clearTimeout(timeout);
|
|
809
|
+
clearInterval(pollInterval);
|
|
810
|
+
this.agyProcess?.kill();
|
|
811
|
+
resolve(quota);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
}, 1e3);
|
|
818
|
+
this.agyProcess.on("error", (err) => {
|
|
819
|
+
clearTimeout(timeout);
|
|
820
|
+
clearInterval(pollInterval);
|
|
821
|
+
reject(new SourceUnavailableError("cli", `agy failed: ${err.message}`));
|
|
822
|
+
});
|
|
823
|
+
this.agyProcess.on("exit", () => {
|
|
824
|
+
clearTimeout(timeout);
|
|
825
|
+
clearInterval(pollInterval);
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Probe a port for quota data
|
|
831
|
+
*/
|
|
832
|
+
async probePort(port) {
|
|
833
|
+
for (const scheme of ["https", "http"]) {
|
|
834
|
+
const baseUrl = `${scheme}://127.0.0.1:${port}`;
|
|
835
|
+
const client = new ConnectClient(baseUrl);
|
|
836
|
+
try {
|
|
837
|
+
const reachable = await client.probe();
|
|
838
|
+
if (!reachable) continue;
|
|
839
|
+
const summary = await client.fetchQuotaSummary();
|
|
840
|
+
if (summary && summary.groups.length > 0) {
|
|
841
|
+
const email = (await client.fetchUserStatus())?.email;
|
|
842
|
+
return parseQuotaSummary(summary, email);
|
|
843
|
+
}
|
|
844
|
+
const userStatus = await client.fetchUserStatus();
|
|
845
|
+
if (userStatus) return parseUserStatus(userStatus);
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
destroy() {
|
|
852
|
+
this.agyProcess?.kill();
|
|
853
|
+
this.agyProcess = null;
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
async function findAgyBinary() {
|
|
857
|
+
if (process.env.ANTIGRAVITY_CLI_PATH) {
|
|
858
|
+
return process.env.ANTIGRAVITY_CLI_PATH;
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const cmd = isWindows() ? "where agy" : "which agy";
|
|
862
|
+
const output = await execAsync2(cmd);
|
|
863
|
+
const path = output.stdout.trim();
|
|
864
|
+
if (path) return path;
|
|
865
|
+
} catch {
|
|
866
|
+
}
|
|
867
|
+
const commonPaths = isWindows() ? [
|
|
868
|
+
`${process.env.LOCALAPPDATA || ""}/antigravity-cli/agy.exe`,
|
|
869
|
+
`${process.env.PROGRAMFILES || ""}/antigravity-cli/agy.exe`
|
|
870
|
+
] : [
|
|
871
|
+
`${process.env.HOME}/.local/bin/agy`,
|
|
872
|
+
"/opt/homebrew/bin/agy",
|
|
873
|
+
"/usr/local/bin/agy"
|
|
874
|
+
];
|
|
875
|
+
for (const p of commonPaths) {
|
|
876
|
+
if (!p) continue;
|
|
877
|
+
try {
|
|
878
|
+
await execAsync2(`test -x "${p}"`);
|
|
879
|
+
return p;
|
|
880
|
+
} catch {
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// src/sources/oauth/index.ts
|
|
887
|
+
var OAuthSource = class {
|
|
888
|
+
name = "oauth";
|
|
889
|
+
priority = 3;
|
|
890
|
+
client;
|
|
891
|
+
constructor(client) {
|
|
892
|
+
this.client = client;
|
|
893
|
+
}
|
|
894
|
+
async isAvailable() {
|
|
895
|
+
try {
|
|
896
|
+
await this.client.getAccessToken();
|
|
897
|
+
return true;
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
async fetchQuota() {
|
|
903
|
+
debug("oauth", "Fetching quota via Cloud Code API");
|
|
904
|
+
try {
|
|
905
|
+
const summaryRaw = await this.client.fetchQuotaSummary();
|
|
906
|
+
const summary = this.parseRemoteSummary(summaryRaw);
|
|
907
|
+
if (summary && summary.groups.length > 0) {
|
|
908
|
+
const statusRaw2 = await this.client.fetchUserStatus();
|
|
909
|
+
const email = this.extractEmail(statusRaw2);
|
|
910
|
+
debug("oauth", "Got quota summary via OAuth");
|
|
911
|
+
return parseQuotaSummary(summary, email);
|
|
912
|
+
}
|
|
913
|
+
const statusRaw = await this.client.fetchUserStatus();
|
|
914
|
+
const status = this.parseRemoteStatus(statusRaw);
|
|
915
|
+
if (status) {
|
|
916
|
+
debug("oauth", "Got user status via OAuth");
|
|
917
|
+
return parseUserStatus(status);
|
|
918
|
+
}
|
|
919
|
+
} catch (err) {
|
|
920
|
+
debug("oauth", `Fetch failed: ${err}`);
|
|
921
|
+
}
|
|
922
|
+
throw new SourceUnavailableError("oauth", "Could not fetch quota via Cloud Code API");
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Parse remote summary response into QuotaSummaryResponse format
|
|
926
|
+
*/
|
|
927
|
+
parseRemoteSummary(raw) {
|
|
928
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
929
|
+
const data = raw;
|
|
930
|
+
const groups = data.groups;
|
|
931
|
+
if (!Array.isArray(groups)) return null;
|
|
932
|
+
return {
|
|
933
|
+
groups: groups.map((g) => ({
|
|
934
|
+
displayName: String(g.displayName || ""),
|
|
935
|
+
buckets: Array.isArray(g.buckets) ? g.buckets.map((b) => ({
|
|
936
|
+
bucketId: String(b.bucketId || ""),
|
|
937
|
+
displayName: String(b.displayName || ""),
|
|
938
|
+
description: String(b.description || ""),
|
|
939
|
+
remainingFraction: typeof b.remaining?.remainingFraction === "number" ? b.remaining.remainingFraction : typeof b.remainingFraction === "number" ? b.remainingFraction : 1,
|
|
940
|
+
resetTime: typeof b.resetTime === "string" ? b.resetTime : void 0
|
|
941
|
+
})) : []
|
|
942
|
+
})),
|
|
943
|
+
raw
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Parse remote user status response
|
|
948
|
+
*/
|
|
949
|
+
parseRemoteStatus(raw) {
|
|
950
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
951
|
+
return { raw };
|
|
952
|
+
}
|
|
953
|
+
extractEmail(raw) {
|
|
954
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
955
|
+
const data = raw;
|
|
956
|
+
const userStatus = data.userStatus;
|
|
957
|
+
return userStatus?.email || data.email;
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
// src/sources/oauth/client.ts
|
|
962
|
+
import https2 from "https";
|
|
963
|
+
import { URL as URL2 } from "url";
|
|
964
|
+
|
|
965
|
+
// src/sources/cloud-code-api.ts
|
|
966
|
+
var CLOUD_CODE_ENDPOINTS = {
|
|
967
|
+
/** Load code assist configuration */
|
|
968
|
+
loadCodeAssist: "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
|
969
|
+
/** Onboard a new user */
|
|
970
|
+
onboardUser: "https://cloudcode-pa.googleapis.com/v1internal:onboardUser",
|
|
971
|
+
/** Fetch available models for the account */
|
|
972
|
+
fetchAvailableModels: "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
|
|
973
|
+
/** Fetch raw quota data */
|
|
974
|
+
retrieveUserQuota: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
|
|
975
|
+
/** Fetch quota summary with weekly + session buckets */
|
|
976
|
+
retrieveUserQuotaSummary: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"
|
|
977
|
+
};
|
|
978
|
+
var CLOUD_CODE_DEFAULT_BODY = {
|
|
979
|
+
metadata: {
|
|
980
|
+
ideName: "antigravity",
|
|
981
|
+
extensionName: "antigravity",
|
|
982
|
+
locale: "en",
|
|
983
|
+
ideVersion: "unknown"
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var OAUTH_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com";
|
|
987
|
+
var OAUTH_CLIENT_SECRET = "d-FL95Q19q7MQmFpd7hHD0Ty";
|
|
988
|
+
var OAUTH_SCOPES = [
|
|
989
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
990
|
+
"https://www.googleapis.com/auth/cloudcode"
|
|
991
|
+
].join(" ");
|
|
992
|
+
|
|
993
|
+
// src/sources/oauth/client.ts
|
|
994
|
+
var OAuthClient = class {
|
|
995
|
+
tokens = null;
|
|
996
|
+
constructor(tokens) {
|
|
997
|
+
this.tokens = tokens || null;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Generate the OAuth authorization URL for user login
|
|
1001
|
+
*/
|
|
1002
|
+
getAuthUrl() {
|
|
1003
|
+
const params = new URLSearchParams({
|
|
1004
|
+
client_id: OAUTH_CLIENT_ID,
|
|
1005
|
+
redirect_uri: "http://127.0.0.1:0/callback",
|
|
1006
|
+
response_type: "code",
|
|
1007
|
+
scope: OAUTH_SCOPES,
|
|
1008
|
+
access_type: "offline",
|
|
1009
|
+
prompt: "consent"
|
|
1010
|
+
});
|
|
1011
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Exchange an authorization code for tokens
|
|
1015
|
+
*/
|
|
1016
|
+
async exchangeCode(code) {
|
|
1017
|
+
const body = new URLSearchParams({
|
|
1018
|
+
code,
|
|
1019
|
+
client_id: OAUTH_CLIENT_ID,
|
|
1020
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
1021
|
+
redirect_uri: "http://127.0.0.1:0/callback",
|
|
1022
|
+
grant_type: "authorization_code"
|
|
1023
|
+
});
|
|
1024
|
+
const response = await this.postUrlEncoded("https://oauth2.googleapis.com/token", body);
|
|
1025
|
+
const data = response;
|
|
1026
|
+
this.tokens = {
|
|
1027
|
+
accessToken: data.access_token,
|
|
1028
|
+
refreshToken: data.refresh_token || "",
|
|
1029
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
1030
|
+
};
|
|
1031
|
+
return this.tokens;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Refresh the access token
|
|
1035
|
+
*/
|
|
1036
|
+
async refreshAccessToken() {
|
|
1037
|
+
if (!this.tokens?.refreshToken) {
|
|
1038
|
+
throw new Error("No refresh token available");
|
|
1039
|
+
}
|
|
1040
|
+
const body = new URLSearchParams({
|
|
1041
|
+
client_id: OAUTH_CLIENT_ID,
|
|
1042
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
1043
|
+
refresh_token: this.tokens.refreshToken,
|
|
1044
|
+
grant_type: "refresh_token"
|
|
1045
|
+
});
|
|
1046
|
+
const response = await this.postUrlEncoded("https://oauth2.googleapis.com/token", body);
|
|
1047
|
+
const data = response;
|
|
1048
|
+
this.tokens = {
|
|
1049
|
+
...this.tokens,
|
|
1050
|
+
accessToken: data.access_token,
|
|
1051
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
1052
|
+
};
|
|
1053
|
+
return this.tokens;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get a valid access token, refreshing if necessary
|
|
1057
|
+
*/
|
|
1058
|
+
async getAccessToken() {
|
|
1059
|
+
if (!this.tokens) throw new Error("Not authenticated");
|
|
1060
|
+
if (Date.now() >= this.tokens.expiresAt - 6e4) {
|
|
1061
|
+
await this.refreshAccessToken();
|
|
1062
|
+
}
|
|
1063
|
+
return this.tokens.accessToken;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Get stored tokens
|
|
1067
|
+
*/
|
|
1068
|
+
getTokens() {
|
|
1069
|
+
return this.tokens;
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Make an authenticated POST request to the Cloud Code API
|
|
1073
|
+
*/
|
|
1074
|
+
async fetchQuotaSummary() {
|
|
1075
|
+
const token = await this.getAccessToken();
|
|
1076
|
+
return this.authenticatedPost(
|
|
1077
|
+
CLOUD_CODE_ENDPOINTS.retrieveUserQuotaSummary,
|
|
1078
|
+
CLOUD_CODE_DEFAULT_BODY,
|
|
1079
|
+
token
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
async fetchUserStatus() {
|
|
1083
|
+
const token = await this.getAccessToken();
|
|
1084
|
+
return this.authenticatedPost(
|
|
1085
|
+
CLOUD_CODE_ENDPOINTS.loadCodeAssist,
|
|
1086
|
+
CLOUD_CODE_DEFAULT_BODY,
|
|
1087
|
+
token
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
async fetchAvailableModels() {
|
|
1091
|
+
const token = await this.getAccessToken();
|
|
1092
|
+
return this.authenticatedPost(
|
|
1093
|
+
CLOUD_CODE_ENDPOINTS.fetchAvailableModels,
|
|
1094
|
+
CLOUD_CODE_DEFAULT_BODY,
|
|
1095
|
+
token
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
// ── HTTP Helpers ────────────────────────────────────────────────
|
|
1099
|
+
authenticatedPost(url, body, token) {
|
|
1100
|
+
return new Promise((resolve, reject) => {
|
|
1101
|
+
const parsed = new URL2(url);
|
|
1102
|
+
const data = JSON.stringify(body);
|
|
1103
|
+
const options = {
|
|
1104
|
+
hostname: parsed.hostname,
|
|
1105
|
+
path: parsed.pathname,
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
headers: {
|
|
1108
|
+
"Content-Type": "application/json",
|
|
1109
|
+
"Authorization": `Bearer ${token}`,
|
|
1110
|
+
"Content-Length": Buffer.byteLength(data)
|
|
1111
|
+
},
|
|
1112
|
+
timeout: 1e4
|
|
1113
|
+
};
|
|
1114
|
+
const req = https2.request(options, (res) => {
|
|
1115
|
+
let responseData = "";
|
|
1116
|
+
res.on("data", (chunk) => {
|
|
1117
|
+
responseData += chunk;
|
|
1118
|
+
});
|
|
1119
|
+
res.on("end", () => {
|
|
1120
|
+
try {
|
|
1121
|
+
resolve(JSON.parse(responseData));
|
|
1122
|
+
} catch {
|
|
1123
|
+
reject(new Error(`Invalid JSON: ${responseData.slice(0, 200)}`));
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
req.on("error", reject);
|
|
1128
|
+
req.on("timeout", () => {
|
|
1129
|
+
req.destroy();
|
|
1130
|
+
reject(new Error("Timeout"));
|
|
1131
|
+
});
|
|
1132
|
+
req.write(data);
|
|
1133
|
+
req.end();
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
postUrlEncoded(url, body) {
|
|
1137
|
+
return new Promise((resolve, reject) => {
|
|
1138
|
+
const parsed = new URL2(url);
|
|
1139
|
+
const data = body.toString();
|
|
1140
|
+
const options = {
|
|
1141
|
+
hostname: parsed.hostname,
|
|
1142
|
+
path: parsed.pathname,
|
|
1143
|
+
method: "POST",
|
|
1144
|
+
headers: {
|
|
1145
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1146
|
+
"Content-Length": Buffer.byteLength(data)
|
|
1147
|
+
},
|
|
1148
|
+
timeout: 1e4
|
|
1149
|
+
};
|
|
1150
|
+
const req = https2.request(options, (res) => {
|
|
1151
|
+
let responseData = "";
|
|
1152
|
+
res.on("data", (chunk) => {
|
|
1153
|
+
responseData += chunk;
|
|
1154
|
+
});
|
|
1155
|
+
res.on("end", () => {
|
|
1156
|
+
try {
|
|
1157
|
+
resolve(JSON.parse(responseData));
|
|
1158
|
+
} catch {
|
|
1159
|
+
reject(new Error(`Invalid JSON: ${responseData.slice(0, 200)}`));
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
1163
|
+
req.on("error", reject);
|
|
1164
|
+
req.on("timeout", () => {
|
|
1165
|
+
req.destroy();
|
|
1166
|
+
reject(new Error("Timeout"));
|
|
1167
|
+
});
|
|
1168
|
+
req.write(data);
|
|
1169
|
+
req.end();
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/sources/index.ts
|
|
1175
|
+
var SourceRegistry = class {
|
|
1176
|
+
sources = [];
|
|
1177
|
+
health = /* @__PURE__ */ new Map();
|
|
1178
|
+
constructor() {
|
|
1179
|
+
this.sources.push(new LocalSource());
|
|
1180
|
+
this.sources.push(new CliSource());
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Register an additional data source (e.g., OAuth)
|
|
1184
|
+
*/
|
|
1185
|
+
addSource(source) {
|
|
1186
|
+
const idx = this.sources.findIndex((s) => s.priority > source.priority);
|
|
1187
|
+
if (idx === -1) {
|
|
1188
|
+
this.sources.push(source);
|
|
1189
|
+
} else {
|
|
1190
|
+
this.sources.splice(idx, 0, source);
|
|
1191
|
+
}
|
|
1192
|
+
debug("registry", `Registered source: ${source.name} (priority ${source.priority})`);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Remove a source by name
|
|
1196
|
+
*/
|
|
1197
|
+
removeSource(name) {
|
|
1198
|
+
this.sources = this.sources.filter((s) => s.name !== name);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Fetch quota using the best available source
|
|
1202
|
+
*
|
|
1203
|
+
* @param preferredSource Force a specific source
|
|
1204
|
+
*/
|
|
1205
|
+
async fetchQuota(preferredSource) {
|
|
1206
|
+
if (preferredSource) {
|
|
1207
|
+
const source = this.sources.find((s) => s.name === preferredSource);
|
|
1208
|
+
if (!source) throw new Error(`Source "${preferredSource}" not registered`);
|
|
1209
|
+
const snapshot = await source.fetchQuota();
|
|
1210
|
+
this.health.set(preferredSource, { available: true, lastCheck: Date.now() });
|
|
1211
|
+
return snapshot;
|
|
1212
|
+
}
|
|
1213
|
+
const errors = [];
|
|
1214
|
+
for (const source of this.sources) {
|
|
1215
|
+
debug("registry", `Trying source: ${source.name}`);
|
|
1216
|
+
try {
|
|
1217
|
+
const available = await source.isAvailable();
|
|
1218
|
+
if (!available) {
|
|
1219
|
+
debug("registry", `Source ${source.name} not available`);
|
|
1220
|
+
this.health.set(source.name, { available: false, lastCheck: Date.now() });
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
const snapshot = await source.fetchQuota();
|
|
1224
|
+
this.health.set(source.name, { available: true, lastCheck: Date.now() });
|
|
1225
|
+
debug("registry", `Success with source: ${source.name}`);
|
|
1226
|
+
return snapshot;
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1229
|
+
errors.push(`${source.name}: ${msg}`);
|
|
1230
|
+
this.health.set(source.name, { available: false, lastCheck: Date.now() });
|
|
1231
|
+
debug("registry", `Source ${source.name} failed: ${msg}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
throw new SourceUnavailableError("all", `No sources available:
|
|
1235
|
+
${errors.map((e) => ` \u2022 ${e}`).join("\n")}`);
|
|
1236
|
+
}
|
|
1237
|
+
/**
|
|
1238
|
+
* Get health status of all sources
|
|
1239
|
+
*/
|
|
1240
|
+
getHealth() {
|
|
1241
|
+
return new Map(this.health);
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Get registered source names
|
|
1245
|
+
*/
|
|
1246
|
+
getSourceNames() {
|
|
1247
|
+
return this.sources.map((s) => s.name);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
// src/sdk/cache.ts
|
|
1252
|
+
var QuotaCache = class {
|
|
1253
|
+
cache = /* @__PURE__ */ new Map();
|
|
1254
|
+
ttlMs;
|
|
1255
|
+
constructor(ttlMs = 5 * 60 * 1e3) {
|
|
1256
|
+
this.ttlMs = ttlMs;
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Get cached quota if available and not expired
|
|
1260
|
+
*/
|
|
1261
|
+
get(email, source) {
|
|
1262
|
+
const key = `${email}:${source}`;
|
|
1263
|
+
const entry = this.cache.get(key);
|
|
1264
|
+
if (!entry) return null;
|
|
1265
|
+
const age = Date.now() - entry.cachedAt;
|
|
1266
|
+
if (age > this.ttlMs) {
|
|
1267
|
+
this.cache.delete(key);
|
|
1268
|
+
debug("cache", `Expired entry for ${email} (age: ${Math.round(age / 1e3)}s)`);
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
debug("cache", `Cache hit for ${email} (age: ${Math.round(age / 1e3)}s)`);
|
|
1272
|
+
return entry.snapshot;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Store a quota snapshot in cache
|
|
1276
|
+
*/
|
|
1277
|
+
set(email, source, snapshot) {
|
|
1278
|
+
const key = `${email}:${source}`;
|
|
1279
|
+
this.cache.set(key, {
|
|
1280
|
+
snapshot,
|
|
1281
|
+
cachedAt: Date.now()
|
|
1282
|
+
});
|
|
1283
|
+
debug("cache", `Cached snapshot for ${email}`);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Check if a cached entry is still fresh
|
|
1287
|
+
*/
|
|
1288
|
+
isFresh(email, source) {
|
|
1289
|
+
const key = `${email}:${source}`;
|
|
1290
|
+
const entry = this.cache.get(key);
|
|
1291
|
+
if (!entry) return false;
|
|
1292
|
+
return Date.now() - entry.cachedAt <= this.ttlMs;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Clear all cached entries
|
|
1296
|
+
*/
|
|
1297
|
+
clear() {
|
|
1298
|
+
this.cache.clear();
|
|
1299
|
+
debug("cache", "Cleared all cache entries");
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Get cache age in seconds for a key
|
|
1303
|
+
*/
|
|
1304
|
+
getAgeMs(email, source) {
|
|
1305
|
+
const key = `${email}:${source}`;
|
|
1306
|
+
const entry = this.cache.get(key);
|
|
1307
|
+
if (!entry) return null;
|
|
1308
|
+
return Date.now() - entry.cachedAt;
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// src/sdk/store.ts
|
|
1313
|
+
import Database from "better-sqlite3";
|
|
1314
|
+
import { join as join2 } from "path";
|
|
1315
|
+
|
|
1316
|
+
// src/core/config.ts
|
|
1317
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1318
|
+
import { join } from "path";
|
|
1319
|
+
import { homedir } from "os";
|
|
1320
|
+
var CONFIG_DIR_NAME = "when-does-my-quota-refresh";
|
|
1321
|
+
var DEFAULT_CONFIG = {
|
|
1322
|
+
defaultMode: "local",
|
|
1323
|
+
cacheTtlMs: 5 * 60 * 1e3,
|
|
1324
|
+
// 5 minutes
|
|
1325
|
+
refreshIntervalMs: 5 * 60 * 1e3,
|
|
1326
|
+
wakeupModels: ["claude-sonnet-4-5", "gemini-3-flash", "gemini-3-pro-low"],
|
|
1327
|
+
wakeupAccounts: [],
|
|
1328
|
+
notifications: true,
|
|
1329
|
+
plugins: [],
|
|
1330
|
+
daemon: {
|
|
1331
|
+
pollIntervalMs: 60 * 1e3,
|
|
1332
|
+
enabled: false
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
function getConfigDir() {
|
|
1336
|
+
const home = homedir();
|
|
1337
|
+
switch (process.platform) {
|
|
1338
|
+
case "darwin":
|
|
1339
|
+
return join(home, "Library", "Application Support", CONFIG_DIR_NAME);
|
|
1340
|
+
case "win32":
|
|
1341
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), CONFIG_DIR_NAME);
|
|
1342
|
+
default:
|
|
1343
|
+
return join(home, ".config", CONFIG_DIR_NAME);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function getConfigPath() {
|
|
1347
|
+
return join(getConfigDir(), "config.json");
|
|
1348
|
+
}
|
|
1349
|
+
function getDataDir() {
|
|
1350
|
+
return join(getConfigDir(), "data");
|
|
1351
|
+
}
|
|
1352
|
+
function ensureDirs() {
|
|
1353
|
+
const configDir = getConfigDir();
|
|
1354
|
+
const dataDir = getDataDir();
|
|
1355
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
|
|
1356
|
+
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
|
|
1357
|
+
}
|
|
1358
|
+
function loadConfig() {
|
|
1359
|
+
try {
|
|
1360
|
+
const path = getConfigPath();
|
|
1361
|
+
if (!existsSync(path)) return DEFAULT_CONFIG;
|
|
1362
|
+
const raw = readFileSync(path, "utf-8");
|
|
1363
|
+
const parsed = JSON.parse(raw);
|
|
1364
|
+
debug("config", `Loaded config from ${path}`);
|
|
1365
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
debug("config", "Failed to load config, using defaults", err);
|
|
1368
|
+
return DEFAULT_CONFIG;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function saveConfig(config) {
|
|
1372
|
+
ensureDirs();
|
|
1373
|
+
const path = getConfigPath();
|
|
1374
|
+
writeFileSync(path, JSON.stringify(config, null, 2));
|
|
1375
|
+
debug("config", `Saved config to ${path}`);
|
|
1376
|
+
}
|
|
1377
|
+
function getConfigDirPath() {
|
|
1378
|
+
return getConfigDir();
|
|
1379
|
+
}
|
|
1380
|
+
function getDataDirPath() {
|
|
1381
|
+
return getDataDir();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/sdk/store.ts
|
|
1385
|
+
var db = null;
|
|
1386
|
+
function getDb() {
|
|
1387
|
+
if (db) return db;
|
|
1388
|
+
ensureDirs();
|
|
1389
|
+
const dbPath = join2(getDataDirPath(), "history.db");
|
|
1390
|
+
db = new Database(dbPath);
|
|
1391
|
+
db.pragma("journal_mode = WAL");
|
|
1392
|
+
initSchema(db);
|
|
1393
|
+
debug("store", `Opened database at ${dbPath}`);
|
|
1394
|
+
return db;
|
|
1395
|
+
}
|
|
1396
|
+
function initSchema(database) {
|
|
1397
|
+
database.exec(`
|
|
1398
|
+
CREATE TABLE IF NOT EXISTS quota_snapshots (
|
|
1399
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1400
|
+
timestamp TEXT NOT NULL,
|
|
1401
|
+
email TEXT NOT NULL,
|
|
1402
|
+
method TEXT NOT NULL,
|
|
1403
|
+
gemini_weekly_remaining REAL,
|
|
1404
|
+
gemini_session_remaining REAL,
|
|
1405
|
+
claude_weekly_remaining REAL,
|
|
1406
|
+
claude_session_remaining REAL,
|
|
1407
|
+
raw_json TEXT
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_email_ts
|
|
1411
|
+
ON quota_snapshots(email, timestamp);
|
|
1412
|
+
`);
|
|
1413
|
+
}
|
|
1414
|
+
function storeSnapshot(snapshot) {
|
|
1415
|
+
const database = getDb();
|
|
1416
|
+
let geminiWeekly = null;
|
|
1417
|
+
let geminiSession = null;
|
|
1418
|
+
let claudeWeekly = null;
|
|
1419
|
+
let claudeSession = null;
|
|
1420
|
+
for (const group of snapshot.groups) {
|
|
1421
|
+
const lower = group.displayName.toLowerCase();
|
|
1422
|
+
for (const bucket of group.buckets) {
|
|
1423
|
+
const bucketLower = bucket.displayName.toLowerCase();
|
|
1424
|
+
if (lower.includes("gemini")) {
|
|
1425
|
+
if (bucketLower.includes("weekly") || bucketLower.includes("week")) {
|
|
1426
|
+
geminiWeekly = bucket.remainingFraction;
|
|
1427
|
+
} else {
|
|
1428
|
+
geminiSession = bucket.remainingFraction;
|
|
1429
|
+
}
|
|
1430
|
+
} else if (lower.includes("claude") || lower.includes("gpt")) {
|
|
1431
|
+
if (bucketLower.includes("weekly") || bucketLower.includes("week")) {
|
|
1432
|
+
claudeWeekly = bucket.remainingFraction;
|
|
1433
|
+
} else {
|
|
1434
|
+
claudeSession = bucket.remainingFraction;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
const stmt = database.prepare(`
|
|
1440
|
+
INSERT INTO quota_snapshots
|
|
1441
|
+
(timestamp, email, method, gemini_weekly_remaining, gemini_session_remaining,
|
|
1442
|
+
claude_weekly_remaining, claude_session_remaining, raw_json)
|
|
1443
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1444
|
+
`);
|
|
1445
|
+
stmt.run(
|
|
1446
|
+
snapshot.timestamp,
|
|
1447
|
+
snapshot.email || "unknown",
|
|
1448
|
+
snapshot.method,
|
|
1449
|
+
geminiWeekly,
|
|
1450
|
+
geminiSession,
|
|
1451
|
+
claudeWeekly,
|
|
1452
|
+
claudeSession,
|
|
1453
|
+
JSON.stringify(snapshot)
|
|
1454
|
+
);
|
|
1455
|
+
debug("store", `Stored snapshot for ${snapshot.email}`);
|
|
1456
|
+
}
|
|
1457
|
+
function getHistory(email, options) {
|
|
1458
|
+
const database = getDb();
|
|
1459
|
+
const days = options?.days || 7;
|
|
1460
|
+
const limit = options?.limit || 1e3;
|
|
1461
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1462
|
+
const stmt = database.prepare(`
|
|
1463
|
+
SELECT timestamp, email, gemini_weekly_remaining, gemini_session_remaining,
|
|
1464
|
+
claude_weekly_remaining, claude_session_remaining
|
|
1465
|
+
FROM quota_snapshots
|
|
1466
|
+
WHERE email = ? AND timestamp >= ?
|
|
1467
|
+
ORDER BY timestamp DESC
|
|
1468
|
+
LIMIT ?
|
|
1469
|
+
`);
|
|
1470
|
+
const rows = stmt.all(email, since, limit);
|
|
1471
|
+
return rows.map((row) => ({
|
|
1472
|
+
timestamp: String(row.timestamp),
|
|
1473
|
+
email: String(row.email),
|
|
1474
|
+
geminiWeeklyRemaining: row.gemini_weekly_remaining,
|
|
1475
|
+
geminiSessionRemaining: row.gemini_session_remaining,
|
|
1476
|
+
claudeWeeklyRemaining: row.claude_weekly_remaining,
|
|
1477
|
+
claudeSessionRemaining: row.claude_session_remaining
|
|
1478
|
+
}));
|
|
1479
|
+
}
|
|
1480
|
+
function getLatestSnapshot(email) {
|
|
1481
|
+
const history = getHistory(email, { limit: 1 });
|
|
1482
|
+
return history[0] || null;
|
|
1483
|
+
}
|
|
1484
|
+
function getDailySummary(email, days = 7) {
|
|
1485
|
+
const database = getDb();
|
|
1486
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1e3).toISOString();
|
|
1487
|
+
const stmt = database.prepare(`
|
|
1488
|
+
SELECT
|
|
1489
|
+
date(timestamp) as date,
|
|
1490
|
+
AVG(gemini_weekly_remaining) as gemini_weekly_avg,
|
|
1491
|
+
AVG(gemini_session_remaining) as gemini_session_avg,
|
|
1492
|
+
AVG(claude_weekly_remaining) as claude_weekly_avg,
|
|
1493
|
+
AVG(claude_session_remaining) as claude_session_avg,
|
|
1494
|
+
COUNT(*) as count
|
|
1495
|
+
FROM quota_snapshots
|
|
1496
|
+
WHERE email = ? AND timestamp >= ?
|
|
1497
|
+
GROUP BY date(timestamp)
|
|
1498
|
+
ORDER BY date(timestamp) ASC
|
|
1499
|
+
`);
|
|
1500
|
+
const rows = stmt.all(email, since);
|
|
1501
|
+
return rows.map((row) => ({
|
|
1502
|
+
date: String(row.date),
|
|
1503
|
+
geminiWeeklyAvg: row.gemini_weekly_avg,
|
|
1504
|
+
geminiSessionAvg: row.gemini_session_avg,
|
|
1505
|
+
claudeWeeklyAvg: row.claude_weekly_avg,
|
|
1506
|
+
claudeSessionAvg: row.claude_session_avg,
|
|
1507
|
+
count: Number(row.count)
|
|
1508
|
+
}));
|
|
1509
|
+
}
|
|
1510
|
+
function cleanupHistory(keepDays = 90) {
|
|
1511
|
+
const database = getDb();
|
|
1512
|
+
const cutoff = new Date(Date.now() - keepDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
1513
|
+
const result = database.prepare(
|
|
1514
|
+
"DELETE FROM quota_snapshots WHERE timestamp < ?"
|
|
1515
|
+
).run(cutoff);
|
|
1516
|
+
return result.changes;
|
|
1517
|
+
}
|
|
1518
|
+
function closeStore() {
|
|
1519
|
+
if (db) {
|
|
1520
|
+
db.close();
|
|
1521
|
+
db = null;
|
|
1522
|
+
debug("store", "Database closed");
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/sdk/accounts/storage.ts
|
|
1527
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
1528
|
+
import { join as join3 } from "path";
|
|
1529
|
+
var TOKENS_DIR = "accounts";
|
|
1530
|
+
function getStorePath() {
|
|
1531
|
+
return join3(getConfigDirPath(), TOKENS_DIR, "tokens.json");
|
|
1532
|
+
}
|
|
1533
|
+
function loadStore() {
|
|
1534
|
+
const path = getStorePath();
|
|
1535
|
+
if (!existsSync2(path)) return { accounts: [] };
|
|
1536
|
+
try {
|
|
1537
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
1538
|
+
} catch {
|
|
1539
|
+
return { accounts: [] };
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function saveStore(store) {
|
|
1543
|
+
ensureDirs();
|
|
1544
|
+
writeFileSync2(getStorePath(), JSON.stringify(store, null, 2));
|
|
1545
|
+
debug("account-storage", `Saved ${store.accounts.length} account(s)`);
|
|
1546
|
+
}
|
|
1547
|
+
function saveAccountTokens(email, tokens) {
|
|
1548
|
+
const store = loadStore();
|
|
1549
|
+
const existing = store.accounts.findIndex((a) => a.email === email);
|
|
1550
|
+
const account = {
|
|
1551
|
+
email,
|
|
1552
|
+
tokens,
|
|
1553
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1554
|
+
};
|
|
1555
|
+
if (existing >= 0) {
|
|
1556
|
+
store.accounts[existing] = account;
|
|
1557
|
+
} else {
|
|
1558
|
+
store.accounts.push(account);
|
|
1559
|
+
}
|
|
1560
|
+
if (store.accounts.length === 1) {
|
|
1561
|
+
store.activeEmail = email;
|
|
1562
|
+
}
|
|
1563
|
+
saveStore(store);
|
|
1564
|
+
}
|
|
1565
|
+
function getAccountTokens(email) {
|
|
1566
|
+
const store = loadStore();
|
|
1567
|
+
const account = store.accounts.find((a) => a.email === email);
|
|
1568
|
+
return account?.tokens || null;
|
|
1569
|
+
}
|
|
1570
|
+
function getActiveAccountTokens() {
|
|
1571
|
+
const store = loadStore();
|
|
1572
|
+
if (!store.activeEmail) return null;
|
|
1573
|
+
const account = store.accounts.find((a) => a.email === store.activeEmail);
|
|
1574
|
+
return account?.tokens || null;
|
|
1575
|
+
}
|
|
1576
|
+
function setActiveAccount(email) {
|
|
1577
|
+
const store = loadStore();
|
|
1578
|
+
if (!store.accounts.find((a) => a.email === email)) return false;
|
|
1579
|
+
store.activeEmail = email;
|
|
1580
|
+
saveStore(store);
|
|
1581
|
+
return true;
|
|
1582
|
+
}
|
|
1583
|
+
function removeAccount(email) {
|
|
1584
|
+
const store = loadStore();
|
|
1585
|
+
const idx = store.accounts.findIndex((a) => a.email === email);
|
|
1586
|
+
if (idx < 0) return false;
|
|
1587
|
+
store.accounts.splice(idx, 1);
|
|
1588
|
+
if (store.activeEmail === email) {
|
|
1589
|
+
store.activeEmail = store.accounts[0]?.email;
|
|
1590
|
+
}
|
|
1591
|
+
saveStore(store);
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
function listAccounts() {
|
|
1595
|
+
const store = loadStore();
|
|
1596
|
+
return store.accounts.map((a) => ({
|
|
1597
|
+
email: a.email,
|
|
1598
|
+
isActive: a.email === store.activeEmail,
|
|
1599
|
+
addedAt: a.addedAt
|
|
1600
|
+
}));
|
|
1601
|
+
}
|
|
1602
|
+
function getAllAccountTokens() {
|
|
1603
|
+
const store = loadStore();
|
|
1604
|
+
return store.accounts.map((a) => a.tokens);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/sdk/accounts/auth.ts
|
|
1608
|
+
import open from "open";
|
|
1609
|
+
import http2 from "http";
|
|
1610
|
+
import { URL as URL3 } from "url";
|
|
1611
|
+
async function loginAccount() {
|
|
1612
|
+
const client = new OAuthClient();
|
|
1613
|
+
const authUrl = client.getAuthUrl();
|
|
1614
|
+
debug("auth", "Starting OAuth login flow");
|
|
1615
|
+
const code = await new Promise((resolve, reject) => {
|
|
1616
|
+
const server = http2.createServer(async (req, res) => {
|
|
1617
|
+
const url = new URL3(req.url || "/", `http://127.0.0.1`);
|
|
1618
|
+
const code2 = url.searchParams.get("code");
|
|
1619
|
+
if (code2) {
|
|
1620
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1621
|
+
res.end(`
|
|
1622
|
+
<html><body style="font-family: sans-serif; text-align: center; padding: 50px;">
|
|
1623
|
+
<h1>\u2705 Login successful!</h1>
|
|
1624
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
1625
|
+
</body></html>
|
|
1626
|
+
`);
|
|
1627
|
+
server.close();
|
|
1628
|
+
resolve(code2);
|
|
1629
|
+
} else {
|
|
1630
|
+
const error = url.searchParams.get("error");
|
|
1631
|
+
if (error) {
|
|
1632
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1633
|
+
res.end(`<h1>Login failed: ${error}</h1>`);
|
|
1634
|
+
server.close();
|
|
1635
|
+
reject(new AuthError(`OAuth error: ${error}`));
|
|
1636
|
+
} else {
|
|
1637
|
+
res.writeHead(404);
|
|
1638
|
+
res.end("Waiting for OAuth callback...");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1643
|
+
const addr = server.address();
|
|
1644
|
+
if (typeof addr === "object" && addr) {
|
|
1645
|
+
debug("auth", `Callback server on port ${addr.port}`);
|
|
1646
|
+
open(authUrl).catch(() => {
|
|
1647
|
+
console.log(`
|
|
1648
|
+
Open this URL in your browser:
|
|
1649
|
+
|
|
1650
|
+
${authUrl}
|
|
1651
|
+
`);
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
setTimeout(() => {
|
|
1656
|
+
server.close();
|
|
1657
|
+
reject(new AuthError("Login timed out after 5 minutes"));
|
|
1658
|
+
}, 5 * 60 * 1e3);
|
|
1659
|
+
});
|
|
1660
|
+
const tokens = await client.exchangeCode(code);
|
|
1661
|
+
const email = tokens.email || "unknown@gmail.com";
|
|
1662
|
+
tokens.email = email;
|
|
1663
|
+
saveAccountTokens(email, tokens);
|
|
1664
|
+
setActiveAccount(email);
|
|
1665
|
+
debug("auth", `Logged in as ${email}`);
|
|
1666
|
+
return email;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// src/sdk/quota.ts
|
|
1670
|
+
var QuotaClient = class {
|
|
1671
|
+
registry;
|
|
1672
|
+
cache;
|
|
1673
|
+
verbose;
|
|
1674
|
+
constructor(options) {
|
|
1675
|
+
this.verbose = options?.verbose || false;
|
|
1676
|
+
setVerbose(this.verbose);
|
|
1677
|
+
const config = loadConfig();
|
|
1678
|
+
this.registry = new SourceRegistry();
|
|
1679
|
+
this.cache = new QuotaCache(options?.cacheTtlMs || config.cacheTtlMs);
|
|
1680
|
+
this.registerOAuthSources();
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Fetch quota for the active account (or all accounts)
|
|
1684
|
+
*/
|
|
1685
|
+
async fetchQuota(options) {
|
|
1686
|
+
if (options?.allAccounts) {
|
|
1687
|
+
return this.fetchAllAccounts(options);
|
|
1688
|
+
}
|
|
1689
|
+
if (!options?.refresh) {
|
|
1690
|
+
}
|
|
1691
|
+
const snapshot = await this.registry.fetchQuota(options?.source);
|
|
1692
|
+
if (snapshot.email) {
|
|
1693
|
+
this.cache.set(snapshot.email, snapshot.method, snapshot);
|
|
1694
|
+
storeSnapshot(snapshot);
|
|
1695
|
+
}
|
|
1696
|
+
return snapshot;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Fetch quota for all stored accounts
|
|
1700
|
+
*/
|
|
1701
|
+
async fetchAllAccounts(options) {
|
|
1702
|
+
const tokens = getAllAccountTokens();
|
|
1703
|
+
const snapshots = [];
|
|
1704
|
+
try {
|
|
1705
|
+
const snapshot = await this.registry.fetchQuota(options?.source);
|
|
1706
|
+
snapshots.push(snapshot);
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
debug("quota", `Active account fetch failed: ${err}`);
|
|
1709
|
+
}
|
|
1710
|
+
for (const token of tokens) {
|
|
1711
|
+
if (!token.email) continue;
|
|
1712
|
+
if (snapshots.some((s) => s.email === token.email)) continue;
|
|
1713
|
+
try {
|
|
1714
|
+
const client = new OAuthClient(token);
|
|
1715
|
+
const source = new OAuthSource(client);
|
|
1716
|
+
const snapshot = await source.fetchQuota();
|
|
1717
|
+
snapshots.push(snapshot);
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
debug("quota", `Failed to fetch for ${token.email}: ${err}`);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return snapshots;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Get the full dashboard data for all accounts
|
|
1726
|
+
*/
|
|
1727
|
+
async getFullDashboard() {
|
|
1728
|
+
const snapshots = await this.fetchAllAccounts();
|
|
1729
|
+
const history = {};
|
|
1730
|
+
const dailySummary = {};
|
|
1731
|
+
for (const snapshot of snapshots) {
|
|
1732
|
+
if (!snapshot.email) continue;
|
|
1733
|
+
history[snapshot.email] = getHistory(snapshot.email, { days: 1, limit: 50 });
|
|
1734
|
+
dailySummary[snapshot.email] = getDailySummary(snapshot.email, 7);
|
|
1735
|
+
}
|
|
1736
|
+
return { snapshots, history, dailySummary };
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Get usage history for an account
|
|
1740
|
+
*/
|
|
1741
|
+
getHistory(email, days) {
|
|
1742
|
+
return getHistory(email, { days });
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Get daily summary for an account
|
|
1746
|
+
*/
|
|
1747
|
+
getDailySummary(email, days) {
|
|
1748
|
+
return getDailySummary(email, days);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Register OAuth sources for all stored accounts
|
|
1752
|
+
*/
|
|
1753
|
+
registerOAuthSources() {
|
|
1754
|
+
const tokens = getAllAccountTokens();
|
|
1755
|
+
for (const token of tokens) {
|
|
1756
|
+
if (!token.accessToken) continue;
|
|
1757
|
+
try {
|
|
1758
|
+
const client = new OAuthClient(token);
|
|
1759
|
+
const source = new OAuthSource(client);
|
|
1760
|
+
this.registry.addSource(source);
|
|
1761
|
+
debug("quota", `Registered OAuth source for ${token.email}`);
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
debug("quota", `Failed to register OAuth for ${token.email}: ${err}`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Get the source registry (for doctor/status checks)
|
|
1769
|
+
*/
|
|
1770
|
+
getRegistry() {
|
|
1771
|
+
return this.registry;
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
// src/sdk/index.ts
|
|
1776
|
+
function createClient(options) {
|
|
1777
|
+
return new QuotaClient(options);
|
|
1778
|
+
}
|
|
1779
|
+
export {
|
|
1780
|
+
AuthError,
|
|
1781
|
+
QuotaCache,
|
|
1782
|
+
QuotaClient,
|
|
1783
|
+
QuotaFetchError,
|
|
1784
|
+
SourceRegistry,
|
|
1785
|
+
SourceUnavailableError,
|
|
1786
|
+
cleanupHistory,
|
|
1787
|
+
closeStore,
|
|
1788
|
+
createClient,
|
|
1789
|
+
getAccountTokens,
|
|
1790
|
+
getActiveAccountTokens,
|
|
1791
|
+
getConfigDirPath,
|
|
1792
|
+
getDailySummary,
|
|
1793
|
+
getDataDirPath,
|
|
1794
|
+
getHistory,
|
|
1795
|
+
getLatestSnapshot,
|
|
1796
|
+
listAccounts,
|
|
1797
|
+
loadConfig,
|
|
1798
|
+
loginAccount,
|
|
1799
|
+
removeAccount,
|
|
1800
|
+
saveConfig,
|
|
1801
|
+
setActiveAccount,
|
|
1802
|
+
storeSnapshot
|
|
1803
|
+
};
|
|
1804
|
+
//# sourceMappingURL=index.js.map
|