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
panels/base.py ADDED
@@ -0,0 +1,83 @@
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 sys
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Any, Protocol
12
+
13
+ if sys.platform == "darwin":
14
+ from Foundation import NSBundle, NSUserDefaults
15
+ else:
16
+ NSBundle = None
17
+ NSUserDefaults = None
18
+
19
+ if TYPE_CHECKING:
20
+ from menubar import PopoverState
21
+
22
+ ACTIVE_PANEL_DEFAULTS_KEY = "usage.activePanelId"
23
+
24
+
25
+ def next_panel_eviction_id(
26
+ panel_ids: list[str], active_panel_id: str, pending_evictions: set[str]
27
+ ) -> str | None:
28
+ return next(
29
+ (
30
+ panel_id
31
+ for panel_id in panel_ids
32
+ if panel_id != active_panel_id and panel_id not in pending_evictions
33
+ ),
34
+ None,
35
+ )
36
+
37
+
38
+ class Panel(Protocol):
39
+ id: str
40
+ i18n_key: str
41
+ claude_card_height: float
42
+ codex_card_height: float
43
+ agy_card_height: float
44
+ service_alert_height: float
45
+
46
+ def build_view(self, delegate: Any) -> Any: ...
47
+ def apply_state(self, view: Any, state: PopoverState) -> None: ...
48
+ def preferred_size(self) -> tuple[float, float]: ...
49
+
50
+
51
+ def load_active_panel_id(defaults: Any | None = None) -> str:
52
+ if defaults is None and sys.platform == "win32":
53
+ from prefs import _load_preferences
54
+
55
+ value = _load_preferences().get(ACTIVE_PANEL_DEFAULTS_KEY)
56
+ return str(value) if isinstance(value, str) and value else "classic"
57
+ assert NSUserDefaults is not None
58
+ store = defaults if defaults is not None else NSUserDefaults.standardUserDefaults()
59
+ value = store.stringForKey_(ACTIVE_PANEL_DEFAULTS_KEY)
60
+ return str(value) if value else "classic"
61
+
62
+
63
+ def save_active_panel_id(panel_id: str, defaults: Any | None = None) -> None:
64
+ if defaults is None and sys.platform == "win32":
65
+ from prefs import _load_preferences, _save_preferences
66
+
67
+ preferences = _load_preferences()
68
+ preferences[ACTIVE_PANEL_DEFAULTS_KEY] = panel_id
69
+ _save_preferences(preferences)
70
+ return
71
+ assert NSUserDefaults is not None
72
+ store = defaults if defaults is not None else NSUserDefaults.standardUserDefaults()
73
+ store.setObject_forKey_(panel_id, ACTIVE_PANEL_DEFAULTS_KEY)
74
+
75
+
76
+ def resolve_resource(name: str) -> str:
77
+ bundle = NSBundle.mainBundle() if NSBundle is not None else None
78
+ if bundle is not None:
79
+ stem, _, ext = name.rpartition(".")
80
+ path = bundle.pathForResource_ofType_(stem, ext)
81
+ if path:
82
+ return str(path)
83
+ return str(Path(__file__).resolve().parent.parent / "assets" / name)
@@ -0,0 +1,140 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+
8
+ MIN_PANEL_HEIGHT = 240.0
9
+
10
+ CONTENT_HEIGHT_SCRIPT = """
11
+ <script>
12
+ (function() {
13
+ var applyState = window.usageApplyState;
14
+ if (typeof applyState !== "function") return;
15
+ var scheduled = false;
16
+ var lastPostedHeight = null;
17
+ function naturalContentHeight() {
18
+ var wrap = document.querySelector(".wrap");
19
+ if (!wrap) return null;
20
+ // A panel can explicitly mark a flexible region whose current laid-out
21
+ // height is part of the design (for example, world_cup's empty pitch).
22
+ // Preserve only those declared floors while releasing the viewport height
23
+ // chain; ordinary content can still contract when a quota row disappears.
24
+ var floors = Array.from(
25
+ wrap.querySelectorAll("[data-usage-height-floor]"),
26
+ function(element) {
27
+ return {
28
+ element: element,
29
+ height: element.getBoundingClientRect().height,
30
+ minHeight: element.style.minHeight
31
+ };
32
+ }
33
+ );
34
+ // Panels nest .wrap differently: most put it straight in <body>, but the
35
+ // viewport-based ones (world_cup, aquarium, black_hole, ...) insert a
36
+ // padded .viewport in between. Walk the real ancestor chain instead of
37
+ // assuming a fixed depth, so every 100%-height link is released and every
38
+ // layer's spacing is counted.
39
+ var chain = [];
40
+ for (var element = wrap; element; element = element.parentElement) {
41
+ chain.push(element);
42
+ }
43
+ var properties = ["height", "minHeight", "maxHeight"];
44
+ var saved = chain.map(function(element) {
45
+ return properties.map(function(property) { return element.style[property]; });
46
+ });
47
+ try {
48
+ chain.forEach(function(element) {
49
+ element.style.height = "auto";
50
+ element.style.minHeight = "0";
51
+ element.style.maxHeight = "none";
52
+ });
53
+ floors.forEach(function(floor) {
54
+ floor.element.style.minHeight = floor.height + "px";
55
+ });
56
+ // Force reflow with viewport constraints disabled. Restoration remains
57
+ // in this synchronous task, so the temporary styles are never painted.
58
+ var total = wrap.getBoundingClientRect().height;
59
+ chain.forEach(function(element, index) {
60
+ var style = window.getComputedStyle(element);
61
+ total += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0);
62
+ // wrap's own rect already covers its padding and border; every
63
+ // ancestor wraps additional spacing around the measured box.
64
+ if (index === 0) return;
65
+ total +=
66
+ (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0) +
67
+ (parseFloat(style.borderTopWidth) || 0) + (parseFloat(style.borderBottomWidth) || 0);
68
+ });
69
+ return Math.ceil(total);
70
+ } finally {
71
+ chain.forEach(function(element, elementIndex) {
72
+ properties.forEach(function(property, propertyIndex) {
73
+ element.style[property] = saved[elementIndex][propertyIndex];
74
+ });
75
+ });
76
+ floors.forEach(function(floor) {
77
+ floor.element.style.minHeight = floor.minHeight;
78
+ });
79
+ }
80
+ }
81
+ function reportContentHeight() {
82
+ scheduled = false;
83
+ var height = naturalContentHeight();
84
+ var bridge = window.webkit && window.webkit.messageHandlers
85
+ && window.webkit.messageHandlers.usage;
86
+ if (Number.isFinite(height) && height > 0 && height !== lastPostedHeight && bridge
87
+ && typeof bridge.postMessage === "function") {
88
+ lastPostedHeight = height;
89
+ bridge.postMessage(JSON.stringify({ action: "content_height", height: height }));
90
+ }
91
+ }
92
+ function requestContentHeight() {
93
+ if (scheduled) return;
94
+ scheduled = true;
95
+ // Measure after the browser has committed DOM, font, and layout changes.
96
+ // A second frame catches WebView2's first visible layout without relying
97
+ // on a user clicking another control.
98
+ requestAnimationFrame(function() {
99
+ requestAnimationFrame(reportContentHeight);
100
+ });
101
+ }
102
+ window.usageRequestContentHeight = requestContentHeight;
103
+ window.usageApplyState = function usageApplyStateWithDynamicHeight(state) {
104
+ var result = applyState.apply(this, arguments);
105
+ requestContentHeight();
106
+ return result;
107
+ };
108
+ var wrap = document.querySelector(".wrap");
109
+ if (wrap && typeof MutationObserver === "function") {
110
+ new MutationObserver(requestContentHeight).observe(wrap, {
111
+ childList: true,
112
+ characterData: true,
113
+ subtree: true
114
+ });
115
+ }
116
+ if (wrap && typeof ResizeObserver === "function") {
117
+ new ResizeObserver(requestContentHeight).observe(wrap);
118
+ }
119
+ if (document.fonts && document.fonts.ready) {
120
+ document.fonts.ready.then(requestContentHeight);
121
+ }
122
+ requestContentHeight();
123
+ })();
124
+ </script>
125
+ """.strip()
126
+
127
+
128
+ def inject_content_height_script(html: str) -> str:
129
+ """Install the wrapper after the panel has defined usageApplyState."""
130
+ return html.replace("</body>", f"{CONTENT_HEIGHT_SCRIPT}\n</body>", 1)
131
+
132
+
133
+ def clamp_content_height(height: object, maximum: float) -> float | None:
134
+ """Validate an untrusted JS measurement and clamp it to the usable screen."""
135
+ if isinstance(height, bool) or not isinstance(height, (int, float)):
136
+ return None
137
+ value = float(height)
138
+ if not math.isfinite(value) or value <= 0 or maximum < MIN_PANEL_HEIGHT:
139
+ return None
140
+ return min(max(value, MIN_PANEL_HEIGHT), maximum)
panels/payload.py ADDED
@@ -0,0 +1,178 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import json
8
+ import os
9
+ import sys
10
+ from functools import cache, lru_cache
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ if TYPE_CHECKING:
15
+ from menubar_state import PopoverState, QuotaRowState
16
+
17
+ CORE_SCRIPT_FILENAME = "panels/panel_core.js"
18
+
19
+
20
+ def resolve_resource(name: str) -> str:
21
+ resource_root = os.environ.get("RESOURCEPATH")
22
+ if resource_root:
23
+ bundled = Path(resource_root) / name
24
+ if bundled.exists():
25
+ return str(bundled)
26
+ frozen_root = getattr(sys, "_MEIPASS", None)
27
+ if frozen_root:
28
+ bundled = Path(frozen_root) / "assets" / name
29
+ if bundled.exists():
30
+ return str(bundled)
31
+ return str(Path(__file__).resolve().parent.parent / "assets" / name)
32
+
33
+
34
+ def _i18n_path() -> Path:
35
+ resource_root = os.environ.get("RESOURCEPATH")
36
+ if resource_root:
37
+ bundled = Path(resource_root) / "i18n.json"
38
+ if bundled.exists():
39
+ return bundled
40
+ frozen_root = getattr(sys, "_MEIPASS", None)
41
+ if frozen_root:
42
+ bundled = Path(frozen_root) / "i18n.json"
43
+ if bundled.exists():
44
+ return bundled
45
+ return Path(__file__).resolve().parent.parent / "i18n.json"
46
+
47
+
48
+ def _new_state_payload(view: Any, payload: dict[str, object]) -> str | None:
49
+ if payload == getattr(view, "_last_injected_state", None):
50
+ return None
51
+ encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
52
+ view._last_injected_state = payload
53
+ if encoded == view._last_injected_payload:
54
+ return None
55
+ view._last_injected_payload = encoded
56
+ return encoded
57
+
58
+
59
+ @cache
60
+ def _load_panel_html(filename: str) -> str:
61
+ html = Path(resolve_resource(f"panels/{filename}")).read_text(encoding="utf-8")
62
+ return (
63
+ html.replace("{{CLAUDE_ICON}}", _data_uri("claude.webp"))
64
+ .replace("{{CODEX_ICON}}", _data_uri("codex.webp"))
65
+ .replace("{{CORE_SCRIPT}}", _load_core_script())
66
+ .replace("{{I18N_BUNDLE}}", json.dumps(_load_i18n_bundle(), ensure_ascii=False))
67
+ )
68
+
69
+
70
+ @lru_cache(maxsize=1)
71
+ def _load_core_script() -> str:
72
+ return Path(resolve_resource(CORE_SCRIPT_FILENAME)).read_text(encoding="utf-8")
73
+
74
+
75
+ @lru_cache(maxsize=1)
76
+ def _load_i18n_bundle() -> dict[str, dict[str, str]]:
77
+ data = json.loads(_i18n_path().read_text(encoding="utf-8"))
78
+ return {
79
+ str(lang): {str(key): str(value) for key, value in values.items()}
80
+ for lang, values in data.items()
81
+ }
82
+
83
+
84
+ @lru_cache(maxsize=4)
85
+ def _data_uri(asset_name: str) -> str:
86
+ path = Path(resolve_resource(asset_name))
87
+ mime = "image/png" if path.suffix.lower() == ".png" else "image/webp"
88
+ data = base64.b64encode(path.read_bytes()).decode("ascii")
89
+ return f"data:{mime};base64,{data}"
90
+
91
+
92
+ def _row_payload(row: QuotaRowState) -> dict[str, object]:
93
+ return {
94
+ "percent": row.percent,
95
+ "percentText": row.percent_text,
96
+ "resetText": row.reset_text,
97
+ "warning": row.warning,
98
+ "available": row.available,
99
+ "title": row.title,
100
+ }
101
+
102
+
103
+ def _state_payload(
104
+ state: PopoverState, *, system_accent_color: str | None = None
105
+ ) -> dict[str, object]:
106
+ codex_rows = {
107
+ key: _row_payload(row)
108
+ for key, row in (("session", state.codex_session), ("weekly", state.codex_weekly))
109
+ if row.title
110
+ }
111
+ project_payloads = []
112
+ for rows in (
113
+ state.projects,
114
+ state.projects_yesterday,
115
+ state.projects_7d,
116
+ state.projects_30d,
117
+ state.projects_all,
118
+ ):
119
+ project_payloads.append(
120
+ [
121
+ {
122
+ "name": name,
123
+ "tokens": tokens,
124
+ "tokensText": _fmt_tokens(tokens),
125
+ "costText": _fmt_cost(cost),
126
+ }
127
+ for name, tokens, cost in rows
128
+ ]
129
+ )
130
+ payload: dict[str, object] = {
131
+ "language": state.language,
132
+ "claude": {
133
+ "session": _row_payload(state.claude_session),
134
+ "weekly": _row_payload(state.claude_weekly),
135
+ },
136
+ "codex": {
137
+ **codex_rows,
138
+ "stale": state.codex_stale,
139
+ "credits": state.codex_credits,
140
+ },
141
+ "agy": {
142
+ "session": _row_payload(state.agy_session),
143
+ "weekly": _row_payload(state.agy_weekly),
144
+ "groupName": state.agy_group_name,
145
+ "stale": state.agy_stale,
146
+ },
147
+ "projects": project_payloads[0],
148
+ "projectsYesterday": project_payloads[1],
149
+ "projects7d": project_payloads[2],
150
+ "projects30d": project_payloads[3],
151
+ "projectsAll": project_payloads[4],
152
+ "hideClaude": state.hide_claude,
153
+ "hideCodex": state.hide_codex,
154
+ "hideAgy": state.hide_agy,
155
+ "cardOrder": list(state.card_order),
156
+ "historyError": state.history_error,
157
+ "statusline": state.statusline,
158
+ "talent": state.talent,
159
+ "footer": {
160
+ "rate": state.rate_text,
161
+ "status": state.status_text,
162
+ "today": state.today_text,
163
+ "yesterday": state.yesterday_text,
164
+ "serviceAlerts": list(state.service_alerts),
165
+ "showInstall": state.show_install_button,
166
+ },
167
+ }
168
+ if system_accent_color is not None:
169
+ payload["system_accent_color"] = system_accent_color
170
+ return payload
171
+
172
+
173
+ def _fmt_tokens(tokens: int) -> str:
174
+ return f"{tokens:,}"
175
+
176
+
177
+ def _fmt_cost(cost: float | None) -> str:
178
+ return "--" if cost is None else f"${cost:.2f}"