gusage 1.1.1 → 1.2.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/README.md +1 -0
- package/dist/index.js +149 -11
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ gusage --json --watch 5s | jq .
|
|
|
67
67
|
- `-n, --notify [threshold]`: Send a desktop notification when a model drops below the threshold percent (default: `20`). Requires `--watch`.
|
|
68
68
|
- `-j, --json`: Output raw JSON instead of a table. Can be combined with `--watch` for streaming data.
|
|
69
69
|
- `--no-color`: Disable color output (also respects `NO_COLOR` env var).
|
|
70
|
+
- `-v, --version`: Show version.
|
|
70
71
|
|
|
71
72
|
## Requirements
|
|
72
73
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,56 @@ 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";
|
|
11
|
+
// package.json
|
|
12
|
+
var package_default = {
|
|
13
|
+
name: "gusage",
|
|
14
|
+
version: "1.2.0",
|
|
15
|
+
description: "A standalone CLI to export Gemini CLI quota and usage statistics",
|
|
16
|
+
module: "index.ts",
|
|
17
|
+
type: "module",
|
|
18
|
+
bin: {
|
|
19
|
+
gusage: "dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
files: [
|
|
22
|
+
"dist/index.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
author: "Abdellah Hariti <haritiabdellah@gmail.com>",
|
|
27
|
+
license: "MIT",
|
|
28
|
+
private: false,
|
|
29
|
+
scripts: {
|
|
30
|
+
format: "prettier --write .",
|
|
31
|
+
prepare: "husky",
|
|
32
|
+
build: "bun build ./index.ts --outfile dist/index.js && chmod +x dist/index.js",
|
|
33
|
+
prepack: "bun run build"
|
|
34
|
+
},
|
|
35
|
+
"lint-staged": {
|
|
36
|
+
"*": "prettier --write --ignore-unknown",
|
|
37
|
+
"*.ts": "bash -c 'bun x tsc --noEmit'"
|
|
38
|
+
},
|
|
39
|
+
devDependencies: {
|
|
40
|
+
"@types/bun": "latest",
|
|
41
|
+
husky: "^9.1.7",
|
|
42
|
+
"lint-staged": "^16.2.7",
|
|
43
|
+
prettier: "^3.8.1"
|
|
44
|
+
},
|
|
45
|
+
peerDependencies: {
|
|
46
|
+
typescript: "^5"
|
|
47
|
+
},
|
|
48
|
+
keywords: [
|
|
49
|
+
"gemini-cli",
|
|
50
|
+
"gemini",
|
|
51
|
+
"google-gemini",
|
|
52
|
+
"quota",
|
|
53
|
+
"usage",
|
|
54
|
+
"stats",
|
|
55
|
+
"monitoring"
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// index.ts
|
|
10
60
|
var OAUTH_CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com";
|
|
11
61
|
var OAUTH_CLIENT_SECRET = "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";
|
|
12
62
|
var CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
@@ -36,6 +86,56 @@ function formatRelativeTime(dateString) {
|
|
|
36
86
|
return `${h}h ${m}m`;
|
|
37
87
|
return `${m}m`;
|
|
38
88
|
}
|
|
89
|
+
function sendNotification(title, message) {
|
|
90
|
+
const spawnDetached = (command, args) => {
|
|
91
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
92
|
+
child.on("error", () => {});
|
|
93
|
+
child.unref();
|
|
94
|
+
};
|
|
95
|
+
if (process.platform === "darwin") {
|
|
96
|
+
const escapedMessage = message.replace(/"/g, "\\\"");
|
|
97
|
+
const escapedTitle = title.replace(/"/g, "\\\"");
|
|
98
|
+
const script = `display notification "${escapedMessage}" with title "${escapedTitle}"`;
|
|
99
|
+
spawnDetached("osascript", ["-e", script]);
|
|
100
|
+
} else if (process.platform === "linux") {
|
|
101
|
+
spawnDetached("notify-send", [title, message]);
|
|
102
|
+
} else if (process.platform === "win32") {
|
|
103
|
+
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);`;
|
|
104
|
+
spawnDetached("powershell", ["-Command", command]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function checkNotificationSupport() {
|
|
108
|
+
if (process.platform === "linux") {
|
|
109
|
+
try {
|
|
110
|
+
execSync("which notify-send", { stdio: "ignore" });
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
function parseNotifyThresholdArg(raw) {
|
|
119
|
+
if (raw === undefined)
|
|
120
|
+
return null;
|
|
121
|
+
const thresholdPct = Number(raw);
|
|
122
|
+
if (!Number.isFinite(thresholdPct) || thresholdPct < 0 || thresholdPct > 100) {
|
|
123
|
+
throw new Error("Error: --notify threshold must be a number between 0 and 100.");
|
|
124
|
+
}
|
|
125
|
+
return thresholdPct / 100;
|
|
126
|
+
}
|
|
127
|
+
function validateNotifyRuntime(notifyThreshold, isWatching, hasNotificationSupport) {
|
|
128
|
+
if (notifyThreshold === null)
|
|
129
|
+
return null;
|
|
130
|
+
if (!isWatching)
|
|
131
|
+
return "Error: --notify requires --watch.";
|
|
132
|
+
if (!hasNotificationSupport)
|
|
133
|
+
return "Error: --notify is not supported on this system (missing notify-send).";
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function shouldSendThresholdNotification(prevFraction, currentFraction, threshold) {
|
|
137
|
+
return prevFraction !== undefined && prevFraction > threshold && currentFraction <= threshold;
|
|
138
|
+
}
|
|
39
139
|
function parseVersion(modelId) {
|
|
40
140
|
const match = modelId.match(/gemini-(\d+)(?:\.(\d+))?-(.*)/);
|
|
41
141
|
if (match) {
|
|
@@ -89,18 +189,25 @@ function padVisual(str, width, side = "right") {
|
|
|
89
189
|
return side === "right" ? str + pad : pad + str;
|
|
90
190
|
}
|
|
91
191
|
async function main() {
|
|
192
|
+
const packageVersion = package_default.version;
|
|
92
193
|
const args = Bun.argv.slice(2);
|
|
93
194
|
const watchIdx = args.findIndex((a) => a === "--watch" || a === "-w");
|
|
94
195
|
if (watchIdx !== -1 && (watchIdx === args.length - 1 || args[watchIdx + 1].startsWith("-"))) {
|
|
95
196
|
args.splice(watchIdx + 1, 0, "10s");
|
|
96
197
|
}
|
|
198
|
+
const notifyIdx = args.findIndex((a) => a === "--notify" || a === "-n");
|
|
199
|
+
if (notifyIdx !== -1 && (notifyIdx === args.length - 1 || args[notifyIdx + 1].startsWith("-"))) {
|
|
200
|
+
args.splice(notifyIdx + 1, 0, "20");
|
|
201
|
+
}
|
|
97
202
|
const { values } = parseArgs({
|
|
98
203
|
args,
|
|
99
204
|
options: {
|
|
100
205
|
help: { type: "boolean", short: "h" },
|
|
101
|
-
|
|
206
|
+
version: { type: "boolean", short: "v" },
|
|
207
|
+
json: { type: "boolean", short: "j" },
|
|
102
208
|
"no-color": { type: "boolean" },
|
|
103
|
-
watch: { type: "string", short: "w" }
|
|
209
|
+
watch: { type: "string", short: "w" },
|
|
210
|
+
notify: { type: "string", short: "n" }
|
|
104
211
|
},
|
|
105
212
|
strict: true
|
|
106
213
|
});
|
|
@@ -110,19 +217,22 @@ Usage: gusage [options]
|
|
|
110
217
|
|
|
111
218
|
Options:
|
|
112
219
|
-h, --help Show this help message
|
|
220
|
+
-v, --version Show version and exit
|
|
113
221
|
-w, --watch [interval] Update live every interval (default: 10s).
|
|
114
222
|
Supports combined units: 20s, 5m, 1m20s.
|
|
115
|
-
-
|
|
223
|
+
-n, --notify [threshold] Show a critical OS notification if any model falls below
|
|
224
|
+
the threshold (default: 20%; requires --watch).
|
|
225
|
+
-j, --json Output raw JSON instead of a table
|
|
116
226
|
--no-color Disable color output
|
|
117
227
|
`);
|
|
118
228
|
return;
|
|
119
229
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
process.exit(1);
|
|
230
|
+
if (values.version) {
|
|
231
|
+
console.log(packageVersion);
|
|
232
|
+
return;
|
|
124
233
|
}
|
|
125
234
|
const isWatching = values.watch !== undefined;
|
|
235
|
+
const isJson = values.json === true;
|
|
126
236
|
let intervalMs = 1e4;
|
|
127
237
|
let intervalStr = "10s";
|
|
128
238
|
if (values.watch) {
|
|
@@ -146,6 +256,20 @@ Options:
|
|
|
146
256
|
}
|
|
147
257
|
}
|
|
148
258
|
const useColor = !values["no-color"] && !process.env.NO_COLOR && process.stdout.isTTY;
|
|
259
|
+
let notifyThreshold;
|
|
260
|
+
const lastFractions = new Map;
|
|
261
|
+
try {
|
|
262
|
+
notifyThreshold = parseNotifyThresholdArg(values.notify);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
console.error(message);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
const notifyRuntimeError = validateNotifyRuntime(notifyThreshold, isWatching, checkNotificationSupport());
|
|
269
|
+
if (notifyRuntimeError) {
|
|
270
|
+
console.error(notifyRuntimeError);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
149
273
|
const colors = {
|
|
150
274
|
reset: useColor ? "\x1B[0m" : "",
|
|
151
275
|
dim: useColor ? "\x1B[2m" : "",
|
|
@@ -242,11 +366,22 @@ Options:
|
|
|
242
366
|
return vB.minor - vA.minor;
|
|
243
367
|
return vB.suffix.localeCompare(vA.suffix);
|
|
244
368
|
});
|
|
369
|
+
if (notifyThreshold !== null) {
|
|
370
|
+
for (const b of quotaData.buckets) {
|
|
371
|
+
const fraction = b.remainingFraction ?? 0;
|
|
372
|
+
const modelId = b.modelId;
|
|
373
|
+
const prevFraction = lastFractions.get(modelId);
|
|
374
|
+
if (shouldSendThresholdNotification(prevFraction, fraction, notifyThreshold)) {
|
|
375
|
+
sendNotification("Gemini Quota Alert", `Model ${modelId} has dropped below ${Math.round(notifyThreshold * 100)}%.`);
|
|
376
|
+
}
|
|
377
|
+
lastFractions.set(modelId, fraction);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
245
380
|
}
|
|
246
|
-
if (isWatching)
|
|
381
|
+
if (isWatching && !isJson)
|
|
247
382
|
process.stdout.write(colors.clear);
|
|
248
|
-
if (
|
|
249
|
-
console.log(JSON.stringify(quotaData.buckets || []
|
|
383
|
+
if (isJson) {
|
|
384
|
+
console.log(JSON.stringify(quotaData.buckets || []));
|
|
250
385
|
} else {
|
|
251
386
|
if (!quotaData.buckets || quotaData.buckets.length === 0) {
|
|
252
387
|
console.log("No quota data available.");
|
|
@@ -286,7 +421,7 @@ Options:
|
|
|
286
421
|
console.log("");
|
|
287
422
|
}
|
|
288
423
|
});
|
|
289
|
-
if (isWatching) {
|
|
424
|
+
if (isWatching && !isJson) {
|
|
290
425
|
console.log(`
|
|
291
426
|
${colors.dim}Updating every ${intervalStr}, press q to quit${colors.reset}`);
|
|
292
427
|
}
|
|
@@ -374,5 +509,8 @@ async function refreshAccessToken(refreshToken) {
|
|
|
374
509
|
return response.json();
|
|
375
510
|
}
|
|
376
511
|
export {
|
|
512
|
+
validateNotifyRuntime,
|
|
513
|
+
shouldSendThresholdNotification,
|
|
514
|
+
parseNotifyThresholdArg,
|
|
377
515
|
main as default
|
|
378
516
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gusage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|