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.
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/PKG-INFO +1 -1
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/pyproject.toml +1 -1
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/src/gemini_cli_usage/__init__.py +112 -22
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/.github/workflows/publish.yml +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/.gitignore +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/README.md +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/tests/test_gemini_cli_usage.py +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.4}/uv.lock +0 -0
|
@@ -30,10 +30,56 @@ import urllib.request
|
|
|
30
30
|
from datetime import UTC, datetime
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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}{
|
|
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} {
|
|
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
|
-
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|