gusage 1.1.1 → 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/dist/index.js +94 -12
- package/package.json +3 -2
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",
|