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
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)
|