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