antigravity-usage 0.1.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/dist/index.js ADDED
@@ -0,0 +1,2732 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/version.ts
7
+ var version = "0.1.0";
8
+
9
+ // src/core/logger.ts
10
+ var debugMode = false;
11
+ function setDebugMode(enabled) {
12
+ debugMode = enabled;
13
+ }
14
+ function isDebugMode() {
15
+ return debugMode;
16
+ }
17
+ function debug(category, message, data) {
18
+ if (!debugMode) return;
19
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
20
+ const prefix = `[${timestamp}] [${category}]`;
21
+ if (data !== void 0) {
22
+ console.error(`${prefix} ${message}`, data);
23
+ } else {
24
+ console.error(`${prefix} ${message}`);
25
+ }
26
+ }
27
+ function info(message) {
28
+ console.log(message);
29
+ }
30
+ function warn(message) {
31
+ console.warn(`\u26A0\uFE0F ${message}`);
32
+ }
33
+ function error(message) {
34
+ console.error(`\u274C ${message}`);
35
+ }
36
+ function success(message) {
37
+ console.log(`\u2705 ${message}`);
38
+ }
39
+
40
+ // src/google/oauth.ts
41
+ import { createServer } from "http";
42
+ import { URL as URL2, URLSearchParams } from "url";
43
+ import open from "open";
44
+
45
+ // src/accounts/types.ts
46
+ var DEFAULT_CONFIG = {
47
+ version: "2.0",
48
+ activeAccount: null,
49
+ preferences: {
50
+ cacheTTL: 300
51
+ }
52
+ };
53
+
54
+ // src/accounts/storage.ts
55
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, rmSync } from "fs";
56
+ import { join as join2 } from "path";
57
+
58
+ // src/core/env.ts
59
+ import { homedir, platform } from "os";
60
+ import { join } from "path";
61
+ function getPlatform() {
62
+ const p = platform();
63
+ if (p === "win32") return "windows";
64
+ if (p === "darwin") return "macos";
65
+ return "linux";
66
+ }
67
+ function getConfigDir() {
68
+ const p = getPlatform();
69
+ const home = homedir();
70
+ switch (p) {
71
+ case "windows":
72
+ return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "antigravity-usage");
73
+ case "macos":
74
+ return join(home, "Library", "Application Support", "antigravity-usage");
75
+ case "linux":
76
+ default:
77
+ return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "antigravity-usage");
78
+ }
79
+ }
80
+ function getTokensPath() {
81
+ return join(getConfigDir(), "tokens.json");
82
+ }
83
+ function getAccountsDir() {
84
+ return join(getConfigDir(), "accounts");
85
+ }
86
+ function getAccountDir(email) {
87
+ const safeName = email.replace(/[^a-zA-Z0-9@._-]/g, "_");
88
+ return join(getAccountsDir(), safeName);
89
+ }
90
+ function getGlobalConfigPath() {
91
+ return join(getConfigDir(), "config.json");
92
+ }
93
+
94
+ // src/accounts/storage.ts
95
+ function ensureAccountsDir() {
96
+ const dir = getAccountsDir();
97
+ if (!existsSync(dir)) {
98
+ debug("accounts-storage", `Creating accounts directory: ${dir}`);
99
+ mkdirSync(dir, { recursive: true });
100
+ }
101
+ }
102
+ function ensureAccountDir(email) {
103
+ ensureAccountsDir();
104
+ const dir = getAccountDir(email);
105
+ if (!existsSync(dir)) {
106
+ debug("accounts-storage", `Creating account directory: ${dir}`);
107
+ mkdirSync(dir, { recursive: true });
108
+ }
109
+ }
110
+ function accountExists(email) {
111
+ const dir = getAccountDir(email);
112
+ return existsSync(dir) && existsSync(join2(dir, "tokens.json"));
113
+ }
114
+ function listAccountEmails() {
115
+ const accountsDir = getAccountsDir();
116
+ if (!existsSync(accountsDir)) {
117
+ return [];
118
+ }
119
+ try {
120
+ const entries = readdirSync(accountsDir, { withFileTypes: true });
121
+ const emails = [];
122
+ for (const entry of entries) {
123
+ if (entry.isDirectory()) {
124
+ const tokensPath = join2(accountsDir, entry.name, "tokens.json");
125
+ if (existsSync(tokensPath)) {
126
+ emails.push(entry.name);
127
+ }
128
+ }
129
+ }
130
+ return emails;
131
+ } catch (err) {
132
+ debug("accounts-storage", "Failed to list accounts", err);
133
+ return [];
134
+ }
135
+ }
136
+ function saveAccountTokens(email, tokens) {
137
+ ensureAccountDir(email);
138
+ const path = join2(getAccountDir(email), "tokens.json");
139
+ debug("accounts-storage", `Saving tokens for ${email}`);
140
+ writeFileSync(path, JSON.stringify(tokens, null, 2), { mode: 384 });
141
+ }
142
+ function loadAccountTokens(email) {
143
+ const path = join2(getAccountDir(email), "tokens.json");
144
+ if (!existsSync(path)) {
145
+ debug("accounts-storage", `No tokens file for ${email}`);
146
+ return null;
147
+ }
148
+ try {
149
+ const content = readFileSync(path, "utf-8");
150
+ return JSON.parse(content);
151
+ } catch (err) {
152
+ debug("accounts-storage", `Failed to parse tokens for ${email}`, err);
153
+ return null;
154
+ }
155
+ }
156
+ function saveAccountMetadata(email, metadata) {
157
+ ensureAccountDir(email);
158
+ const path = join2(getAccountDir(email), "metadata.json");
159
+ debug("accounts-storage", `Saving metadata for ${email}`);
160
+ writeFileSync(path, JSON.stringify(metadata, null, 2), { mode: 384 });
161
+ }
162
+ function loadAccountMetadata(email) {
163
+ const path = join2(getAccountDir(email), "metadata.json");
164
+ if (!existsSync(path)) {
165
+ return null;
166
+ }
167
+ try {
168
+ const content = readFileSync(path, "utf-8");
169
+ return JSON.parse(content);
170
+ } catch (err) {
171
+ debug("accounts-storage", `Failed to parse metadata for ${email}`, err);
172
+ return null;
173
+ }
174
+ }
175
+ function updateLastUsed(email) {
176
+ const metadata = loadAccountMetadata(email);
177
+ if (metadata) {
178
+ metadata.lastUsed = (/* @__PURE__ */ new Date()).toISOString();
179
+ saveAccountMetadata(email, metadata);
180
+ }
181
+ }
182
+ function saveAccountCache(email, cache) {
183
+ ensureAccountDir(email);
184
+ const path = join2(getAccountDir(email), "cache.json");
185
+ debug("accounts-storage", `Saving cache for ${email}`);
186
+ writeFileSync(path, JSON.stringify(cache, null, 2));
187
+ }
188
+ function loadAccountCache(email) {
189
+ const path = join2(getAccountDir(email), "cache.json");
190
+ if (!existsSync(path)) {
191
+ return null;
192
+ }
193
+ try {
194
+ const content = readFileSync(path, "utf-8");
195
+ return JSON.parse(content);
196
+ } catch (err) {
197
+ debug("accounts-storage", `Failed to parse cache for ${email}`, err);
198
+ return null;
199
+ }
200
+ }
201
+ function deleteAccount(email) {
202
+ const dir = getAccountDir(email);
203
+ if (!existsSync(dir)) {
204
+ debug("accounts-storage", `Account ${email} does not exist`);
205
+ return false;
206
+ }
207
+ try {
208
+ rmSync(dir, { recursive: true, force: true });
209
+ debug("accounts-storage", `Deleted account ${email}`);
210
+ return true;
211
+ } catch (err) {
212
+ debug("accounts-storage", `Failed to delete account ${email}`, err);
213
+ return false;
214
+ }
215
+ }
216
+
217
+ // src/accounts/config.ts
218
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
219
+ import { dirname } from "path";
220
+ function loadConfig() {
221
+ const path = getGlobalConfigPath();
222
+ if (!existsSync2(path)) {
223
+ debug("config", "No config file found, using defaults");
224
+ return { ...DEFAULT_CONFIG };
225
+ }
226
+ try {
227
+ const content = readFileSync2(path, "utf-8");
228
+ const config = JSON.parse(content);
229
+ return {
230
+ ...DEFAULT_CONFIG,
231
+ ...config,
232
+ preferences: {
233
+ ...DEFAULT_CONFIG.preferences,
234
+ ...config.preferences
235
+ }
236
+ };
237
+ } catch (err) {
238
+ debug("config", "Failed to parse config, using defaults", err);
239
+ return { ...DEFAULT_CONFIG };
240
+ }
241
+ }
242
+ function saveConfig(config) {
243
+ const path = getGlobalConfigPath();
244
+ const dir = dirname(path);
245
+ if (!existsSync2(dir)) {
246
+ mkdirSync2(dir, { recursive: true });
247
+ }
248
+ debug("config", `Saving config to ${path}`);
249
+ writeFileSync2(path, JSON.stringify(config, null, 2));
250
+ }
251
+ function getActiveAccountEmail() {
252
+ const config = loadConfig();
253
+ return config.activeAccount;
254
+ }
255
+ function setActiveAccountEmail(email) {
256
+ const config = loadConfig();
257
+ config.activeAccount = email;
258
+ saveConfig(config);
259
+ }
260
+ function getCacheTTL() {
261
+ const config = loadConfig();
262
+ return config.preferences.cacheTTL;
263
+ }
264
+
265
+ // src/accounts/cache.ts
266
+ function isCacheValid(email) {
267
+ const cache = loadAccountCache(email);
268
+ if (!cache || !cache.data) {
269
+ debug("cache", `No valid cache for ${email}`);
270
+ return false;
271
+ }
272
+ const cachedAt = new Date(cache.cachedAt).getTime();
273
+ const ttlMs = cache.ttl * 1e3;
274
+ const now = Date.now();
275
+ const isValid = now - cachedAt < ttlMs;
276
+ debug("cache", `Cache for ${email} is ${isValid ? "valid" : "stale"}`);
277
+ return isValid;
278
+ }
279
+ function getCacheAge(email) {
280
+ const cache = loadAccountCache(email);
281
+ if (!cache) {
282
+ return null;
283
+ }
284
+ const cachedAt = new Date(cache.cachedAt).getTime();
285
+ return Math.floor((Date.now() - cachedAt) / 1e3);
286
+ }
287
+ function saveCache(email, data) {
288
+ const ttl = getCacheTTL();
289
+ const cache = {
290
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
291
+ ttl,
292
+ data
293
+ };
294
+ saveAccountCache(email, cache);
295
+ debug("cache", `Cached quota for ${email}, TTL: ${ttl}s`);
296
+ }
297
+ function loadCache(email) {
298
+ const cache = loadAccountCache(email);
299
+ return cache?.data || null;
300
+ }
301
+ function loadCacheWithMeta(email) {
302
+ return loadAccountCache(email);
303
+ }
304
+
305
+ // src/accounts/manager.ts
306
+ var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
307
+ var AccountManager = class _AccountManager {
308
+ static instance = null;
309
+ constructor() {
310
+ }
311
+ static getInstance() {
312
+ if (!_AccountManager.instance) {
313
+ _AccountManager.instance = new _AccountManager();
314
+ }
315
+ return _AccountManager.instance;
316
+ }
317
+ /**
318
+ * Reset instance (for testing)
319
+ */
320
+ static resetInstance() {
321
+ _AccountManager.instance = null;
322
+ }
323
+ /**
324
+ * Get all account emails
325
+ */
326
+ getAccountEmails() {
327
+ return listAccountEmails();
328
+ }
329
+ /**
330
+ * Get active account email
331
+ */
332
+ getActiveEmail() {
333
+ return getActiveAccountEmail();
334
+ }
335
+ /**
336
+ * Set active account
337
+ */
338
+ setActiveAccount(email) {
339
+ if (!accountExists(email)) {
340
+ debug("account-manager", `Account ${email} does not exist`);
341
+ return false;
342
+ }
343
+ setActiveAccountEmail(email);
344
+ updateLastUsed(email);
345
+ debug("account-manager", `Switched to account ${email}`);
346
+ return true;
347
+ }
348
+ /**
349
+ * Check if an account exists
350
+ */
351
+ hasAccount(email) {
352
+ return accountExists(email);
353
+ }
354
+ /**
355
+ * Get account status
356
+ */
357
+ getAccountStatus(email) {
358
+ const tokens = loadAccountTokens(email);
359
+ if (!tokens) {
360
+ return "invalid";
361
+ }
362
+ const now = Date.now();
363
+ if (now >= tokens.expiresAt - EXPIRY_BUFFER_MS) {
364
+ if (tokens.refreshToken) {
365
+ return "expired";
366
+ }
367
+ return "invalid";
368
+ }
369
+ return "valid";
370
+ }
371
+ /**
372
+ * Get detailed account info
373
+ */
374
+ getAccountInfo(email) {
375
+ if (!accountExists(email)) {
376
+ return null;
377
+ }
378
+ const activeEmail = getActiveAccountEmail();
379
+ const tokens = loadAccountTokens(email);
380
+ const metadata = loadAccountMetadata(email);
381
+ const cache = loadCacheWithMeta(email);
382
+ const status = this.getAccountStatus(email);
383
+ return {
384
+ email,
385
+ isActive: email === activeEmail,
386
+ tokens,
387
+ metadata,
388
+ cache,
389
+ status
390
+ };
391
+ }
392
+ /**
393
+ * Get account summaries for list display
394
+ */
395
+ getAccountSummaries() {
396
+ const emails = this.getAccountEmails();
397
+ const activeEmail = getActiveAccountEmail();
398
+ return emails.map((email) => {
399
+ const metadata = loadAccountMetadata(email);
400
+ const cache = loadCacheWithMeta(email);
401
+ const status = this.getAccountStatus(email);
402
+ let cachedCredits = null;
403
+ if (cache?.data?.promptCredits) {
404
+ const pc = cache.data.promptCredits;
405
+ cachedCredits = {
406
+ used: pc.monthly - pc.available,
407
+ limit: pc.monthly
408
+ };
409
+ }
410
+ return {
411
+ email,
412
+ isActive: email === activeEmail,
413
+ status,
414
+ lastUsed: metadata?.lastUsed || null,
415
+ cachedCredits
416
+ };
417
+ });
418
+ }
419
+ /**
420
+ * Add a new account after successful OAuth
421
+ */
422
+ addAccount(tokens, email) {
423
+ debug("account-manager", `Adding account ${email}`);
424
+ saveAccountTokens(email, tokens);
425
+ const now = (/* @__PURE__ */ new Date()).toISOString();
426
+ const metadata = {
427
+ email,
428
+ addedAt: now,
429
+ lastUsed: now
430
+ };
431
+ saveAccountMetadata(email, metadata);
432
+ setActiveAccountEmail(email);
433
+ debug("account-manager", `Account ${email} added and set as active`);
434
+ }
435
+ /**
436
+ * Update tokens for existing account
437
+ */
438
+ updateTokens(email, tokens) {
439
+ if (!accountExists(email)) {
440
+ debug("account-manager", `Cannot update tokens: account ${email} does not exist`);
441
+ return;
442
+ }
443
+ saveAccountTokens(email, tokens);
444
+ updateLastUsed(email);
445
+ debug("account-manager", `Updated tokens for ${email}`);
446
+ }
447
+ /**
448
+ * Remove an account
449
+ */
450
+ removeAccount(email) {
451
+ if (!accountExists(email)) {
452
+ debug("account-manager", `Account ${email} does not exist`);
453
+ return false;
454
+ }
455
+ const activeEmail = getActiveAccountEmail();
456
+ if (email === activeEmail) {
457
+ setActiveAccountEmail(null);
458
+ }
459
+ const deleted = deleteAccount(email);
460
+ if (deleted && email === activeEmail) {
461
+ const remaining = this.getAccountEmails();
462
+ if (remaining.length > 0) {
463
+ setActiveAccountEmail(remaining[0]);
464
+ debug("account-manager", `Set ${remaining[0]} as new active account`);
465
+ }
466
+ }
467
+ return deleted;
468
+ }
469
+ /**
470
+ * Remove all accounts
471
+ */
472
+ removeAllAccounts() {
473
+ const emails = this.getAccountEmails();
474
+ let count = 0;
475
+ for (const email of emails) {
476
+ if (deleteAccount(email)) {
477
+ count++;
478
+ }
479
+ }
480
+ setActiveAccountEmail(null);
481
+ debug("account-manager", `Removed ${count} accounts`);
482
+ return count;
483
+ }
484
+ /**
485
+ * Get tokens for an account
486
+ */
487
+ getTokens(email) {
488
+ return loadAccountTokens(email);
489
+ }
490
+ /**
491
+ * Get tokens for active account
492
+ */
493
+ getActiveTokens() {
494
+ const email = getActiveAccountEmail();
495
+ if (!email) {
496
+ return null;
497
+ }
498
+ return loadAccountTokens(email);
499
+ }
500
+ /**
501
+ * Check if cache is valid for an account
502
+ */
503
+ isCacheValid(email) {
504
+ return isCacheValid(email);
505
+ }
506
+ /**
507
+ * Get cache age in seconds
508
+ */
509
+ getCacheAge(email) {
510
+ return getCacheAge(email);
511
+ }
512
+ };
513
+ function getAccountManager() {
514
+ return AccountManager.getInstance();
515
+ }
516
+
517
+ // src/google/oauth.ts
518
+ var OAUTH_CONFIG = {
519
+ clientId: process.env.ANTIGRAVITY_OAUTH_CLIENT_ID || "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
520
+ clientSecret: process.env.ANTIGRAVITY_OAUTH_CLIENT_SECRET || "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",
521
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
522
+ tokenUrl: "https://oauth2.googleapis.com/token",
523
+ scopes: [
524
+ "https://www.googleapis.com/auth/cloud-platform",
525
+ "https://www.googleapis.com/auth/userinfo.email"
526
+ ]
527
+ };
528
+ function generateState() {
529
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
530
+ }
531
+ async function getAvailablePort(preferredPort) {
532
+ return new Promise((resolve, reject) => {
533
+ const server = createServer();
534
+ server.listen(preferredPort || 0, "127.0.0.1", () => {
535
+ const address = server.address();
536
+ if (address && typeof address === "object") {
537
+ const port = address.port;
538
+ server.close(() => resolve(port));
539
+ } else {
540
+ reject(new Error("Failed to get server address"));
541
+ }
542
+ });
543
+ server.on("error", reject);
544
+ });
545
+ }
546
+ async function exchangeCodeForTokens(code, redirectUri) {
547
+ debug("oauth", "Exchanging code for tokens");
548
+ const params = new URLSearchParams({
549
+ code,
550
+ client_id: OAUTH_CONFIG.clientId,
551
+ client_secret: OAUTH_CONFIG.clientSecret,
552
+ redirect_uri: redirectUri,
553
+ grant_type: "authorization_code"
554
+ });
555
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
556
+ method: "POST",
557
+ headers: {
558
+ "Content-Type": "application/x-www-form-urlencoded"
559
+ },
560
+ body: params.toString()
561
+ });
562
+ if (!response.ok) {
563
+ const error2 = await response.text();
564
+ debug("oauth", "Token exchange failed", error2);
565
+ throw new Error(`Token exchange failed: ${response.status} ${error2}`);
566
+ }
567
+ const data = await response.json();
568
+ debug("oauth", "Token exchange successful");
569
+ return data;
570
+ }
571
+ async function getUserEmail(accessToken) {
572
+ debug("oauth", "Fetching user info");
573
+ try {
574
+ const response = await fetch("https://www.googleapis.com/oauth2/v2/userinfo", {
575
+ headers: {
576
+ Authorization: `Bearer ${accessToken}`
577
+ }
578
+ });
579
+ if (response.ok) {
580
+ const data = await response.json();
581
+ return data.email;
582
+ }
583
+ } catch (err) {
584
+ debug("oauth", "Failed to get user info", err);
585
+ }
586
+ return void 0;
587
+ }
588
+ async function fetchProjectId(accessToken) {
589
+ debug("oauth", "Fetching project ID from Cloud Code API");
590
+ try {
591
+ const response = await fetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
592
+ method: "POST",
593
+ headers: {
594
+ "Authorization": `Bearer ${accessToken}`,
595
+ "Content-Type": "application/json",
596
+ "User-Agent": "antigravity/1.11.3 Darwin/arm64"
597
+ },
598
+ body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } })
599
+ });
600
+ if (response.ok) {
601
+ const data = await response.json();
602
+ debug("oauth", `Got project ID: ${data.cloudaicompanionProject}`);
603
+ return data.cloudaicompanionProject;
604
+ }
605
+ debug("oauth", `Failed to get project ID: ${response.status}`);
606
+ } catch (err) {
607
+ debug("oauth", "Error fetching project ID", err);
608
+ }
609
+ return void 0;
610
+ }
611
+ async function startOAuthFlow(options = {}) {
612
+ const port = await getAvailablePort(options.port);
613
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
614
+ const state = generateState();
615
+ debug("oauth", `Starting OAuth flow on port ${port}`);
616
+ const authParams = new URLSearchParams({
617
+ client_id: OAUTH_CONFIG.clientId,
618
+ redirect_uri: redirectUri,
619
+ response_type: "code",
620
+ scope: OAUTH_CONFIG.scopes.join(" "),
621
+ access_type: "offline",
622
+ prompt: "consent",
623
+ state
624
+ });
625
+ const authUrl = `${OAUTH_CONFIG.authUrl}?${authParams.toString()}`;
626
+ return new Promise((resolve) => {
627
+ let resolved = false;
628
+ const server = createServer(async (req, res) => {
629
+ if (resolved) return;
630
+ const url = new URL2(req.url || "/", `http://127.0.0.1:${port}`);
631
+ if (url.pathname === "/callback") {
632
+ const code = url.searchParams.get("code");
633
+ const returnedState = url.searchParams.get("state");
634
+ const errorParam = url.searchParams.get("error");
635
+ if (errorParam) {
636
+ res.writeHead(400, { "Content-Type": "text/html" });
637
+ res.end("<html><body><h1>Login Failed</h1><p>You can close this window.</p></body></html>");
638
+ resolved = true;
639
+ server.close();
640
+ resolve({ success: false, error: errorParam });
641
+ return;
642
+ }
643
+ if (!code || returnedState !== state) {
644
+ res.writeHead(400, { "Content-Type": "text/html" });
645
+ res.end("<html><body><h1>Invalid Request</h1><p>State mismatch or missing code.</p></body></html>");
646
+ resolved = true;
647
+ server.close();
648
+ resolve({ success: false, error: "Invalid callback" });
649
+ return;
650
+ }
651
+ try {
652
+ const tokenResponse = await exchangeCodeForTokens(code, redirectUri);
653
+ const email = await getUserEmail(tokenResponse.access_token);
654
+ let projectId;
655
+ try {
656
+ projectId = await fetchProjectId(tokenResponse.access_token);
657
+ } catch (err) {
658
+ debug("oauth", "Failed to fetch project ID during login (will fetch on demand)", err);
659
+ }
660
+ const tokens = {
661
+ accessToken: tokenResponse.access_token,
662
+ refreshToken: tokenResponse.refresh_token || "",
663
+ expiresAt: Date.now() + tokenResponse.expires_in * 1e3,
664
+ email,
665
+ projectId
666
+ };
667
+ if (email) {
668
+ getAccountManager().addAccount(tokens, email);
669
+ }
670
+ res.writeHead(200, { "Content-Type": "text/html" });
671
+ res.end(`
672
+ <html>
673
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
674
+ <h1>Login Successful!</h1>
675
+ <p>You are now logged in${email ? ` as <strong>${email}</strong>` : ""}.</p>
676
+ <p>You can close this window and return to the terminal.</p>
677
+ </body>
678
+ </html>
679
+ `);
680
+ resolved = true;
681
+ server.close();
682
+ resolve({ success: true, email });
683
+ } catch (err) {
684
+ res.writeHead(500, { "Content-Type": "text/html" });
685
+ res.end("<html><body><h1>Login Failed</h1><p>Token exchange failed.</p></body></html>");
686
+ resolved = true;
687
+ server.close();
688
+ resolve({ success: false, error: err instanceof Error ? err.message : "Unknown error" });
689
+ }
690
+ }
691
+ });
692
+ server.listen(port, "127.0.0.1", async () => {
693
+ info("");
694
+ info("Opening browser for Google login...");
695
+ info("");
696
+ if (options.noBrowser) {
697
+ info("Open this URL in your browser:");
698
+ info(authUrl);
699
+ } else {
700
+ try {
701
+ await open(authUrl);
702
+ info("If the browser did not open, visit this URL:");
703
+ info(authUrl);
704
+ } catch (err) {
705
+ debug("oauth", "Failed to open browser", err);
706
+ info("Could not open browser. Please visit this URL:");
707
+ info(authUrl);
708
+ }
709
+ }
710
+ info("");
711
+ info("Waiting for authentication...");
712
+ });
713
+ setTimeout(() => {
714
+ if (!resolved) {
715
+ resolved = true;
716
+ server.close();
717
+ resolve({ success: false, error: "Login timed out" });
718
+ }
719
+ }, 2 * 60 * 1e3);
720
+ });
721
+ }
722
+ async function refreshAccessToken(refreshToken) {
723
+ debug("oauth", "Refreshing access token");
724
+ const params = new URLSearchParams({
725
+ refresh_token: refreshToken,
726
+ client_id: OAUTH_CONFIG.clientId,
727
+ client_secret: OAUTH_CONFIG.clientSecret,
728
+ grant_type: "refresh_token"
729
+ });
730
+ const response = await fetch(OAUTH_CONFIG.tokenUrl, {
731
+ method: "POST",
732
+ headers: {
733
+ "Content-Type": "application/x-www-form-urlencoded"
734
+ },
735
+ body: params.toString()
736
+ });
737
+ if (!response.ok) {
738
+ const error2 = await response.text();
739
+ debug("oauth", "Token refresh failed", error2);
740
+ throw new Error(`Token refresh failed: ${response.status}`);
741
+ }
742
+ const data = await response.json();
743
+ debug("oauth", "Token refresh successful");
744
+ return data;
745
+ }
746
+
747
+ // src/google/storage.ts
748
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "fs";
749
+ import { dirname as dirname2 } from "path";
750
+ function saveTokens(tokens) {
751
+ const email = tokens.email;
752
+ if (!email) {
753
+ const path = getTokensPath();
754
+ const dir = dirname2(path);
755
+ debug("storage", `Saving tokens to legacy path ${path}`);
756
+ if (!existsSync3(dir)) {
757
+ mkdirSync3(dir, { recursive: true });
758
+ }
759
+ writeFileSync3(path, JSON.stringify(tokens, null, 2), { mode: 384 });
760
+ return;
761
+ }
762
+ debug("storage", `Saving tokens for account ${email}`);
763
+ saveAccountTokens(email, tokens);
764
+ if (!getActiveAccountEmail()) {
765
+ setActiveAccountEmail(email);
766
+ }
767
+ }
768
+ function loadTokens() {
769
+ const activeEmail = getActiveAccountEmail();
770
+ if (activeEmail) {
771
+ const tokens = loadAccountTokens(activeEmail);
772
+ if (tokens) {
773
+ debug("storage", `Loaded tokens for active account ${activeEmail}`);
774
+ return tokens;
775
+ }
776
+ }
777
+ const legacyPath = getTokensPath();
778
+ debug("storage", `Loading tokens from legacy path ${legacyPath}`);
779
+ if (!existsSync3(legacyPath)) {
780
+ debug("storage", "No tokens file found");
781
+ return null;
782
+ }
783
+ try {
784
+ const content = readFileSync3(legacyPath, "utf-8");
785
+ const tokens = JSON.parse(content);
786
+ debug("storage", "Tokens loaded successfully from legacy path");
787
+ return tokens;
788
+ } catch (err) {
789
+ debug("storage", "Failed to parse tokens file", err);
790
+ return null;
791
+ }
792
+ }
793
+ function hasTokens() {
794
+ const activeEmail = getActiveAccountEmail();
795
+ if (activeEmail && accountExists(activeEmail)) {
796
+ return true;
797
+ }
798
+ return existsSync3(getTokensPath());
799
+ }
800
+ function getStorageInfo() {
801
+ const configDir = getConfigDir();
802
+ const activeEmail = getActiveAccountEmail();
803
+ let tokensPath;
804
+ let exists;
805
+ if (activeEmail) {
806
+ tokensPath = `${getAccountDir(activeEmail)}/tokens.json`;
807
+ exists = accountExists(activeEmail);
808
+ } else {
809
+ tokensPath = getTokensPath();
810
+ exists = existsSync3(tokensPath);
811
+ }
812
+ return {
813
+ configDir,
814
+ tokensPath,
815
+ exists
816
+ };
817
+ }
818
+
819
+ // src/core/errors.ts
820
+ var NotLoggedInError = class extends Error {
821
+ constructor(message = "Not logged in. Run: antigravity-usage login") {
822
+ super(message);
823
+ this.name = "NotLoggedInError";
824
+ }
825
+ };
826
+ var AuthenticationError = class extends Error {
827
+ constructor(message = "Authentication failed. Please login again.") {
828
+ super(message);
829
+ this.name = "AuthenticationError";
830
+ }
831
+ };
832
+ var NetworkError = class extends Error {
833
+ constructor(message = "Network error. Please check your connection.") {
834
+ super(message);
835
+ this.name = "NetworkError";
836
+ }
837
+ };
838
+ var RateLimitError = class extends Error {
839
+ retryAfterMs;
840
+ constructor(message = "Rate limited. Please try again later.", retryAfterMs) {
841
+ super(message);
842
+ this.name = "RateLimitError";
843
+ this.retryAfterMs = retryAfterMs;
844
+ }
845
+ };
846
+ var APIError = class extends Error {
847
+ statusCode;
848
+ constructor(message, statusCode) {
849
+ super(message);
850
+ this.name = "APIError";
851
+ this.statusCode = statusCode;
852
+ }
853
+ };
854
+ var TokenRefreshError = class extends Error {
855
+ constructor(message = "Failed to refresh token. Please login again.") {
856
+ super(message);
857
+ this.name = "TokenRefreshError";
858
+ }
859
+ };
860
+ var AntigravityNotRunningError = class extends Error {
861
+ constructor(message = "Antigravity language server is not running. Please start Antigravity in your IDE.") {
862
+ super(message);
863
+ this.name = "AntigravityNotRunningError";
864
+ }
865
+ };
866
+ var LocalConnectionError = class extends Error {
867
+ constructor(message = "Failed to connect to local Antigravity server.") {
868
+ super(message);
869
+ this.name = "LocalConnectionError";
870
+ }
871
+ };
872
+ var PortDetectionError = class extends Error {
873
+ constructor(message = "Could not detect Antigravity server port.") {
874
+ super(message);
875
+ this.name = "PortDetectionError";
876
+ }
877
+ };
878
+ var NoAuthMethodAvailableError = class extends Error {
879
+ constructor(message = "Unable to fetch quota: Antigravity is not running and you are not logged in.\n\nPlease do one of the following:\n \u2022 Run Antigravity in your IDE (VSCode, etc.), or\n \u2022 Login with: antigravity-usage login") {
880
+ super(message);
881
+ this.name = "NoAuthMethodAvailableError";
882
+ }
883
+ };
884
+
885
+ // src/google/token-manager.ts
886
+ var EXPIRY_BUFFER_MS2 = 5 * 60 * 1e3;
887
+ var TokenManager = class {
888
+ tokens = null;
889
+ accountEmail = null;
890
+ constructor(email) {
891
+ if (email) {
892
+ this.accountEmail = email;
893
+ this.tokens = loadAccountTokens(email);
894
+ } else {
895
+ this.accountEmail = getActiveAccountEmail();
896
+ this.tokens = loadTokens();
897
+ }
898
+ }
899
+ /**
900
+ * Get the email this manager is for
901
+ */
902
+ getAccountEmail() {
903
+ return this.accountEmail || this.tokens?.email || null;
904
+ }
905
+ /**
906
+ * Check if user is logged in (has tokens)
907
+ */
908
+ isLoggedIn() {
909
+ if (this.accountEmail) {
910
+ return accountExists(this.accountEmail) && this.tokens !== null;
911
+ }
912
+ return hasTokens() && this.tokens !== null;
913
+ }
914
+ /**
915
+ * Get the stored email
916
+ */
917
+ getEmail() {
918
+ return this.tokens?.email;
919
+ }
920
+ /**
921
+ * Get token expiry time
922
+ */
923
+ getExpiresAt() {
924
+ if (!this.tokens) return void 0;
925
+ return new Date(this.tokens.expiresAt);
926
+ }
927
+ /**
928
+ * Get stored project ID
929
+ */
930
+ getProjectId() {
931
+ return this.tokens?.projectId;
932
+ }
933
+ /**
934
+ * Check if token is expired or about to expire
935
+ */
936
+ isTokenExpired() {
937
+ if (!this.tokens) return true;
938
+ return Date.now() >= this.tokens.expiresAt - EXPIRY_BUFFER_MS2;
939
+ }
940
+ /**
941
+ * Get a valid access token, refreshing if necessary
942
+ */
943
+ async getValidAccessToken() {
944
+ if (!this.tokens) {
945
+ throw new NotLoggedInError();
946
+ }
947
+ debug("token-manager", "Checking token validity");
948
+ if (this.isTokenExpired()) {
949
+ debug("token-manager", "Token expired or expiring soon, refreshing...");
950
+ await this.refreshToken();
951
+ }
952
+ return this.tokens.accessToken;
953
+ }
954
+ /**
955
+ * Refresh the access token
956
+ */
957
+ async refreshToken() {
958
+ if (!this.tokens?.refreshToken) {
959
+ throw new NotLoggedInError("No refresh token available. Please login again.");
960
+ }
961
+ try {
962
+ debug("token-manager", "Refreshing token...");
963
+ const response = await refreshAccessToken(this.tokens.refreshToken);
964
+ this.tokens = {
965
+ accessToken: response.access_token,
966
+ refreshToken: response.refresh_token || this.tokens.refreshToken,
967
+ expiresAt: Date.now() + response.expires_in * 1e3,
968
+ email: this.tokens.email,
969
+ projectId: this.tokens.projectId
970
+ };
971
+ if (this.accountEmail) {
972
+ saveAccountTokens(this.accountEmail, this.tokens);
973
+ updateLastUsed(this.accountEmail);
974
+ } else {
975
+ saveTokens(this.tokens);
976
+ }
977
+ debug("token-manager", "Token refreshed successfully");
978
+ } catch (err) {
979
+ debug("token-manager", "Token refresh failed", err);
980
+ throw new TokenRefreshError();
981
+ }
982
+ }
983
+ /**
984
+ * Reload tokens from disk
985
+ */
986
+ reload() {
987
+ if (this.accountEmail) {
988
+ this.tokens = loadAccountTokens(this.accountEmail);
989
+ } else {
990
+ this.tokens = loadTokens();
991
+ }
992
+ }
993
+ };
994
+ var tokenManagerInstance = null;
995
+ function getTokenManager() {
996
+ if (!tokenManagerInstance) {
997
+ tokenManagerInstance = new TokenManager();
998
+ }
999
+ return tokenManagerInstance;
1000
+ }
1001
+ function getTokenManagerForAccount(email) {
1002
+ return new TokenManager(email);
1003
+ }
1004
+ function resetTokenManager() {
1005
+ tokenManagerInstance = null;
1006
+ }
1007
+
1008
+ // src/commands/login.ts
1009
+ async function loginCommand(options) {
1010
+ const manager = getAccountManager();
1011
+ const existingAccounts = manager.getAccountEmails();
1012
+ if (existingAccounts.length > 0) {
1013
+ info(`You have ${existingAccounts.length} account(s). Adding another account...`);
1014
+ }
1015
+ const result = await startOAuthFlow({
1016
+ noBrowser: options.noBrowser,
1017
+ port: options.port
1018
+ });
1019
+ if (result.success) {
1020
+ resetTokenManager();
1021
+ success(`Logged in successfully${result.email ? ` as ${result.email}` : ""}!`);
1022
+ const accounts = manager.getAccountEmails();
1023
+ if (accounts.length > 1) {
1024
+ info(`
1025
+ You now have ${accounts.length} accounts. Use \`antigravity-usage accounts list\` to see all.`);
1026
+ }
1027
+ process.exit(0);
1028
+ } else {
1029
+ error(`Login failed: ${result.error}`);
1030
+ process.exit(1);
1031
+ }
1032
+ }
1033
+
1034
+ // src/commands/logout.ts
1035
+ function logoutCommand(options, email) {
1036
+ const manager = getAccountManager();
1037
+ if (options.all) {
1038
+ const count = manager.removeAllAccounts();
1039
+ resetTokenManager();
1040
+ if (count > 0) {
1041
+ success(`Logged out of ${count} account(s).`);
1042
+ } else {
1043
+ warn("No accounts to log out.");
1044
+ }
1045
+ return;
1046
+ }
1047
+ if (email) {
1048
+ if (!manager.hasAccount(email)) {
1049
+ warn(`Account '${email}' not found.`);
1050
+ return;
1051
+ }
1052
+ const removed2 = manager.removeAccount(email);
1053
+ resetTokenManager();
1054
+ if (removed2) {
1055
+ success(`Logged out of ${email}.`);
1056
+ const remaining = manager.getAccountEmails();
1057
+ if (remaining.length > 0) {
1058
+ info(`Active account: ${manager.getActiveEmail() || "none"}`);
1059
+ }
1060
+ } else {
1061
+ warn(`Could not log out of ${email}.`);
1062
+ }
1063
+ return;
1064
+ }
1065
+ const activeEmail = manager.getActiveEmail();
1066
+ if (!activeEmail) {
1067
+ warn("Not logged in.");
1068
+ return;
1069
+ }
1070
+ const removed = manager.removeAccount(activeEmail);
1071
+ resetTokenManager();
1072
+ if (removed) {
1073
+ success(`Logged out of ${activeEmail}.`);
1074
+ const remaining = manager.getAccountEmails();
1075
+ if (remaining.length > 0) {
1076
+ const newActive = manager.getActiveEmail();
1077
+ info(`Switched to: ${newActive}`);
1078
+ }
1079
+ } else {
1080
+ warn("Could not delete account.");
1081
+ }
1082
+ }
1083
+
1084
+ // src/core/mask.ts
1085
+ function maskToken(token) {
1086
+ if (!token) return "";
1087
+ if (token.length <= 10) return "***";
1088
+ const first = token.slice(0, 6);
1089
+ const last = token.slice(-4);
1090
+ return `${first}...${last}`;
1091
+ }
1092
+ function maskEmail(email) {
1093
+ if (!email) return "";
1094
+ const [local, domain] = email.split("@");
1095
+ if (!domain) return email;
1096
+ if (local.length <= 2) {
1097
+ return `${local[0] || ""}**@${domain}`;
1098
+ }
1099
+ return `${local.slice(0, 2)}**@${domain}`;
1100
+ }
1101
+
1102
+ // src/commands/status.ts
1103
+ import Table from "cli-table3";
1104
+ function showSingleAccountStatus(email) {
1105
+ const tokenManager = email ? getTokenManagerForAccount(email) : getTokenManager();
1106
+ console.log();
1107
+ console.log("\u{1F4CD} Antigravity Usage Status");
1108
+ console.log("\u2500".repeat(40));
1109
+ if (!tokenManager.isLoggedIn()) {
1110
+ warn("Not logged in");
1111
+ console.log();
1112
+ info("Run `antigravity-usage login` to authenticate.");
1113
+ console.log();
1114
+ return;
1115
+ }
1116
+ const accountEmail = tokenManager.getEmail();
1117
+ const expiresAt = tokenManager.getExpiresAt();
1118
+ const isExpired = tokenManager.isTokenExpired();
1119
+ console.log(`\u2705 Logged in: Yes`);
1120
+ if (accountEmail) {
1121
+ console.log(`\u{1F4E7} Email: ${maskEmail(accountEmail)}`);
1122
+ }
1123
+ if (expiresAt) {
1124
+ const expiryStr = expiresAt.toLocaleString();
1125
+ const status = isExpired ? " (expired/expiring soon)" : "";
1126
+ console.log(`\u23F0 Token expires: ${expiryStr}${status}`);
1127
+ }
1128
+ if (isDebugMode()) {
1129
+ const tokens = email ? getAccountManager().getTokens(email) : getAccountManager().getActiveTokens();
1130
+ if (tokens) {
1131
+ console.log();
1132
+ console.log("Debug info:");
1133
+ console.log(` Access token: ${maskToken(tokens.accessToken)}`);
1134
+ console.log(` Refresh token: ${maskToken(tokens.refreshToken)}`);
1135
+ }
1136
+ }
1137
+ console.log();
1138
+ }
1139
+ function showAllAccountsStatus() {
1140
+ const manager = getAccountManager();
1141
+ const emails = manager.getAccountEmails();
1142
+ const activeEmail = manager.getActiveEmail();
1143
+ console.log();
1144
+ console.log("\u{1F4CD} Antigravity Usage Status - All Accounts");
1145
+ console.log("\u2550".repeat(60));
1146
+ if (emails.length === 0) {
1147
+ warn("No accounts found.");
1148
+ console.log();
1149
+ info("Run `antigravity-usage login` to add an account.");
1150
+ console.log();
1151
+ return;
1152
+ }
1153
+ const table = new Table({
1154
+ head: ["Account", "Logged In", "Token Expiry"],
1155
+ style: {
1156
+ head: ["cyan"],
1157
+ border: ["gray"]
1158
+ },
1159
+ colWidths: [30, 12, 28]
1160
+ });
1161
+ for (const email of emails) {
1162
+ const tokenManager = getTokenManagerForAccount(email);
1163
+ const isActive = email === activeEmail;
1164
+ const nameDisplay = isActive ? `${email} [*]` : email;
1165
+ if (tokenManager.isLoggedIn()) {
1166
+ const expiresAt = tokenManager.getExpiresAt();
1167
+ const isExpired = tokenManager.isTokenExpired();
1168
+ let expiryDisplay = "-";
1169
+ if (expiresAt) {
1170
+ expiryDisplay = expiresAt.toLocaleString();
1171
+ if (isExpired) {
1172
+ expiryDisplay = `\u26A0\uFE0F ${expiryDisplay}`;
1173
+ }
1174
+ }
1175
+ table.push([
1176
+ nameDisplay,
1177
+ "\u2705",
1178
+ expiryDisplay
1179
+ ]);
1180
+ } else {
1181
+ table.push([
1182
+ nameDisplay,
1183
+ "\u274C",
1184
+ "Invalid or missing"
1185
+ ]);
1186
+ }
1187
+ }
1188
+ console.log(table.toString());
1189
+ console.log();
1190
+ console.log("[*] = active account");
1191
+ console.log();
1192
+ }
1193
+ function statusCommand(options = {}) {
1194
+ if (options.all) {
1195
+ showAllAccountsStatus();
1196
+ return;
1197
+ }
1198
+ if (options.account) {
1199
+ const manager = getAccountManager();
1200
+ if (!manager.hasAccount(options.account)) {
1201
+ warn(`Account '${options.account}' not found.`);
1202
+ return;
1203
+ }
1204
+ showSingleAccountStatus(options.account);
1205
+ return;
1206
+ }
1207
+ showSingleAccountStatus();
1208
+ }
1209
+
1210
+ // src/google/cloudcode.ts
1211
+ var BASE_URL = "https://cloudcode-pa.googleapis.com";
1212
+ var USER_AGENT = "antigravity/1.11.3 Darwin/arm64";
1213
+ var CloudCodeClient = class {
1214
+ constructor(tokenManager) {
1215
+ this.tokenManager = tokenManager;
1216
+ }
1217
+ projectId;
1218
+ /**
1219
+ * Make an authenticated API request
1220
+ */
1221
+ async request(endpoint, body) {
1222
+ const token = await this.tokenManager.getValidAccessToken();
1223
+ const url = `${BASE_URL}${endpoint}`;
1224
+ debug("cloudcode", `Calling ${endpoint}`);
1225
+ try {
1226
+ const response = await fetch(url, {
1227
+ method: "POST",
1228
+ headers: {
1229
+ "Authorization": `Bearer ${token}`,
1230
+ "Content-Type": "application/json",
1231
+ "User-Agent": USER_AGENT
1232
+ },
1233
+ body: body ? JSON.stringify(body) : void 0
1234
+ });
1235
+ debug("cloudcode", `Response status: ${response.status}`);
1236
+ if (response.status === 401 || response.status === 403) {
1237
+ const errorBody = await response.text();
1238
+ debug("cloudcode", `Auth error body: ${errorBody}`);
1239
+ throw new AuthenticationError("Authentication failed. Please run: antigravity-usage login");
1240
+ }
1241
+ if (response.status === 429) {
1242
+ const retryAfter = response.headers.get("retry-after");
1243
+ const retryMs = retryAfter ? parseInt(retryAfter) * 1e3 : void 0;
1244
+ throw new RateLimitError("Rate limited by Google API", retryMs);
1245
+ }
1246
+ if (response.status >= 500) {
1247
+ throw new APIError(`Server error: ${response.status}`, response.status);
1248
+ }
1249
+ if (!response.ok) {
1250
+ const errorText = await response.text();
1251
+ debug("cloudcode", "API error response", errorText);
1252
+ throw new APIError(`API request failed: ${response.status}`, response.status);
1253
+ }
1254
+ const data = await response.json();
1255
+ debug("cloudcode", "API call successful");
1256
+ return data;
1257
+ } catch (err) {
1258
+ if (err instanceof AuthenticationError || err instanceof RateLimitError || err instanceof APIError) {
1259
+ throw err;
1260
+ }
1261
+ if (err instanceof TypeError && err.message.includes("fetch")) {
1262
+ throw new NetworkError("Network error. Please check your connection.");
1263
+ }
1264
+ throw err;
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Load code assist status and plan info
1269
+ * Also extracts project ID for subsequent calls
1270
+ */
1271
+ async loadCodeAssist() {
1272
+ const response = await this.request("/v1internal:loadCodeAssist", {
1273
+ metadata: { ideType: "ANTIGRAVITY" }
1274
+ });
1275
+ if (response.cloudaicompanionProject) {
1276
+ this.projectId = response.cloudaicompanionProject;
1277
+ debug("cloudcode", `Project ID: ${this.projectId}`);
1278
+ }
1279
+ return response;
1280
+ }
1281
+ /**
1282
+ * Fetch available models with quota info
1283
+ * Requires project ID from loadCodeAssist
1284
+ */
1285
+ async fetchAvailableModels() {
1286
+ const body = this.projectId ? { project: this.projectId } : {};
1287
+ return this.request("/v1internal:fetchAvailableModels", body);
1288
+ }
1289
+ };
1290
+
1291
+ // src/google/parser.ts
1292
+ function parseResetTime(resetTime) {
1293
+ if (!resetTime) return void 0;
1294
+ try {
1295
+ const resetDate = new Date(resetTime);
1296
+ const now = Date.now();
1297
+ const diff = resetDate.getTime() - now;
1298
+ return diff > 0 ? diff : void 0;
1299
+ } catch {
1300
+ return void 0;
1301
+ }
1302
+ }
1303
+ function parseModelInfo(modelId, model) {
1304
+ const quotaInfo = model.quotaInfo;
1305
+ return {
1306
+ label: model.displayName || model.label || modelId,
1307
+ modelId,
1308
+ remainingPercentage: quotaInfo?.remainingFraction,
1309
+ isExhausted: quotaInfo?.isExhausted ?? quotaInfo?.remainingFraction === 0,
1310
+ resetTime: quotaInfo?.resetTime,
1311
+ timeUntilResetMs: parseResetTime(quotaInfo?.resetTime)
1312
+ };
1313
+ }
1314
+ function parsePromptCredits(response) {
1315
+ const monthly = response.planInfo?.monthlyPromptCredits;
1316
+ const available = response.availablePromptCredits;
1317
+ if (monthly === void 0 || available === void 0) {
1318
+ return void 0;
1319
+ }
1320
+ const used = monthly - available;
1321
+ const usedPercentage = monthly > 0 ? used / monthly : 0;
1322
+ const remainingPercentage = monthly > 0 ? available / monthly : 0;
1323
+ return {
1324
+ available,
1325
+ monthly,
1326
+ usedPercentage,
1327
+ remainingPercentage
1328
+ };
1329
+ }
1330
+ function shouldShowModel(modelId, model) {
1331
+ if (modelId.startsWith("chat_") || modelId.startsWith("tab_")) {
1332
+ return false;
1333
+ }
1334
+ if (modelId.includes("image")) {
1335
+ return false;
1336
+ }
1337
+ if (modelId.startsWith("rev")) {
1338
+ return false;
1339
+ }
1340
+ if (modelId.includes("mquery") || modelId.includes("lite")) {
1341
+ return false;
1342
+ }
1343
+ if (!model.quotaInfo) {
1344
+ return false;
1345
+ }
1346
+ return true;
1347
+ }
1348
+ function parseQuotaSnapshot(codeAssistResponse, modelsResponse, email) {
1349
+ debug("parser", "Parsing quota snapshot");
1350
+ const promptCredits = parsePromptCredits(codeAssistResponse);
1351
+ const planType = codeAssistResponse.planInfo?.planType;
1352
+ const modelsMap = modelsResponse.models || {};
1353
+ const models = [];
1354
+ for (const [modelId, modelInfo] of Object.entries(modelsMap)) {
1355
+ if (shouldShowModel(modelId, modelInfo)) {
1356
+ models.push(parseModelInfo(modelId, modelInfo));
1357
+ }
1358
+ }
1359
+ models.sort((a, b) => a.label.localeCompare(b.label));
1360
+ debug("parser", `Parsed ${models.length} models`);
1361
+ return {
1362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1363
+ method: "google",
1364
+ email,
1365
+ planType,
1366
+ promptCredits,
1367
+ models
1368
+ };
1369
+ }
1370
+
1371
+ // src/local/process-detector.ts
1372
+ import { exec } from "child_process";
1373
+ import { promisify } from "util";
1374
+ var execAsync = promisify(exec);
1375
+ async function detectAntigravityProcess() {
1376
+ const platform2 = process.platform;
1377
+ debug("process-detector", `Detecting Antigravity process on platform: ${platform2}`);
1378
+ if (platform2 === "win32") {
1379
+ return detectOnWindows();
1380
+ } else {
1381
+ return detectOnUnix();
1382
+ }
1383
+ }
1384
+ async function detectOnUnix() {
1385
+ try {
1386
+ const { stdout } = await execAsync("ps aux");
1387
+ const lines = stdout.split("\n");
1388
+ for (const line of lines) {
1389
+ if (line.toLowerCase().includes("antigravity") && (line.includes("language-server") || line.includes("lsp") || line.includes("server"))) {
1390
+ debug("process-detector", `Found potential Antigravity process: ${line}`);
1391
+ const processInfo = parseUnixProcessLine(line);
1392
+ if (processInfo) {
1393
+ return processInfo;
1394
+ }
1395
+ }
1396
+ }
1397
+ debug("process-detector", "No Antigravity process found");
1398
+ return null;
1399
+ } catch (err) {
1400
+ debug("process-detector", "Error detecting process on Unix", err);
1401
+ return null;
1402
+ }
1403
+ }
1404
+ function parseUnixProcessLine(line) {
1405
+ const parts = line.trim().split(/\s+/);
1406
+ if (parts.length < 11) {
1407
+ return null;
1408
+ }
1409
+ const pid = parseInt(parts[1], 10);
1410
+ if (isNaN(pid)) {
1411
+ return null;
1412
+ }
1413
+ const commandLine = parts.slice(10).join(" ");
1414
+ const csrfToken = extractArgument(commandLine, "--csrf_token");
1415
+ const extensionServerPort = extractArgument(commandLine, "--extension_server_port");
1416
+ return {
1417
+ pid,
1418
+ csrfToken: csrfToken || void 0,
1419
+ extensionServerPort: extensionServerPort ? parseInt(extensionServerPort, 10) : void 0,
1420
+ commandLine
1421
+ };
1422
+ }
1423
+ async function detectOnWindows() {
1424
+ try {
1425
+ const { stdout } = await execAsync(
1426
+ `wmic process where "name like '%antigravity%' or commandline like '%antigravity%'" get processid,commandline /format:csv`,
1427
+ { maxBuffer: 10 * 1024 * 1024 }
1428
+ // 10MB buffer for long command lines
1429
+ );
1430
+ const lines = stdout.split("\n").filter((line) => line.trim() && !line.includes("Node,CommandLine,ProcessId"));
1431
+ for (const line of lines) {
1432
+ const parts = line.split(",");
1433
+ if (parts.length >= 3) {
1434
+ const commandLine = parts.slice(1, -1).join(",");
1435
+ const pid = parseInt(parts[parts.length - 1].trim(), 10);
1436
+ if (!isNaN(pid) && commandLine.toLowerCase().includes("antigravity")) {
1437
+ debug("process-detector", `Found Antigravity process on Windows: PID ${pid}`);
1438
+ const csrfToken = extractArgument(commandLine, "--csrf_token");
1439
+ const extensionServerPort = extractArgument(commandLine, "--extension_server_port");
1440
+ return {
1441
+ pid,
1442
+ csrfToken: csrfToken || void 0,
1443
+ extensionServerPort: extensionServerPort ? parseInt(extensionServerPort, 10) : void 0,
1444
+ commandLine
1445
+ };
1446
+ }
1447
+ }
1448
+ }
1449
+ return await detectOnWindowsPowerShell();
1450
+ } catch (err) {
1451
+ debug("process-detector", "Error detecting process on Windows with WMIC, trying PowerShell", err);
1452
+ return await detectOnWindowsPowerShell();
1453
+ }
1454
+ }
1455
+ async function detectOnWindowsPowerShell() {
1456
+ try {
1457
+ const { stdout } = await execAsync(
1458
+ `powershell -Command "Get-Process | Where-Object { $_.ProcessName -like '*antigravity*' } | Select-Object Id, ProcessName | ConvertTo-Json"`
1459
+ );
1460
+ if (!stdout.trim()) {
1461
+ return null;
1462
+ }
1463
+ const processes = JSON.parse(stdout);
1464
+ const processList = Array.isArray(processes) ? processes : [processes];
1465
+ for (const proc of processList) {
1466
+ if (proc.Id) {
1467
+ const { stdout: cmdLine } = await execAsync(
1468
+ `powershell -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId = ${proc.Id}').CommandLine"`
1469
+ );
1470
+ const commandLine = cmdLine.trim();
1471
+ const csrfToken = extractArgument(commandLine, "--csrf_token");
1472
+ const extensionServerPort = extractArgument(commandLine, "--extension_server_port");
1473
+ return {
1474
+ pid: proc.Id,
1475
+ csrfToken: csrfToken || void 0,
1476
+ extensionServerPort: extensionServerPort ? parseInt(extensionServerPort, 10) : void 0,
1477
+ commandLine
1478
+ };
1479
+ }
1480
+ }
1481
+ return null;
1482
+ } catch (err) {
1483
+ debug("process-detector", "Error detecting process on Windows with PowerShell", err);
1484
+ return null;
1485
+ }
1486
+ }
1487
+ function extractArgument(commandLine, argName) {
1488
+ const eqRegex = new RegExp(`${argName}=([^\\s"']+|"[^"]*"|'[^']*')`, "i");
1489
+ const eqMatch = commandLine.match(eqRegex);
1490
+ if (eqMatch) {
1491
+ return eqMatch[1].replace(/^["']|["']$/g, "");
1492
+ }
1493
+ const spaceRegex = new RegExp(`${argName}\\s+([^\\s"']+|"[^"]*"|'[^']*')`, "i");
1494
+ const spaceMatch = commandLine.match(spaceRegex);
1495
+ if (spaceMatch) {
1496
+ return spaceMatch[1].replace(/^["']|["']$/g, "");
1497
+ }
1498
+ return null;
1499
+ }
1500
+
1501
+ // src/local/port-detective.ts
1502
+ import { exec as exec2 } from "child_process";
1503
+ import { promisify as promisify2 } from "util";
1504
+ var execAsync2 = promisify2(exec2);
1505
+ async function discoverPorts(pid) {
1506
+ const platform2 = process.platform;
1507
+ debug("port-detective", `Discovering ports for PID ${pid} on platform: ${platform2}`);
1508
+ if (platform2 === "win32") {
1509
+ return discoverPortsOnWindows(pid);
1510
+ } else if (platform2 === "darwin") {
1511
+ return discoverPortsOnMacOS(pid);
1512
+ } else {
1513
+ return discoverPortsOnLinux(pid);
1514
+ }
1515
+ }
1516
+ async function discoverPortsOnMacOS(pid) {
1517
+ try {
1518
+ const { stdout } = await execAsync2(`lsof -nP -iTCP -sTCP:LISTEN -a -p ${pid}`);
1519
+ const ports = [];
1520
+ const lines = stdout.split("\n");
1521
+ for (const line of lines) {
1522
+ const match = line.match(/:(\d+)\s+\(LISTEN\)/);
1523
+ if (match) {
1524
+ const port = parseInt(match[1], 10);
1525
+ if (!isNaN(port) && !ports.includes(port)) {
1526
+ ports.push(port);
1527
+ }
1528
+ }
1529
+ }
1530
+ debug("port-detective", `Found ports on macOS: ${ports.join(", ")}`);
1531
+ return ports;
1532
+ } catch (err) {
1533
+ debug("port-detective", "Error discovering ports on macOS", err);
1534
+ return [];
1535
+ }
1536
+ }
1537
+ async function discoverPortsOnLinux(pid) {
1538
+ try {
1539
+ const { stdout } = await execAsync2(`ss -tlnp | grep "pid=${pid},"`);
1540
+ const ports = [];
1541
+ const lines = stdout.split("\n");
1542
+ for (const line of lines) {
1543
+ const match = line.match(/:(\d+)\s/);
1544
+ if (match) {
1545
+ const port = parseInt(match[1], 10);
1546
+ if (!isNaN(port) && !ports.includes(port)) {
1547
+ ports.push(port);
1548
+ }
1549
+ }
1550
+ }
1551
+ if (ports.length > 0) {
1552
+ debug("port-detective", `Found ports on Linux (ss): ${ports.join(", ")}`);
1553
+ return ports;
1554
+ }
1555
+ return await discoverPortsOnLinuxNetstat(pid);
1556
+ } catch {
1557
+ return await discoverPortsOnLinuxNetstat(pid);
1558
+ }
1559
+ }
1560
+ async function discoverPortsOnLinuxNetstat(pid) {
1561
+ try {
1562
+ const { stdout } = await execAsync2(`netstat -tlnp 2>/dev/null | grep "${pid}/"`);
1563
+ const ports = [];
1564
+ const lines = stdout.split("\n");
1565
+ for (const line of lines) {
1566
+ const match = line.match(/:(\d+)\s/);
1567
+ if (match) {
1568
+ const port = parseInt(match[1], 10);
1569
+ if (!isNaN(port) && !ports.includes(port)) {
1570
+ ports.push(port);
1571
+ }
1572
+ }
1573
+ }
1574
+ debug("port-detective", `Found ports on Linux (netstat): ${ports.join(", ")}`);
1575
+ return ports;
1576
+ } catch (err) {
1577
+ debug("port-detective", "Error discovering ports on Linux", err);
1578
+ return [];
1579
+ }
1580
+ }
1581
+ async function discoverPortsOnWindows(pid) {
1582
+ try {
1583
+ const { stdout } = await execAsync2("netstat -ano");
1584
+ const ports = [];
1585
+ const lines = stdout.split("\n");
1586
+ for (const line of lines) {
1587
+ if (line.includes("LISTENING")) {
1588
+ const parts = line.trim().split(/\s+/);
1589
+ const linePid = parseInt(parts[parts.length - 1], 10);
1590
+ if (linePid === pid) {
1591
+ const localAddr = parts[1];
1592
+ const portMatch = localAddr.match(/:(\d+)$/);
1593
+ if (portMatch) {
1594
+ const port = parseInt(portMatch[1], 10);
1595
+ if (!isNaN(port) && !ports.includes(port)) {
1596
+ ports.push(port);
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ debug("port-detective", `Found ports on Windows: ${ports.join(", ")}`);
1603
+ return ports;
1604
+ } catch (err) {
1605
+ debug("port-detective", "Error discovering ports on Windows", err);
1606
+ return [];
1607
+ }
1608
+ }
1609
+
1610
+ // src/local/port-prober.ts
1611
+ import https from "https";
1612
+ import http from "http";
1613
+ async function probeForConnectAPI(ports, csrfToken, timeout = 500) {
1614
+ debug("port-prober", `Probing ${ports.length} ports: ${ports.join(", ")}`);
1615
+ const probePromises = ports.map((port) => probePort(port, csrfToken, timeout));
1616
+ const results = await Promise.allSettled(probePromises);
1617
+ for (let i = 0; i < results.length; i++) {
1618
+ const result = results[i];
1619
+ if (result.status === "fulfilled" && result.value) {
1620
+ debug("port-prober", `Found working endpoint: ${result.value.baseUrl}`);
1621
+ return result.value;
1622
+ }
1623
+ }
1624
+ debug("port-prober", "No working Connect API endpoint found");
1625
+ return null;
1626
+ }
1627
+ async function probePort(port, csrfToken, timeout = 500) {
1628
+ const httpsResult = await probeHttps(port, timeout, csrfToken);
1629
+ if (httpsResult) {
1630
+ return httpsResult;
1631
+ }
1632
+ const httpResult = await probeHttp(port, timeout);
1633
+ if (httpResult) {
1634
+ return httpResult;
1635
+ }
1636
+ return null;
1637
+ }
1638
+ function probeHttps(port, timeout, csrfToken) {
1639
+ return new Promise((resolve) => {
1640
+ const options = {
1641
+ hostname: "127.0.0.1",
1642
+ port,
1643
+ path: "/exa.language_server_pb.LanguageServerService/GetUnleashData",
1644
+ method: "POST",
1645
+ timeout,
1646
+ rejectUnauthorized: false,
1647
+ // Allow self-signed certificates
1648
+ headers: {
1649
+ "Content-Type": "application/json",
1650
+ "Connect-Protocol-Version": "1",
1651
+ ...csrfToken ? { "X-Codeium-Csrf-Token": csrfToken } : {}
1652
+ }
1653
+ };
1654
+ const req = https.request(options, (res) => {
1655
+ if (res.statusCode === 200) {
1656
+ debug("port-prober", `HTTPS Connect RPC probe on port ${port}: status ${res.statusCode} - valid connect port`);
1657
+ resolve({
1658
+ baseUrl: `https://127.0.0.1:${port}`,
1659
+ protocol: "https",
1660
+ port
1661
+ });
1662
+ } else {
1663
+ debug("port-prober", `HTTPS probe on port ${port}: status ${res.statusCode} - not connect port`);
1664
+ resolve(null);
1665
+ }
1666
+ res.resume();
1667
+ });
1668
+ req.on("error", (err) => {
1669
+ debug("port-prober", `HTTPS probe on port ${port} failed: ${err.message}`);
1670
+ resolve(null);
1671
+ });
1672
+ req.on("timeout", () => {
1673
+ debug("port-prober", `HTTPS probe on port ${port} timed out`);
1674
+ req.destroy();
1675
+ resolve(null);
1676
+ });
1677
+ req.write(JSON.stringify({ wrapper_data: {} }));
1678
+ req.end();
1679
+ });
1680
+ }
1681
+ function probeHttp(port, timeout) {
1682
+ return new Promise((resolve) => {
1683
+ const options = {
1684
+ hostname: "localhost",
1685
+ port,
1686
+ path: "/",
1687
+ method: "GET",
1688
+ timeout
1689
+ };
1690
+ const req = http.request(options, (res) => {
1691
+ debug("port-prober", `HTTP probe on port ${port}: status ${res.statusCode}`);
1692
+ resolve({
1693
+ baseUrl: `http://localhost:${port}`,
1694
+ protocol: "http",
1695
+ port
1696
+ });
1697
+ res.resume();
1698
+ });
1699
+ req.on("error", (err) => {
1700
+ debug("port-prober", `HTTP probe on port ${port} failed: ${err.message}`);
1701
+ resolve(null);
1702
+ });
1703
+ req.on("timeout", () => {
1704
+ debug("port-prober", `HTTP probe on port ${port} timed out`);
1705
+ req.destroy();
1706
+ resolve(null);
1707
+ });
1708
+ req.end();
1709
+ });
1710
+ }
1711
+
1712
+ // src/local/connect-client.ts
1713
+ import https2 from "https";
1714
+ import http2 from "http";
1715
+ var ConnectClient = class {
1716
+ baseUrl;
1717
+ csrfToken;
1718
+ isHttps;
1719
+ constructor(baseUrl, csrfToken) {
1720
+ this.baseUrl = baseUrl;
1721
+ this.csrfToken = csrfToken;
1722
+ this.isHttps = baseUrl.startsWith("https://");
1723
+ debug("connect-client", `Initialized with baseUrl: ${baseUrl}, hasToken: ${!!csrfToken}`);
1724
+ }
1725
+ /**
1726
+ * Get user status including quota information
1727
+ * Uses Connect RPC protocol to communicate with Antigravity language server
1728
+ */
1729
+ async getUserStatus() {
1730
+ debug("connect-client", "Fetching user status via Connect RPC");
1731
+ const endpoint = "/exa.language_server_pb.LanguageServerService/GetUserStatus";
1732
+ try {
1733
+ const response = await this.request("POST", endpoint, {
1734
+ metadata: {
1735
+ ideName: "antigravity",
1736
+ extensionName: "antigravity",
1737
+ locale: "en"
1738
+ }
1739
+ });
1740
+ if (response) {
1741
+ debug("connect-client", `Got response from ${endpoint}`);
1742
+ return this.parseUserStatus(response);
1743
+ }
1744
+ } catch (err) {
1745
+ debug("connect-client", `Connect RPC call failed: ${err}`);
1746
+ throw new Error(`Failed to fetch user status: ${err instanceof Error ? err.message : "Unknown error"}`);
1747
+ }
1748
+ throw new Error("Could not fetch user status from Connect RPC endpoint");
1749
+ }
1750
+ /**
1751
+ * Make an HTTP(S) request to the Connect API
1752
+ */
1753
+ request(method, path, body) {
1754
+ return new Promise((resolve, reject) => {
1755
+ const url = new URL(path, this.baseUrl);
1756
+ const headers = {
1757
+ "Accept": "application/json",
1758
+ "Content-Type": "application/json",
1759
+ "Connect-Protocol-Version": "1"
1760
+ };
1761
+ if (this.csrfToken) {
1762
+ headers["X-Codeium-Csrf-Token"] = this.csrfToken;
1763
+ }
1764
+ const options = {
1765
+ hostname: url.hostname,
1766
+ port: url.port,
1767
+ path: url.pathname,
1768
+ method,
1769
+ headers,
1770
+ timeout: 5e3,
1771
+ rejectUnauthorized: false
1772
+ // Allow self-signed certificates
1773
+ };
1774
+ const protocol = this.isHttps ? https2 : http2;
1775
+ const req = protocol.request(options, (res) => {
1776
+ let data = "";
1777
+ res.on("data", (chunk) => {
1778
+ data += chunk;
1779
+ });
1780
+ res.on("end", () => {
1781
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
1782
+ try {
1783
+ const parsed = JSON.parse(data);
1784
+ resolve(parsed);
1785
+ } catch {
1786
+ resolve(data);
1787
+ }
1788
+ } else if (res.statusCode === 404) {
1789
+ reject(new Error(`Endpoint not found: ${path}`));
1790
+ } else {
1791
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
1792
+ }
1793
+ });
1794
+ });
1795
+ req.on("error", (err) => {
1796
+ reject(err);
1797
+ });
1798
+ req.on("timeout", () => {
1799
+ req.destroy();
1800
+ reject(new Error("Request timed out"));
1801
+ });
1802
+ if (body) {
1803
+ req.write(JSON.stringify(body));
1804
+ }
1805
+ req.end();
1806
+ });
1807
+ }
1808
+ /**
1809
+ * Parse raw API response into ConnectUserStatus
1810
+ */
1811
+ parseUserStatus(response) {
1812
+ debug("connect-client", "Raw response:", JSON.stringify(response, null, 2));
1813
+ const status = {
1814
+ raw: response
1815
+ };
1816
+ if (typeof response !== "object" || response === null) {
1817
+ return status;
1818
+ }
1819
+ const data = response;
1820
+ const userStatus = data.userStatus || data;
1821
+ if ("email" in userStatus && typeof userStatus.email === "string") {
1822
+ status.email = userStatus.email;
1823
+ }
1824
+ if ("isAuthenticated" in userStatus) {
1825
+ status.isAuthenticated = Boolean(userStatus.isAuthenticated);
1826
+ }
1827
+ status.quota = this.extractQuota(userStatus);
1828
+ return status;
1829
+ }
1830
+ /**
1831
+ * Extract quota information from response
1832
+ */
1833
+ extractQuota(data) {
1834
+ const quota = {};
1835
+ const planStatus = data.planStatus;
1836
+ if (planStatus) {
1837
+ const available = planStatus.availablePromptCredits;
1838
+ const planInfo = planStatus.planInfo;
1839
+ const monthly = planInfo?.monthlyPromptCredits;
1840
+ if (typeof available === "number" && typeof monthly === "number") {
1841
+ const used = monthly - available;
1842
+ quota.promptCredits = {
1843
+ used,
1844
+ limit: monthly,
1845
+ remaining: available
1846
+ };
1847
+ }
1848
+ }
1849
+ const cascadeData = data.cascadeModelConfigData;
1850
+ const clientModelConfigs = cascadeData?.clientModelConfigs;
1851
+ if (Array.isArray(clientModelConfigs)) {
1852
+ quota.models = clientModelConfigs.map(this.parseModel.bind(this));
1853
+ }
1854
+ return quota;
1855
+ }
1856
+ /**
1857
+ * Parse a single model from the response
1858
+ */
1859
+ parseModel(model) {
1860
+ if (typeof model !== "object" || model === null) {
1861
+ return {
1862
+ modelId: "unknown",
1863
+ isExhausted: false
1864
+ };
1865
+ }
1866
+ const m = model;
1867
+ const modelOrAlias = m.modelOrAlias;
1868
+ const modelId = typeof modelOrAlias?.model === "string" ? modelOrAlias.model : "unknown";
1869
+ const quotaInfo = m.quotaInfo;
1870
+ const remainingFraction = typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : void 0;
1871
+ const resetTime = typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0;
1872
+ return {
1873
+ modelId,
1874
+ displayName: typeof m.label === "string" ? m.label : void 0,
1875
+ label: typeof m.label === "string" ? m.label : void 0,
1876
+ quota: {
1877
+ remaining: void 0,
1878
+ limit: void 0,
1879
+ usedPercentage: remainingFraction !== void 0 ? 1 - remainingFraction : void 0,
1880
+ remainingPercentage: remainingFraction,
1881
+ resetTime,
1882
+ timeUntilResetMs: resetTime ? this.parseResetTime(resetTime) : void 0
1883
+ },
1884
+ isExhausted: remainingFraction === 0
1885
+ };
1886
+ }
1887
+ /**
1888
+ * Parse reset time to milliseconds until reset
1889
+ */
1890
+ parseResetTime(resetTime) {
1891
+ try {
1892
+ const resetDate = new Date(resetTime);
1893
+ const now = Date.now();
1894
+ const diff = resetDate.getTime() - now;
1895
+ return diff > 0 ? diff : void 0;
1896
+ } catch {
1897
+ return void 0;
1898
+ }
1899
+ }
1900
+ };
1901
+
1902
+ // src/local/local-parser.ts
1903
+ function parseLocalQuotaSnapshot(userStatus) {
1904
+ debug("local-parser", "Parsing local user status into QuotaSnapshot");
1905
+ const snapshot = {
1906
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1907
+ method: "local",
1908
+ email: userStatus.email,
1909
+ models: []
1910
+ };
1911
+ if (userStatus.quota?.promptCredits) {
1912
+ snapshot.promptCredits = parsePromptCredits2(userStatus.quota.promptCredits);
1913
+ }
1914
+ if (userStatus.quota?.models) {
1915
+ snapshot.models = userStatus.quota.models.map(parseModelQuota);
1916
+ }
1917
+ debug("local-parser", `Parsed ${snapshot.models.length} models`);
1918
+ return snapshot;
1919
+ }
1920
+ function parsePromptCredits2(credits) {
1921
+ if (!credits) {
1922
+ return void 0;
1923
+ }
1924
+ const limit = credits.limit ?? 0;
1925
+ const remaining = credits.remaining ?? limit;
1926
+ const used = credits.used ?? limit - remaining;
1927
+ if (limit === 0) {
1928
+ return void 0;
1929
+ }
1930
+ const usedPercentage = limit > 0 ? used / limit : 0;
1931
+ const remainingPercentage = limit > 0 ? remaining / limit : 1;
1932
+ return {
1933
+ available: remaining,
1934
+ monthly: limit,
1935
+ usedPercentage,
1936
+ remainingPercentage
1937
+ };
1938
+ }
1939
+ function parseModelQuota(model) {
1940
+ const quota = model.quota;
1941
+ return {
1942
+ label: model.label || model.displayName || model.modelId,
1943
+ modelId: model.modelId,
1944
+ remainingPercentage: quota?.remainingPercentage,
1945
+ isExhausted: model.isExhausted ?? quota?.remainingPercentage === 0,
1946
+ resetTime: quota?.resetTime,
1947
+ timeUntilResetMs: quota?.timeUntilResetMs
1948
+ };
1949
+ }
1950
+
1951
+ // src/quota/service.ts
1952
+ async function fetchQuota(method = "auto") {
1953
+ if (method === "auto") {
1954
+ try {
1955
+ debug("service", "Auto mode: trying local method first");
1956
+ return await fetchQuotaLocal();
1957
+ } catch (err) {
1958
+ debug("service", "Auto mode: local method failed", err);
1959
+ const tokenManager = getTokenManager();
1960
+ if (tokenManager.isLoggedIn()) {
1961
+ debug("service", "User is logged in, falling back to Google method");
1962
+ return fetchQuotaGoogle();
1963
+ }
1964
+ throw new NoAuthMethodAvailableError();
1965
+ }
1966
+ }
1967
+ if (method === "local") {
1968
+ return fetchQuotaLocal();
1969
+ }
1970
+ return fetchQuotaGoogle();
1971
+ }
1972
+ async function fetchQuotaGoogle() {
1973
+ debug("service", "Fetching quota from Google");
1974
+ const tokenManager = getTokenManager();
1975
+ const email = tokenManager.getEmail();
1976
+ const client = new CloudCodeClient(tokenManager);
1977
+ const codeAssistResponse = await client.loadCodeAssist();
1978
+ debug("service", "Code assist response received", JSON.stringify(codeAssistResponse));
1979
+ let modelsResponse = {};
1980
+ try {
1981
+ modelsResponse = await client.fetchAvailableModels();
1982
+ debug("service", "Models response received", JSON.stringify(modelsResponse));
1983
+ } catch (err) {
1984
+ debug("service", "Failed to fetch models (might need different permissions)", err);
1985
+ }
1986
+ const snapshot = parseQuotaSnapshot(codeAssistResponse, modelsResponse, email);
1987
+ debug("service", "Quota snapshot created");
1988
+ return snapshot;
1989
+ }
1990
+ async function fetchQuotaLocal() {
1991
+ debug("service", "Fetching quota from local Antigravity server");
1992
+ const processInfo = await detectAntigravityProcess();
1993
+ if (!processInfo) {
1994
+ throw new AntigravityNotRunningError();
1995
+ }
1996
+ debug("service", `Found Antigravity process: PID ${processInfo.pid}`);
1997
+ const ports = await discoverPorts(processInfo.pid);
1998
+ if (ports.length === 0) {
1999
+ throw new PortDetectionError();
2000
+ }
2001
+ debug("service", `Discovered ${ports.length} listening ports: ${ports.join(", ")}`);
2002
+ const probeResult = await probeForConnectAPI(ports, processInfo.csrfToken);
2003
+ if (!probeResult) {
2004
+ throw new LocalConnectionError("Could not find Antigravity Connect API on any port");
2005
+ }
2006
+ debug("service", `Found Connect API at ${probeResult.baseUrl}`);
2007
+ const client = new ConnectClient(probeResult.baseUrl, processInfo.csrfToken);
2008
+ const userStatus = await client.getUserStatus();
2009
+ debug("service", "User status received from local server");
2010
+ const snapshot = parseLocalQuotaSnapshot(userStatus);
2011
+ debug("service", "Local quota snapshot created");
2012
+ return snapshot;
2013
+ }
2014
+
2015
+ // src/quota/format.ts
2016
+ import Table2 from "cli-table3";
2017
+ function formatTimeUntilReset(ms) {
2018
+ if (ms === void 0 || ms <= 0) return "N/A";
2019
+ const hours = Math.floor(ms / (1e3 * 60 * 60));
2020
+ const minutes = Math.floor(ms % (1e3 * 60 * 60) / (1e3 * 60));
2021
+ if (hours > 0) {
2022
+ return `${hours}h ${minutes}m`;
2023
+ }
2024
+ return `${minutes}m`;
2025
+ }
2026
+ function formatRemaining(model) {
2027
+ if (model.isExhausted) {
2028
+ return "\u274C EXHAUSTED";
2029
+ }
2030
+ if (model.remainingPercentage === void 0) {
2031
+ return "N/A";
2032
+ }
2033
+ const pct = Math.round(model.remainingPercentage * 100);
2034
+ if (pct >= 75) return `\u{1F7E2} ${pct}%`;
2035
+ if (pct >= 50) return `\u{1F7E1} ${pct}%`;
2036
+ if (pct >= 25) return `\u{1F7E0} ${pct}%`;
2037
+ return `\u{1F534} ${pct}%`;
2038
+ }
2039
+ function printQuotaTable(snapshot) {
2040
+ const timestamp = new Date(snapshot.timestamp).toLocaleString();
2041
+ console.log();
2042
+ console.log(`\u{1F4CA} Antigravity Quota Status (via ${snapshot.method.toUpperCase()})`);
2043
+ console.log(` Retrieved: ${timestamp}`);
2044
+ if (snapshot.email || snapshot.planType) {
2045
+ const userParts = [];
2046
+ if (snapshot.email) {
2047
+ userParts.push(`\u{1F464} ${snapshot.email}`);
2048
+ }
2049
+ if (snapshot.planType) {
2050
+ userParts.push(`\u{1F4CB} Plan: ${snapshot.planType}`);
2051
+ }
2052
+ console.log(` ${userParts.join(" | ")}`);
2053
+ }
2054
+ console.log();
2055
+ if (snapshot.models.length > 0) {
2056
+ const table = new Table2({
2057
+ head: ["Model", "Remaining", "Resets In"],
2058
+ style: {
2059
+ head: ["cyan"],
2060
+ border: ["gray"]
2061
+ }
2062
+ });
2063
+ for (const model of snapshot.models) {
2064
+ table.push([
2065
+ model.label,
2066
+ formatRemaining(model),
2067
+ formatTimeUntilReset(model.timeUntilResetMs)
2068
+ ]);
2069
+ }
2070
+ console.log(table.toString());
2071
+ } else {
2072
+ console.log("No model quota information available.");
2073
+ }
2074
+ console.log();
2075
+ }
2076
+ function printQuotaJson(snapshot) {
2077
+ console.log(JSON.stringify(snapshot, null, 2));
2078
+ }
2079
+
2080
+ // src/render/table.ts
2081
+ import Table3 from "cli-table3";
2082
+ function formatRelativeTime(isoDate) {
2083
+ if (!isoDate) return "Never";
2084
+ const date = new Date(isoDate);
2085
+ const now = Date.now();
2086
+ const diffMs = now - date.getTime();
2087
+ const minutes = Math.floor(diffMs / (1e3 * 60));
2088
+ const hours = Math.floor(diffMs / (1e3 * 60 * 60));
2089
+ const days = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
2090
+ if (minutes < 1) return "Just now";
2091
+ if (minutes < 60) return `${minutes}m ago`;
2092
+ if (hours < 24) return `${hours}h ago`;
2093
+ if (days === 1) return "Yesterday";
2094
+ return `${days} days ago`;
2095
+ }
2096
+ function formatStatus(status) {
2097
+ switch (status) {
2098
+ case "valid":
2099
+ return "\u2705";
2100
+ case "expired":
2101
+ return "\u26A0\uFE0F";
2102
+ case "invalid":
2103
+ return "\u274C";
2104
+ default:
2105
+ return "\u2753";
2106
+ }
2107
+ }
2108
+ function formatCredits(credits) {
2109
+ if (!credits) return "-";
2110
+ return `${credits.limit - credits.used} / ${credits.limit}`;
2111
+ }
2112
+ function renderAccountsTable(accounts) {
2113
+ if (accounts.length === 0) {
2114
+ console.log("\n\u{1F4ED} No accounts found.");
2115
+ console.log("\n\u{1F4A1} Run `antigravity-usage login` to add an account.\n");
2116
+ return;
2117
+ }
2118
+ console.log("\n\u{1F4CA} Antigravity Accounts");
2119
+ console.log("\u2550".repeat(60));
2120
+ const totalWidth = process.stdout.columns || 80;
2121
+ const isSmallTerminal = totalWidth < 90;
2122
+ const colWidths = isSmallTerminal ? [25, 8, 12, 12] : [30, 10, 15, 15];
2123
+ const finalColWidths = totalWidth < 60 ? void 0 : colWidths;
2124
+ const tableOptions = {
2125
+ head: ["Account", "Status", "Credits", "Last Used"],
2126
+ style: {
2127
+ head: ["cyan"],
2128
+ border: ["gray"]
2129
+ }
2130
+ };
2131
+ if (finalColWidths) {
2132
+ tableOptions.colWidths = finalColWidths;
2133
+ }
2134
+ const table = new Table3(tableOptions);
2135
+ for (const account of accounts) {
2136
+ const nameDisplay = account.isActive ? `${account.email} [*]` : account.email;
2137
+ table.push([
2138
+ nameDisplay,
2139
+ formatStatus(account.status),
2140
+ formatCredits(account.cachedCredits),
2141
+ formatRelativeTime(account.lastUsed)
2142
+ ]);
2143
+ }
2144
+ console.log(table.toString());
2145
+ console.log("\n[*] = active account\n");
2146
+ }
2147
+ function formatQuotaRemainingBar(remainingPercentage) {
2148
+ const width = 10;
2149
+ const filled = Math.round(remainingPercentage / 100 * width);
2150
+ const empty = width - filled;
2151
+ const filledChar = "\u2588";
2152
+ const emptyChar = "\u2591";
2153
+ return `${filledChar.repeat(filled)}${emptyChar.repeat(empty)} ${Math.round(remainingPercentage)}%`;
2154
+ }
2155
+ function renderAllQuotaTable(results) {
2156
+ if (results.length === 0) {
2157
+ console.log("\n\u{1F4ED} No accounts found.");
2158
+ console.log("\n\u{1F4A1} Run `antigravity-usage login` to add an account.\n");
2159
+ return;
2160
+ }
2161
+ const sortedResults = [...results].sort((a, b) => {
2162
+ if (a.status === "error" && b.status !== "error") return 1;
2163
+ if (a.status !== "error" && b.status === "error") return -1;
2164
+ if (a.status === "error" && b.status === "error") return 0;
2165
+ const getRemaining = (result) => {
2166
+ const firstModel = result.snapshot?.models?.[0];
2167
+ if (!firstModel) return -1;
2168
+ if (firstModel.isExhausted) return 0;
2169
+ return firstModel.remainingPercentage ?? -1;
2170
+ };
2171
+ const aRemaining = getRemaining(a);
2172
+ const bRemaining = getRemaining(b);
2173
+ return bRemaining - aRemaining;
2174
+ });
2175
+ console.log("\n\u{1F4CA} Quota Overview - All Accounts");
2176
+ console.log("\u2550".repeat(70));
2177
+ const totalWidth = process.stdout.columns || 80;
2178
+ let colWidths;
2179
+ if (totalWidth < 80) {
2180
+ colWidths = void 0;
2181
+ } else if (totalWidth < 100) {
2182
+ colWidths = [25, 8, 12, 18];
2183
+ } else {
2184
+ colWidths = [30, 10, 15, 20];
2185
+ }
2186
+ const tableOptions = {
2187
+ head: ["Account", "Source", "Credits", "Quota Remaining"],
2188
+ style: {
2189
+ head: ["cyan"],
2190
+ border: ["gray"]
2191
+ }
2192
+ };
2193
+ if (colWidths) {
2194
+ tableOptions.colWidths = colWidths;
2195
+ }
2196
+ const table = new Table3(tableOptions);
2197
+ const errors = [];
2198
+ for (const result of sortedResults) {
2199
+ const nameDisplay = result.isActive ? `${result.email} [*]` : result.email;
2200
+ if (result.status === "error") {
2201
+ table.push([
2202
+ nameDisplay,
2203
+ "-",
2204
+ "-",
2205
+ result.error || "Error"
2206
+ ]);
2207
+ errors.push(`${result.email}: ${result.error}`);
2208
+ } else {
2209
+ const snapshot = result.snapshot;
2210
+ const source = result.status === "cached" ? `Cached (${formatCacheAge(result.cacheAge)})` : snapshot?.method.toUpperCase() || "-";
2211
+ let credits = "-";
2212
+ if (snapshot?.promptCredits) {
2213
+ const pc = snapshot.promptCredits;
2214
+ credits = `${pc.available} / ${pc.monthly}`;
2215
+ }
2216
+ let quotaRemaining = "-";
2217
+ if (snapshot?.models && snapshot.models.length > 0) {
2218
+ const minRemaining = Math.min(
2219
+ ...snapshot.models.filter((m) => m.remainingPercentage !== void 0).map((m) => m.remainingPercentage)
2220
+ );
2221
+ if (isFinite(minRemaining)) {
2222
+ const remainingPct = minRemaining * 100;
2223
+ quotaRemaining = formatQuotaRemainingBar(remainingPct);
2224
+ } else if (snapshot.models.some((m) => m.isExhausted)) {
2225
+ quotaRemaining = "\u274C EXHAUSTED";
2226
+ }
2227
+ }
2228
+ table.push([
2229
+ nameDisplay,
2230
+ source,
2231
+ credits,
2232
+ quotaRemaining
2233
+ ]);
2234
+ }
2235
+ }
2236
+ console.log(table.toString());
2237
+ if (errors.length > 0) {
2238
+ console.log(`
2239
+ \u26A0\uFE0F ${errors.length} account(s) had errors:`);
2240
+ for (const err of errors) {
2241
+ console.log(` - ${err}`);
2242
+ }
2243
+ }
2244
+ console.log("\n[*] = active account");
2245
+ console.log("\u{1F4A1} Use --refresh to fetch latest data\n");
2246
+ }
2247
+ function formatCacheAge(seconds) {
2248
+ if (seconds === void 0) return "?";
2249
+ if (seconds < 60) return `${seconds}s`;
2250
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
2251
+ return `${Math.floor(seconds / 3600)}h`;
2252
+ }
2253
+
2254
+ // src/commands/quota.ts
2255
+ async function fetchSingleAccountQuota(options) {
2256
+ const manager = getAccountManager();
2257
+ const accountEmail = options.account || manager.getActiveEmail();
2258
+ const originalActiveEmail = manager.getActiveEmail();
2259
+ let method = options.method || "auto";
2260
+ if (options.account && method !== "google") {
2261
+ debug("quota", `Account specified, forcing google method (local uses IDE account)`);
2262
+ method = "google";
2263
+ }
2264
+ if (method === "google") {
2265
+ const tokenManager = options.account ? getTokenManagerForAccount(options.account) : getTokenManager();
2266
+ if (!tokenManager.isLoggedIn()) {
2267
+ error("Not logged in. Run: antigravity-usage login");
2268
+ process.exit(1);
2269
+ }
2270
+ }
2271
+ try {
2272
+ let accountSwitched = false;
2273
+ if (options.account && options.account !== originalActiveEmail) {
2274
+ debug("quota", `Temporarily switching to account ${options.account} for fetch`);
2275
+ manager.setActiveAccount(options.account);
2276
+ accountSwitched = true;
2277
+ }
2278
+ try {
2279
+ debug("quota", `Fetching quota via ${method} method...`);
2280
+ const snapshot = await fetchQuota(method);
2281
+ if (accountEmail) {
2282
+ saveCache(accountEmail, snapshot);
2283
+ }
2284
+ if (options.json) {
2285
+ printQuotaJson(snapshot);
2286
+ } else {
2287
+ printQuotaTable(snapshot);
2288
+ }
2289
+ } finally {
2290
+ if (accountSwitched && originalActiveEmail) {
2291
+ debug("quota", `Restoring active account to ${originalActiveEmail}`);
2292
+ manager.setActiveAccount(originalActiveEmail);
2293
+ }
2294
+ }
2295
+ } catch (err) {
2296
+ handleQuotaError(err);
2297
+ }
2298
+ }
2299
+ async function fetchAllAccountsQuota(options) {
2300
+ const manager = getAccountManager();
2301
+ const emails = manager.getAccountEmails();
2302
+ const activeEmail = manager.getActiveEmail();
2303
+ if (emails.length === 0) {
2304
+ error("No accounts found. Run: antigravity-usage login");
2305
+ process.exit(1);
2306
+ }
2307
+ if (options.refresh) {
2308
+ info("\u{1F504} Refreshing quota data for all accounts...\n");
2309
+ }
2310
+ const results = [];
2311
+ for (const email of emails) {
2312
+ const isActive = email === activeEmail;
2313
+ try {
2314
+ if (!options.refresh && isCacheValid(email)) {
2315
+ const cached = loadCache(email);
2316
+ if (cached) {
2317
+ debug("quota", `Using cached data for ${email}`);
2318
+ results.push({
2319
+ email,
2320
+ isActive,
2321
+ status: "cached",
2322
+ snapshot: cached,
2323
+ cacheAge: getCacheAge(email) || 0
2324
+ });
2325
+ continue;
2326
+ }
2327
+ }
2328
+ debug("quota", `Fetching fresh data for ${email}`);
2329
+ const snapshot = await fetchQuotaForAccount(email, options.method || "auto");
2330
+ saveCache(email, snapshot);
2331
+ results.push({
2332
+ email,
2333
+ isActive,
2334
+ status: "success",
2335
+ snapshot
2336
+ });
2337
+ } catch (err) {
2338
+ debug("quota", `Error fetching quota for ${email}:`, err);
2339
+ const cached = loadCache(email);
2340
+ if (cached) {
2341
+ results.push({
2342
+ email,
2343
+ isActive,
2344
+ status: "cached",
2345
+ snapshot: cached,
2346
+ cacheAge: getCacheAge(email) || 0
2347
+ });
2348
+ } else {
2349
+ results.push({
2350
+ email,
2351
+ isActive,
2352
+ status: "error",
2353
+ error: err instanceof Error ? err.message : "Unknown error"
2354
+ });
2355
+ }
2356
+ }
2357
+ }
2358
+ if (options.json) {
2359
+ console.log(JSON.stringify(results, null, 2));
2360
+ } else {
2361
+ renderAllQuotaTable(results);
2362
+ }
2363
+ }
2364
+ async function fetchQuotaForAccount(email, method) {
2365
+ const manager = getAccountManager();
2366
+ const originalActiveEmail = manager.getActiveEmail();
2367
+ let effectiveMethod = method;
2368
+ if (method === "auto" || method === "local") {
2369
+ effectiveMethod = "google";
2370
+ debug("quota", `Forcing Google API for multi-account fetch (email: ${email})`);
2371
+ }
2372
+ let accountSwitched = false;
2373
+ if (email !== originalActiveEmail) {
2374
+ debug("quota", `Switching to ${email} for fetch`);
2375
+ manager.setActiveAccount(email);
2376
+ resetTokenManager();
2377
+ accountSwitched = true;
2378
+ }
2379
+ try {
2380
+ const snapshot = await fetchQuota(effectiveMethod);
2381
+ return snapshot;
2382
+ } finally {
2383
+ if (accountSwitched && originalActiveEmail) {
2384
+ debug("quota", `Restoring active account to ${originalActiveEmail}`);
2385
+ manager.setActiveAccount(originalActiveEmail);
2386
+ resetTokenManager();
2387
+ }
2388
+ }
2389
+ }
2390
+ function handleQuotaError(err) {
2391
+ if (err instanceof NoAuthMethodAvailableError) {
2392
+ error(err.message);
2393
+ process.exit(1);
2394
+ }
2395
+ if (err instanceof AntigravityNotRunningError) {
2396
+ error(err.message);
2397
+ console.log("\nTip: Make sure Antigravity is running in your IDE (VSCode, etc.)");
2398
+ process.exit(1);
2399
+ }
2400
+ if (err instanceof LocalConnectionError) {
2401
+ error(err.message);
2402
+ console.log("\nTip: Try restarting your IDE or the Antigravity extension.");
2403
+ process.exit(1);
2404
+ }
2405
+ if (err instanceof PortDetectionError) {
2406
+ error(err.message);
2407
+ console.log("\nTip: This may happen if the Antigravity language server is still starting up.");
2408
+ process.exit(1);
2409
+ }
2410
+ if (err instanceof NotLoggedInError) {
2411
+ error(err.message);
2412
+ process.exit(1);
2413
+ }
2414
+ if (err instanceof AuthenticationError) {
2415
+ error(err.message);
2416
+ process.exit(1);
2417
+ }
2418
+ if (err instanceof NetworkError) {
2419
+ error(err.message);
2420
+ process.exit(1);
2421
+ }
2422
+ if (err instanceof RateLimitError) {
2423
+ error(err.message);
2424
+ if (err.retryAfterMs) {
2425
+ const seconds = Math.ceil(err.retryAfterMs / 1e3);
2426
+ console.log(`Retry after ${seconds} seconds.`);
2427
+ }
2428
+ process.exit(1);
2429
+ }
2430
+ if (err instanceof APIError) {
2431
+ error(err.message);
2432
+ process.exit(1);
2433
+ }
2434
+ error(`Failed to fetch quota: ${err instanceof Error ? err.message : "Unknown error"}`);
2435
+ debug("quota", "Error details", err);
2436
+ process.exit(1);
2437
+ }
2438
+ async function quotaCommand(options) {
2439
+ if (options.all) {
2440
+ await fetchAllAccountsQuota(options);
2441
+ } else {
2442
+ await fetchSingleAccountQuota(options);
2443
+ }
2444
+ }
2445
+
2446
+ // src/commands/doctor.ts
2447
+ function doctorCommand() {
2448
+ console.log();
2449
+ console.log("\u{1FA7A} Antigravity Usage - Diagnostics");
2450
+ console.log("\u2550".repeat(50));
2451
+ console.log();
2452
+ console.log("\u{1F4E6} Version");
2453
+ console.log("\u2500".repeat(40));
2454
+ console.log(` CLI version: ${version}`);
2455
+ console.log(` Node.js: ${process.version}`);
2456
+ console.log(` Platform: ${getPlatform()}`);
2457
+ console.log();
2458
+ const storage = getStorageInfo();
2459
+ console.log("\u{1F4C1} Configuration");
2460
+ console.log("\u2500".repeat(40));
2461
+ console.log(` Config dir: ${storage.configDir}`);
2462
+ console.log(` Tokens file: ${storage.tokensPath}`);
2463
+ console.log(` Tokens exist: ${storage.exists ? "Yes" : "No"}`);
2464
+ console.log();
2465
+ const tokenManager = getTokenManager();
2466
+ console.log("\u{1F510} Authentication");
2467
+ console.log("\u2500".repeat(40));
2468
+ if (!tokenManager.isLoggedIn()) {
2469
+ console.log(" Status: Not logged in");
2470
+ console.log();
2471
+ console.log(" \u{1F4A1} Run `antigravity-usage login` to authenticate.");
2472
+ } else {
2473
+ console.log(" Status: Logged in");
2474
+ const email = tokenManager.getEmail();
2475
+ if (email) {
2476
+ console.log(` Email: ${maskEmail(email)}`);
2477
+ }
2478
+ const expiresAt = tokenManager.getExpiresAt();
2479
+ if (expiresAt) {
2480
+ const isExpired = tokenManager.isTokenExpired();
2481
+ console.log(` Token expires: ${expiresAt.toLocaleString()}`);
2482
+ console.log(` Token valid: ${isExpired ? "No (needs refresh)" : "Yes"}`);
2483
+ }
2484
+ }
2485
+ console.log();
2486
+ console.log("\u{1F527} OAuth Configuration");
2487
+ console.log("\u2500".repeat(40));
2488
+ const hasClientId = !!process.env.ANTIGRAVITY_OAUTH_CLIENT_ID;
2489
+ const hasClientSecret = !!process.env.ANTIGRAVITY_OAUTH_CLIENT_SECRET;
2490
+ if (hasClientId || hasClientSecret) {
2491
+ console.log(" Using custom OAuth credentials:");
2492
+ console.log(` ANTIGRAVITY_OAUTH_CLIENT_ID: ${hasClientId ? "Set" : "Not set"}`);
2493
+ console.log(` ANTIGRAVITY_OAUTH_CLIENT_SECRET: ${hasClientSecret ? "Set" : "Not set"}`);
2494
+ } else {
2495
+ console.log(" \u2705 Using built-in OAuth credentials");
2496
+ console.log(" \u{1F4A1} Set ANTIGRAVITY_OAUTH_CLIENT_ID and ANTIGRAVITY_OAUTH_CLIENT_SECRET");
2497
+ console.log(" environment variables to use custom credentials.");
2498
+ }
2499
+ console.log();
2500
+ }
2501
+
2502
+ // src/commands/accounts.ts
2503
+ function listAccountsCommand(options) {
2504
+ const manager = getAccountManager();
2505
+ const summaries = manager.getAccountSummaries();
2506
+ renderAccountsTable(summaries);
2507
+ if (options.refresh) {
2508
+ info("Use `antigravity-usage quota --all --refresh` to fetch fresh quota data.");
2509
+ }
2510
+ }
2511
+ async function addAccountCommand() {
2512
+ info("Adding a new account...");
2513
+ const result = await startOAuthFlow();
2514
+ if (result.success) {
2515
+ success(`Account added successfully${result.email ? `: ${result.email}` : ""}!`);
2516
+ const manager = getAccountManager();
2517
+ const summaries = manager.getAccountSummaries();
2518
+ console.log("\nYour accounts:");
2519
+ renderAccountsTable(summaries);
2520
+ } else {
2521
+ error(`Failed to add account: ${result.error}`);
2522
+ process.exit(1);
2523
+ }
2524
+ }
2525
+ function switchAccountCommand(email) {
2526
+ const manager = getAccountManager();
2527
+ if (!manager.hasAccount(email)) {
2528
+ error(`Account '${email}' not found.`);
2529
+ const emails = manager.getAccountEmails();
2530
+ if (emails.length > 0) {
2531
+ console.log("\nAvailable accounts:");
2532
+ for (const e of emails) {
2533
+ console.log(` - ${e}`);
2534
+ }
2535
+ } else {
2536
+ info("\nNo accounts found. Run `antigravity-usage login` to add one.");
2537
+ }
2538
+ process.exit(1);
2539
+ }
2540
+ const switched = manager.setActiveAccount(email);
2541
+ if (switched) {
2542
+ success(`Switched to account: ${email}`);
2543
+ } else {
2544
+ error(`Failed to switch to account: ${email}`);
2545
+ process.exit(1);
2546
+ }
2547
+ }
2548
+ function removeAccountCommand(email, options) {
2549
+ const manager = getAccountManager();
2550
+ if (!manager.hasAccount(email)) {
2551
+ error(`Account '${email}' not found.`);
2552
+ process.exit(1);
2553
+ }
2554
+ if (!options.force) {
2555
+ warn(`This will remove account '${email}' and all its data.`);
2556
+ info("Use --force to skip this warning.");
2557
+ }
2558
+ const removed = manager.removeAccount(email);
2559
+ if (removed) {
2560
+ success(`Account '${email}' removed.`);
2561
+ const remaining = manager.getAccountEmails();
2562
+ if (remaining.length > 0) {
2563
+ const active = manager.getActiveEmail();
2564
+ console.log(`
2565
+ Active account: ${active || "none"}`);
2566
+ console.log(`Remaining accounts: ${remaining.length}`);
2567
+ } else {
2568
+ info("\nNo accounts remaining. Run `antigravity-usage login` to add one.");
2569
+ }
2570
+ } else {
2571
+ error(`Failed to remove account: ${email}`);
2572
+ process.exit(1);
2573
+ }
2574
+ }
2575
+ function currentAccountCommand() {
2576
+ const manager = getAccountManager();
2577
+ const active = manager.getActiveEmail();
2578
+ if (active) {
2579
+ console.log();
2580
+ console.log(`\u{1F4CD} Active account: ${active}`);
2581
+ const info2 = manager.getAccountInfo(active);
2582
+ if (info2) {
2583
+ const statusIcon = info2.status === "valid" ? "\u2705" : info2.status === "expired" ? "\u26A0\uFE0F" : "\u274C";
2584
+ console.log(` Status: ${statusIcon} ${info2.status}`);
2585
+ if (info2.tokens?.expiresAt) {
2586
+ const expiresAt = new Date(info2.tokens.expiresAt).toLocaleString();
2587
+ console.log(` Token expires: ${expiresAt}`);
2588
+ }
2589
+ }
2590
+ console.log();
2591
+ } else {
2592
+ warn("No active account set.");
2593
+ const emails = manager.getAccountEmails();
2594
+ if (emails.length > 0) {
2595
+ console.log("\nAvailable accounts:");
2596
+ for (const e of emails) {
2597
+ console.log(` - ${e}`);
2598
+ }
2599
+ info("\nRun `antigravity-usage accounts switch <email>` to set an active account.");
2600
+ } else {
2601
+ info("\nRun `antigravity-usage login` to add an account.");
2602
+ }
2603
+ }
2604
+ }
2605
+ async function refreshAccountCommand(email, options) {
2606
+ const manager = getAccountManager();
2607
+ if (options.all) {
2608
+ const emails = manager.getAccountEmails();
2609
+ if (emails.length === 0) {
2610
+ warn("No accounts to refresh.");
2611
+ return;
2612
+ }
2613
+ console.log(`
2614
+ \u{1F504} Refreshing ${emails.length} account(s)...
2615
+ `);
2616
+ let successCount = 0;
2617
+ let failCount = 0;
2618
+ for (const e of emails) {
2619
+ try {
2620
+ const tokenManager = getTokenManagerForAccount(e);
2621
+ if (tokenManager.isTokenExpired()) {
2622
+ await tokenManager.refreshToken();
2623
+ success(` \u2705 ${e}`);
2624
+ successCount++;
2625
+ } else {
2626
+ info(` \u23ED\uFE0F ${e} (token still valid)`);
2627
+ successCount++;
2628
+ }
2629
+ } catch (err) {
2630
+ error(` \u274C ${e}: ${err instanceof Error ? err.message : "Failed"}`);
2631
+ failCount++;
2632
+ }
2633
+ }
2634
+ resetTokenManager();
2635
+ console.log();
2636
+ if (failCount > 0) {
2637
+ warn(`${failCount} account(s) need re-authentication. Run: antigravity-usage login`);
2638
+ } else {
2639
+ success(`All ${successCount} account(s) refreshed successfully!`);
2640
+ }
2641
+ return;
2642
+ }
2643
+ const targetEmail = email || manager.getActiveEmail();
2644
+ if (!targetEmail) {
2645
+ error("No account specified and no active account.");
2646
+ info("Usage: antigravity-usage accounts refresh <email>");
2647
+ info(" or: antigravity-usage accounts refresh --all");
2648
+ process.exit(1);
2649
+ }
2650
+ if (!manager.hasAccount(targetEmail)) {
2651
+ error(`Account '${targetEmail}' not found.`);
2652
+ process.exit(1);
2653
+ }
2654
+ console.log(`
2655
+ \u{1F504} Refreshing ${targetEmail}...`);
2656
+ try {
2657
+ const tokenManager = getTokenManagerForAccount(targetEmail);
2658
+ if (!tokenManager.isTokenExpired()) {
2659
+ info(`Token for ${targetEmail} is still valid.`);
2660
+ return;
2661
+ }
2662
+ await tokenManager.refreshToken();
2663
+ resetTokenManager();
2664
+ success(`
2665
+ \u2705 Token refreshed for ${targetEmail}`);
2666
+ } catch (err) {
2667
+ error(`
2668
+ \u274C Failed to refresh token: ${err instanceof Error ? err.message : "Unknown error"}`);
2669
+ info("\nThe refresh token may be expired. Please re-authenticate:");
2670
+ info(` antigravity-usage accounts switch ${targetEmail}`);
2671
+ info(" antigravity-usage login");
2672
+ process.exit(1);
2673
+ }
2674
+ }
2675
+ async function accountsCommand(subcommand, args, options) {
2676
+ switch (subcommand) {
2677
+ case "list":
2678
+ listAccountsCommand({ refresh: options.refresh });
2679
+ break;
2680
+ case "add":
2681
+ await addAccountCommand();
2682
+ break;
2683
+ case "switch":
2684
+ if (!args[0]) {
2685
+ error("Please specify an account email to switch to.");
2686
+ console.log("Usage: antigravity-usage accounts switch <email>");
2687
+ process.exit(1);
2688
+ }
2689
+ switchAccountCommand(args[0]);
2690
+ break;
2691
+ case "remove":
2692
+ if (!args[0]) {
2693
+ error("Please specify an account email to remove.");
2694
+ console.log("Usage: antigravity-usage accounts remove <email>");
2695
+ process.exit(1);
2696
+ }
2697
+ removeAccountCommand(args[0], { force: options.force });
2698
+ break;
2699
+ case "current":
2700
+ currentAccountCommand();
2701
+ break;
2702
+ case "refresh":
2703
+ await refreshAccountCommand(args[0], { all: options.all });
2704
+ break;
2705
+ default:
2706
+ listAccountsCommand({ refresh: options.refresh });
2707
+ }
2708
+ }
2709
+
2710
+ // src/index.ts
2711
+ var program = new Command();
2712
+ program.name("antigravity-usage").description("CLI tool to check Antigravity model quota via Google Cloud Code API").version(version).option("--debug", "Enable debug mode").hook("preAction", (thisCommand) => {
2713
+ const opts = thisCommand.opts();
2714
+ if (opts.debug) {
2715
+ setDebugMode(true);
2716
+ }
2717
+ });
2718
+ program.command("login").description("Authenticate with Google (adds a new account)").option("--no-browser", "Do not open browser, print URL instead").option("-p, --port <port>", "Port for OAuth callback server", parseInt).action(loginCommand);
2719
+ program.command("logout [email]").description("Remove stored credentials").option("--all", "Logout from all accounts").action((email, options) => logoutCommand(options, email));
2720
+ program.command("status").description("Show current authentication status").option("--all", "Show status for all accounts").option("-a, --account <email>", "Show status for specific account").action(statusCommand);
2721
+ program.command("quota", { isDefault: true }).description("Fetch and display quota information").option("--json", "Output as JSON").option("-m, --method <method>", "Method to use: auto (default), local, or google", "auto").option("--all", "Show quota for all accounts").option("-a, --account <email>", "Show quota for specific account").option("--refresh", "Force refresh (ignore cache)").action(quotaCommand);
2722
+ var accountsCmd = program.command("accounts").description("Manage multiple accounts");
2723
+ accountsCmd.command("list").description("List all accounts").option("--refresh", "Show refresh tip").action((options) => accountsCommand("list", [], options));
2724
+ accountsCmd.command("add").description("Add a new account (triggers OAuth login)").action(() => accountsCommand("add", [], {}));
2725
+ accountsCmd.command("switch <email>").description("Switch to a different account").action((email) => accountsCommand("switch", [email], {}));
2726
+ accountsCmd.command("remove <email>").description("Remove an account").option("--force", "Skip confirmation").action((email, options) => accountsCommand("remove", [email], options));
2727
+ accountsCmd.command("current").description("Show current active account").action(() => accountsCommand("current", [], {}));
2728
+ accountsCmd.command("refresh [email]").description("Refresh account tokens").option("--all", "Refresh all accounts").action((email, options) => accountsCommand("refresh", email ? [email] : [], options));
2729
+ accountsCmd.action(() => accountsCommand("list", [], {}));
2730
+ program.command("doctor").description("Run diagnostics and show configuration").action(doctorCommand);
2731
+ program.parse();
2732
+ //# sourceMappingURL=index.js.map