gemini-cli-usage 0.1.0__tar.gz → 0.1.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemini-cli-usage
3
- Version: 0.1.0
3
+ Version: 0.1.4
4
4
  Summary: Gemini CLI quota monitor — fetches Code Assist quota data from Google's backend
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gemini-cli-usage"
3
- version = "0.1.0"
3
+ version = "0.1.4"
4
4
  description = "Gemini CLI quota monitor — fetches Code Assist quota data from Google's backend"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -30,10 +30,56 @@ import urllib.request
30
30
  from datetime import UTC, datetime
31
31
  from pathlib import Path
32
32
 
33
- GEMINI_DIR = Path.home() / ".gemini"
34
- OAUTH_FILE = GEMINI_DIR / "oauth_creds.json"
35
- SETTINGS_FILE = GEMINI_DIR / "settings.json"
36
- DEFAULT_USAGE_FILE = GEMINI_DIR / "usage-limits.json"
33
+ _NATIVE_GEMINI_DIR = Path.home() / ".gemini"
34
+
35
+
36
+ def _wsl_gemini_dirs() -> list[Path]:
37
+ """Return candidate .gemini dirs from WSL distros (Windows only)."""
38
+ if sys.platform != "win32":
39
+ return []
40
+ import subprocess
41
+
42
+ try:
43
+ result = subprocess.run(
44
+ ["wsl", "-l", "-q"],
45
+ capture_output=True,
46
+ timeout=5,
47
+ )
48
+ raw = result.stdout.decode("utf-16-le", errors="replace")
49
+ distros = [d.strip() for d in raw.splitlines() if d.strip()]
50
+ except Exception:
51
+ return []
52
+
53
+ dirs: list[Path] = []
54
+ for distro in distros:
55
+ wsl_home = Path(f"//wsl$/{distro}/home")
56
+ try:
57
+ if wsl_home.is_dir():
58
+ for user_dir in wsl_home.iterdir():
59
+ candidate = user_dir / ".gemini"
60
+ if candidate.is_dir():
61
+ dirs.append(candidate)
62
+ except OSError:
63
+ continue
64
+ return dirs
65
+
66
+
67
+ def _find_gemini_file(filename: str) -> Path:
68
+ """Return the first existing .gemini/<filename> from native + WSL paths."""
69
+ native = _NATIVE_GEMINI_DIR / filename
70
+ if native.exists():
71
+ return native
72
+ for wsl_dir in _wsl_gemini_dirs():
73
+ candidate = wsl_dir / filename
74
+ if candidate.exists():
75
+ return candidate
76
+ return native # default even if missing
77
+
78
+
79
+ GEMINI_DIR = _NATIVE_GEMINI_DIR
80
+ OAUTH_FILE = _find_gemini_file("oauth_creds.json")
81
+ SETTINGS_FILE = _find_gemini_file("settings.json")
82
+ DEFAULT_USAGE_FILE = _find_gemini_file("usage-limits.json")
37
83
 
38
84
  DAEMON_INTERVAL = 300 # 5 minutes
39
85
  CACHE_MAX_AGE = 300
