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.
Files changed (109) hide show
  1. adapters/__init__.py +5 -0
  2. adapters/agy.py +68 -0
  3. adapters/claude.py +215 -0
  4. adapters/codex.py +209 -0
  5. adapters/rate_limits.py +76 -0
  6. adapters/registry.py +17 -0
  7. adapters/types.py +139 -0
  8. agy_disk_cache.py +135 -0
  9. agy_loader.py +416 -0
  10. agy_quota_probe.py +748 -0
  11. agy_window_keeper.py +185 -0
  12. analyzer/__init__.py +5 -0
  13. analyzer/aggregator.py +139 -0
  14. analyzer/blocks.py +80 -0
  15. analyzer/diagnoser.py +638 -0
  16. analyzer/insights.py +277 -0
  17. analyzer/persona_loader.py +199 -0
  18. analyzer/reporter.py +989 -0
  19. analyzer/subscription.py +108 -0
  20. burn_rate.py +75 -0
  21. cache_quarantine.py +50 -0
  22. codex_disk_cache.py +227 -0
  23. codex_events.py +136 -0
  24. codex_fork_replay.py +111 -0
  25. codex_loader.py +1426 -0
  26. codex_paths.py +20 -0
  27. critter_frames.py +26 -0
  28. discussion_bridge.py +1196 -0
  29. discussion_cli.py +844 -0
  30. discussion_session.py +622 -0
  31. discussion_usage.py +13 -0
  32. discussion_window.py +955 -0
  33. disk_cache_common.py +132 -0
  34. disk_cache_lifecycle.py +39 -0
  35. doctor.py +452 -0
  36. fsevents_watch.py +207 -0
  37. history_disk_cache.py +110 -0
  38. history_loader.py +416 -0
  39. i18n.py +88 -0
  40. jsonl_limits.py +17 -0
  41. jsonl_utils.py +40 -0
  42. login_item.py +154 -0
  43. main.py +387 -0
  44. menubar.py +1201 -0
  45. menubar_actions.py +204 -0
  46. menubar_agy.py +193 -0
  47. menubar_chrome.py +156 -0
  48. menubar_menu.py +169 -0
  49. menubar_notify.py +102 -0
  50. menubar_popover.py +233 -0
  51. menubar_prefs.py +118 -0
  52. menubar_refresh.py +285 -0
  53. menubar_state.py +1200 -0
  54. menubar_title.py +157 -0
  55. menubar_update.py +123 -0
  56. panel_window.py +78 -0
  57. panel_window_state.py +159 -0
  58. panels/__init__.py +186 -0
  59. panels/base.py +83 -0
  60. panels/dynamic_height.py +140 -0
  61. panels/payload.py +178 -0
  62. panels/web_panel.py +513 -0
  63. panels/window_drag.py +56 -0
  64. prefs.py +44 -0
  65. pricing.py +452 -0
  66. project_resolver.py +112 -0
  67. service_status.py +383 -0
  68. session_hooks.py +1154 -0
  69. setup_app.py +171 -0
  70. setup_hook.py +1011 -0
  71. statusline_settings.py +160 -0
  72. talent_market_bridge.py +243 -0
  73. time_utils.py +24 -0
  74. tui.py +288 -0
  75. tui_sprite.py +206 -0
  76. ui/__init__.py +5 -0
  77. ui/html_report.py +923 -0
  78. ui/report_scripts.py +251 -0
  79. ui/report_styles.py +370 -0
  80. ui/tables.py +888 -0
  81. update_checker.py +156 -0
  82. update_gate.py +66 -0
  83. update_release_notes.py +49 -0
  84. usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
  85. usage_cli-0.29.32.dist-info/METADATA +223 -0
  86. usage_cli-0.29.32.dist-info/RECORD +109 -0
  87. usage_cli-0.29.32.dist-info/WHEEL +5 -0
  88. usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
  89. usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
  90. usage_cli-0.29.32.dist-info/top_level.txt +80 -0
  91. usage_cli.py +827 -0
  92. usage_client.py +487 -0
  93. usage_diagnosis_snapshot.py +143 -0
  94. usage_dir_sweeper.py +100 -0
  95. usage_lang.py +79 -0
  96. usage_logging.py +75 -0
  97. usage_notifications.py +96 -0
  98. usage_rate.py +97 -0
  99. usage_session_resume.py +913 -0
  100. usage_statusline.py +810 -0
  101. usage_statusline_agy.py +397 -0
  102. usage_statusline_forwarder.py +88 -0
  103. usage_terse_mode.py +223 -0
  104. usage_terse_reminder.py +151 -0
  105. win_login_item.py +53 -0
  106. window_keeper.py +264 -0
  107. windows_watch.py +443 -0
  108. wintray.py +2014 -0
  109. 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", {})
@@ -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()}…"