usage-cli 0.29.32__py3-none-any.whl
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.
- adapters/__init__.py +5 -0
- adapters/agy.py +68 -0
- adapters/claude.py +215 -0
- adapters/codex.py +209 -0
- adapters/rate_limits.py +76 -0
- adapters/registry.py +17 -0
- adapters/types.py +139 -0
- agy_disk_cache.py +135 -0
- agy_loader.py +416 -0
- agy_quota_probe.py +748 -0
- agy_window_keeper.py +185 -0
- analyzer/__init__.py +5 -0
- analyzer/aggregator.py +139 -0
- analyzer/blocks.py +80 -0
- analyzer/diagnoser.py +638 -0
- analyzer/insights.py +277 -0
- analyzer/persona_loader.py +199 -0
- analyzer/reporter.py +989 -0
- analyzer/subscription.py +108 -0
- burn_rate.py +75 -0
- cache_quarantine.py +50 -0
- codex_disk_cache.py +227 -0
- codex_events.py +136 -0
- codex_fork_replay.py +111 -0
- codex_loader.py +1426 -0
- codex_paths.py +20 -0
- critter_frames.py +26 -0
- discussion_bridge.py +1196 -0
- discussion_cli.py +844 -0
- discussion_session.py +622 -0
- discussion_usage.py +13 -0
- discussion_window.py +955 -0
- disk_cache_common.py +132 -0
- disk_cache_lifecycle.py +39 -0
- doctor.py +452 -0
- fsevents_watch.py +207 -0
- history_disk_cache.py +110 -0
- history_loader.py +416 -0
- i18n.py +88 -0
- jsonl_limits.py +17 -0
- jsonl_utils.py +40 -0
- login_item.py +154 -0
- main.py +387 -0
- menubar.py +1201 -0
- menubar_actions.py +204 -0
- menubar_agy.py +193 -0
- menubar_chrome.py +156 -0
- menubar_menu.py +169 -0
- menubar_notify.py +102 -0
- menubar_popover.py +233 -0
- menubar_prefs.py +118 -0
- menubar_refresh.py +285 -0
- menubar_state.py +1200 -0
- menubar_title.py +157 -0
- menubar_update.py +123 -0
- panel_window.py +78 -0
- panel_window_state.py +159 -0
- panels/__init__.py +186 -0
- panels/base.py +83 -0
- panels/dynamic_height.py +140 -0
- panels/payload.py +178 -0
- panels/web_panel.py +513 -0
- panels/window_drag.py +56 -0
- prefs.py +44 -0
- pricing.py +452 -0
- project_resolver.py +112 -0
- service_status.py +383 -0
- session_hooks.py +1154 -0
- setup_app.py +171 -0
- setup_hook.py +1011 -0
- statusline_settings.py +160 -0
- talent_market_bridge.py +243 -0
- time_utils.py +24 -0
- tui.py +288 -0
- tui_sprite.py +206 -0
- ui/__init__.py +5 -0
- ui/html_report.py +923 -0
- ui/report_scripts.py +251 -0
- ui/report_styles.py +370 -0
- ui/tables.py +888 -0
- update_checker.py +156 -0
- update_gate.py +66 -0
- update_release_notes.py +49 -0
- usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
- usage_cli-0.29.32.dist-info/METADATA +223 -0
- usage_cli-0.29.32.dist-info/RECORD +109 -0
- usage_cli-0.29.32.dist-info/WHEEL +5 -0
- usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
- usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
- usage_cli-0.29.32.dist-info/top_level.txt +80 -0
- usage_cli.py +827 -0
- usage_client.py +487 -0
- usage_diagnosis_snapshot.py +143 -0
- usage_dir_sweeper.py +100 -0
- usage_lang.py +79 -0
- usage_logging.py +75 -0
- usage_notifications.py +96 -0
- usage_rate.py +97 -0
- usage_session_resume.py +913 -0
- usage_statusline.py +810 -0
- usage_statusline_agy.py +397 -0
- usage_statusline_forwarder.py +88 -0
- usage_terse_mode.py +223 -0
- usage_terse_reminder.py +151 -0
- win_login_item.py +53 -0
- window_keeper.py +264 -0
- windows_watch.py +443 -0
- wintray.py +2014 -0
- wintray_menu.py +136 -0
update_checker.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
GITHUB_RELEASES_API = "https://api.github.com/repos/aqua5230/usage/releases/latest"
|
|
17
|
+
# A release payload is tens of KB; cap the read so a broken endpoint cannot make
|
|
18
|
+
# us buffer an unbounded response.
|
|
19
|
+
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class ReleaseInfo:
|
|
24
|
+
version: str
|
|
25
|
+
html_url: str
|
|
26
|
+
body: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ReleaseCheckResult:
|
|
31
|
+
release: ReleaseInfo | None
|
|
32
|
+
failed: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_version(version: str) -> tuple[int, int, int, tuple[str, ...] | None] | None:
|
|
36
|
+
match = re.match(r"^(\d+)\.(\d+)\.(\d+)(.*)?$", version)
|
|
37
|
+
if match is None:
|
|
38
|
+
return None
|
|
39
|
+
suffix = match.group(4) or ""
|
|
40
|
+
prerelease: tuple[str, ...] | None = None
|
|
41
|
+
if suffix.startswith("-") or suffix.startswith("."):
|
|
42
|
+
prerelease = tuple(part for part in suffix[1:].split("+", 1)[0].split(".") if part)
|
|
43
|
+
if not prerelease:
|
|
44
|
+
return None
|
|
45
|
+
elif suffix and not suffix.startswith("+"):
|
|
46
|
+
return None
|
|
47
|
+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)), prerelease)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compare_versions(a: str, b: str) -> int:
|
|
51
|
+
parsed_a = _parse_version(a)
|
|
52
|
+
parsed_b = _parse_version(b)
|
|
53
|
+
if parsed_a is None or parsed_b is None:
|
|
54
|
+
raise ValueError("versions must use MAJOR.MINOR.PATCH numeric format")
|
|
55
|
+
base_a = parsed_a[:3]
|
|
56
|
+
base_b = parsed_b[:3]
|
|
57
|
+
if base_a < base_b:
|
|
58
|
+
return -1
|
|
59
|
+
if base_a > base_b:
|
|
60
|
+
return 1
|
|
61
|
+
prerelease_a = parsed_a[3]
|
|
62
|
+
prerelease_b = parsed_b[3]
|
|
63
|
+
if prerelease_a is None and prerelease_b is None:
|
|
64
|
+
return 0
|
|
65
|
+
if prerelease_a is None:
|
|
66
|
+
return 1
|
|
67
|
+
if prerelease_b is None:
|
|
68
|
+
return -1
|
|
69
|
+
for part_a, part_b in zip(prerelease_a, prerelease_b, strict=False):
|
|
70
|
+
is_digit_a = part_a.isdigit()
|
|
71
|
+
is_digit_b = part_b.isdigit()
|
|
72
|
+
if is_digit_a and is_digit_b:
|
|
73
|
+
value_a = int(part_a)
|
|
74
|
+
value_b = int(part_b)
|
|
75
|
+
if value_a < value_b:
|
|
76
|
+
return -1
|
|
77
|
+
if value_a > value_b:
|
|
78
|
+
return 1
|
|
79
|
+
continue
|
|
80
|
+
if is_digit_a != is_digit_b:
|
|
81
|
+
return -1 if is_digit_a else 1
|
|
82
|
+
if part_a < part_b:
|
|
83
|
+
return -1
|
|
84
|
+
if part_a > part_b:
|
|
85
|
+
return 1
|
|
86
|
+
if len(prerelease_a) < len(prerelease_b):
|
|
87
|
+
return -1
|
|
88
|
+
if len(prerelease_a) > len(prerelease_b):
|
|
89
|
+
return 1
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_latest_release(current_version: str, *, timeout: float = 5.0) -> ReleaseInfo | None:
|
|
94
|
+
return check_latest_release_result(current_version, timeout=timeout).release
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def check_latest_release_result(
|
|
98
|
+
current_version: str,
|
|
99
|
+
*,
|
|
100
|
+
timeout: float = 5.0,
|
|
101
|
+
) -> ReleaseCheckResult:
|
|
102
|
+
if _parse_version(current_version) is None:
|
|
103
|
+
return ReleaseCheckResult(None)
|
|
104
|
+
|
|
105
|
+
request = urllib.request.Request(
|
|
106
|
+
GITHUB_RELEASES_API,
|
|
107
|
+
headers={
|
|
108
|
+
"Accept": "application/vnd.github+json",
|
|
109
|
+
"User-Agent": f"usage/{current_version}",
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
114
|
+
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
|
115
|
+
if len(raw) > MAX_RESPONSE_BYTES:
|
|
116
|
+
raise ValueError("release response exceeds the size limit")
|
|
117
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
118
|
+
except (
|
|
119
|
+
OSError,
|
|
120
|
+
UnicodeDecodeError,
|
|
121
|
+
ValueError,
|
|
122
|
+
urllib.error.URLError,
|
|
123
|
+
urllib.error.HTTPError,
|
|
124
|
+
):
|
|
125
|
+
return ReleaseCheckResult(None, failed=True)
|
|
126
|
+
|
|
127
|
+
release = _release_from_payload(payload)
|
|
128
|
+
if release is None:
|
|
129
|
+
return ReleaseCheckResult(None)
|
|
130
|
+
try:
|
|
131
|
+
if compare_versions(current_version, release.version) >= 0:
|
|
132
|
+
return ReleaseCheckResult(None)
|
|
133
|
+
except ValueError:
|
|
134
|
+
return ReleaseCheckResult(None)
|
|
135
|
+
return ReleaseCheckResult(release)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _release_from_payload(payload: Any) -> ReleaseInfo | None:
|
|
139
|
+
if not isinstance(payload, dict):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
tag_name = payload.get("tag_name")
|
|
143
|
+
html_url = payload.get("html_url")
|
|
144
|
+
body = payload.get("body", "")
|
|
145
|
+
if not isinstance(tag_name, str) or not isinstance(html_url, str):
|
|
146
|
+
return None
|
|
147
|
+
# The URL is handed to webbrowser.open(); anything but https is not a release page.
|
|
148
|
+
if not html_url.startswith("https://"):
|
|
149
|
+
return None
|
|
150
|
+
if not isinstance(body, str):
|
|
151
|
+
body = ""
|
|
152
|
+
|
|
153
|
+
version = tag_name.removeprefix("v")
|
|
154
|
+
if _parse_version(version) is None:
|
|
155
|
+
return None
|
|
156
|
+
return ReleaseInfo(version=version, html_url=html_url, body=body)
|
update_gate.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import update_checker
|
|
13
|
+
|
|
14
|
+
AUTO_CHECK_TTL_SECONDS = 24 * 60 * 60
|
|
15
|
+
UPDATE_DISMISS_SECONDS = 24 * 3600
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dismissed_recently(prefs: dict[str, Any]) -> bool:
|
|
19
|
+
dismissed_at = prefs.get("update_dismissed_at")
|
|
20
|
+
if isinstance(dismissed_at, int | float):
|
|
21
|
+
return (time.time() - float(dismissed_at)) < UPDATE_DISMISS_SECONDS
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def auto_check_is_due(prefs: dict[str, Any]) -> bool:
|
|
26
|
+
cached = prefs.get("last_update_check")
|
|
27
|
+
checked_at = cached.get("checked_at") if isinstance(cached, dict) else None
|
|
28
|
+
if not isinstance(checked_at, int | float):
|
|
29
|
+
return True
|
|
30
|
+
return (time.time() - float(checked_at)) >= AUTO_CHECK_TTL_SECONDS
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def stale_cache_reset(prefs: dict[str, Any], current_version: str) -> dict[str, Any] | None:
|
|
34
|
+
cached = prefs.get("last_update_check")
|
|
35
|
+
if (
|
|
36
|
+
isinstance(cached, dict)
|
|
37
|
+
and isinstance(cached.get("latest_version"), str)
|
|
38
|
+
and cached.get("current_version") != current_version
|
|
39
|
+
and update_checker.compare_versions(current_version, cached["latest_version"]) >= 0
|
|
40
|
+
):
|
|
41
|
+
return {
|
|
42
|
+
**cached,
|
|
43
|
+
"current_version": current_version,
|
|
44
|
+
"latest_version": current_version,
|
|
45
|
+
}
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_check_cache_entry(
|
|
50
|
+
current_version: str,
|
|
51
|
+
release: update_checker.ReleaseInfo | None,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"checked_at": time.time(),
|
|
55
|
+
"current_version": current_version,
|
|
56
|
+
"latest_version": release.version if release else current_version,
|
|
57
|
+
"release_url": release.html_url if release else None,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_alert_choice(result_code: int, release_version: str) -> tuple[str, dict[str, str]]:
|
|
62
|
+
if result_code == 1000:
|
|
63
|
+
return ("open", {})
|
|
64
|
+
if result_code == 1002:
|
|
65
|
+
return ("skip", {"update_skipped_version": release_version})
|
|
66
|
+
return ("dismiss", {})
|
update_release_notes.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
_HEADING_RE = re.compile(r"^[ \t]{0,3}#{1,6}[ \t]+(.*?)[ \t]*#*[ \t]*$")
|
|
10
|
+
_BULLET_RE = re.compile(r"^([ \t]*)[-*][ \t]+(.*)$")
|
|
11
|
+
_LINK_RE = re.compile(r"\[([^\]]+)\]\([^)]+\)")
|
|
12
|
+
_STRONG_RE = re.compile(r"\*\*(.+?)\*\*|__(.+?)__")
|
|
13
|
+
_INLINE_CODE_RE = re.compile(r"`([^`\n]+)`")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _format_inline_markdown(text: str) -> str:
|
|
17
|
+
text = _LINK_RE.sub(r"\1", text)
|
|
18
|
+
text = _STRONG_RE.sub(lambda match: match.group(1) or match.group(2), text)
|
|
19
|
+
return _INLINE_CODE_RE.sub(r"\1", text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def format_release_notes(body: str, limit: int) -> str:
|
|
23
|
+
"""Convert GitHub Release Markdown into compact plain text for an alert."""
|
|
24
|
+
if not isinstance(body, str) or limit <= 0:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
lines: list[str] = []
|
|
28
|
+
for line in body.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
29
|
+
heading = _HEADING_RE.match(line)
|
|
30
|
+
if heading:
|
|
31
|
+
if lines and lines[-1]:
|
|
32
|
+
lines.append("")
|
|
33
|
+
lines.append(_format_inline_markdown(heading.group(1)))
|
|
34
|
+
lines.append("")
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
line = _format_inline_markdown(line)
|
|
38
|
+
lines.append(_BULLET_RE.sub(r"\1• \2", line))
|
|
39
|
+
|
|
40
|
+
text = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip()
|
|
41
|
+
if len(text) <= limit:
|
|
42
|
+
return text
|
|
43
|
+
if limit == 1:
|
|
44
|
+
return "…"
|
|
45
|
+
|
|
46
|
+
boundary = max(text.rfind(" ", 0, limit), text.rfind("\n", 0, limit))
|
|
47
|
+
if boundary < 0:
|
|
48
|
+
return "…"
|
|
49
|
+
return f"{text[:boundary].rstrip()}…"
|