letmecode 0.1.12 → 0.1.15

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.
@@ -1,7 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import https from "node:https";
4
- import { createRequire } from "node:module";
5
4
  import fs from "node:fs";
6
5
  import os from "node:os";
7
6
  import path from "node:path";
@@ -10,7 +9,6 @@ import { promisify } from "node:util";
10
9
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
11
10
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
12
11
  import { resolveUsageRate } from "./pricing.js";
13
- const require = createRequire(import.meta.url);
14
12
  const execFileAsync = promisify(execFile);
15
13
  const RATE_CARD = {
16
14
  "gemini-3.5-flash": {
@@ -68,21 +66,36 @@ const RATE_CARD = {
68
66
  const UNPRICED_MODELS = new Set([
69
67
  "gpt-oss-120b"
70
68
  ]);
71
- const ANTIGRAVITY_PRIMARY_WINDOW_MINUTES = 5 * 60;
72
- const ANTIGRAVITY_WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
73
69
  const ANTIGRAVITY_QUOTA_SUMMARY_PATH = "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary";
74
70
  const ANTIGRAVITY_USER_STATUS_PATH = "/exa.language_server_pb.LanguageServerService/GetUserStatus";
75
- const ANTIGRAVITY_DEBUG_LOG_PATH = process.env.LETMECODE_ANTIGRAVITY_DEBUG_LOG ??
76
- path.join(os.tmpdir(), "letmecode-antigravity-debug.jsonl");
77
- const GEMINI_QUOTA_MODELS = [
78
- "gemini-3.5-flash",
79
- "gemini-3.1-pro",
80
- "gemini-3-flash"
81
- ];
82
- const THIRD_PARTY_QUOTA_MODELS = [
83
- "claude-opus-4-6",
84
- "claude-sonnet-4-6",
85
- "gpt-oss-120b"
71
+ const ANTIGRAVITY_CACHE_ROOT = path.join(os.homedir(), ".config", "tokscale", "antigravity-cache");
72
+ const QUOTA_WINDOWS = {
73
+ "5h": {
74
+ scope: "primary",
75
+ windowMinutes: 300
76
+ },
77
+ weekly: {
78
+ scope: "secondary",
79
+ windowMinutes: 10080
80
+ }
81
+ };
82
+ const QUOTA_MODEL_GROUPS = [
83
+ {
84
+ pattern: /gemini/,
85
+ models: [
86
+ "gemini-3.5-flash",
87
+ "gemini-3.1-pro",
88
+ "gemini-3-flash"
89
+ ]
90
+ },
91
+ {
92
+ pattern: /claude|gpt/,
93
+ models: [
94
+ "claude-opus-4-6",
95
+ "claude-sonnet-4-6",
96
+ "gpt-oss-120b"
97
+ ]
98
+ }
86
99
  ];
87
100
  const MODEL_ALIASES = {
88
101
  "gemini-3-flash-a": "gemini-3-flash",
@@ -96,8 +109,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
96
109
  constructor(options = {}) {
97
110
  super("antigravity", "Antigravity");
98
111
  this.collectUsage =
99
- options.collectUsage ??
100
- collectAntigravityUsageFromTokscale;
112
+ options.collectUsage ?? readAntigravityUsageCache;
101
113
  this.collectQuota =
102
114
  options.collectQuota ??
103
115
  collectAntigravityQuotaFromLocalRpc;
@@ -115,7 +127,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
115
127
  ? quotaResult.value
116
128
  : null;
117
129
  if (usageResult.status === "rejected") {
118
- warnings.push("Could not synchronize Antigravity token usage through Tokscale.");
130
+ warnings.push("Could not read Antigravity token usage cache.");
119
131
  }
120
132
  if (quotaResult.status === "rejected") {
121
133
  warnings.push("Live Antigravity quota is unavailable. Ensure the Antigravity IDE is running.");
@@ -123,9 +135,6 @@ export class AntigravityUsageProvider extends UsageProviderBase {
123
135
  else if (quotaResult.value.entries.length === 0) {
124
136
  warnings.push("Antigravity local quota RPC responded, but no recognized model quota windows were found.");
125
137
  }
126
- if (isAntigravityDebugEnabled()) {
127
- warnings.push(`Antigravity debug log: ${ANTIGRAVITY_DEBUG_LOG_PATH}`);
128
- }
129
138
  const selectedRecords = deduplicateRecords(records);
130
139
  const duplicateEvents = records.length - selectedRecords.length;
131
140
  if (duplicateEvents > 0) {
@@ -152,7 +161,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
152
161
  if (unknownPricedModels.length > 0) {
153
162
  warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
154
163
  }
155
- const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
164
+ const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, quotaSnapshot.planType, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
156
165
  return {
157
166
  providerId: this.id,
158
167
  providerLabel: this.label,
@@ -166,7 +175,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
166
175
  ...new Set(limitWindows.map((window) => window.planType))
167
176
  ],
168
177
  rootLabel: "Tokscale usage + Antigravity local quota",
169
- rootPath: getAntigravityCacheRoot()
178
+ rootPath: ANTIGRAVITY_CACHE_ROOT
170
179
  },
171
180
  modelUsage,
172
181
  dayUsage: buildDailyUsageRows(byDay),
@@ -175,121 +184,47 @@ export class AntigravityUsageProvider extends UsageProviderBase {
175
184
  warnings,
176
185
  analytics: quotaSnapshot?.userIdHash
177
186
  ? {
178
- agentName: normalizeAnalyticsAgentName(this.label),
187
+ agentName: this.label.replace(/\s/g, ""),
179
188
  userIdHash: quotaSnapshot.userIdHash
180
189
  }
181
190
  : undefined
182
191
  };
183
192
  }
184
193
  }
185
- export async function collectAntigravityUsage() {
186
- return collectAntigravityUsageFromTokscale();
187
- }
188
- export async function collectAntigravityQuota() {
189
- return collectAntigravityQuotaFromLocalRpc();
190
- }
191
- async function collectAntigravityUsageFromTokscale() {
192
- await runTokscale([
193
- "antigravity",
194
- "sync"
195
- ]);
196
- return readAntigravityUsageCache(getAntigravityCacheRoot());
197
- }
198
- async function runTokscale(args) {
199
- return execFileAsync(process.execPath, [require.resolve("@tokscale/cli/dist/index.js"), ...args], {
200
- encoding: "utf8",
201
- maxBuffer: 32 * 1024 * 1024
202
- });
203
- }
204
194
  async function collectAntigravityQuotaFromLocalRpc() {
205
195
  const server = await findAntigravityLocalServer();
206
196
  if (!server) {
207
197
  throw new Error("Antigravity local language server was not found.");
208
198
  }
209
- const fetchedAt = Date.now();
210
- const [quotaResult, statusResult] = await Promise.allSettled([
211
- readAntigravityQuotaSummary(server),
212
- readAntigravityUserStatus(server)
213
- ]);
214
- printAntigravityUserStatusResponse(statusResult);
215
- if (quotaResult.status === "rejected") {
216
- throw quotaResult.reason;
217
- }
218
- const entries = parseAntigravityQuotaEntries(quotaResult.value);
219
- const planType = statusResult.status === "fulfilled"
220
- ? parseAntigravityPlanType(statusResult.value)
221
- : "unknown";
222
- const userIdHash = statusResult.status === "fulfilled"
223
- ? parseAntigravityUserIdHash(statusResult.value, normalizeAnalyticsAgentName("Antigravity"))
224
- : null;
225
- for (const entry of entries) {
226
- entry.planType = planType;
227
- }
228
- await writeAntigravityDebugEvent("quota-rpc-response", {
229
- port: server.port,
230
- quotaPath: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
231
- userStatusPath: ANTIGRAVITY_USER_STATUS_PATH,
232
- planType,
233
- entries: entries.map((entry) => ({
234
- limitId: entry.limitId,
235
- remainingFraction: entry.remainingFraction,
236
- resetAt: new Date(entry.resetAt).toISOString(),
237
- windowMinutes: entry.windowMinutes,
238
- scope: entry.scope,
239
- modelIds: entry.modelIds
240
- })),
241
- ...(isAntigravityRawDebugEnabled()
242
- ? {
243
- quotaPayload: quotaResult.value,
244
- userStatusPayload: statusResult.status === "fulfilled"
245
- ? statusResult.value
246
- : {
247
- error: statusResult.reason instanceof Error
248
- ? statusResult.reason.message
249
- : String(statusResult.reason)
250
- }
199
+ const [quota, status] = await Promise.all([
200
+ rpc(server, ANTIGRAVITY_QUOTA_SUMMARY_PATH),
201
+ rpc(server, ANTIGRAVITY_USER_STATUS_PATH, {
202
+ metadata: {
203
+ ideName: "antigravity",
204
+ extensionName: "antigravity",
205
+ ideVersion: "unknown",
206
+ locale: "en"
251
207
  }
252
- : {})
253
- });
208
+ }).catch(() => null)
209
+ ]);
254
210
  return {
255
- entries,
256
- fetchedAt,
257
- userIdHash
211
+ entries: parseAntigravityQuotaEntries(quota),
212
+ fetchedAt: Date.now(),
213
+ planType: parseAntigravityPlanType(status),
214
+ userIdHash: parseAntigravityUserIdHash(status)
258
215
  };
259
216
  }
260
- function printAntigravityUserStatusResponse(statusResult) {
261
- try {
262
- if (statusResult.status === "fulfilled") {
263
- console.error("Antigravity user status response:", JSON.stringify(statusResult.value, null, 2));
264
- return;
265
- }
266
- console.error("Antigravity user status response error:", statusResult.reason instanceof Error
267
- ? statusResult.reason.message
268
- : String(statusResult.reason));
269
- }
270
- catch {
271
- // Debug printing must never disturb usage collection.
272
- }
273
- }
274
- function recordsForQuotaWindow(quota, records) {
275
- if (quota.modelIds.length === 0) {
276
- return [];
277
- }
278
- const endMs = quota.resetAt;
279
- const startMs = endMs - quota.windowMinutes * 60000;
217
+ function buildAntigravityLimitWindow(quota, planType, records, fetchedAt) {
218
+ const startAt = quota.resetAt - quota.windowMinutes * 60000;
280
219
  const modelIds = new Set(quota.modelIds.map(resolveModelId));
281
- return records.filter((record) => {
282
- const modelId = resolveModelId(record.modelId);
283
- return (record.timestamp >= startMs &&
284
- record.timestamp < endMs &&
285
- modelIds.has(modelId));
286
- });
287
- }
288
- function buildAntigravityLimitWindow(quota, records, fetchedAt) {
289
- const matchingRecords = recordsForQuotaWindow(quota, records);
290
220
  const byModel = new Map();
291
- for (const record of matchingRecords) {
221
+ for (const record of records) {
292
222
  const modelId = resolveModelId(record.modelId);
223
+ if (record.timestamp < startAt ||
224
+ record.timestamp >= quota.resetAt ||
225
+ !modelIds.has(modelId)) {
226
+ continue;
227
+ }
293
228
  addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
294
229
  }
295
230
  const modelUsage = [...byModel.entries()]
@@ -306,7 +241,7 @@ function buildAntigravityLimitWindow(quota, records, fetchedAt) {
306
241
  // window and may not match Antigravity's internal quota accounting exactly.
307
242
  return {
308
243
  scope: quota.scope,
309
- planType: quota.planType,
244
+ planType,
310
245
  limitId: quota.limitId,
311
246
  windowMinutes: quota.windowMinutes,
312
247
  startTimeUtcIso: new Date(quota.resetAt - quota.windowMinutes * 60000).toISOString(),
@@ -320,236 +255,88 @@ function buildAntigravityLimitWindow(quota, records, fetchedAt) {
320
255
  eventCount: totals.eventCount
321
256
  };
322
257
  }
323
- const antigravityHttpsAgent = new https.Agent({
324
- rejectUnauthorized: false
325
- });
326
258
  async function findAntigravityLocalServer() {
327
259
  const process = await findAntigravityProcess();
328
260
  if (!process) {
329
- await writeAntigravityDebugEvent("process-not-found", {});
330
261
  return null;
331
262
  }
332
- const ports = await findListeningLoopbackPorts(process.pid);
333
- await writeAntigravityDebugEvent("process-found", {
334
- pid: process.pid,
335
- ports
336
- });
337
- return probeAntigravityPorts(ports, process.csrfToken);
338
- }
339
- async function findAntigravityProcess() {
340
- const fromProc = await findAntigravityProcessFromProc();
341
- if (fromProc) {
342
- return fromProc;
343
- }
344
- return findAntigravityProcessFromPs();
345
- }
346
- async function findAntigravityProcessFromProc() {
347
- let entries;
348
- try {
349
- entries = await fs.promises.readdir("/proc", { withFileTypes: true });
350
- }
351
- catch {
352
- return null;
353
- }
354
- for (const entry of entries) {
355
- if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
356
- continue;
263
+ for (const port of await findListeningPorts(process.pid)) {
264
+ const server = {
265
+ port,
266
+ csrfToken: process.csrfToken
267
+ };
268
+ try {
269
+ await rpc(server, ANTIGRAVITY_QUOTA_SUMMARY_PATH);
270
+ return server;
357
271
  }
358
- const pid = Number(entry.name);
359
- const cmdline = await readProcCmdline(path.join("/proc", entry.name, "cmdline"));
360
- const process = parseAntigravityProcessFromArgs(pid, cmdline);
361
- if (process) {
362
- return process;
272
+ catch {
273
+ // Try the next loopback listener owned by the same Antigravity process.
363
274
  }
364
275
  }
365
276
  return null;
366
277
  }
367
- async function readProcCmdline(filePath) {
368
- try {
369
- const content = await fs.promises.readFile(filePath);
370
- return content
371
- .toString("utf8")
372
- .split("\0")
373
- .filter(Boolean);
374
- }
375
- catch {
376
- return [];
377
- }
378
- }
379
- async function findAntigravityProcessFromPs() {
380
- try {
381
- const { stdout } = await execFileAsync("ps", ["-eo", "pid=,args="], {
382
- encoding: "utf8",
383
- maxBuffer: 4 * 1024 * 1024,
384
- timeout: 5000
385
- });
386
- for (const line of stdout.split(/\r?\n/)) {
387
- const match = line.match(/^\s*(\d+)\s+(.+)$/);
388
- if (!match) {
389
- continue;
390
- }
391
- const process = parseAntigravityProcessFromArgs(Number(match[1]), splitCommandLineForDiscovery(match[2]));
392
- if (process) {
393
- return process;
394
- }
278
+ async function findAntigravityProcess() {
279
+ const entries = await fs.promises.readdir("/proc").catch(() => []);
280
+ for (const entry of entries) {
281
+ if (!/^\d+$/.test(entry)) {
282
+ continue;
395
283
  }
396
- }
397
- catch {
398
- return null;
399
- }
400
- return null;
401
- }
402
- function splitCommandLineForDiscovery(value) {
403
- return value.match(/"[^"]*"|'[^']*'|\S+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
404
- }
405
- function parseAntigravityProcessFromArgs(pid, args) {
406
- if (!Number.isInteger(pid) || pid <= 0 || !isAntigravityLanguageServerCommand(args)) {
407
- return null;
408
- }
409
- const csrfToken = readNamedArg(args, "--csrf_token")?.trim();
410
- if (!csrfToken) {
411
- return null;
412
- }
413
- return { pid, csrfToken };
414
- }
415
- function isAntigravityLanguageServerCommand(args) {
416
- const normalized = args.join(" ").toLowerCase();
417
- return (normalized.includes("antigravity") &&
418
- (normalized.includes("language-server") ||
419
- normalized.includes("language_server") ||
420
- normalized.includes("extension-server") ||
421
- normalized.includes("extension_server")));
422
- }
423
- function readNamedArg(args, name) {
424
- for (let index = 0; index < args.length; index += 1) {
425
- const arg = args[index];
426
- if (arg === name) {
427
- return args[index + 1] ?? null;
284
+ const args = await fs.promises
285
+ .readFile(`/proc/${entry}/cmdline`, "utf8")
286
+ .then((value) => value.split("\0").filter(Boolean))
287
+ .catch(() => []);
288
+ const command = args.join(" ").toLowerCase();
289
+ if (!command.includes("antigravity") ||
290
+ !/(language|extension)[_-]server/.test(command)) {
291
+ continue;
428
292
  }
429
- if (arg.startsWith(`${name}=`)) {
430
- return arg.slice(name.length + 1);
293
+ const tokenArg = args.find((arg) => arg.startsWith("--csrf_token="));
294
+ const tokenIndex = args.indexOf("--csrf_token");
295
+ const csrfToken = tokenArg?.slice("--csrf_token=".length) ??
296
+ args[tokenIndex + 1];
297
+ if (csrfToken) {
298
+ return {
299
+ pid: Number(entry),
300
+ csrfToken
301
+ };
431
302
  }
432
303
  }
433
304
  return null;
434
305
  }
435
- async function findListeningLoopbackPorts(pid) {
436
- const parsers = [
437
- () => findListeningLoopbackPortsWithSs(pid),
438
- () => findListeningLoopbackPortsWithLsof(pid)
306
+ async function findListeningPorts(pid) {
307
+ const { stdout } = await execFileAsync("ss", ["-H", "-ltnp"], { encoding: "utf8", timeout: 5000 });
308
+ return [
309
+ ...new Set(stdout
310
+ .split("\n")
311
+ .filter((line) => line.includes(`pid=${pid},`))
312
+ .flatMap((line) => [
313
+ ...line.matchAll(/(?:127\.0\.0\.1|\[::1\]):(\d+)/g)
314
+ ])
315
+ .map((match) => Number(match[1])))
439
316
  ];
440
- for (const parse of parsers) {
441
- const ports = await parse();
442
- if (ports.length > 0) {
443
- return ports;
444
- }
445
- }
446
- return [];
447
317
  }
448
- async function findListeningLoopbackPortsWithSs(pid) {
449
- try {
450
- const { stdout } = await execFileAsync("ss", ["-H", "-ltnp"], {
451
- encoding: "utf8",
452
- maxBuffer: 1024 * 1024,
453
- timeout: 5000
454
- });
455
- return uniquePorts(stdout
456
- .split(/\r?\n/)
457
- .filter((line) => line.includes(`pid=${pid},`) && isLoopbackListenLine(line))
458
- .map(extractPortFromListenLine));
459
- }
460
- catch {
461
- return [];
462
- }
463
- }
464
- async function findListeningLoopbackPortsWithLsof(pid) {
465
- try {
466
- const { stdout } = await execFileAsync("lsof", ["-Pan", "-p", String(pid), "-iTCP", "-sTCP:LISTEN"], {
467
- encoding: "utf8",
468
- maxBuffer: 1024 * 1024,
469
- timeout: 5000
470
- });
471
- return uniquePorts(stdout
472
- .split(/\r?\n/)
473
- .filter(isLoopbackListenLine)
474
- .map(extractPortFromListenLine));
475
- }
476
- catch {
477
- return [];
478
- }
479
- }
480
- function isLoopbackListenLine(line) {
481
- return /(?:127\.0\.0\.1|localhost|\[::1\]|::1):\d+\b/.test(line);
482
- }
483
- function extractPortFromListenLine(line) {
484
- const matches = [...line.matchAll(/(?:127\.0\.0\.1|localhost|\[::1\]|::1):(\d+)/g)];
485
- const value = matches.at(-1)?.[1];
486
- const port = value ? Number(value) : NaN;
487
- return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
488
- }
489
- function uniquePorts(ports) {
490
- return [...new Set(ports.filter((port) => port !== null))];
491
- }
492
- async function probeAntigravityPorts(ports, csrfToken) {
493
- for (const port of ports) {
494
- const server = { port, csrfToken };
495
- try {
496
- await readAntigravityQuotaSummary(server);
497
- await writeAntigravityDebugEvent("port-probe-ok", { port });
498
- return server;
499
- }
500
- catch (error) {
501
- await writeAntigravityDebugEvent("port-probe-failed", {
502
- port,
503
- error: error instanceof Error ? error.message : String(error)
504
- });
505
- // Try the next loopback listener owned by the same Antigravity process.
506
- }
507
- }
508
- return null;
509
- }
510
- async function readAntigravityQuotaSummary(server) {
511
- return requestAntigravityQuotaSummary(server);
512
- }
513
- async function readAntigravityUserStatus(server) {
514
- return requestAntigravityUserStatus(server);
515
- }
516
- function requestAntigravityQuotaSummary(server) {
517
- return requestAntigravityRpc(server, ANTIGRAVITY_QUOTA_SUMMARY_PATH, {});
518
- }
519
- function requestAntigravityUserStatus(server) {
520
- return requestAntigravityRpc(server, ANTIGRAVITY_USER_STATUS_PATH, {
521
- metadata: {
522
- ideName: "antigravity",
523
- extensionName: "antigravity",
524
- ideVersion: "unknown",
525
- locale: "en"
526
- }
527
- });
528
- }
529
- function requestAntigravityRpc(server, rpcPath, payload) {
318
+ function rpc(server, endpoint, payload = {}) {
530
319
  const body = JSON.stringify(payload);
531
320
  return new Promise((resolve, reject) => {
532
321
  const request = https.request({
533
322
  hostname: "127.0.0.1",
534
323
  port: server.port,
535
- path: rpcPath,
324
+ path: endpoint,
536
325
  method: "POST",
326
+ rejectUnauthorized: false,
537
327
  timeout: 5000,
538
- agent: antigravityHttpsAgent,
539
328
  headers: {
540
329
  "X-Codeium-Csrf-Token": server.csrfToken,
541
330
  "Content-Type": "application/json",
542
- "Connect-Protocol-Version": "1",
543
- Accept: "application/json",
544
- "Content-Length": Buffer.byteLength(body)
331
+ "Connect-Protocol-Version": "1"
545
332
  }
546
333
  }, (response) => {
547
334
  const chunks = [];
548
335
  response.on("data", (chunk) => chunks.push(chunk));
549
336
  response.on("end", () => {
550
337
  const responseBody = Buffer.concat(chunks).toString("utf8");
551
- if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
552
- reject(new Error(`Unexpected Antigravity RPC response from ${rpcPath}: ${response.statusCode ?? "unknown"}`));
338
+ if (!response.statusCode || response.statusCode >= 300) {
339
+ reject(new Error(`RPC failed: ${response.statusCode ?? "unknown"}`));
553
340
  return;
554
341
  }
555
342
  try {
@@ -561,253 +348,54 @@ function requestAntigravityRpc(server, rpcPath, payload) {
561
348
  });
562
349
  });
563
350
  request.on("timeout", () => {
564
- request.destroy(new Error(`Timed out reading Antigravity RPC ${rpcPath}.`));
351
+ request.destroy(new Error(`Timed out reading Antigravity RPC ${endpoint}.`));
565
352
  });
566
353
  request.on("error", reject);
567
354
  request.end(body);
568
355
  });
569
356
  }
570
357
  export function parseAntigravityQuotaEntries(payload) {
571
- const root = asRecord(payload);
572
- const response = asRecord(root?.response);
573
- const groups = asArray(response?.groups);
574
- const entries = [];
575
- for (const groupValue of groups) {
576
- const group = asRecord(groupValue);
577
- if (!group) {
578
- continue;
358
+ const groups = payload.response?.groups ?? [];
359
+ return groups.flatMap((group) => {
360
+ const modelIds = resolveQuotaGroupModelIds(`${group.displayName ?? ""} ${group.description ?? ""}`);
361
+ if (!modelIds.length) {
362
+ return [];
579
363
  }
580
- const displayName = asString(group.displayName) ?? "";
581
- const description = asString(group.description) ?? "";
582
- const modelIds = resolveQuotaGroupModelIds(displayName, description);
583
- if (modelIds.length === 0) {
584
- void writeAntigravityDebugEvent("quota-group-skipped", {
585
- displayName,
586
- description
587
- });
588
- continue;
589
- }
590
- for (const bucketValue of asArray(group.buckets)) {
591
- const bucket = asRecord(bucketValue);
592
- if (!bucket) {
593
- continue;
594
- }
595
- const bucketId = asString(bucket.bucketId);
596
- const windowConfig = resolveQuotaWindow(asString(bucket.window));
597
- const remainingFraction = asFiniteNumber(bucket.remainingFraction);
598
- const resetTime = asString(bucket.resetTime);
599
- const resetAt = resetTime === null ? NaN : Date.parse(resetTime);
600
- if (!bucketId ||
601
- remainingFraction === null ||
602
- remainingFraction < 0 ||
603
- remainingFraction > 1 ||
364
+ return (group.buckets ?? []).flatMap((bucket) => {
365
+ const window = bucket.window
366
+ ? QUOTA_WINDOWS[bucket.window]
367
+ : undefined;
368
+ const resetAt = Date.parse(bucket.resetTime ?? "");
369
+ if (!bucket.bucketId ||
370
+ window === undefined ||
604
371
  !Number.isFinite(resetAt) ||
605
- !windowConfig) {
606
- continue;
372
+ typeof bucket.remainingFraction !== "number" ||
373
+ bucket.remainingFraction < 0 ||
374
+ bucket.remainingFraction > 1) {
375
+ return [];
607
376
  }
608
- entries.push({
609
- limitId: bucketId,
610
- modelIds,
611
- remainingFraction,
612
- resetAt,
613
- windowMinutes: windowConfig.windowMinutes,
614
- scope: windowConfig.scope,
615
- planType: "unknown"
616
- });
617
- }
618
- }
619
- return entries;
377
+ return [{
378
+ limitId: bucket.bucketId,
379
+ modelIds,
380
+ remainingFraction: bucket.remainingFraction,
381
+ resetAt,
382
+ ...window
383
+ }];
384
+ });
385
+ });
620
386
  }
621
387
  export function parseAntigravityPlanType(payload) {
622
- const root = asRecord(payload);
623
- const response = asRecord(root?.response);
624
- const candidates = [
625
- readNestedString(root, [
626
- "userStatus",
627
- "planStatus",
628
- "planInfo",
629
- "planName"
630
- ]),
631
- readNestedString(root, [
632
- "userStatus",
633
- "planStatus",
634
- "planInfo",
635
- "planDisplayName"
636
- ]),
637
- readNestedString(root, [
638
- "userStatus",
639
- "planStatus",
640
- "planName"
641
- ]),
642
- readNestedString(root, [
643
- "userStatus",
644
- "planName"
645
- ]),
646
- readNestedString(response, [
647
- "userStatus",
648
- "planStatus",
649
- "planInfo",
650
- "planName"
651
- ]),
652
- readNestedString(response, [
653
- "userStatus",
654
- "planStatus",
655
- "planInfo",
656
- "planDisplayName"
657
- ]),
658
- readNestedString(response, [
659
- "userStatus",
660
- "planStatus",
661
- "planName"
662
- ]),
663
- readNestedString(response, [
664
- "userStatus",
665
- "planName"
666
- ]),
667
- readNestedString(response, [
668
- "planStatus",
669
- "planInfo",
670
- "planName"
671
- ]),
672
- readNestedString(response, [
673
- "planInfo",
674
- "planName"
675
- ]),
676
- readNestedString(response, ["planName"]),
677
- readNestedString(root, ["planName"])
678
- ];
679
- const rawPlan = candidates.find((value) => Boolean(value));
680
- return normalizeAntigravityPlanType(rawPlan ?? null);
388
+ const planName = payload.response?.userStatus?.planStatus?.planInfo?.planName;
389
+ return typeof planName === "string" && planName ? planName : "unknown";
681
390
  }
682
- export function parseAntigravityUserIdHash(payload, agentName) {
683
- const root = asRecord(payload);
684
- const response = asRecord(root?.response);
685
- const email = readNestedString(root, ["userStatus", "email"]) ??
686
- readNestedString(response, ["userStatus", "email"]) ??
687
- readNestedString(root, ["email"]) ??
688
- readNestedString(response, ["email"]);
689
- return buildUserIdHash([agentName, email ?? ""]);
690
- }
691
- function readNestedString(value, pathParts) {
692
- let current = value;
693
- for (const part of pathParts) {
694
- const record = asRecord(current);
695
- if (!record) {
696
- return null;
697
- }
698
- current = record[part];
699
- }
700
- return asString(current);
701
- }
702
- function normalizeAntigravityPlanType(value) {
703
- if (!value) {
704
- return "unknown";
705
- }
706
- const normalized = value.toLowerCase();
707
- if (normalized.includes("ultra")) {
708
- return "ultra";
709
- }
710
- if (normalized.includes("pro") || normalized.includes("premium")) {
711
- return "pro";
712
- }
713
- if (normalized.includes("free") || normalized.includes("standard")) {
714
- return "free";
715
- }
716
- return value;
717
- }
718
- function normalizeAnalyticsAgentName(label) {
719
- return label.replace(/\s+/g, "");
720
- }
721
- function buildUserIdHash(parts) {
722
- if (parts.some((part) => !part)) {
723
- return null;
724
- }
725
- return createHash("md5").update(parts.join("-")).digest("hex");
726
- }
727
- function resolveQuotaWindow(window) {
728
- switch (window) {
729
- case "5h":
730
- return {
731
- scope: "primary",
732
- windowMinutes: ANTIGRAVITY_PRIMARY_WINDOW_MINUTES
733
- };
734
- case "weekly":
735
- return {
736
- scope: "secondary",
737
- windowMinutes: ANTIGRAVITY_WEEKLY_WINDOW_MINUTES
738
- };
739
- default:
740
- return null;
741
- }
742
- }
743
- function resolveQuotaGroupModelIds(displayName, description) {
744
- const text = `${displayName} ${description}`.toLowerCase();
745
- if (text.includes("gemini")) {
746
- return GEMINI_QUOTA_MODELS;
747
- }
748
- if (text.includes("claude") || text.includes("gpt")) {
749
- return THIRD_PARTY_QUOTA_MODELS;
750
- }
751
- return [];
752
- }
753
- function asRecord(value) {
754
- return value && typeof value === "object" && !Array.isArray(value)
755
- ? value
391
+ export function parseAntigravityUserIdHash(payload) {
392
+ const email = payload.response?.userStatus?.email;
393
+ return typeof email === "string" && email
394
+ ? createHash("md5").update(email).digest("hex")
756
395
  : null;
757
396
  }
758
- function asArray(value) {
759
- return Array.isArray(value) ? value : [];
760
- }
761
- function asString(value) {
762
- return typeof value === "string" && value.trim() ? value.trim() : null;
763
- }
764
- function asFiniteNumber(value) {
765
- return typeof value === "number" && Number.isFinite(value) ? value : null;
766
- }
767
- function isAntigravityDebugEnabled() {
768
- const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY;
769
- return value === "1" || value === "true" || value === "yes";
770
- }
771
- function isAntigravityRawDebugEnabled() {
772
- const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY_RAW;
773
- return value === "1" || value === "true" || value === "yes";
774
- }
775
- async function writeAntigravityDebugEvent(event, data) {
776
- if (!isAntigravityDebugEnabled()) {
777
- return;
778
- }
779
- const line = JSON.stringify({
780
- timestamp: new Date().toISOString(),
781
- event,
782
- data: redactDebugValue(data)
783
- });
784
- try {
785
- await fs.promises.mkdir(path.dirname(ANTIGRAVITY_DEBUG_LOG_PATH), {
786
- recursive: true
787
- });
788
- await fs.promises.appendFile(ANTIGRAVITY_DEBUG_LOG_PATH, `${line}\n`, "utf8");
789
- }
790
- catch {
791
- // Debug logging must never break provider stats collection.
792
- }
793
- }
794
- function redactDebugValue(value, key = "") {
795
- if (isSensitiveDebugKey(key)) {
796
- return "[redacted]";
797
- }
798
- if (Array.isArray(value)) {
799
- return value.map((item) => redactDebugValue(item));
800
- }
801
- if (!value || typeof value !== "object") {
802
- return value;
803
- }
804
- return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
805
- entryKey,
806
- redactDebugValue(entryValue, entryKey)
807
- ]));
808
- }
809
- function isSensitiveDebugKey(key) {
810
- return /token|csrf|authorization|cookie|email/i.test(key);
397
+ function resolveQuotaGroupModelIds(text) {
398
+ return (QUOTA_MODEL_GROUPS.find(({ pattern }) => pattern.test(text.toLowerCase()))?.models ?? []);
811
399
  }
812
400
  function numberOrZero(value) {
813
401
  return typeof value === "number" && Number.isFinite(value)
@@ -820,8 +408,8 @@ function clampPercent(value) {
820
408
  }
821
409
  return Math.min(100, Math.max(0, value));
822
410
  }
823
- async function readAntigravityUsageCache(cacheRoot) {
824
- const sessionsRoot = path.join(cacheRoot, "sessions");
411
+ async function readAntigravityUsageCache() {
412
+ const sessionsRoot = path.join(ANTIGRAVITY_CACHE_ROOT, "sessions");
825
413
  const records = [];
826
414
  for await (const filePath of walkJsonlFiles(sessionsRoot)) {
827
415
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
@@ -875,9 +463,6 @@ function usageRecordFromCacheEntry(value) {
875
463
  reasoning: numberOrZero(entry.reasoning)
876
464
  };
877
465
  }
878
- function getAntigravityCacheRoot() {
879
- return path.join(os.homedir(), ".config", "tokscale", "antigravity-cache");
880
- }
881
466
  async function* walkJsonlFiles(directory) {
882
467
  let entries;
883
468
  try {