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
fsevents_watch.py ADDED
@@ -0,0 +1,207 @@
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 contextlib
10
+ import ctypes
11
+ import logging
12
+ import os
13
+ from collections.abc import Iterable
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from codex_paths import codex_home
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # --- FSEvents (ctypes) for event-driven UI refresh ---
23
+ _FSEVENTS_AVAILABLE = False
24
+ _fs_callback_ref: Any = None # prevent GC of ctypes callback
25
+
26
+ try:
27
+ _cs_lib = ctypes.cdll.LoadLibrary(
28
+ "/System/Library/Frameworks/CoreServices.framework/CoreServices",
29
+ )
30
+ _cf_lib = ctypes.cdll.LoadLibrary(
31
+ "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation",
32
+ )
33
+ _FSEventStreamCallback = ctypes.CFUNCTYPE(
34
+ None,
35
+ ctypes.c_void_p,
36
+ ctypes.c_void_p,
37
+ ctypes.c_size_t,
38
+ ctypes.c_void_p,
39
+ ctypes.POINTER(ctypes.c_uint32),
40
+ ctypes.POINTER(ctypes.c_uint64),
41
+ )
42
+ _cs_lib.FSEventStreamCreate.restype = ctypes.c_void_p
43
+ _cs_lib.FSEventStreamCreate.argtypes = [
44
+ ctypes.c_void_p,
45
+ _FSEventStreamCallback,
46
+ ctypes.c_void_p,
47
+ ctypes.c_void_p,
48
+ ctypes.c_uint64,
49
+ ctypes.c_double,
50
+ ctypes.c_uint32,
51
+ ]
52
+ _cs_lib.FSEventStreamScheduleWithRunLoop.argtypes = [
53
+ ctypes.c_void_p,
54
+ ctypes.c_void_p,
55
+ ctypes.c_void_p,
56
+ ]
57
+ _cs_lib.FSEventStreamStart.restype = ctypes.c_int
58
+ _cs_lib.FSEventStreamStart.argtypes = [ctypes.c_void_p]
59
+ _cs_lib.FSEventStreamStop.argtypes = [ctypes.c_void_p]
60
+ _cs_lib.FSEventStreamInvalidate.argtypes = [ctypes.c_void_p]
61
+ _cs_lib.FSEventStreamRelease.argtypes = [ctypes.c_void_p]
62
+ _cf_lib.CFRunLoopGetCurrent.restype = ctypes.c_void_p
63
+ _cf_lib.CFArrayCreate.restype = ctypes.c_void_p
64
+ _cf_lib.CFArrayCreate.argtypes = [
65
+ ctypes.c_void_p,
66
+ ctypes.POINTER(ctypes.c_void_p),
67
+ ctypes.c_long,
68
+ ctypes.c_void_p,
69
+ ]
70
+ _cf_lib.CFStringCreateWithCString.restype = ctypes.c_void_p
71
+ _cf_lib.CFStringCreateWithCString.argtypes = [
72
+ ctypes.c_void_p,
73
+ ctypes.c_char_p,
74
+ ctypes.c_uint32,
75
+ ]
76
+ _kCFStringEncodingUTF8 = 0x08000100
77
+ _kFSEventStreamCreateFlagNoDefer = 0x00000002
78
+ _kFSEventStreamCreateFlagFileEvents = 0x00000010
79
+ _kFSEventStreamEventIdSinceNow = 0xFFFFFFFFFFFFFFFF
80
+ _FSEVENTS_AVAILABLE = True
81
+ except (OSError, AttributeError):
82
+ pass
83
+
84
+ _FSEVENT_RESCAN_FLAGS = 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008
85
+ _FSEVENT_UNSCOPED_FLAGS = 0x00000020 | 0x00000040 | 0x00000080
86
+ _FSEVENT_ITEM_IS_FILE = 0x00010000
87
+ _FSEVENT_ITEM_TYPE_FLAGS = 0x00010000 | 0x00020000 | 0x00040000
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class FileEventChanges:
92
+ paths: frozenset[Path]
93
+ needs_full_scan: bool = False
94
+
95
+
96
+ def classify_file_events(paths: Iterable[str], flags: Iterable[int]) -> FileEventChanges:
97
+ """Return file paths whose scope is reliable, or request a full scan."""
98
+ path_list = list(paths)
99
+ flag_list = list(flags)
100
+ if len(path_list) != len(flag_list):
101
+ return FileEventChanges(frozenset(), needs_full_scan=True)
102
+
103
+ dirty_paths: set[Path] = set()
104
+ needs_full_scan = False
105
+ for raw_path, event_flags in zip(path_list, flag_list, strict=True):
106
+ if event_flags & (_FSEVENT_RESCAN_FLAGS | _FSEVENT_UNSCOPED_FLAGS):
107
+ needs_full_scan = True
108
+ continue
109
+ if event_flags & _FSEVENT_ITEM_TYPE_FLAGS != _FSEVENT_ITEM_IS_FILE:
110
+ needs_full_scan = True
111
+ continue
112
+ dirty_paths.add(Path(raw_path))
113
+ return FileEventChanges(frozenset(dirty_paths), needs_full_scan)
114
+
115
+
116
+ def usage_watch_paths() -> list[Path]:
117
+ """Return existing agent directories that contain usage history records."""
118
+ home = Path.home()
119
+ codex_dir = codex_home()
120
+ return [
121
+ path
122
+ for path in (
123
+ home / ".claude" / "projects",
124
+ codex_dir / "sessions",
125
+ codex_dir / "archived_sessions",
126
+ )
127
+ if path.exists()
128
+ ]
129
+
130
+
131
+ def setup_fsevents(delegate: Any) -> Any:
132
+ """Start FSEventStream watching agent usage directories; returns a handle or None."""
133
+ global _fs_callback_ref
134
+ if not _FSEVENTS_AVAILABLE:
135
+ return None
136
+ try:
137
+ watch_paths = usage_watch_paths()
138
+ if not watch_paths:
139
+ return None
140
+ cf_path_values = [
141
+ _cf_lib.CFStringCreateWithCString(
142
+ None,
143
+ str(path).encode("utf-8"),
144
+ _kCFStringEncodingUTF8,
145
+ )
146
+ for path in watch_paths
147
+ ]
148
+ paths_arr = (ctypes.c_void_p * len(cf_path_values))(*cf_path_values)
149
+ cf_paths = _cf_lib.CFArrayCreate(None, paths_arr, len(cf_path_values), None)
150
+
151
+ def _on_fs_event(
152
+ _stream: Any,
153
+ _info: Any,
154
+ _num: Any,
155
+ _paths: Any,
156
+ _flags: Any,
157
+ _ids: Any,
158
+ ) -> None:
159
+ try:
160
+ event_count = int(_num)
161
+ raw_paths = ctypes.cast(_paths, ctypes.POINTER(ctypes.c_char_p))
162
+ paths = [os.fsdecode(raw_paths[index]) for index in range(event_count)]
163
+ flags = [int(_flags[index]) for index in range(event_count)]
164
+ changes = classify_file_events(paths, flags)
165
+ except Exception:
166
+ changes = FileEventChanges(frozenset(), needs_full_scan=True)
167
+ delegate.performSelectorOnMainThread_withObject_waitUntilDone_(
168
+ "refreshFromFileEvent:",
169
+ changes,
170
+ False,
171
+ )
172
+
173
+ _fs_callback_ref = _FSEventStreamCallback(_on_fs_event)
174
+ stream = _cs_lib.FSEventStreamCreate(
175
+ None,
176
+ _fs_callback_ref,
177
+ None,
178
+ cf_paths,
179
+ _kFSEventStreamEventIdSinceNow,
180
+ 0.5,
181
+ _kFSEventStreamCreateFlagNoDefer | _kFSEventStreamCreateFlagFileEvents,
182
+ )
183
+ if not stream:
184
+ return None
185
+ rl = _cf_lib.CFRunLoopGetCurrent()
186
+ mode = _cf_lib.CFStringCreateWithCString(
187
+ None,
188
+ b"kCFRunLoopDefaultMode",
189
+ _kCFStringEncodingUTF8,
190
+ )
191
+ _cs_lib.FSEventStreamScheduleWithRunLoop(stream, rl, mode)
192
+ _cs_lib.FSEventStreamStart(stream)
193
+ return stream
194
+ except Exception:
195
+ if os.environ.get("USAGE_DEBUG") == "1":
196
+ logger.warning("FSEvents setup failed", exc_info=True)
197
+ return None
198
+
199
+
200
+ def cleanup_fsevents(stream: Any) -> None:
201
+ """Stop and release an FSEventStream."""
202
+ if not _FSEVENTS_AVAILABLE or not stream:
203
+ return
204
+ with contextlib.suppress(Exception):
205
+ _cs_lib.FSEventStreamStop(stream)
206
+ _cs_lib.FSEventStreamInvalidate(stream)
207
+ _cs_lib.FSEventStreamRelease(stream)
history_disk_cache.py ADDED
@@ -0,0 +1,110 @@
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
+ """Sharded disk persistence for history_loader's JSONL parse cache."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ from collections import OrderedDict
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from disk_cache_common import (
18
+ _SHARD_COUNT,
19
+ _deserialize_usage_entry,
20
+ _encoded_payload,
21
+ _load_payload,
22
+ _remove_legacy_cache,
23
+ _serialize_usage_entry,
24
+ _shard_index,
25
+ _shard_path,
26
+ _write_if_changed,
27
+ )
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ __all__ = ["_shard_index", "_shard_path", "flush_caches", "seed_caches"]
32
+
33
+ _FileCache = OrderedDict[Path, Any]
34
+
35
+
36
+ def _load_shard(path: Path, schema_version: int) -> dict[str, Any] | None:
37
+ payload = _load_payload(path, schema_version)
38
+ if payload is None:
39
+ return None
40
+ files = payload.get("files")
41
+ if not isinstance(files, dict):
42
+ path.unlink(missing_ok=True)
43
+ return None
44
+ return files
45
+
46
+
47
+ def seed_caches(
48
+ cache_path: Path,
49
+ schema_version: int,
50
+ maxsize: int,
51
+ file_cache: _FileCache,
52
+ ) -> None:
53
+ from history_loader import _FileCacheEntry
54
+
55
+ _remove_legacy_cache(cache_path)
56
+ for index in range(_SHARD_COUNT):
57
+ files = _load_shard(_shard_path(cache_path, index), schema_version)
58
+ if files is None:
59
+ continue
60
+ for path_str, file_data in files.items():
61
+ if not isinstance(file_data, dict):
62
+ continue
63
+ try:
64
+ path = Path(path_str)
65
+ entries_data = file_data["entries"]
66
+ if not isinstance(entries_data, list):
67
+ continue
68
+ if len(file_cache) >= maxsize:
69
+ file_cache.popitem(last=False)
70
+ file_cache[path] = _FileCacheEntry(
71
+ mtime=float(file_data["mtime"]),
72
+ size=int(file_data["size"]),
73
+ entries=[_deserialize_usage_entry(entry) for entry in entries_data],
74
+ confirmed_offset=int(file_data["confirmed_offset"]),
75
+ confirmed_prefix_digest=bytes.fromhex(file_data["confirmed_prefix_digest"]),
76
+ )
77
+ except (KeyError, TypeError, ValueError):
78
+ continue
79
+
80
+
81
+ def flush_caches(
82
+ cache_path: Path,
83
+ schema_version: int,
84
+ file_cache: _FileCache,
85
+ ) -> None:
86
+ """Atomically replace only shards whose serialized contents changed."""
87
+ try:
88
+ _remove_legacy_cache(cache_path)
89
+ shards: list[dict[str, Any]] = [{} for _ in range(_SHARD_COUNT)]
90
+ for path, entry in file_cache.items():
91
+ shards[_shard_index(path)][str(path)] = {
92
+ "mtime": entry.mtime,
93
+ "size": entry.size,
94
+ "entries": [_serialize_usage_entry(item) for item in entry.entries],
95
+ "confirmed_offset": entry.confirmed_offset,
96
+ "confirmed_prefix_digest": entry.confirmed_prefix_digest.hex(),
97
+ }
98
+
99
+ for index, files in enumerate(shards):
100
+ path = _shard_path(cache_path, index)
101
+ if files:
102
+ _write_if_changed(
103
+ path,
104
+ _encoded_payload({"schema_version": schema_version, "files": files}),
105
+ )
106
+ else:
107
+ path.unlink(missing_ok=True)
108
+ except Exception as exc:
109
+ if os.environ.get("USAGE_DEBUG") == "1":
110
+ logger.warning("failed to write history jsonl cache %s: %s", cache_path, exc)