ag-quota 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Philipp
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # ag-quota
2
+
3
+ Antigravity quota fetching library + CLI to inspect your Antigravity quota usage.
4
+
5
+ This package is **not** affiliated with or endorsed by Opencode. If you want the Opencode integration, install the plugin package `opencode-ag-quota` from this repo.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g ag-quota
11
+ ```
12
+
13
+ ## CLI
14
+
15
+ ```bash
16
+ # Auto mode (tries cloud first, falls back to local)
17
+ ag-quota
18
+
19
+ # Force cloud source (uses opencode auth credentials)
20
+ ag-quota --source=cloud
21
+
22
+ # Force local source (requires language server running)
23
+ ag-quota --source=local
24
+
25
+ # JSON output for scripts
26
+ ag-quota --json
27
+ ```
28
+
29
+ ### Example Output
30
+
31
+ ```
32
+ Antigravity Quotas (Source: Cloud API, 10:34:53 PM):
33
+ ------------------------------------------------------------
34
+ Claude/GPT : 83.3% remaining (Resets in: 3h 58m)
35
+ Flash : 100.0% remaining (Resets in: 3h 34m)
36
+ Pro : 95.0% remaining (Resets in: 55m)
37
+ ```
38
+
39
+ ## Library
40
+
41
+ ### Unified Quota Fetching (Recommended)
42
+
43
+ ```ts
44
+ import { fetchQuota } from "ag-quota";
45
+
46
+ const shellRunner = async (cmd: string) => {
47
+ const { execSync } = await import("node:child_process");
48
+ return execSync(cmd).toString();
49
+ };
50
+
51
+ const result = await fetchQuota("auto", shellRunner);
52
+
53
+ console.log(`Source: ${result.source}`);
54
+ for (const cat of result.categories) {
55
+ console.log(`${cat.category}: ${(cat.remainingFraction * 100).toFixed(1)}%`);
56
+ }
57
+ ```
58
+
59
+ ### Cloud-Only Fetching
60
+
61
+ ```ts
62
+ import { fetchCloudQuota, hasCloudCredentials } from "ag-quota";
63
+
64
+ if (await hasCloudCredentials()) {
65
+ const token = process.env.AG_ACCESS_TOKEN;
66
+ const projectId = process.env.AG_PROJECT_ID;
67
+ if (!token) throw new Error("Set AG_ACCESS_TOKEN");
68
+
69
+ const result = await fetchCloudQuota(token, projectId);
70
+ console.log(`Account: ${result.account.email}`);
71
+ }
72
+ ```
73
+
74
+ ### Local Server Fetching
75
+
76
+ ```ts
77
+ import { fetchAntigravityStatus } from "ag-quota";
78
+
79
+ const shellRunner = async (cmd: string) => {
80
+ const { execSync } = await import("node:child_process");
81
+ return execSync(cmd).toString();
82
+ };
83
+
84
+ const { userStatus } = await fetchAntigravityStatus(shellRunner);
85
+ const configs = userStatus.cascadeModelConfigData?.clientModelConfigs || [];
86
+ for (const model of configs) {
87
+ const quota = model.quotaInfo?.remainingFraction ?? 0;
88
+ console.log(`${model.label ?? model.modelName}: ${(quota * 100).toFixed(1)}%`);
89
+ }
90
+ ```
91
+
92
+ ## Plugin Configuration Notes
93
+
94
+ The CLI/library does not read Opencode config files. The Opencode plugin reads:
95
+
96
+ - Project: `.opencode/ag-quota.json`
97
+ - Global: `~/.config/opencode/ag-quota.json`
98
+
99
+ See the repo root `README.md` for plugin configuration, and the full defaults in `ag-quota.json`.
100
+
101
+ ## Requirements
102
+
103
+ - Node.js >= 18
104
+ - Cloud mode: `opencode auth login` credentials available
105
+ - Local mode: Antigravity language server running
106
+
107
+ ## Acknowledgments
108
+
109
+ Cloud quota fetching based on:
110
+ - [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) by [@NoeFabris](https://github.com/NoeFabris)
111
+ - [vscode-antigravity-cockpit](https://github.com/jlcodes99/vscode-antigravity-cockpit) by [@jlcodes99](https://github.com/jlcodes99)
112
+
113
+ ## License
114
+
115
+ ISC
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true,
8
+ configurable: true,
9
+ set: (newValue) => all[name] = () => newValue
10
+ });
11
+ };
12
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
13
+
14
+ // src/cloud.ts
15
+ var exports_cloud = {};
16
+ __export(exports_cloud, {
17
+ fetchCloudQuota: () => fetchCloudQuota
18
+ });
19
+ async function fetchAvailableModels(accessToken, projectId) {
20
+ const payload = projectId ? { project: projectId } : {};
21
+ let lastError = null;
22
+ const headers = {
23
+ ...CLOUDCODE_HEADERS,
24
+ Authorization: `Bearer ${accessToken}`
25
+ };
26
+ for (const endpoint of CLOUDCODE_ENDPOINTS) {
27
+ try {
28
+ const url = `${endpoint}/v1internal:fetchAvailableModels`;
29
+ const response = await fetch(url, {
30
+ method: "POST",
31
+ headers,
32
+ body: JSON.stringify(payload)
33
+ });
34
+ if (response.status === 401) {
35
+ throw new Error("Authorization expired or invalid.");
36
+ }
37
+ if (response.status === 403) {
38
+ throw new Error("Access forbidden (403). Check your account permissions.");
39
+ }
40
+ if (!response.ok) {
41
+ const text = await response.text();
42
+ throw new Error(`Cloud Code API error ${response.status}: ${text.slice(0, 200)}`);
43
+ }
44
+ return await response.json();
45
+ } catch (error) {
46
+ lastError = error instanceof Error ? error : new Error(String(error));
47
+ if (lastError.message.includes("Authorization") || lastError.message.includes("forbidden") || lastError.message.includes("invalid_grant")) {
48
+ throw lastError;
49
+ }
50
+ }
51
+ }
52
+ throw lastError || new Error("All Cloud Code API endpoints failed");
53
+ }
54
+ async function fetchCloudQuota(accessToken, projectId) {
55
+ if (!accessToken) {
56
+ throw new Error("Access token is required for cloud quota fetching");
57
+ }
58
+ const response = await fetchAvailableModels(accessToken, projectId);
59
+ const models = [];
60
+ if (response.models) {
61
+ for (const [modelKey, info] of Object.entries(response.models)) {
62
+ if (!info.quotaInfo)
63
+ continue;
64
+ models.push({
65
+ modelName: info.model || modelKey,
66
+ label: info.displayName || modelKey,
67
+ quotaInfo: {
68
+ remainingFraction: info.quotaInfo.remainingFraction ?? 0,
69
+ resetTime: info.quotaInfo.resetTime
70
+ }
71
+ });
72
+ }
73
+ }
74
+ return {
75
+ account: {
76
+ projectId
77
+ },
78
+ models,
79
+ timestamp: Date.now()
80
+ };
81
+ }
82
+ var CLOUDCODE_ENDPOINTS, CLOUDCODE_HEADERS;
83
+ var init_cloud = __esm(() => {
84
+ CLOUDCODE_ENDPOINTS = [
85
+ "https://daily-cloudcode-pa.sandbox.googleapis.com",
86
+ "https://autopush-cloudcode-pa.sandbox.googleapis.com",
87
+ "https://cloudcode-pa.googleapis.com"
88
+ ];
89
+ CLOUDCODE_HEADERS = {
90
+ "Content-Type": "application/json",
91
+ "User-Agent": "antigravity/1.11.5 windows/amd64",
92
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
93
+ "Client-Metadata": '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
94
+ };
95
+ });
96
+
97
+ // src/cli.ts
98
+ import { execSync } from "node:child_process";
99
+
100
+ // src/index.ts
101
+ init_cloud();
102
+ import * as http from "node:http";
103
+ import * as https from "node:https";
104
+ var API_ENDPOINTS = {
105
+ GET_USER_STATUS: "/exa.language_server_pb.LanguageServerService/GetUserStatus"
106
+ };
107
+ function makeRequest(port, csrfToken, path, body) {
108
+ return new Promise((resolve, reject) => {
109
+ const payload = JSON.stringify(body);
110
+ const options = {
111
+ hostname: "127.0.0.1",
112
+ port,
113
+ path,
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ "Content-Length": Buffer.byteLength(payload),
118
+ "X-Codeium-Csrf-Token": csrfToken,
119
+ "Connect-Protocol-Version": "1"
120
+ },
121
+ timeout: 2000
122
+ };
123
+ const handleResponse = (response) => {
124
+ let data = "";
125
+ response.on("data", (chunk) => {
126
+ data += chunk.toString();
127
+ });
128
+ response.on("end", () => {
129
+ try {
130
+ resolve(JSON.parse(data));
131
+ } catch {
132
+ reject(new Error("JSON parse error"));
133
+ }
134
+ });
135
+ };
136
+ const req = https.request({ ...options, rejectUnauthorized: false }, handleResponse);
137
+ req.on("error", () => {
138
+ const reqHttp = http.request(options, handleResponse);
139
+ reqHttp.on("error", (err) => reject(err));
140
+ reqHttp.write(payload);
141
+ reqHttp.end();
142
+ });
143
+ req.write(payload);
144
+ req.end();
145
+ });
146
+ }
147
+ async function fetchAntigravityStatus(runShell) {
148
+ let procOutput = "";
149
+ try {
150
+ procOutput = await runShell('ps aux | grep -E "csrf_token|language_server" | grep -v grep');
151
+ } catch {
152
+ procOutput = "";
153
+ }
154
+ const lines = procOutput.split(`
155
+ `);
156
+ let csrfToken = "";
157
+ let cmdLinePort = 0;
158
+ for (const line of lines) {
159
+ const csrfMatch = line.match(/--csrf_token[=\s]+([\w-]+)/i);
160
+ if (csrfMatch?.[1])
161
+ csrfToken = csrfMatch[1];
162
+ const portMatch = line.match(/--extension_server_port[=\s]+(\d+)/i);
163
+ if (portMatch?.[1])
164
+ cmdLinePort = parseInt(portMatch[1], 10);
165
+ if (csrfToken && cmdLinePort)
166
+ break;
167
+ }
168
+ if (!csrfToken) {
169
+ throw new Error("Antigravity CSRF token not found. Is the Language Server running?");
170
+ }
171
+ let netstatOutput = "";
172
+ try {
173
+ netstatOutput = await runShell('ss -tlnp | grep -E "language_server|opencode|node"');
174
+ } catch {
175
+ netstatOutput = "";
176
+ }
177
+ const portMatches = netstatOutput.match(/:(\d+)/g);
178
+ let ports = portMatches ? portMatches.map((p) => parseInt(p.replace(":", ""), 10)) : [];
179
+ if (cmdLinePort && !ports.includes(cmdLinePort)) {
180
+ ports.unshift(cmdLinePort);
181
+ }
182
+ ports = Array.from(new Set(ports));
183
+ if (ports.length === 0) {
184
+ throw new Error("No listening ports found for Antigravity. Check if the server is active.");
185
+ }
186
+ let userStatus = null;
187
+ let lastError = null;
188
+ for (const p of ports) {
189
+ try {
190
+ const resp = await makeRequest(p, csrfToken, API_ENDPOINTS.GET_USER_STATUS, { metadata: { ideName: "opencode" } });
191
+ if (resp?.userStatus) {
192
+ userStatus = resp.userStatus;
193
+ break;
194
+ }
195
+ } catch (e) {
196
+ lastError = e instanceof Error ? e : new Error(String(e));
197
+ continue;
198
+ }
199
+ }
200
+ if (!userStatus) {
201
+ throw new Error(`Could not communicate with Antigravity API. ${lastError?.message ?? ""}`);
202
+ }
203
+ return {
204
+ userStatus,
205
+ timestamp: Date.now()
206
+ };
207
+ }
208
+ function formatRelativeTime(targetDate) {
209
+ const now = new Date;
210
+ const diffMs = targetDate.getTime() - now.getTime();
211
+ if (diffMs <= 0)
212
+ return "now";
213
+ const diffMins = Math.floor(diffMs / (1000 * 60));
214
+ const diffHours = Math.floor(diffMins / 60);
215
+ const remainingMins = diffMins % 60;
216
+ if (diffHours > 0) {
217
+ return `${diffHours}h ${remainingMins}m`;
218
+ }
219
+ return `${diffMins}m`;
220
+ }
221
+ function categorizeModel(label) {
222
+ const lowerLabel = label.toLowerCase();
223
+ if (lowerLabel.includes("flash")) {
224
+ return "Flash";
225
+ }
226
+ if (lowerLabel.includes("gemini") || lowerLabel.includes("pro")) {
227
+ return "Pro";
228
+ }
229
+ return "Claude/GPT";
230
+ }
231
+ function groupModelsByCategory(models) {
232
+ const categories = {};
233
+ for (const model of models) {
234
+ const label = model.label || model.modelName || "";
235
+ const category = categorizeModel(label);
236
+ const fraction = model.quotaInfo?.remainingFraction ?? 0;
237
+ const resetTime = model.quotaInfo?.resetTime ? new Date(model.quotaInfo.resetTime) : null;
238
+ if (!categories[category] || fraction < categories[category].remainingFraction) {
239
+ categories[category] = { remainingFraction: fraction, resetTime };
240
+ }
241
+ }
242
+ const result = [];
243
+ for (const cat of ["Flash", "Pro", "Claude/GPT"]) {
244
+ if (categories[cat]) {
245
+ result.push({
246
+ category: cat,
247
+ remainingFraction: categories[cat].remainingFraction,
248
+ resetTime: categories[cat].resetTime
249
+ });
250
+ }
251
+ }
252
+ return result;
253
+ }
254
+ async function fetchQuota(source, shellRunner, cloudAuth) {
255
+ const { fetchCloudQuota: fetchCloudQuota2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
256
+ if (source === "cloud" || source === "auto") {
257
+ if (cloudAuth) {
258
+ try {
259
+ const cloudResult = await fetchCloudQuota2(cloudAuth.accessToken, cloudAuth.projectId);
260
+ const categories2 = groupModelsByCategory(cloudResult.models);
261
+ return {
262
+ source: "cloud",
263
+ categories: categories2,
264
+ models: cloudResult.models,
265
+ timestamp: cloudResult.timestamp
266
+ };
267
+ } catch (error) {
268
+ if (source === "cloud") {
269
+ throw error;
270
+ }
271
+ }
272
+ } else if (source === "cloud") {
273
+ throw new Error("Cloud access token not provided. Cannot fetch cloud quota.");
274
+ }
275
+ }
276
+ if (!shellRunner) {
277
+ throw new Error("Shell runner required for local quota fetching");
278
+ }
279
+ const localResult = await fetchAntigravityStatus(shellRunner);
280
+ const models = localResult.userStatus.cascadeModelConfigData?.clientModelConfigs || [];
281
+ const categories = groupModelsByCategory(models);
282
+ return {
283
+ source: "local",
284
+ categories,
285
+ models,
286
+ timestamp: localResult.timestamp
287
+ };
288
+ }
289
+
290
+ // src/cli-auth.ts
291
+ import { readFileSync } from "node:fs";
292
+ import { homedir } from "node:os";
293
+ import { join } from "node:path";
294
+ var ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
295
+ var ANTIGRAVITY_CLIENT_SECRET = "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";
296
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
297
+ function getAccountsFilePath() {
298
+ return join(homedir(), ".config", "opencode", "antigravity-accounts.json");
299
+ }
300
+ function loadAccounts() {
301
+ const accountsPath = getAccountsFilePath();
302
+ try {
303
+ const content = readFileSync(accountsPath, "utf-8");
304
+ const data = JSON.parse(content);
305
+ if (!data.accounts || data.accounts.length === 0) {
306
+ throw new Error("No accounts found in antigravity-accounts.json");
307
+ }
308
+ return data;
309
+ } catch (error) {
310
+ if (error.code === "ENOENT") {
311
+ throw new Error("Antigravity accounts file not found.");
312
+ }
313
+ throw error;
314
+ }
315
+ }
316
+ async function refreshAccessToken(refreshToken) {
317
+ const response = await fetch(TOKEN_URL, {
318
+ method: "POST",
319
+ headers: {
320
+ "Content-Type": "application/x-www-form-urlencoded"
321
+ },
322
+ body: new URLSearchParams({
323
+ client_id: ANTIGRAVITY_CLIENT_ID,
324
+ client_secret: ANTIGRAVITY_CLIENT_SECRET,
325
+ refresh_token: refreshToken,
326
+ grant_type: "refresh_token"
327
+ }).toString()
328
+ });
329
+ if (!response.ok) {
330
+ const errorText = await response.text();
331
+ throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
332
+ }
333
+ const data = await response.json();
334
+ return data.access_token;
335
+ }
336
+ async function getCLICloudCredentials() {
337
+ try {
338
+ const accountsFile = loadAccounts();
339
+ const activeAccount = accountsFile.accounts[accountsFile.activeIndex] ?? accountsFile.accounts[0];
340
+ if (!activeAccount) {
341
+ return null;
342
+ }
343
+ const accessToken = await refreshAccessToken(activeAccount.refreshToken);
344
+ return {
345
+ accessToken,
346
+ projectId: activeAccount.projectId,
347
+ email: activeAccount.email
348
+ };
349
+ } catch (error) {
350
+ if (error instanceof Error && error.message.includes("not found")) {
351
+ return null;
352
+ }
353
+ throw error;
354
+ }
355
+ }
356
+
357
+ // src/cli.ts
358
+ var shellRunner = async (cmd) => execSync(cmd).toString();
359
+ function parseArgs() {
360
+ const args = process.argv.slice(2);
361
+ let source = "auto";
362
+ let json = false;
363
+ let help = false;
364
+ let token = "";
365
+ let projectId = "";
366
+ for (const arg of args) {
367
+ if (arg === "--json") {
368
+ json = true;
369
+ } else if (arg === "--help" || arg === "-h") {
370
+ help = true;
371
+ } else if (arg === "--source=cloud" || arg === "-s=cloud") {
372
+ source = "cloud";
373
+ } else if (arg === "--source=local" || arg === "-s=local") {
374
+ source = "local";
375
+ } else if (arg === "--source=auto" || arg === "-s=auto") {
376
+ source = "auto";
377
+ } else if (arg.startsWith("--token=")) {
378
+ token = arg.split("=")[1];
379
+ } else if (arg.startsWith("--project-id=")) {
380
+ projectId = arg.split("=")[1];
381
+ } else if (arg.startsWith("--source=") || arg.startsWith("-s=")) {
382
+ const value = arg.split("=")[1];
383
+ console.error(`Invalid source: ${value}. Use 'cloud', 'local', or 'auto'.`);
384
+ process.exit(1);
385
+ }
386
+ }
387
+ return { source, json, help, token, projectId };
388
+ }
389
+ async function run() {
390
+ const { source, json: isJson, help: isHelp, token, projectId } = parseArgs();
391
+ if (isHelp) {
392
+ console.log(`
393
+ Usage: ag-quota [options]
394
+
395
+ Options:
396
+ --source=<cloud|local|auto> Quota source (default: auto)
397
+ -s=<cloud|local|auto> Alias for --source
398
+ --token=<token> Access token (override auto-discovery)
399
+ --project-id=<id> Google Cloud Project ID (optional)
400
+ --json Output result as JSON
401
+ -h, --help Show this help message
402
+
403
+ Sources:
404
+ cloud Fetch from Cloud Code API (uses auto-discovery or --token)
405
+ local Fetch from local language server process
406
+ auto Try cloud first, fallback to local (default)
407
+
408
+ Examples:
409
+ ag-quota # Auto-detect (tries cloud then local)
410
+ ag-quota --source=cloud # Force cloud (auto-discover token)
411
+ ag-quota --token=... # Force cloud with specific token
412
+ ag-quota --source=local # Force local source
413
+ ag-quota --json # Output as JSON
414
+ `);
415
+ process.exit(0);
416
+ }
417
+ try {
418
+ let cloudAuth = token ? { accessToken: token, projectId } : undefined;
419
+ if (!cloudAuth && (source === "cloud" || source === "auto")) {
420
+ const creds = await getCLICloudCredentials();
421
+ if (creds) {
422
+ cloudAuth = { accessToken: creds.accessToken, projectId: creds.projectId };
423
+ }
424
+ }
425
+ if (source === "cloud" && !cloudAuth) {
426
+ throw new Error("Cloud credentials not found. Run 'opencode auth login' or provide --token.");
427
+ }
428
+ const result = await fetchQuota(source, shellRunner, cloudAuth);
429
+ if (isJson) {
430
+ console.log(JSON.stringify({
431
+ source: result.source,
432
+ timestamp: result.timestamp,
433
+ categories: result.categories.map((cat) => ({
434
+ name: cat.category,
435
+ remainingFraction: cat.remainingFraction,
436
+ remainingPercentage: parseFloat((cat.remainingFraction * 100).toFixed(1)),
437
+ resetTime: cat.resetTime?.toISOString() ?? null,
438
+ resetsIn: cat.resetTime ? formatRelativeTime(cat.resetTime) : null
439
+ }))
440
+ }, null, 2));
441
+ return;
442
+ }
443
+ const sourceLabel = result.source === "cloud" ? "Cloud API" : "Local Server";
444
+ console.log(`
445
+ Antigravity Quotas (Source: ${sourceLabel}, ${new Date(result.timestamp).toLocaleTimeString()}):`);
446
+ console.log("------------------------------------------------------------");
447
+ for (const cat of result.categories) {
448
+ const remaining = (cat.remainingFraction * 100).toFixed(1);
449
+ let output = `${cat.category.padEnd(20)}: ${remaining.padStart(5)}% remaining`;
450
+ if (cat.resetTime) {
451
+ output += ` (Resets in: ${formatRelativeTime(cat.resetTime)})`;
452
+ }
453
+ console.log(output);
454
+ }
455
+ console.log("");
456
+ } catch (error) {
457
+ const message = error instanceof Error ? error.message : String(error);
458
+ if (isJson) {
459
+ console.log(JSON.stringify({ error: message }, null, 2));
460
+ } else {
461
+ console.error("Error:", message);
462
+ }
463
+ process.exit(1);
464
+ }
465
+ }
466
+ run();
@@ -0,0 +1,12 @@
1
+ export interface CLICloudCredentials {
2
+ accessToken: string;
3
+ projectId?: string;
4
+ email: string;
5
+ }
6
+ /**
7
+ * Attempt to get cloud credentials from the local environment.
8
+ * Returns null if no credentials found or file missing.
9
+ * Throws if file exists but is invalid or refresh fails.
10
+ */
11
+ export declare function getCLICloudCredentials(): Promise<CLICloudCredentials | null>;
12
+ //# sourceMappingURL=cli-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-auth.d.ts","sourceRoot":"","sources":["../src/cli-auth.ts"],"names":[],"mappings":"AA8CA,MAAM,WAAW,mBAAmB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACjB;AAqDD;;;;GAIG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CA2BlF"}
@@ -0,0 +1,89 @@
1
+ /*
2
+ * Authentication helper for the CLI.
3
+ *
4
+ * This logic is duplicated here specifically for the CLI to be standalone
5
+ * and user-friendly, without polluting the core library exports which
6
+ * should remain pure and environment-agnostic.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { join } from "node:path";
11
+ // ============================================================================
12
+ // Constants
13
+ // ============================================================================
14
+ const ANTIGRAVITY_CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
15
+ const ANTIGRAVITY_CLIENT_SECRET = "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";
16
+ const TOKEN_URL = "https://oauth2.googleapis.com/token";
17
+ // ============================================================================
18
+ // Logic
19
+ // ============================================================================
20
+ function getAccountsFilePath() {
21
+ return join(homedir(), ".config", "opencode", "antigravity-accounts.json");
22
+ }
23
+ function loadAccounts() {
24
+ const accountsPath = getAccountsFilePath();
25
+ try {
26
+ const content = readFileSync(accountsPath, "utf-8");
27
+ const data = JSON.parse(content);
28
+ if (!data.accounts || data.accounts.length === 0) {
29
+ throw new Error("No accounts found in antigravity-accounts.json");
30
+ }
31
+ return data;
32
+ }
33
+ catch (error) {
34
+ if (error.code === "ENOENT") {
35
+ throw new Error("Antigravity accounts file not found.");
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ async function refreshAccessToken(refreshToken) {
41
+ const response = await fetch(TOKEN_URL, {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/x-www-form-urlencoded",
45
+ },
46
+ body: new URLSearchParams({
47
+ client_id: ANTIGRAVITY_CLIENT_ID,
48
+ client_secret: ANTIGRAVITY_CLIENT_SECRET,
49
+ refresh_token: refreshToken,
50
+ grant_type: "refresh_token",
51
+ }).toString(),
52
+ });
53
+ if (!response.ok) {
54
+ const errorText = await response.text();
55
+ throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
56
+ }
57
+ const data = (await response.json());
58
+ return data.access_token;
59
+ }
60
+ /**
61
+ * Attempt to get cloud credentials from the local environment.
62
+ * Returns null if no credentials found or file missing.
63
+ * Throws if file exists but is invalid or refresh fails.
64
+ */
65
+ export async function getCLICloudCredentials() {
66
+ try {
67
+ // Load accounts
68
+ const accountsFile = loadAccounts();
69
+ const activeAccount = accountsFile.accounts[accountsFile.activeIndex] ?? accountsFile.accounts[0];
70
+ if (!activeAccount) {
71
+ return null;
72
+ }
73
+ // Get access token
74
+ const accessToken = await refreshAccessToken(activeAccount.refreshToken);
75
+ return {
76
+ accessToken,
77
+ projectId: activeAccount.projectId,
78
+ email: activeAccount.email,
79
+ };
80
+ }
81
+ catch (error) {
82
+ // If file not found, just return null (not available)
83
+ if (error instanceof Error && error.message.includes("not found")) {
84
+ return null;
85
+ }
86
+ // If other error (parsing, network), throw it
87
+ throw error;
88
+ }
89
+ }