usage-cli 0.34.11__tar.gz → 0.34.12__tar.gz

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 (287) hide show
  1. {usage_cli-0.34.11/usage_cli.egg-info → usage_cli-0.34.12}/PKG-INFO +1 -1
  2. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/claude.py +3 -4
  3. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/rate_limits.py +2 -1
  4. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/blocks.py +4 -1
  5. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/reporter.py +4 -1
  6. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/session_hooks.py +5 -1
  7. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/setup_hook.py +47 -30
  8. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_disk_cache.py +3 -0
  9. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_loader.py +25 -6
  10. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_quota_probe.py +3 -9
  11. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_loader.py +11 -4
  12. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/muse_quota_probe.py +5 -9
  13. {usage_cli-0.34.11 → usage_cli-0.34.12}/main.py +9 -1
  14. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/actions.py +3 -3
  15. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/fsevents_watch.py +7 -5
  16. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/state.py +3 -5
  17. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/title.py +2 -2
  18. {usage_cli-0.34.11 → usage_cli-0.34.12}/pyproject.toml +1 -1
  19. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/agy_window_keeper.py +3 -9
  20. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/codex_window_keeper.py +3 -9
  21. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/window_keeper.py +3 -9
  22. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_claude.py +22 -0
  23. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_rate_limits.py +17 -0
  24. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_loader.py +22 -0
  25. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_quota_probe.py +19 -0
  26. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_window_keeper.py +44 -0
  27. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_analyzer_blocks.py +20 -0
  28. usage_cli-0.34.12/tests/test_check_release_version.py +47 -0
  29. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_check_upstream.py +8 -1
  30. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_pane.py +80 -1
  31. usage_cli-0.34.12/tests/test_cli_lookup.py +123 -0
  32. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_loader.py +44 -1
  33. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_window_keeper.py +20 -0
  34. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_fsevents_watch.py +27 -0
  35. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_html_report_snapshot.py +8 -0
  36. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar.py +147 -0
  37. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_state.py +38 -0
  38. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_quota_probe.py +20 -0
  39. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_filter_js.py +54 -0
  40. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_reporter_build_data.py +55 -1
  41. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook.py +172 -0
  42. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_ui_tables.py +99 -0
  43. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_cli.py +52 -0
  44. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_window_keeper.py +20 -0
  45. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/html_report.py +1 -1
  46. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_filter.py +7 -5
  47. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/tables.py +17 -9
  48. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/cli.py +12 -3
  49. {usage_cli-0.34.11 → usage_cli-0.34.12/usage_cli.egg-info}/PKG-INFO +1 -1
  50. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/SOURCES.txt +3 -0
  51. usage_cli-0.34.12/usage_common/cli_lookup.py +62 -0
  52. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/pricing.py +6 -1
  53. {usage_cli-0.34.11 → usage_cli-0.34.12}/LICENSE +0 -0
  54. {usage_cli-0.34.11 → usage_cli-0.34.12}/README.md +0 -0
  55. {usage_cli-0.34.11 → usage_cli-0.34.12}/THIRD_PARTY_NOTICES.md +0 -0
  56. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/__init__.py +0 -0
  57. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/agy.py +0 -0
  58. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/codex.py +0 -0
  59. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/grok.py +0 -0
  60. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/muse.py +0 -0
  61. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/registry.py +0 -0
  62. {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/types.py +0 -0
  63. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/__init__.py +0 -0
  64. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/aggregator.py +0 -0
  65. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/diagnoser.py +0 -0
  66. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/diagnosis_snapshot.py +0 -0
  67. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/dispatch_ledger.py +0 -0
  68. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/history_persist.py +0 -0
  69. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/insights.py +0 -0
  70. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/ledger_backfill.py +0 -0
  71. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/persona_loader.py +0 -0
  72. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/subscription.py +0 -0
  73. {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/usage_snapshot.py +0 -0
  74. {usage_cli-0.34.11 → usage_cli-0.34.12}/i18n.json +0 -0
  75. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/__init__.py +0 -0
  76. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/claude_pane.py +0 -0
  77. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/codex_hook_trust.py +0 -0
  78. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/login_item.py +0 -0
  79. {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/statusline_settings.py +0 -0
  80. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/__init__.py +0 -0
  81. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/_shared_cache_file.py +0 -0
  82. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/cache_quarantine.py +0 -0
  83. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/chromium_cache.py +0 -0
  84. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_desktop.py +0 -0
  85. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_paths.py +0 -0
  86. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_usage.py +0 -0
  87. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_disk_cache.py +0 -0
  88. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_events.py +0 -0
  89. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_fork_replay.py +0 -0
  90. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_paths.py +0 -0
  91. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/disk_cache_common.py +0 -0
  92. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/disk_cache_lifecycle.py +0 -0
  93. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/grok_loader.py +0 -0
  94. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/grok_quota_probe.py +0 -0
  95. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/history_disk_cache.py +0 -0
  96. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/history_loader.py +0 -0
  97. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/jsonl_limits.py +0 -0
  98. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/jsonl_utils.py +0 -0
  99. {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/muse_loader.py +0 -0
  100. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/__init__.py +0 -0
  101. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/agy.py +0 -0
  102. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/app.py +0 -0
  103. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/chrome.py +0 -0
  104. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/grok.py +0 -0
  105. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/manual_refresh.py +0 -0
  106. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/menu.py +0 -0
  107. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/muse.py +0 -0
  108. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/new_badge.py +0 -0
  109. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/notify.py +0 -0
  110. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/popover.py +0 -0
  111. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/prefs.py +0 -0
  112. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/refresh.py +0 -0
  113. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/update.py +0 -0
  114. {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/whats_new.py +0 -0
  115. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/__init__.py +0 -0
  116. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/base.py +0 -0
  117. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/catalog.py +0 -0
  118. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/dynamic_height.py +0 -0
  119. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_scale.py +0 -0
  120. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_window.py +0 -0
  121. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_window_state.py +0 -0
  122. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/payload.py +0 -0
  123. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/web_panel.py +0 -0
  124. {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/window_drag.py +0 -0
  125. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/__init__.py +0 -0
  126. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/burn_rate.py +0 -0
  127. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/keeper_outcome.py +0 -0
  128. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/notifications.py +0 -0
  129. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/quota_snapshot.py +0 -0
  130. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/status_payload.py +0 -0
  131. {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/usage_rate.py +0 -0
  132. {usage_cli-0.34.11 → usage_cli-0.34.12}/setup.cfg +0 -0
  133. {usage_cli-0.34.11 → usage_cli-0.34.12}/setup_app.py +0 -0
  134. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_codex.py +0 -0
  135. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_aggregator.py +0 -0
  136. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_adapter.py +0 -0
  137. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_disk_cache.py +0 -0
  138. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_analyzer_pipeline.py +0 -0
  139. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_app_version.py +0 -0
  140. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_burn_rate.py +0 -0
  141. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_cache_quarantine.py +0 -0
  142. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_chromium_cache.py +0 -0
  143. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_desktop.py +0 -0
  144. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_expired_windows.py +0 -0
  145. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_paths.py +0 -0
  146. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_consistency.py +0 -0
  147. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_hook_trust.py +0 -0
  148. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_compact_cards.py +0 -0
  149. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_fresh_cache.py +0 -0
  150. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_quota_display.py +0 -0
  151. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_quota_review.py +0 -0
  152. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_diagnoser.py +0 -0
  153. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_disk_cache_shards.py +0 -0
  154. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_dispatch_ledger.py +0 -0
  155. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_doctor.py +0 -0
  156. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_dynamic_height.py +0 -0
  157. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_grok_loader.py +0 -0
  158. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_grok_quota_probe.py +0 -0
  159. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_history_loader.py +0 -0
  160. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_history_persist.py +0 -0
  161. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_hook_scripts_are_standalone.py +0 -0
  162. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_format_fallback.py +0 -0
  163. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_key_parity.py +0 -0
  164. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_packaged_path.py +0 -0
  165. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_insights.py +0 -0
  166. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_jsonl_utils.py +0 -0
  167. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_keeper_menu.py +0 -0
  168. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_keeper_outcome.py +0 -0
  169. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_ledger_backfill.py +0 -0
  170. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_login_item.py +0 -0
  171. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_main.py +0 -0
  172. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_manual_refresh.py +0 -0
  173. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_agy.py +0 -0
  174. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_grok.py +0 -0
  175. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_muse.py +0 -0
  176. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_notify.py +0 -0
  177. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_prefs.py +0 -0
  178. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_update.py +0 -0
  179. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_coverage.py +0 -0
  180. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_loader.py +0 -0
  181. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_review_regressions.py +0 -0
  182. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_new_badge.py +0 -0
  183. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_packaged_resources.py +0 -0
  184. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_packaging.py +0 -0
  185. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_footer_layout.py +0 -0
  186. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_scale.py +0 -0
  187. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_window_state.py +0 -0
  188. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panels.py +0 -0
  189. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panels_lazy_import.py +0 -0
  190. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_persona_loader.py +0 -0
  191. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_pricing.py +0 -0
  192. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_project_resolver.py +0 -0
  193. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_quota_aware.py +0 -0
  194. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_quota_label.py +0 -0
  195. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_chart_redesign.py +0 -0
  196. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_cold_read.py +0 -0
  197. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_daily_chart.py +0 -0
  198. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_headings.py +0 -0
  199. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_share_mask.py +0 -0
  200. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_service_status.py +0 -0
  201. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_session_resume.py +0 -0
  202. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_app.py +0 -0
  203. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_coexistence.py +0 -0
  204. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_resume.py +0 -0
  205. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_terse.py +0 -0
  206. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_subprocess_utils.py +0 -0
  207. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_subscription.py +0 -0
  208. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_taskbar_menu.py +0 -0
  209. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_taskbar_obstacles.py +0 -0
  210. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_time_utils.py +0 -0
  211. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_tui.py +0 -0
  212. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_tui_sprite.py +0 -0
  213. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_checker.py +0 -0
  214. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_gate.py +0 -0
  215. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_release_notes.py +0 -0
  216. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_client.py +0 -0
  217. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_diagnosis_snapshot.py +0 -0
  218. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_dir_sweeper.py +0 -0
  219. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_lang.py +0 -0
  220. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_logging.py +0 -0
  221. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_notifications.py +0 -0
  222. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_rate.py +0 -0
  223. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_snapshot.py +0 -0
  224. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_snapshot_replay.py +0 -0
  225. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline.py +0 -0
  226. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_agy.py +0 -0
  227. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_forwarder.py +0 -0
  228. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_grok.py +0 -0
  229. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_terse_mode.py +0 -0
  230. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_terse_reminder.py +0 -0
  231. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_web_panel_payload.py +0 -0
  232. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_whats_new.py +0 -0
  233. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_wheel_packaging.py +0 -0
  234. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_win_login_item.py +0 -0
  235. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_windows_manifest.py +0 -0
  236. {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_wintray.py +0 -0
  237. {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/__init__.py +0 -0
  238. {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/app.py +0 -0
  239. {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/sprite.py +0 -0
  240. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/__init__.py +0 -0
  241. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_charts.py +0 -0
  242. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_daily_chart.py +0 -0
  243. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_ledger.py +0 -0
  244. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_scripts.py +0 -0
  245. {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_styles.py +0 -0
  246. {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/__init__.py +0 -0
  247. {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/checker.py +0 -0
  248. {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/gate.py +0 -0
  249. {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/release_notes.py +0 -0
  250. {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/whats_new.py +0 -0
  251. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/__init__.py +0 -0
  252. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/doctor.py +0 -0
  253. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/dependency_links.txt +0 -0
  254. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/entry_points.txt +0 -0
  255. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/requires.txt +0 -0
  256. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/top_level.txt +0 -0
  257. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/__init__.py +0 -0
  258. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/app_version.py +0 -0
  259. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/i18n.py +0 -0
  260. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/prefs.py +0 -0
  261. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/project_resolver.py +0 -0
  262. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/service_status.py +0 -0
  263. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/subprocess_utils.py +0 -0
  264. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/time_utils.py +0 -0
  265. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_dir_sweeper.py +0 -0
  266. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_lang.py +0 -0
  267. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_logging.py +0 -0
  268. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/__init__.py +0 -0
  269. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_quota_aware.py +0 -0
  270. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_session_resume.py +0 -0
  271. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline.py +0 -0
  272. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_agy.py +0 -0
  273. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_forwarder.py +0 -0
  274. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_grok.py +0 -0
  275. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_terse_mode.py +0 -0
  276. {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_terse_reminder.py +0 -0
  277. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/__init__.py +0 -0
  278. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/app.py +0 -0
  279. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/login_item.py +0 -0
  280. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/menu.py +0 -0
  281. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/prefs.py +0 -0
  282. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/quota_label.py +0 -0
  283. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_menu.py +0 -0
  284. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_obstacles.py +0 -0
  285. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_overlay.py +0 -0
  286. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/watch.py +0 -0
  287. {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/whats_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.34.11
3
+ Version: 0.34.12
4
4
  Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
5
5
  License-Expression: AGPL-3.0-only
6
6
  Requires-Python: >=3.13
@@ -16,6 +16,7 @@ from typing import Any
16
16
  from loaders.claude_paths import claude_config_dirs
17
17
  from loaders.jsonl_utils import iter_jsonl_dicts
18
18
  from usage_common.project_resolver import resolve_project_name
19
+ from usage_common.time_utils import parse_optional_iso8601_utc
19
20
 
20
21
  from .types import AgentInfo
21
22
 
@@ -190,10 +191,8 @@ def _parse_assistant_entry(data: dict[str, Any], project: str) -> list[UsageEntr
190
191
  else 0
191
192
  )
192
193
 
193
- timestamp_str = data.get("timestamp", "")
194
- try:
195
- ts = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
196
- except (ValueError, AttributeError):
194
+ ts = parse_optional_iso8601_utc(data.get("timestamp", ""))
195
+ if ts is None:
197
196
  return []
198
197
 
199
198
  message_id = message.get("id", "")
@@ -36,7 +36,8 @@ def _read_status() -> dict[str, Any] | None:
36
36
  try:
37
37
  with open(path, "r", encoding="utf-8") as f:
38
38
  data: dict[str, Any] = json.load(f)
39
- return data
39
+ if isinstance(data, dict):
40
+ return data
40
41
  except (json.JSONDecodeError, OSError, UnicodeDecodeError):
41
42
  continue
42
43
  return None
@@ -56,7 +56,10 @@ def analyze_blocks(entries: list[UsageEntry]) -> list[SessionBlock]:
56
56
  block.is_active = True
57
57
  elapsed = (now - block.start_time).total_seconds() / 60
58
58
  if elapsed > 0:
59
- block.burn_rate = block.total_tokens / elapsed
59
+ active_tokens = (
60
+ block.input_tokens + block.output_tokens + block.cache_creation_tokens
61
+ )
62
+ block.burn_rate = active_tokens / elapsed
60
63
 
61
64
  return blocks
62
65
 
@@ -7,6 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import contextlib
10
+ from bisect import bisect_right
10
11
  from collections import defaultdict
11
12
  from concurrent.futures import ThreadPoolExecutor
12
13
  from dataclasses import dataclass
@@ -146,6 +147,7 @@ class CubeModelData(TypedDict):
146
147
 
147
148
 
148
149
  class CubeReportData(TypedDict):
150
+ today: NotRequired[str]
149
151
  dates: list[str]
150
152
  agents: list[CubeAgentData]
151
153
  models: list[CubeModelData]
@@ -1236,6 +1238,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1236
1238
  )
1237
1239
  cube_rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
1238
1240
  cube: CubeReportData = {
1241
+ "today": today.isoformat(),
1239
1242
  "dates": cube_dates,
1240
1243
  "agents": cube_agents,
1241
1244
  "models": cube_models,
@@ -1293,7 +1296,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1293
1296
  start_time = _session_start_text(session.start_time)
1294
1297
  session_rows.append(
1295
1298
  {
1296
- "date_idx": date_indices[start_time[:10]],
1299
+ "date_idx": max(0, bisect_right(cube_dates, start_time[:10]) - 1),
1297
1300
  "project_idx": project_indices[session.project or "unknown"],
1298
1301
  "model_idx": model_indices[session.model or "unknown"],
1299
1302
  "start_time": start_time,
@@ -1158,7 +1158,11 @@ def self_heal() -> None:
1158
1158
  return
1159
1159
  _migrate_bundled_python_commands_if_needed(settings)
1160
1160
  setup_hook._migrate_windows_statusline_command_if_needed(settings)
1161
- if not is_setup() and "statusLine" not in settings:
1161
+ usage_settings = settings.get(BACKUP_KEY)
1162
+ statusline_disabled = (
1163
+ isinstance(usage_settings, dict) and setup_hook.DISABLED_SL_KEY in usage_settings
1164
+ )
1165
+ if not is_setup() and "statusLine" not in settings and not statusline_disabled:
1162
1166
  exit_code = _run_quietly(setup)
1163
1167
  if exit_code == 0:
1164
1168
  _append_self_heal_log("install_hook", "initial setup")
@@ -811,9 +811,7 @@ def _replace_grok_status_line(content: str, replacement: str) -> str | None:
811
811
  else:
812
812
  start, end = section
813
813
  candidate = content[:start] + replacement + content[end:]
814
- try:
815
- tomllib.loads(candidate)
816
- except tomllib.TOMLDecodeError:
814
+ if _validated_table_edit(content, candidate, "ui", "status_line") != candidate:
817
815
  return None
818
816
  return candidate
819
817
 
@@ -887,8 +885,9 @@ def _unsetup_grok() -> bool:
887
885
  restored = content[:start] + content[end:]
888
886
  if start > 0 and restored[:start].endswith("\n"):
889
887
  restored = restored[: start - 1] + restored[start:]
888
+ if _validated_table_edit(content, restored, "ui", "status_line") != restored:
889
+ return False
890
890
  try:
891
- tomllib.loads(restored)
892
891
  _atomic_write_text(GROK_SETTINGS, restored)
893
892
  GROK_PREVIOUS_STATUSLINE.unlink(missing_ok=True)
894
893
  except (OSError, tomllib.TOMLDecodeError):
@@ -969,13 +968,31 @@ def _find_top_level_dotted_line(
969
968
  return dotted_line.search(content, 0, top_level_end)
970
969
 
971
970
 
971
+ def _validated_table_edit(content: str, candidate: str, name: str, key: str) -> str:
972
+ """Reject edits that change anything except the requested TOML key."""
973
+ try:
974
+ before, after = tomllib.loads(content), tomllib.loads(candidate)
975
+ except tomllib.TOMLDecodeError:
976
+ return content
977
+
978
+ def without_target(table: dict[str, Any], path: list[str]) -> None:
979
+ part = path[0]
980
+ if len(path) == 1:
981
+ table.pop(part, None)
982
+ elif isinstance(child := table.get(part), dict):
983
+ without_target(child, path[1:])
984
+ if not child:
985
+ table.pop(part)
986
+
987
+ path = [*name.split("."), key]
988
+ without_target(before, path)
989
+ without_target(after, path)
990
+ return candidate if before == after else content
991
+
992
+
972
993
  def _insert_table_line(content: str, name: str, line: str) -> str:
973
994
  def validated(candidate: str) -> str:
974
- try:
975
- tomllib.loads(candidate)
976
- except tomllib.TOMLDecodeError:
977
- return content
978
- return candidate
995
+ return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
979
996
 
980
997
  table = _find_table(content, name)
981
998
  if table is None:
@@ -1017,11 +1034,9 @@ def _replace_table_line(
1017
1034
  content: str, name: str, line_regex: re.Pattern[str], replacement: str
1018
1035
  ) -> str:
1019
1036
  def validated(candidate: str) -> str:
1020
- try:
1021
- tomllib.loads(candidate)
1022
- except tomllib.TOMLDecodeError:
1023
- return content
1024
- return candidate
1037
+ return _validated_table_edit(
1038
+ content, candidate, name, next(iter(tomllib.loads(replacement)))
1039
+ )
1025
1040
 
1026
1041
  table = _find_table(content, name)
1027
1042
  if table is None:
@@ -1042,13 +1057,9 @@ def _replace_table_line(
1042
1057
  return validated(candidate)
1043
1058
 
1044
1059
 
1045
- def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str]) -> str:
1060
+ def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str], key: str) -> str:
1046
1061
  def validated(candidate: str) -> str:
1047
- try:
1048
- tomllib.loads(candidate)
1049
- except tomllib.TOMLDecodeError:
1050
- return content
1051
- return candidate
1062
+ return _validated_table_edit(content, candidate, name, key)
1052
1063
 
1053
1064
  table = _find_table(content, name)
1054
1065
  if table is None:
@@ -1072,11 +1083,7 @@ def _ensure_table_line(content: str, name: str, line_regex: re.Pattern[str], lin
1072
1083
  itself is absent."""
1073
1084
 
1074
1085
  def validated(candidate: str) -> str:
1075
- try:
1076
- tomllib.loads(candidate)
1077
- except tomllib.TOMLDecodeError:
1078
- return content
1079
- return candidate
1086
+ return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
1080
1087
 
1081
1088
  table = _find_table(content, name)
1082
1089
  if table is None:
@@ -1097,7 +1104,7 @@ def _replace_tui_status_line(content: str, replacement: str) -> str:
1097
1104
 
1098
1105
 
1099
1106
  def _remove_tui_status_line(content: str) -> str:
1100
- return _remove_table_line(content, "tui", _SL_REGEX)
1107
+ return _remove_table_line(content, "tui", _SL_REGEX, "status_line")
1101
1108
 
1102
1109
 
1103
1110
  def _read_codex_config() -> tuple[str, dict[str, Any]] | None:
@@ -1150,6 +1157,10 @@ def _setup_codex() -> None:
1150
1157
  print(_t("setup_codex_config_unmodifiable"))
1151
1158
  return
1152
1159
  _atomic_write_text(CODEX_CONFIG, content)
1160
+ written = _read_codex_config()
1161
+ if written is None or _codex_status_line(written[1]) != CODEX_STATUS_LINE:
1162
+ print(_t("setup_codex_config_unmodifiable"))
1163
+ return
1153
1164
  print(_t("setup_codex_configured"))
1154
1165
  if old is not None and old not in LEGACY_CODEX_STATUS_LINES:
1155
1166
  print(_t("setup_codex_backup_written", path=CODEX_BACKUP))
@@ -1335,15 +1346,20 @@ def unsetup() -> int:
1335
1346
  settings = _load_settings()
1336
1347
  sl = settings.get("statusLine")
1337
1348
  backup = settings.get(BACKUP_KEY)
1338
- parked = backup.pop(DISABLED_SL_KEY, None) if isinstance(backup, dict) else None
1339
- if sl is None:
1340
- sl = parked
1349
+ parked = backup.get(DISABLED_SL_KEY) if isinstance(backup, dict) else None
1341
1350
 
1342
- if _is_usage_hook(sl) or _is_legacy_tt_hook(sl):
1351
+ if _is_usage_hook(sl) or _is_legacy_tt_hook(sl) or (sl is None and parked is not None):
1343
1352
  legacy_backup = settings.get(LEGACY_TT_BACKUP_KEY)
1344
1353
  prev = backup.get(PREV_SL_KEY) if isinstance(backup, dict) else None
1345
1354
  if not isinstance(prev, dict) and isinstance(legacy_backup, dict):
1346
1355
  prev = legacy_backup.get(PREV_SL_KEY)
1356
+ if (
1357
+ not isinstance(prev, dict)
1358
+ and isinstance(parked, dict)
1359
+ and not _is_usage_hook(parked)
1360
+ and not _is_legacy_tt_hook(parked)
1361
+ ):
1362
+ prev = parked
1347
1363
 
1348
1364
  if isinstance(prev, dict):
1349
1365
  settings["statusLine"] = prev
@@ -1354,6 +1370,7 @@ def unsetup() -> int:
1354
1370
 
1355
1371
  if isinstance(backup, dict):
1356
1372
  backup.pop(PREV_SL_KEY, None)
1373
+ backup.pop(DISABLED_SL_KEY, None)
1357
1374
  if not backup:
1358
1375
  del settings[BACKUP_KEY]
1359
1376
  if isinstance(legacy_backup, dict):
@@ -91,11 +91,13 @@ def seed_caches(
91
91
  continue
92
92
  if len(file_cache) >= maxsize:
93
93
  file_cache.popitem(last=False)
94
+ wal = file_data.get("wal")
94
95
  file_cache[Path(path_str)] = _FileCacheEntry(
95
96
  mtime=float(file_data["mtime"]),
96
97
  size=int(file_data["size"]),
97
98
  entries=[_deserialize_entry(entry) for entry in entries_data],
98
99
  skipped_missing_dedup_key=int(file_data["skipped_missing_dedup_key"]),
100
+ wal=None if wal is None else (int(wal[0]), int(wal[1])),
99
101
  )
100
102
  except (KeyError, TypeError, ValueError):
101
103
  continue
@@ -118,6 +120,7 @@ def flush_caches(
118
120
  str(path): {
119
121
  "mtime": entry.mtime,
120
122
  "size": entry.size,
123
+ "wal": entry.wal,
121
124
  "entries": [_serialize_entry(item) for item in entry.entries],
122
125
  "skipped_missing_dedup_key": entry.skipped_missing_dedup_key,
123
126
  }
@@ -38,7 +38,7 @@ AGY_SESSIONS_DIR = Path(os.path.expanduser("~/.gemini/antigravity-cli/conversati
38
38
  # below that count evicts databases parsed during the prior refresh, causing a
39
39
  # permanent full SQLite/protobuf reparse on every load (LRU thrashing).
40
40
  _FILE_CACHE_MAXSIZE = 4096
41
- _AGY_DB_CACHE_SCHEMA = 3
41
+ _AGY_DB_CACHE_SCHEMA = 4
42
42
  AGY_CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_db_cache.json"))
43
43
  _disk_cache_seeded = False
44
44
  _DISK_CACHE_FLUSH_INTERVAL_S = 300.0
@@ -92,6 +92,7 @@ class _FileCacheEntry:
92
92
  size: int
93
93
  entries: list[AgyUsageEntry]
94
94
  skipped_missing_dedup_key: int
95
+ wal: tuple[int, int] | None = None
95
96
 
96
97
 
97
98
  _file_cache: OrderedDict[Path, _FileCacheEntry] = OrderedDict()
@@ -120,22 +121,24 @@ def load_entries_with_stats(hours_back: int = 0) -> AgyLoadResult:
120
121
  skipped_missing_dedup_key = 0
121
122
  cutoff_ts = cutoff.timestamp() if cutoff else None
122
123
  file_cache_snapshot = {
123
- str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
124
+ str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
124
125
  }
125
126
  for path in AGY_SESSIONS_DIR.glob("*.db"):
126
127
  if cutoff_ts is not None:
127
128
  try:
128
- if path.stat().st_mtime < cutoff_ts:
129
- continue
129
+ modified = path.stat().st_mtime
130
130
  except OSError as exc:
131
131
  logger.warning("failed to stat Antigravity database %s: %s", path, exc)
132
132
  continue
133
+ wal = _wal_fingerprint(path)
134
+ if max(modified, wal[0] / 1e9 if wal else 0.0) < cutoff_ts:
135
+ continue
133
136
  file_entries, skipped = _load_database(path, cutoff, seen_dedup_keys)
134
137
  entries.extend(file_entries)
135
138
  skipped_missing_dedup_key += skipped
136
139
 
137
140
  if {
138
- str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
141
+ str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
139
142
  } != file_cache_snapshot:
140
143
  _disk_cache_dirty = True
141
144
  _flush_caches_to_disk()
@@ -176,6 +179,15 @@ def flush_caches_on_terminate() -> None:
176
179
  _flush_caches_on_terminate(lambda: _flush_caches_to_disk(force=True))
177
180
 
178
181
 
182
+ def _wal_fingerprint(path: Path) -> tuple[int, int] | None:
183
+ """Return the ``-wal`` file's (mtime_ns, size); WAL commits leave the main file untouched."""
184
+ try:
185
+ stat = Path(f"{path}-wal").stat()
186
+ except OSError:
187
+ return None
188
+ return (stat.st_mtime_ns, stat.st_size)
189
+
190
+
179
191
  def _load_database(
180
192
  path: Path,
181
193
  cutoff: datetime | None,
@@ -187,8 +199,14 @@ def _load_database(
187
199
  logger.warning("failed to stat Antigravity database %s: %s", path, exc)
188
200
  return [], 0
189
201
 
202
+ wal = _wal_fingerprint(path)
190
203
  cached = _file_cache.get(path)
191
- if cached is not None and cached.mtime == st.st_mtime and cached.size == st.st_size:
204
+ if (
205
+ cached is not None
206
+ and cached.mtime == st.st_mtime
207
+ and cached.size == st.st_size
208
+ and cached.wal == wal
209
+ ):
192
210
  _file_cache.move_to_end(path)
193
211
  return _filter_entries(
194
212
  cached.entries,
@@ -208,6 +226,7 @@ def _load_database(
208
226
  size=st.st_size,
209
227
  entries=parsed_entries,
210
228
  skipped_missing_dedup_key=skipped_missing_dedup_key,
229
+ wal=wal,
211
230
  )
212
231
  return _filter_entries(parsed_entries, skipped_missing_dedup_key, cutoff, seen_dedup_keys)
213
232
 
@@ -18,7 +18,6 @@ from __future__ import annotations
18
18
  import json
19
19
  import os
20
20
  import platform
21
- import shutil
22
21
  import subprocess
23
22
  import sys
24
23
  import tempfile
@@ -35,6 +34,7 @@ from typing import Any
35
34
  from urllib.error import HTTPError, URLError
36
35
  from urllib.request import Request, urlopen
37
36
 
37
+ from usage_common.cli_lookup import cli_env, resolve_cli
38
38
  from usage_common.subprocess_utils import hidden_console_kwargs
39
39
 
40
40
  CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
@@ -168,6 +168,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
168
168
  capture_output=True,
169
169
  timeout=timeout,
170
170
  stdin=subprocess.DEVNULL,
171
+ env=cli_env(agy),
171
172
  **hidden_console_kwargs(),
172
173
  )
173
174
  except (OSError, subprocess.SubprocessError):
@@ -189,14 +190,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
189
190
 
190
191
  def find_agy() -> str | None:
191
192
  """Find the Antigravity CLI in PATH or common user installation paths."""
192
- path = shutil.which("agy")
193
- if path is not None:
194
- return path
195
- for candidate in ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"):
196
- expanded = os.path.expanduser(candidate)
197
- if os.access(expanded, os.X_OK):
198
- return expanded
199
- return None
193
+ return resolve_cli("agy", ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"))
200
194
 
201
195
 
202
196
  def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
@@ -652,16 +652,23 @@ def _rate_limits_from_websocket_error(
652
652
  headers = _as_dict(event.get("headers"))
653
653
  primary_reset = _as_optional_float(headers.get("X-Codex-Primary-Reset-At"))
654
654
  secondary_reset = _as_optional_float(headers.get("X-Codex-Secondary-Reset-At"))
655
- now_ts = datetime.now(UTC).timestamp()
655
+ recorded_at = _timestamp_from_log_ts(ts)
656
+ recorded_ts = recorded_at.timestamp() if recorded_at is not None else None
656
657
  if primary_reset is None:
657
658
  primary_reset_after = _as_optional_float(headers.get("X-Codex-Primary-Reset-After-Seconds"))
658
- primary_reset = now_ts + primary_reset_after if primary_reset_after is not None else None
659
+ primary_reset = (
660
+ recorded_ts + primary_reset_after
661
+ if recorded_ts is not None and primary_reset_after is not None
662
+ else None
663
+ )
659
664
  if secondary_reset is None:
660
665
  secondary_reset_after = _as_optional_float(
661
666
  headers.get("X-Codex-Secondary-Reset-After-Seconds")
662
667
  )
663
668
  secondary_reset = (
664
- now_ts + secondary_reset_after if secondary_reset_after is not None else None
669
+ recorded_ts + secondary_reset_after
670
+ if recorded_ts is not None and secondary_reset_after is not None
671
+ else None
665
672
  )
666
673
  return _build_rate_limits(
667
674
  primary_pct=_as_optional_float(headers.get("X-Codex-Primary-Used-Percent")),
@@ -669,7 +676,7 @@ def _rate_limits_from_websocket_error(
669
676
  secondary_pct=_as_optional_float(headers.get("X-Codex-Secondary-Used-Percent")),
670
677
  secondary_reset=secondary_reset,
671
678
  model=_event_value(body, "model") or "unknown",
672
- updated_at=_timestamp_from_log_ts(ts),
679
+ updated_at=recorded_at,
673
680
  )
674
681
 
675
682
 
@@ -11,7 +11,6 @@ import logging
11
11
  import math
12
12
  import os
13
13
  import re
14
- import shutil
15
14
  import subprocess
16
15
  import tempfile
17
16
  import threading
@@ -22,6 +21,7 @@ from pathlib import Path
22
21
  from typing import TypedDict, TypeGuard
23
22
 
24
23
  from loaders import muse_loader
24
+ from usage_common.cli_lookup import cli_env, resolve_cli
25
25
  from usage_common.subprocess_utils import hidden_console_kwargs
26
26
 
27
27
  logger = logging.getLogger(__name__)
@@ -54,14 +54,9 @@ class MuseQuotaResult(TypedDict):
54
54
 
55
55
  def find_muse() -> str | None:
56
56
  """Find Muse even when the app inherits a limited GUI PATH."""
57
- executable = shutil.which("muse")
58
- if executable is not None:
59
- return executable
60
- for candidate in ("~/.local/bin/muse", "/opt/homebrew/bin/muse", "/usr/local/bin/muse"):
61
- expanded = os.path.expanduser(candidate)
62
- if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
63
- return expanded
64
- return None
57
+ return resolve_cli(
58
+ "muse", ("~/.local/bin/muse", "/opt/homebrew/bin/muse", "/usr/local/bin/muse")
59
+ )
65
60
 
66
61
 
67
62
  def _number(value: object) -> TypeGuard[int | float]:
@@ -239,6 +234,7 @@ def _probe_worker(executable: str) -> None:
239
234
  stderr=subprocess.PIPE,
240
235
  encoding="utf-8",
241
236
  cwd=PROBE_DIR,
237
+ env=cli_env(executable),
242
238
  **hidden_console_kwargs(),
243
239
  )
244
240
  _probe_process = process
@@ -231,11 +231,19 @@ def main() -> None:
231
231
  session_hooks._migrate_bundled_python_commands_if_needed()
232
232
  raise SystemExit(exit_code)
233
233
  if args.unsetup:
234
- from installer.session_hooks import disable_session_resume, disable_terse_mode
234
+ from installer.claude_pane import disable_claude_beginner, disable_claude_pane
235
+ from installer.session_hooks import (
236
+ disable_quota_aware,
237
+ disable_session_resume,
238
+ disable_terse_mode,
239
+ )
235
240
  from installer.setup_hook import unsetup
236
241
 
237
242
  disable_session_resume()
238
243
  disable_terse_mode()
244
+ disable_quota_aware()
245
+ disable_claude_pane()
246
+ disable_claude_beginner()
239
247
  raise SystemExit(unsetup())
240
248
  preferences_snapshot = _load_preferences()
241
249
  _self_heal()
@@ -177,7 +177,7 @@ def show_forwarder_mode_prompt_if_needed(language: str | None = None) -> None:
177
177
  )
178
178
  if dismissed or setup_hook._detect_current_state(settings) != "external":
179
179
  return
180
- except Exception:
180
+ except (Exception, SystemExit):
181
181
  if os.environ.get("USAGE_DEBUG") == "1":
182
182
  logger.warning("forwarder prompt check failed", exc_info=True)
183
183
  return
@@ -194,13 +194,13 @@ def show_forwarder_mode_prompt_if_needed(language: str | None = None) -> None:
194
194
  if result == 1000:
195
195
  setup_hook.setup(force_forwarder=True)
196
196
  session_hooks._migrate_bundled_python_commands_if_needed()
197
- except Exception:
197
+ except (Exception, SystemExit):
198
198
  if os.environ.get("USAGE_DEBUG") == "1":
199
199
  logger.warning("forwarder setup failed", exc_info=True)
200
200
  finally:
201
201
  try:
202
202
  _set_forwarder_mode_prompt_dismissed()
203
- except Exception:
203
+ except (Exception, SystemExit):
204
204
  if os.environ.get("USAGE_DEBUG") == "1":
205
205
  logger.warning("forwarder prompt dismissal failed", exc_info=True)
206
206
 
@@ -15,6 +15,7 @@ from dataclasses import dataclass
15
15
  from pathlib import Path
16
16
  from typing import Any
17
17
 
18
+ from loaders.claude_paths import claude_config_dirs
18
19
  from loaders.codex_paths import codex_home
19
20
 
20
21
  logger = logging.getLogger(__name__)
@@ -115,14 +116,15 @@ def classify_file_events(paths: Iterable[str], flags: Iterable[int]) -> FileEven
115
116
 
116
117
  def usage_watch_paths() -> list[Path]:
117
118
  """Return existing agent directories that contain usage history records."""
118
- home = Path.home()
119
119
  codex_dir = codex_home()
120
120
  return [
121
121
  path
122
- for path in (
123
- home / ".claude" / "projects",
124
- codex_dir / "sessions",
125
- codex_dir / "archived_sessions",
122
+ for path in dict.fromkeys(
123
+ (
124
+ *(directory / "projects" for directory in claude_config_dirs()),
125
+ codex_dir / "sessions",
126
+ codex_dir / "archived_sessions",
127
+ )
126
128
  )
127
129
  if path.exists()
128
130
  ]
@@ -571,14 +571,12 @@ def project_rows_for_windows(
571
571
  current_time = datetime.now(UTC) if now is None else now
572
572
  local_now = current_time.astimezone()
573
573
  local_today = local_now.date()
574
- local_tz = local_now.tzinfo
575
- assert local_tz is not None
576
- today_start = datetime.combine(local_today, datetime_time.min, tzinfo=local_tz).astimezone(UTC)
574
+ today_start = datetime.combine(local_today, datetime_time.min).astimezone(UTC)
577
575
  tomorrow_start = datetime.combine(
578
- local_today + timedelta(days=1), datetime_time.min, tzinfo=local_tz
576
+ local_today + timedelta(days=1), datetime_time.min
579
577
  ).astimezone(UTC)
580
578
  yesterday_start = datetime.combine(
581
- local_today - timedelta(days=1), datetime_time.min, tzinfo=local_tz
579
+ local_today - timedelta(days=1), datetime_time.min
582
580
  ).astimezone(UTC)
583
581
  cutoff_7d = current_time - timedelta(hours=168)
584
582
  cutoff_30d = current_time - timedelta(hours=720)
@@ -35,7 +35,7 @@ from menubar.state import PopoverState, QuotaRowState, _format_percent
35
35
  class _TitleApp(Protocol):
36
36
  _menubar_text_cache: dict[str, Any]
37
37
  codex_5h_pct: float | None
38
- _last_button_title_key: tuple[str] | None
38
+ _last_button_title_key: tuple[Any, ...] | None
39
39
  _last_plain_title_key: tuple[str] | None
40
40
  status_item: Any
41
41
  interval: float
@@ -166,7 +166,7 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
166
166
  def _set_button_title(app: _TitleApp, state: PopoverState) -> None:
167
167
  app._reschedule_poll_timer(menubar_poll_interval(app, state))
168
168
  title = _compose_title(app, state)
169
- title_key = (title,)
169
+ title_key = (title, tuple(icon for icon, _text in _menubar_items(app, state)))
170
170
  if app._last_button_title_key == title_key:
171
171
  return
172
172
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.34.11"
3
+ version = "0.34.12"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -10,7 +10,6 @@ from __future__ import annotations
10
10
 
11
11
  import logging
12
12
  import os
13
- import shutil
14
13
  import subprocess
15
14
  import threading
16
15
  import time
@@ -19,6 +18,7 @@ from pathlib import Path
19
18
  from menubar.agy import AgyRefreshResult
20
19
  from menubar.prefs import _agy_window_keeper_enabled
21
20
  from quota import keeper_outcome
21
+ from usage_common.cli_lookup import cli_env, resolve_cli
22
22
  from usage_common.subprocess_utils import hidden_console_kwargs
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -63,14 +63,7 @@ def _load_last_ping(path: Path | None = None) -> float | None:
63
63
 
64
64
 
65
65
  def _resolve_agy_bin() -> str | None:
66
- found = shutil.which("agy")
67
- if found:
68
- return found
69
- for candidate in _AGY_BIN_FALLBACKS:
70
- resolved = os.path.expanduser(candidate)
71
- if os.path.isfile(resolved) and os.access(resolved, os.X_OK):
72
- return resolved
73
- return None
66
+ return resolve_cli("agy", _AGY_BIN_FALLBACKS)
74
67
 
75
68
 
76
69
  def _try_acquire() -> bool:
@@ -98,6 +91,7 @@ def _run_agy_ping(agy_bin: str) -> subprocess.CompletedProcess[str]:
98
91
  timeout=PING_TIMEOUT_SECONDS,
99
92
  cwd=os.path.expanduser("~"),
100
93
  check=False,
94
+ env=cli_env(agy_bin),
101
95
  **hidden_console_kwargs(),
102
96
  )
103
97