gusage 1.1.0 → 1.1.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/README.md +9 -0
- package/dist/index.js +94 -12
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ This tool reverse-engineers the internal API handshakes used by the main Gemini
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Live Monitoring:** Real-time quota updates using the `--watch` flag.
|
|
14
|
+
- **Low-Quota Alerts:** Optional desktop notifications with `--notify` when usage drops below a threshold.
|
|
14
15
|
- **Machine Readable:** Supports JSON output for easy integration with other tools.
|
|
15
16
|
- **Fast:** Returns results in sub-second time.
|
|
16
17
|
- **Smart Sorting:** Automatically sorts models to highlight the ones you care about.
|
|
@@ -46,6 +47,12 @@ gusage --watch
|
|
|
46
47
|
# Monitor quota every 1 minute and 20 seconds
|
|
47
48
|
gusage --watch 1m20s
|
|
48
49
|
|
|
50
|
+
# Monitor quota and notify when any model falls below 20% (default threshold)
|
|
51
|
+
gusage --watch --notify
|
|
52
|
+
|
|
53
|
+
# Monitor quota and notify when any model falls below 15%
|
|
54
|
+
gusage --watch 30s --notify 15
|
|
55
|
+
|
|
49
56
|
# Output raw JSON for scripting
|
|
50
57
|
gusage --json | jq .
|
|
51
58
|
|
|
@@ -57,12 +64,14 @@ gusage --json --watch 5s | jq .
|
|
|
57
64
|
|
|
58
65
|
- `-h, --help`: Show help message.
|
|
59
66
|
- `-w, --watch [interval]`: Update live every interval (default: 10s). Supports units like `20s`, `5m`, `1m20s`.
|
|
67
|
+
- `-n, --notify [threshold]`: Send a desktop notification when a model drops below the threshold percent (default: `20`). Requires `--watch`.
|
|
60
68
|
- `-j, --json`: Output raw JSON instead of a table. Can be combined with `--watch` for streaming data.
|
|
61
69
|
- `--no-color`: Disable color output (also respects `NO_COLOR` env var).
|
|
62
70
|
|
|
63
71
|
## Requirements
|
|
64
72
|
|
|
65
73
|
- **Authentication:** You must have already authenticated via the official Gemini CLI (`gemini login`).
|
|
74
|
+
- **Notifications (Linux only):** `notify-send` must be installed when using `--notify`.
|
|
66
75
|
|
|
67
76
|
## License
|
|
68
77
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from "path";
|
|
|
7
7
|
import os from "os";
|
|
8
8
|
import crypto from "crypto";
|
|
9
9
|
import { parseArgs } from "util";
|
|
10
|
+
import { execSync, spawn } from "child_process";
|
|
10
11
|
var OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
11
12
|
var OAUTH_CLIENT_SECRET = "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";
|
|
12
13
|
var CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
@@ -36,6 +37,56 @@ function formatRelativeTime(dateString) {
|
|
|
36
37
|
return `${h}h ${m}m`;
|
|
37
38
|
return `${m}m`;
|
|
38
39
|
}
|
|
40
|
+
function sendNotification(title, message) {
|
|
41
|
+
const spawnDetached = (command, args) => {
|
|
42
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
43
|
+
child.on("error", () => {});
|
|
44
|
+
child.unref();
|
|
45
|
+
};
|
|
46
|
+
if (process.platform === "darwin") {
|
|
47
|
+
const escapedMessage = message.replace(/"/g, "\\\"");
|
|
48
|
+
const escapedTitle = title.replace(/"/g, "\\\"");
|
|
49
|
+
const script = `display notification "${escapedMessage}" with title "${escapedTitle}"`;
|
|
50
|
+
spawnDetached("osascript", ["-e", script]);
|
|
51
|
+
} else if (process.platform === "linux") {
|
|
52
|
+
spawnDetached("notify-send", [title, message]);
|
|
53
|
+
} else if (process.platform === "win32") {
|
|
54
|
+
const command = `Add-Type -AssemblyName System.Windows.Forms; $g = [System.Windows.Forms.NotifyIcon]::new(); $g.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$PSHOME\\powershell.exe"); $g.Visible = $true; $g.ShowBalloonTip(5000, "${title.replace(/"/g, '`"')}", "${message.replace(/"/g, '`"')}", [System.Windows.Forms.ToolTipIcon]::Info);`;
|
|
55
|
+
spawnDetached("powershell", ["-Command", command]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function checkNotificationSupport() {
|
|
59
|
+
if (process.platform === "linux") {
|
|
60
|
+
try {
|
|
61
|
+
execSync("which notify-send", { stdio: "ignore" });
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function parseNotifyThresholdArg(raw) {
|
|
70
|
+
if (raw === undefined)
|
|
71
|
+
return null;
|
|
72
|
+
const thresholdPct = Number(raw);
|
|
73
|
+
if (!Number.isFinite(thresholdPct) || thresholdPct < 0 || thresholdPct > 100) {
|
|
74
|
+
throw new Error("Error: --notify threshold must be a number between 0 and 100.");
|
|
75
|
+
}
|
|
76
|
+
return thresholdPct / 100;
|
|
77
|
+
}
|
|
78
|
+
function validateNotifyRuntime(notifyThreshold, isWatching, hasNotificationSupport) {
|
|
79
|
+
if (notifyThreshold === null)
|
|
80
|
+
return null;
|
|
81
|
+
if (!isWatching)
|
|
82
|
+
return "Error: --notify requires --watch.";
|
|
83
|
+
if (!hasNotificationSupport)
|
|
84
|
+
return "Error: --notify is not supported on this system (missing notify-send).";
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function shouldSendThresholdNotification(prevFraction, currentFraction, threshold) {
|
|
88
|
+
return prevFraction !== undefined && prevFraction > threshold && currentFraction <= threshold;
|
|
89
|
+
}
|
|
39
90
|
function parseVersion(modelId) {
|
|
40
91
|
const match = modelId.match(/gemini-(\d+)(?:\.(\d+))?-(.*)/);
|
|
41
92
|
if (match) {
|
|
@@ -94,13 +145,18 @@ async function main() {
|
|
|
94
145
|
if (watchIdx !== -1 && (watchIdx === args.length - 1 || args[watchIdx + 1].startsWith("-"))) {
|
|
95
146
|
args.splice(watchIdx + 1, 0, "10s");
|
|
96
147
|
}
|
|
148
|
+
const notifyIdx = args.findIndex((a) => a === "--notify" || a === "-n");
|
|
149
|
+
if (notifyIdx !== -1 && (notifyIdx === args.length - 1 || args[notifyIdx + 1].startsWith("-"))) {
|
|
150
|
+
args.splice(notifyIdx + 1, 0, "20");
|
|
151
|
+
}
|
|
97
152
|
const { values } = parseArgs({
|
|
98
153
|
args,
|
|
99
154
|
options: {
|
|
100
155
|
help: { type: "boolean", short: "h" },
|
|
101
|
-
|
|
156
|
+
json: { type: "boolean", short: "j" },
|
|
102
157
|
"no-color": { type: "boolean" },
|
|
103
|
-
watch: { type: "string", short: "w" }
|
|
158
|
+
watch: { type: "string", short: "w" },
|
|
159
|
+
notify: { type: "string", short: "n" }
|
|
104
160
|
},
|
|
105
161
|
strict: true
|
|
106
162
|
});
|
|
@@ -112,17 +168,15 @@ Options:
|
|
|
112
168
|
-h, --help Show this help message
|
|
113
169
|
-w, --watch [interval] Update live every interval (default: 10s).
|
|
114
170
|
Supports combined units: 20s, 5m, 1m20s.
|
|
115
|
-
-
|
|
171
|
+
-n, --notify [threshold] Show a critical OS notification if any model falls below
|
|
172
|
+
the threshold (default: 20%; requires --watch).
|
|
173
|
+
-j, --json Output raw JSON instead of a table
|
|
116
174
|
--no-color Disable color output
|
|
117
175
|
`);
|
|
118
176
|
return;
|
|
119
177
|
}
|
|
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
178
|
const isWatching = values.watch !== undefined;
|
|
179
|
+
const isJson = values.json === true;
|
|
126
180
|
let intervalMs = 1e4;
|
|
127
181
|
let intervalStr = "10s";
|
|
128
182
|
if (values.watch) {
|
|
@@ -146,6 +200,20 @@ Options:
|
|
|
146
200
|
}
|
|
147
201
|
}
|
|
148
202
|
const useColor = !values["no-color"] && !process.env.NO_COLOR && process.stdout.isTTY;
|
|
203
|
+
let notifyThreshold;
|
|
204
|
+
const lastFractions = new Map;
|
|
205
|
+
try {
|
|
206
|
+
notifyThreshold = parseNotifyThresholdArg(values.notify);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
console.error(message);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const notifyRuntimeError = validateNotifyRuntime(notifyThreshold, isWatching, checkNotificationSupport());
|
|
213
|
+
if (notifyRuntimeError) {
|
|
214
|
+
console.error(notifyRuntimeError);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
149
217
|
const colors = {
|
|
150
218
|
reset: useColor ? "\x1B[0m" : "",
|
|
151
219
|
dim: useColor ? "\x1B[2m" : "",
|
|
@@ -242,11 +310,22 @@ Options:
|
|
|
242
310
|
return vB.minor - vA.minor;
|
|
243
311
|
return vB.suffix.localeCompare(vA.suffix);
|
|
244
312
|
});
|
|
313
|
+
if (notifyThreshold !== null) {
|
|
314
|
+
for (const b of quotaData.buckets) {
|
|
315
|
+
const fraction = b.remainingFraction ?? 0;
|
|
316
|
+
const modelId = b.modelId;
|
|
317
|
+
const prevFraction = lastFractions.get(modelId);
|
|
318
|
+
if (shouldSendThresholdNotification(prevFraction, fraction, notifyThreshold)) {
|
|
319
|
+
sendNotification("Gemini Quota Alert", `Model ${modelId} has dropped below ${Math.round(notifyThreshold * 100)}%.`);
|
|
320
|
+
}
|
|
321
|
+
lastFractions.set(modelId, fraction);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
245
324
|
}
|
|
246
|
-
if (isWatching)
|
|
325
|
+
if (isWatching && !isJson)
|
|
247
326
|
process.stdout.write(colors.clear);
|
|
248
|
-
if (
|
|
249
|
-
console.log(JSON.stringify(quotaData.buckets || []
|
|
327
|
+
if (isJson) {
|
|
328
|
+
console.log(JSON.stringify(quotaData.buckets || []));
|
|
250
329
|
} else {
|
|
251
330
|
if (!quotaData.buckets || quotaData.buckets.length === 0) {
|
|
252
331
|
console.log("No quota data available.");
|
|
@@ -286,7 +365,7 @@ Options:
|
|
|
286
365
|
console.log("");
|
|
287
366
|
}
|
|
288
367
|
});
|
|
289
|
-
if (isWatching) {
|
|
368
|
+
if (isWatching && !isJson) {
|
|
290
369
|
console.log(`
|
|
291
370
|
${colors.dim}Updating every ${intervalStr}, press q to quit${colors.reset}`);
|
|
292
371
|
}
|
|
@@ -374,5 +453,8 @@ async function refreshAccessToken(refreshToken) {
|
|
|
374
453
|
return response.json();
|
|
375
454
|
}
|
|
376
455
|
export {
|
|
456
|
+
validateNotifyRuntime,
|
|
457
|
+
shouldSendThresholdNotification,
|
|
458
|
+
parseNotifyThresholdArg,
|
|
377
459
|
main as default
|
|
378
460
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gusage",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "A standalone CLI to export Gemini CLI quota and usage statistics",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"format": "prettier --write .",
|
|
20
20
|
"prepare": "husky",
|
|
21
|
-
"build": "bun build ./index.ts --outfile dist/index.js && chmod +x dist/index.js"
|
|
21
|
+
"build": "bun build ./index.ts --outfile dist/index.js && chmod +x dist/index.js",
|
|
22
|
+
"prepack": "bun run build"
|
|
22
23
|
},
|
|
23
24
|
"lint-staged": {
|
|
24
25
|
"*": "prettier --write --ignore-unknown",
|