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 +15 -0
- package/README.md +115 -0
- package/bin/ag-quota.js +466 -0
- package/dist/cli-auth.d.ts +12 -0
- package/dist/cli-auth.d.ts.map +1 -0
- package/dist/cli-auth.js +89 -0
- package/dist/cloud.d.ts +40 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +110 -0
- package/dist/config.d.ts +101 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +88 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +248 -0
- package/package.json +57 -0
- package/src/cli-auth.ts +136 -0
- package/src/cli.ts +154 -0
- package/src/cloud.ts +195 -0
- package/src/config.ts +193 -0
- package/src/index.test.ts +96 -0
- package/src/index.ts +379 -0
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
|
package/bin/ag-quota.js
ADDED
|
@@ -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"}
|
package/dist/cli-auth.js
ADDED
|
@@ -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
|
+
}
|