gusage 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/index.js +378 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdellah Hariti
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# gusage
|
|
2
|
+
|
|
3
|
+
A standalone, sub-second CLI to export and monitor Gemini CLI quota and usage statistics.
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="assets/demo.png" width="500" />
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
This tool reverse-engineers the internal API handshakes used by the main Gemini CLI to fetch usage statistics.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Live Monitoring:** Real-time quota updates using the `--watch` flag.
|
|
14
|
+
- **Machine Readable:** Supports JSON output for easy integration with other tools.
|
|
15
|
+
- **Fast:** Returns results in sub-second time.
|
|
16
|
+
- **Smart Sorting:** Automatically sorts models to highlight the ones you care about.
|
|
17
|
+
- **Beautiful TUI**
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
You can run it directly without installation using `npx` or `bunx`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bunx gusage
|
|
25
|
+
# or
|
|
26
|
+
npx gusage
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or install it globally:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bun add -g gusage
|
|
33
|
+
# or
|
|
34
|
+
npm install -g gusage
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Display quota in a beautiful table (default)
|
|
41
|
+
gusage
|
|
42
|
+
|
|
43
|
+
# Monitor quota live every 10 seconds
|
|
44
|
+
gusage --watch
|
|
45
|
+
|
|
46
|
+
# Monitor quota every 1 minute and 20 seconds
|
|
47
|
+
gusage --watch 1m20s
|
|
48
|
+
|
|
49
|
+
# Output raw JSON for scripting
|
|
50
|
+
gusage -o json
|
|
51
|
+
|
|
52
|
+
# Disable colors
|
|
53
|
+
gusage --no-color
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
- `-h, --help`: Show help message.
|
|
59
|
+
- `-w, --watch [interval]`: Update live every interval (default: 10s). Supports units like `20s`, `5m`, `1m20s`.
|
|
60
|
+
- `-o, --output-format <fmt>`: Set output format to `table` (default) or `json`.
|
|
61
|
+
- `--no-color`: Disable color output (also respects `NO_COLOR` env var).
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- **Authentication:** You must have already authenticated via the official Gemini CLI (`gemini login`).
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// index.ts
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import os from "os";
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
import { parseArgs } from "util";
|
|
10
|
+
var OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
11
|
+
var OAUTH_CLIENT_SECRET = "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";
|
|
12
|
+
var CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
13
|
+
var CODE_ASSIST_API_VERSION = "v1internal";
|
|
14
|
+
var GEMINI_DIR = ".gemini";
|
|
15
|
+
var MAIN_ACCOUNT_KEY = "main-account";
|
|
16
|
+
var ENCRYPTED_FILE_NAME = "mcp-oauth-tokens-v2.json";
|
|
17
|
+
var LEGACY_OAUTH_FILE = "oauth_creds.json";
|
|
18
|
+
var VALID_GEMINI_MODELS = new Set([
|
|
19
|
+
"gemini-3-pro-preview",
|
|
20
|
+
"gemini-3-flash-preview",
|
|
21
|
+
"gemini-2.5-pro",
|
|
22
|
+
"gemini-2.5-flash",
|
|
23
|
+
"gemini-2.5-flash-lite"
|
|
24
|
+
]);
|
|
25
|
+
var SEONDARY_MODELS = ["gemini-2.5-flash", "gemini-2.5-flash-lite"];
|
|
26
|
+
function formatRelativeTime(dateString) {
|
|
27
|
+
const now = new Date;
|
|
28
|
+
const resetTime = new Date(dateString);
|
|
29
|
+
const diffMs = resetTime.getTime() - now.getTime();
|
|
30
|
+
if (diffMs <= 0)
|
|
31
|
+
return "Resetting...";
|
|
32
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
33
|
+
const h = Math.floor(diffMins / 60);
|
|
34
|
+
const m = diffMins % 60;
|
|
35
|
+
if (h > 0)
|
|
36
|
+
return `${h}h ${m}m`;
|
|
37
|
+
return `${m}m`;
|
|
38
|
+
}
|
|
39
|
+
function parseVersion(modelId) {
|
|
40
|
+
const match = modelId.match(/gemini-(\d+)(?:\.(\d+))?-(.*)/);
|
|
41
|
+
if (match) {
|
|
42
|
+
return {
|
|
43
|
+
major: parseInt(match[1], 10),
|
|
44
|
+
minor: match[2] ? parseInt(match[2], 10) : 0,
|
|
45
|
+
suffix: match[3] || ""
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { major: 0, minor: 0, suffix: modelId };
|
|
49
|
+
}
|
|
50
|
+
function renderProgressBar(fraction, width, useColor, isMuted = false) {
|
|
51
|
+
const BLOCKS = ["", "\u258F", "\u258E", "\u258D", "\u258C", "\u258B", "\u258A", "\u2589"];
|
|
52
|
+
const units = Math.round(Math.max(0, Math.min(1, fraction)) * width * 8);
|
|
53
|
+
const full = Math.floor(units / 8);
|
|
54
|
+
const rem = units % 8;
|
|
55
|
+
if (!useColor) {
|
|
56
|
+
let bar2 = "\u2588".repeat(full);
|
|
57
|
+
if (full < width) {
|
|
58
|
+
bar2 += BLOCKS[rem] || "";
|
|
59
|
+
}
|
|
60
|
+
const remaining = width - visualLength(bar2);
|
|
61
|
+
if (remaining > 0) {
|
|
62
|
+
bar2 += "\u2591".repeat(remaining);
|
|
63
|
+
}
|
|
64
|
+
return bar2;
|
|
65
|
+
}
|
|
66
|
+
const fillCol = fraction < 0.2 ? 160 : isMuted ? 244 : 15;
|
|
67
|
+
const trackCol = 237;
|
|
68
|
+
const fg = (n) => `\x1B[38;5;${n}m`;
|
|
69
|
+
const bg = (n) => `\x1B[48;5;${n}m`;
|
|
70
|
+
const reset = `\x1B[0m`;
|
|
71
|
+
let bar = bg(trackCol);
|
|
72
|
+
for (let i = 0;i < width; i++) {
|
|
73
|
+
if (i < full) {
|
|
74
|
+
bar += fg(fillCol) + "\u2588";
|
|
75
|
+
} else if (i === full && rem > 0) {
|
|
76
|
+
bar += fg(fillCol) + BLOCKS[rem];
|
|
77
|
+
} else {
|
|
78
|
+
bar += fg(trackCol) + "\u2588";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return bar + reset;
|
|
82
|
+
}
|
|
83
|
+
function visualLength(str) {
|
|
84
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
85
|
+
}
|
|
86
|
+
function padVisual(str, width, side = "right") {
|
|
87
|
+
const len = visualLength(str);
|
|
88
|
+
const pad = " ".repeat(Math.max(0, width - len));
|
|
89
|
+
return side === "right" ? str + pad : pad + str;
|
|
90
|
+
}
|
|
91
|
+
async function main() {
|
|
92
|
+
const args = Bun.argv.slice(2);
|
|
93
|
+
const watchIdx = args.findIndex((a) => a === "--watch" || a === "-w");
|
|
94
|
+
if (watchIdx !== -1 && (watchIdx === args.length - 1 || args[watchIdx + 1].startsWith("-"))) {
|
|
95
|
+
args.splice(watchIdx + 1, 0, "10s");
|
|
96
|
+
}
|
|
97
|
+
const { values } = parseArgs({
|
|
98
|
+
args,
|
|
99
|
+
options: {
|
|
100
|
+
help: { type: "boolean", short: "h" },
|
|
101
|
+
"output-format": { type: "string", short: "o", default: "table" },
|
|
102
|
+
"no-color": { type: "boolean" },
|
|
103
|
+
watch: { type: "string", short: "w" }
|
|
104
|
+
},
|
|
105
|
+
strict: true
|
|
106
|
+
});
|
|
107
|
+
if (values.help) {
|
|
108
|
+
console.log(`
|
|
109
|
+
Usage: gusage [options]
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
-h, --help Show this help message
|
|
113
|
+
-w, --watch [interval] Update live every interval (default: 10s).
|
|
114
|
+
Supports combined units: 20s, 5m, 1m20s.
|
|
115
|
+
-o, --output-format <fmt> Output format: table (default), json
|
|
116
|
+
--no-color Disable color output
|
|
117
|
+
`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const outputFormat = values["output-format"];
|
|
121
|
+
if (outputFormat !== "json" && outputFormat !== "table") {
|
|
122
|
+
console.error(`Error: Unsupported output format "${outputFormat}". Use "table" or "json".`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const isWatching = values.watch !== undefined;
|
|
126
|
+
let intervalMs = 1e4;
|
|
127
|
+
let intervalStr = "10s";
|
|
128
|
+
if (values.watch) {
|
|
129
|
+
let totalMs = 0;
|
|
130
|
+
const matches = values.watch.matchAll(/(\d+)(h|m|s)?/g);
|
|
131
|
+
let found = false;
|
|
132
|
+
for (const match of matches) {
|
|
133
|
+
found = true;
|
|
134
|
+
const val = parseInt(match[1], 10);
|
|
135
|
+
const unit = match[2] || "s";
|
|
136
|
+
if (unit === "s")
|
|
137
|
+
totalMs += val * 1000;
|
|
138
|
+
else if (unit === "m")
|
|
139
|
+
totalMs += val * 60000;
|
|
140
|
+
else if (unit === "h")
|
|
141
|
+
totalMs += val * 3600000;
|
|
142
|
+
}
|
|
143
|
+
if (found) {
|
|
144
|
+
intervalMs = totalMs;
|
|
145
|
+
intervalStr = values.watch;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const useColor = !values["no-color"] && !process.env.NO_COLOR && process.stdout.isTTY;
|
|
149
|
+
const colors = {
|
|
150
|
+
reset: useColor ? "\x1B[0m" : "",
|
|
151
|
+
dim: useColor ? "\x1B[2m" : "",
|
|
152
|
+
cyan: useColor ? "\x1B[36m" : "",
|
|
153
|
+
green: useColor ? "\x1B[32m" : "",
|
|
154
|
+
yellow: useColor ? "\x1B[33m" : "",
|
|
155
|
+
red: useColor ? "\x1B[31m" : "",
|
|
156
|
+
clear: useColor ? "\x1B[2J\x1B[H" : "",
|
|
157
|
+
hideCursor: useColor ? "\x1B[?25l" : "",
|
|
158
|
+
showCursor: useColor ? "\x1B[?25h" : ""
|
|
159
|
+
};
|
|
160
|
+
const cleanup = () => {
|
|
161
|
+
if (isWatching && process.stdin.isTTY) {
|
|
162
|
+
process.stdin.setRawMode(false);
|
|
163
|
+
process.stdin.pause();
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(colors.showCursor);
|
|
166
|
+
};
|
|
167
|
+
const CTRL_C = "\x03";
|
|
168
|
+
const CTRL_D = "\x04";
|
|
169
|
+
if (isWatching && process.stdin.isTTY) {
|
|
170
|
+
process.stdin.setRawMode(true);
|
|
171
|
+
process.stdin.resume();
|
|
172
|
+
process.stdin.on("data", (data) => {
|
|
173
|
+
const key = data.toString();
|
|
174
|
+
if (key === "q" || key === CTRL_C || key === CTRL_D) {
|
|
175
|
+
cleanup();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
process.stdout.write(colors.hideCursor);
|
|
180
|
+
}
|
|
181
|
+
process.on("SIGINT", () => {
|
|
182
|
+
cleanup();
|
|
183
|
+
process.exit(0);
|
|
184
|
+
});
|
|
185
|
+
process.on("SIGTERM", () => {
|
|
186
|
+
cleanup();
|
|
187
|
+
process.exit(0);
|
|
188
|
+
});
|
|
189
|
+
const run = async () => {
|
|
190
|
+
const creds = loadLocalCredentials();
|
|
191
|
+
if (!creds) {
|
|
192
|
+
console.error('Error: No credentials found. Please run "gemini login" first.');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
let token = creds.access_token;
|
|
196
|
+
const isExpired = creds.expiry_date && Date.now() > creds.expiry_date - 60000;
|
|
197
|
+
if (isExpired && creds.refresh_token) {
|
|
198
|
+
try {
|
|
199
|
+
const refreshed = await refreshAccessToken(creds.refresh_token);
|
|
200
|
+
token = refreshed.access_token;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
203
|
+
console.error("Error: Failed to refresh access token.", message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const baseUrl = `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}`;
|
|
207
|
+
const authHeader = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
208
|
+
const loadResponse = await fetch(`${baseUrl}:loadCodeAssist`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: authHeader,
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
metadata: { ideType: "GEMINI_CLI", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" }
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
if (!loadResponse.ok) {
|
|
216
|
+
console.error(`Error: loadCodeAssist failed (${loadResponse.status})`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
const loadData = await loadResponse.json();
|
|
220
|
+
const projectId = loadData.cloudaicompanionProject || process.env["GOOGLE_CLOUD_PROJECT"];
|
|
221
|
+
if (!projectId) {
|
|
222
|
+
console.error("Error: Could not determine Project ID.");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const quotaResponse = await fetch(`${baseUrl}:retrieveUserQuota`, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: authHeader,
|
|
228
|
+
body: JSON.stringify({ project: projectId })
|
|
229
|
+
});
|
|
230
|
+
if (!quotaResponse.ok) {
|
|
231
|
+
console.error(`Error: retrieveUserQuota failed (${quotaResponse.status})`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const quotaData = await quotaResponse.json();
|
|
235
|
+
if (quotaData.buckets) {
|
|
236
|
+
quotaData.buckets = quotaData.buckets.filter((b) => b.modelId && VALID_GEMINI_MODELS.has(b.modelId)).sort((a, b) => {
|
|
237
|
+
const vA = parseVersion(a.modelId);
|
|
238
|
+
const vB = parseVersion(b.modelId);
|
|
239
|
+
if (vA.major !== vB.major)
|
|
240
|
+
return vB.major - vA.major;
|
|
241
|
+
if (vA.minor !== vB.minor)
|
|
242
|
+
return vB.minor - vA.minor;
|
|
243
|
+
return vB.suffix.localeCompare(vA.suffix);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (isWatching)
|
|
247
|
+
process.stdout.write(colors.clear);
|
|
248
|
+
if (outputFormat === "json") {
|
|
249
|
+
console.log(JSON.stringify(quotaData.buckets || [], null, 2));
|
|
250
|
+
} else {
|
|
251
|
+
if (!quotaData.buckets || quotaData.buckets.length === 0) {
|
|
252
|
+
console.log("No quota data available.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const headers = ["Gemini Model", "Remaining %", "Reset Time"];
|
|
256
|
+
const BAR_WIDTH = 20;
|
|
257
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
258
|
+
const showBar = terminalWidth > 60;
|
|
259
|
+
const tableData = quotaData.buckets.map((b) => {
|
|
260
|
+
const fraction = b.remainingFraction ?? 0;
|
|
261
|
+
const isMuted = SEONDARY_MODELS.includes(b.modelId);
|
|
262
|
+
const model = b.modelId.replace("gemini-", "");
|
|
263
|
+
const bar = showBar ? renderProgressBar(fraction, BAR_WIDTH, useColor, isMuted) : "";
|
|
264
|
+
const pct = `${Math.round(fraction * 100)}%`.padStart(4);
|
|
265
|
+
const reset = b.resetTime ? formatRelativeTime(b.resetTime) : "N/A";
|
|
266
|
+
return { model, bar, pct, reset, isMuted };
|
|
267
|
+
});
|
|
268
|
+
const modelWidth = Math.max(headers[0].length, ...tableData.map((d) => d.model.length));
|
|
269
|
+
const remainingWidth = Math.max(headers[1].length, showBar ? BAR_WIDTH + 1 + 4 : 4);
|
|
270
|
+
const resetWidth = Math.max(headers[2].length, ...tableData.map((d) => d.reset.length));
|
|
271
|
+
const h0 = headers[0].padEnd(modelWidth);
|
|
272
|
+
const h1 = headers[1].padEnd(remainingWidth);
|
|
273
|
+
const h2 = headers[2].padEnd(resetWidth);
|
|
274
|
+
const headerRow = `${h0} ${h1} ${h2}`;
|
|
275
|
+
console.log(headerRow);
|
|
276
|
+
console.log(`${colors.dim}${"\u2500".repeat(visualLength(headerRow))}${colors.reset}`);
|
|
277
|
+
tableData.forEach((d, idx) => {
|
|
278
|
+
const modelLabel = d.isMuted ? `${colors.dim}${d.model}${colors.reset}` : d.model;
|
|
279
|
+
const m = padVisual(modelLabel, modelWidth);
|
|
280
|
+
const r_content = showBar ? `${d.bar} ${colors.dim}${d.pct}${colors.reset}` : `${colors.dim}${d.pct}${colors.reset}`;
|
|
281
|
+
const r = padVisual(r_content, remainingWidth);
|
|
282
|
+
const resetLabel = d.isMuted ? `${colors.dim}${d.reset}${colors.reset}` : d.reset;
|
|
283
|
+
const t = padVisual(resetLabel, resetWidth);
|
|
284
|
+
console.log(`${m} ${r} ${t}`);
|
|
285
|
+
if (idx < tableData.length - 1) {
|
|
286
|
+
console.log("");
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
if (isWatching) {
|
|
290
|
+
console.log(`
|
|
291
|
+
${colors.dim}Updating every ${intervalStr}, press q to quit${colors.reset}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
await run();
|
|
296
|
+
if (isWatching) {
|
|
297
|
+
setInterval(run, intervalMs);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (import.meta.main) {
|
|
301
|
+
main().catch((err) => {
|
|
302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
+
console.error("Fatal Error:", message);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function getGeminiDir() {
|
|
308
|
+
return path.join(os.homedir(), GEMINI_DIR);
|
|
309
|
+
}
|
|
310
|
+
function deriveEncryptionKey() {
|
|
311
|
+
const salt = `${os.hostname()}-${os.userInfo().username}-gemini-cli`;
|
|
312
|
+
return crypto.scryptSync("gemini-cli-oauth", salt, 32);
|
|
313
|
+
}
|
|
314
|
+
function decrypt(encryptedData, key) {
|
|
315
|
+
const parts = encryptedData.split(":");
|
|
316
|
+
if (parts.length !== 3) {
|
|
317
|
+
throw new Error("Invalid encrypted data format");
|
|
318
|
+
}
|
|
319
|
+
const ivHex = parts[0];
|
|
320
|
+
const authTagHex = parts[1];
|
|
321
|
+
const encrypted = parts[2];
|
|
322
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
323
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
324
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
325
|
+
decipher.setAuthTag(authTag);
|
|
326
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
327
|
+
decrypted += decipher.final("utf8");
|
|
328
|
+
return decrypted;
|
|
329
|
+
}
|
|
330
|
+
function loadLocalCredentials() {
|
|
331
|
+
const geminiDir = getGeminiDir();
|
|
332
|
+
const v2Path = path.join(geminiDir, ENCRYPTED_FILE_NAME);
|
|
333
|
+
if (fs.existsSync(v2Path)) {
|
|
334
|
+
try {
|
|
335
|
+
const data = fs.readFileSync(v2Path, "utf-8");
|
|
336
|
+
const key = deriveEncryptionKey();
|
|
337
|
+
const decrypted = decrypt(data, key);
|
|
338
|
+
const tokens = JSON.parse(decrypted);
|
|
339
|
+
const mainAccount = tokens[MAIN_ACCOUNT_KEY];
|
|
340
|
+
if (mainAccount && mainAccount.token) {
|
|
341
|
+
return {
|
|
342
|
+
access_token: mainAccount.token.accessToken,
|
|
343
|
+
refresh_token: mainAccount.token.refreshToken,
|
|
344
|
+
expiry_date: mainAccount.token.expiresAt,
|
|
345
|
+
token_type: mainAccount.token.tokenType
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
} catch (e) {}
|
|
349
|
+
}
|
|
350
|
+
const legacyPath = path.join(geminiDir, LEGACY_OAUTH_FILE);
|
|
351
|
+
if (fs.existsSync(legacyPath)) {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.parse(fs.readFileSync(legacyPath, "utf-8"));
|
|
354
|
+
} catch (e) {}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
async function refreshAccessToken(refreshToken) {
|
|
359
|
+
const url = "https://oauth2.googleapis.com/token";
|
|
360
|
+
const body = new URLSearchParams({
|
|
361
|
+
client_id: OAUTH_CLIENT_ID,
|
|
362
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
363
|
+
refresh_token: refreshToken,
|
|
364
|
+
grant_type: "refresh_token"
|
|
365
|
+
});
|
|
366
|
+
const response = await fetch(url, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
369
|
+
body: body.toString()
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
|
373
|
+
}
|
|
374
|
+
return response.json();
|
|
375
|
+
}
|
|
376
|
+
export {
|
|
377
|
+
main as default
|
|
378
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gusage",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A standalone CLI to export Gemini CLI quota and usage statistics",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"gusage": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/index.js",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"author": "Abdellah Hariti <haritiabdellah@gmail.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"private": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"format": "prettier --write .",
|
|
20
|
+
"prepare": "husky",
|
|
21
|
+
"build": "bun build ./index.ts --outfile dist/index.js && chmod +x dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"lint-staged": {
|
|
24
|
+
"*": "prettier --write --ignore-unknown",
|
|
25
|
+
"*.ts": "bash -c 'bun x tsc --noEmit'"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"husky": "^9.1.7",
|
|
30
|
+
"lint-staged": "^16.2.7",
|
|
31
|
+
"prettier": "^3.8.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"typescript": "^5"
|
|
35
|
+
}
|
|
36
|
+
}
|