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.
Files changed (2) hide show
  1. package/dist/index.js +94 -12
  2. 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
- "output-format": { type: "string", short: "o", default: "table" },
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
- -o, --output-format <fmt> Output format: table (default), json
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 (outputFormat === "json") {
249
- console.log(JSON.stringify(quotaData.buckets || [], null, 2));
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.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",