@@ -53,6 +99,14 @@ AUTH_LABELS = {
53
99
  OAUTH_CLIENT_ID_PATTERN = re.compile(r"const OAUTH_CLIENT_ID = '([^']+)';")
54
100
  OAUTH_CLIENT_SECRET_PATTERN = re.compile(r"const OAUTH_CLIENT_SECRET = '([^']+)';")
55
101
 
102
+ # ANSI color codes — disabled when stdout is not a terminal (e.g. piped or redirected).
103
+ _TTY = sys.stdout.isatty()
104
+ _RED = "\033[0;31m" if _TTY else ""
105
+ _YELLOW = "\033[0;33m" if _TTY else ""
106
+ _GREEN = "\033[0;32m" if _TTY else ""
107
+ _DIM = "\033[0;90m" if _TTY else ""
108
+ _RESET = "\033[0m" if _TTY else ""
109
+
56
110
 
57
111
  def _read_json(path: Path) -> dict | list | None:
58
112
  try:
@@ -136,14 +190,56 @@ def _write_oauth_credentials(creds: dict):
136
190
 
137
191
 
138
192
  def _get_gemini_cli_oauth2_path() -> Path | None:
193
+ oauth2_rels = [
194
+ Path("node_modules") / "@google" / "gemini-cli-core" / "dist" / "src" / "code_assist" / "oauth2.js",
195
+ Path("node_modules") / "@google" / "gemini-cli" / "node_modules" / "@google" / "gemini-cli-core" / "dist" / "src" / "code_assist" / "oauth2.js",
196
+ ]
197
+
198
+ candidates: list[Path] = []
199
+
200
+ # 1. Try shutil.which
139
201
  gemini_bin = shutil.which("gemini")
140
- if not gemini_bin:
141
- return None
202
+ if gemini_bin:
203
+ resolved = Path(gemini_bin).resolve()
204
+ candidates.extend([resolved.parent, resolved.parent.parent])
205
+
206
+ # 2. On Windows, scan fnm node version directories
207
+ if sys.platform == "win32":
208
+ fnm_dir = Path.home() / "AppData" / "Roaming" / "fnm" / "node-versions"
209
+ if fnm_dir.exists():
210
+ for version_dir in fnm_dir.iterdir():
211
+ candidates.append(version_dir / "installation")
212
+
213
+ # Also check AppData/Local fnm multishells
214
+ fnm_multi = Path.home() / "AppData" / "Local" / "fnm_multishells"
215
+ if fnm_multi.exists():
216
+ for shell_dir in fnm_multi.iterdir():
217
+ candidates.append(shell_dir)
218
+
219
+ # 3. Unix: check common global npm/nvm paths
220
+ else:
221
+ for p in [
222
+ Path("/usr/lib"),
223
+ Path("/usr/local/lib"),
224
+ Path.home() / ".nvm" / "versions",
225
+ ]:
226
+ if p.exists():
227
+ if "nvm" in str(p):
228
+ for v in p.rglob("node"):
229
+ candidates.append(v / "lib")
230
+ else:
231
+ candidates.append(p)
232
+
233
+ for root in candidates:
234
+ for oauth2_rel in oauth2_rels:
235
+ oauth2_path = root / oauth2_rel
236
+ if oauth2_path.exists():
237
+ return oauth2_path
238
+ oauth2_path = root / "lib" / oauth2_rel
239
+ if oauth2_path.exists():
240
+ return oauth2_path
142
241
 
143
- resolved = Path(gemini_bin).resolve()
144
- package_root = resolved.parent.parent
145
- oauth2_path = package_root / "node_modules" / "@google" / "gemini-cli-core" / "dist" / "src" / "code_assist" / "oauth2.js"
146
- return oauth2_path if oauth2_path.exists() else None
242
+ return None
147
243
 
148
244
 
149
245
  def _get_gemini_cli_oauth_client_credentials() -> tuple[str, str] | None:
@@ -409,12 +505,8 @@ def _color_pct(pct: float | int | None) -> str:
409
505
  if pct is None:
410
506
  return "?"
411
507
  p = float(pct)
412
- red = "\033[0;31m"
413
- yellow = "\033[0;33m"
414
- green = "\033[0;32m"
415
- reset = "\033[0m"
416
- color = red if p >= 70 else yellow if p >= 40 else green
417
- return f"{color}{_format_pct(p)}{reset}"
508
+ color = _RED if p >= 70 else _YELLOW if p >= 40 else _GREEN
509
+ return f"{color}{_format_pct(p)}{_RESET}"
418
510
 
419
511
 
420
512
  def _format_pct(pct: float | int | None) -> str:
@@ -427,9 +519,6 @@ def _format_pct(pct: float | int | None) -> str:
427
519
 
428
520
 
429
521
  def _print_status(data: dict):
430
- dim = "\033[0;90m"
431
- reset = "\033[0m"
432
-
433
522
  print(f"Project: {Path(data['project_root']).name}")
434
523
  print(f"Auth: {data.get('auth_label', 'unknown')}")
435
524
 
@@ -454,14 +543,14 @@ def _print_status(data: dict):
454
543
  )
455
544
  print(
456
545
  f" {label:{name_width}s} {_color_pct(bucket.get('used_pct'))} used"
457
- f"{remain_part}{dim}{reset_part}{reset}"
546
+ f"{remain_part}{_DIM}{reset_part}{_RESET}"
458
547
  )
459
548
 
460
549
  for bucket in quota.get("buckets", []):
461
550
  model = bucket.get("model") or "unknown"
462
551
  print_bucket_line(model, bucket)
463
552
  elif data.get("quota_error"):
464
- print(f" {'Quota':20s} {dim}{data['quota_error']}{reset}")
553
+ print(f" {'Quota':20s} {_DIM}{data['quota_error']}{_RESET}")
465
554
 
466
555
 
467
556
  def _statusline_text(data: dict) -> str:
@@ -520,7 +609,8 @@ def cmd_json(args):
520
609
 
521
610
  def cmd_daemon(args):
522
611
  signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
523
- signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
612
+ if hasattr(signal, "SIGTERM"):
613
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
524
614
 
525
615
  root = Path(args.root).resolve() if args.root else None
526
616
  usage_file = get_usage_file()