gemini-cli-usage 0.1.0__tar.gz → 0.1.3__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.3}/PKG-INFO +1 -1
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/pyproject.toml +1 -1
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/src/gemini_cli_usage/__init__.py +64 -16
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/.github/workflows/publish.yml +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/.gitignore +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/README.md +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/tests/test_gemini_cli_usage.py +0 -0
- {gemini_cli_usage-0.1.0 → gemini_cli_usage-0.1.3}/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:
|
|
@@ -409,12 +463,8 @@ def _color_pct(pct: float | int | None) -> str:
|
|
|
409
463
|
if pct is None:
|
|
410
464
|
return "?"
|
|
411
465
|
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}"
|
|
466
|
+
color = _RED if p >= 70 else _YELLOW if p >= 40 else _GREEN
|
|
467
|
+
return f"{color}{_format_pct(p)}{_RESET}"
|
|
418
468
|
|
|
419
469
|
|
|
420
470
|
def _format_pct(pct: float | int | None) -> str:
|
|
@@ -427,9 +477,6 @@ def _format_pct(pct: float | int | None) -> str:
|
|
|
427
477
|
|
|
428
478
|
|
|
429
479
|
def _print_status(data: dict):
|
|
430
|
-
dim = "\033[0;90m"
|
|
431
|
-
reset = "\033[0m"
|
|
432
|
-
|
|
433
480
|
print(f"Project: {Path(data['project_root']).name}")
|
|
434
481
|
print(f"Auth: {data.get('auth_label', 'unknown')}")
|
|
435
482
|
|
|
@@ -454,14 +501,14 @@ def _print_status(data: dict):
|
|
|
454
501
|
)
|
|
455
502
|
print(
|
|
456
503
|
f" {label:{name_width}s} {_color_pct(bucket.get('used_pct'))} used"
|
|
457
|
-
f"{remain_part}{
|
|
504
|
+
f"{remain_part}{_DIM}{reset_part}{_RESET}"
|
|
458
505
|
)
|
|
459
506
|
|
|
460
507
|
for bucket in quota.get("buckets", []):
|
|
461
508
|
model = bucket.get("model") or "unknown"
|
|
462
509
|
print_bucket_line(model, bucket)
|
|
463
510
|
elif data.get("quota_error"):
|
|
464
|
-
print(f" {'Quota':20s} {
|
|
511
|
+
print(f" {'Quota':20s} {_DIM}{data['quota_error']}{_RESET}")
|
|
465
512
|
|
|
466
513
|
|
|
467
514
|
def _statusline_text(data: dict) -> str:
|
|
@@ -520,7 +567,8 @@ def cmd_json(args):
|
|
|
520
567
|
|
|
521
568
|
def cmd_daemon(args):
|
|
522
569
|
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
|
523
|
-
|
|
570
|
+
if hasattr(signal, "SIGTERM"):
|
|
571
|
+
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
|
524
572
|
|
|
525
573
|
root = Path(args.root).resolve() if args.root else None
|
|
526
574
|
usage_file = get_usage_file()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|