usage-cli 0.34.10__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 (288) hide show
  1. {usage_cli-0.34.10/usage_cli.egg-info → usage_cli-0.34.12}/PKG-INFO +1 -1
  2. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/claude.py +3 -4
  3. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/rate_limits.py +5 -4
  4. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/blocks.py +4 -1
  5. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/persona_loader.py +2 -1
  6. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/reporter.py +4 -1
  7. {usage_cli-0.34.10 → usage_cli-0.34.12}/i18n.json +75 -0
  8. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/session_hooks.py +5 -1
  9. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/setup_hook.py +48 -31
  10. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_disk_cache.py +3 -0
  11. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_loader.py +25 -6
  12. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_quota_probe.py +3 -9
  13. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_usage.py +7 -3
  14. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_loader.py +11 -4
  15. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/muse_quota_probe.py +5 -9
  16. {usage_cli-0.34.10 → usage_cli-0.34.12}/main.py +9 -1
  17. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/actions.py +3 -3
  18. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/agy.py +9 -0
  19. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/fsevents_watch.py +7 -5
  20. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/state.py +5 -6
  21. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/title.py +2 -2
  22. {usage_cli-0.34.10 → usage_cli-0.34.12}/pyproject.toml +1 -1
  23. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/agy_window_keeper.py +3 -9
  24. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/codex_window_keeper.py +3 -9
  25. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/window_keeper.py +38 -13
  26. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_claude.py +22 -0
  27. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_rate_limits.py +58 -0
  28. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_loader.py +22 -0
  29. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_quota_probe.py +19 -0
  30. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_window_keeper.py +44 -0
  31. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_analyzer_blocks.py +20 -0
  32. usage_cli-0.34.12/tests/test_check_release_version.py +47 -0
  33. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_check_upstream.py +8 -1
  34. usage_cli-0.34.12/tests/test_claude_expired_windows.py +86 -0
  35. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_pane.py +80 -1
  36. usage_cli-0.34.12/tests/test_cli_lookup.py +123 -0
  37. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_loader.py +44 -1
  38. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_window_keeper.py +20 -0
  39. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_fsevents_watch.py +27 -0
  40. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_html_report_snapshot.py +37 -24
  41. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar.py +184 -0
  42. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_state.py +38 -0
  43. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_quota_probe.py +20 -0
  44. usage_cli-0.34.12/tests/test_report_chart_redesign.py +218 -0
  45. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_cold_read.py +1 -1
  46. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_filter_js.py +94 -22
  47. usage_cli-0.34.12/tests/test_report_headings.py +373 -0
  48. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_reporter_build_data.py +55 -1
  49. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook.py +172 -0
  50. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_ui_tables.py +99 -0
  51. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_cli.py +52 -0
  52. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline.py +14 -11
  53. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_window_keeper.py +96 -0
  54. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/html_report.py +277 -121
  55. usage_cli-0.34.12/ui/report_charts.py +62 -0
  56. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_filter.py +217 -133
  57. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_styles.py +100 -0
  58. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/tables.py +17 -9
  59. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/cli.py +12 -3
  60. {usage_cli-0.34.10 → usage_cli-0.34.12/usage_cli.egg-info}/PKG-INFO +1 -1
  61. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/SOURCES.txt +6 -0
  62. usage_cli-0.34.12/usage_common/cli_lookup.py +62 -0
  63. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/pricing.py +6 -1
  64. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline.py +11 -8
  65. usage_cli-0.34.10/ui/report_charts.py +0 -26
  66. {usage_cli-0.34.10 → usage_cli-0.34.12}/LICENSE +0 -0
  67. {usage_cli-0.34.10 → usage_cli-0.34.12}/README.md +0 -0
  68. {usage_cli-0.34.10 → usage_cli-0.34.12}/THIRD_PARTY_NOTICES.md +0 -0
  69. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/__init__.py +0 -0
  70. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/agy.py +0 -0
  71. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/codex.py +0 -0
  72. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/grok.py +0 -0
  73. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/muse.py +0 -0
  74. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/registry.py +0 -0
  75. {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/types.py +0 -0
  76. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/__init__.py +0 -0
  77. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/aggregator.py +0 -0
  78. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/diagnoser.py +0 -0
  79. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/diagnosis_snapshot.py +0 -0
  80. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/dispatch_ledger.py +0 -0
  81. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/history_persist.py +0 -0
  82. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/insights.py +0 -0
  83. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/ledger_backfill.py +0 -0
  84. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/subscription.py +0 -0
  85. {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/usage_snapshot.py +0 -0
  86. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/__init__.py +0 -0
  87. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/claude_pane.py +0 -0
  88. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/codex_hook_trust.py +0 -0
  89. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/login_item.py +0 -0
  90. {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/statusline_settings.py +0 -0
  91. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/__init__.py +0 -0
  92. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/_shared_cache_file.py +0 -0
  93. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/cache_quarantine.py +0 -0
  94. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/chromium_cache.py +0 -0
  95. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_desktop.py +0 -0
  96. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_paths.py +0 -0
  97. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_disk_cache.py +0 -0
  98. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_events.py +0 -0
  99. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_fork_replay.py +0 -0
  100. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_paths.py +0 -0
  101. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/disk_cache_common.py +0 -0
  102. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/disk_cache_lifecycle.py +0 -0
  103. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/grok_loader.py +0 -0
  104. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/grok_quota_probe.py +0 -0
  105. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/history_disk_cache.py +0 -0
  106. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/history_loader.py +0 -0
  107. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/jsonl_limits.py +0 -0
  108. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/jsonl_utils.py +0 -0
  109. {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/muse_loader.py +0 -0
  110. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/__init__.py +0 -0
  111. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/app.py +0 -0
  112. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/chrome.py +0 -0
  113. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/grok.py +0 -0
  114. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/manual_refresh.py +0 -0
  115. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/menu.py +0 -0
  116. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/muse.py +0 -0
  117. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/new_badge.py +0 -0
  118. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/notify.py +0 -0
  119. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/popover.py +0 -0
  120. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/prefs.py +0 -0
  121. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/refresh.py +0 -0
  122. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/update.py +0 -0
  123. {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/whats_new.py +0 -0
  124. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/__init__.py +0 -0
  125. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/base.py +0 -0
  126. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/catalog.py +0 -0
  127. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/dynamic_height.py +0 -0
  128. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_scale.py +0 -0
  129. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_window.py +0 -0
  130. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_window_state.py +0 -0
  131. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/payload.py +0 -0
  132. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/web_panel.py +0 -0
  133. {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/window_drag.py +0 -0
  134. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/__init__.py +0 -0
  135. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/burn_rate.py +0 -0
  136. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/keeper_outcome.py +0 -0
  137. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/notifications.py +0 -0
  138. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/quota_snapshot.py +0 -0
  139. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/status_payload.py +0 -0
  140. {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/usage_rate.py +0 -0
  141. {usage_cli-0.34.10 → usage_cli-0.34.12}/setup.cfg +0 -0
  142. {usage_cli-0.34.10 → usage_cli-0.34.12}/setup_app.py +0 -0
  143. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_codex.py +0 -0
  144. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_aggregator.py +0 -0
  145. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_adapter.py +0 -0
  146. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_disk_cache.py +0 -0
  147. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_analyzer_pipeline.py +0 -0
  148. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_app_version.py +0 -0
  149. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_burn_rate.py +0 -0
  150. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_cache_quarantine.py +0 -0
  151. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_chromium_cache.py +0 -0
  152. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_desktop.py +0 -0
  153. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_paths.py +0 -0
  154. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_consistency.py +0 -0
  155. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_hook_trust.py +0 -0
  156. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_compact_cards.py +0 -0
  157. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_fresh_cache.py +0 -0
  158. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_quota_display.py +0 -0
  159. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_quota_review.py +0 -0
  160. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_diagnoser.py +0 -0
  161. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_disk_cache_shards.py +0 -0
  162. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_dispatch_ledger.py +0 -0
  163. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_doctor.py +0 -0
  164. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_dynamic_height.py +0 -0
  165. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_grok_loader.py +0 -0
  166. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_grok_quota_probe.py +0 -0
  167. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_history_loader.py +0 -0
  168. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_history_persist.py +0 -0
  169. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_hook_scripts_are_standalone.py +0 -0
  170. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_format_fallback.py +0 -0
  171. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_key_parity.py +0 -0
  172. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_packaged_path.py +0 -0
  173. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_insights.py +0 -0
  174. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_jsonl_utils.py +0 -0
  175. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_keeper_menu.py +0 -0
  176. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_keeper_outcome.py +0 -0
  177. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_ledger_backfill.py +0 -0
  178. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_login_item.py +0 -0
  179. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_main.py +0 -0
  180. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_manual_refresh.py +0 -0
  181. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_agy.py +0 -0
  182. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_grok.py +0 -0
  183. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_muse.py +0 -0
  184. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_notify.py +0 -0
  185. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_prefs.py +0 -0
  186. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_update.py +0 -0
  187. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_coverage.py +0 -0
  188. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_loader.py +0 -0
  189. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_review_regressions.py +0 -0
  190. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_new_badge.py +0 -0
  191. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_packaged_resources.py +0 -0
  192. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_packaging.py +0 -0
  193. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_footer_layout.py +0 -0
  194. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_scale.py +0 -0
  195. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_window_state.py +0 -0
  196. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panels.py +0 -0
  197. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panels_lazy_import.py +0 -0
  198. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_persona_loader.py +0 -0
  199. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_pricing.py +0 -0
  200. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_project_resolver.py +0 -0
  201. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_quota_aware.py +0 -0
  202. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_quota_label.py +0 -0
  203. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_daily_chart.py +0 -0
  204. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_share_mask.py +0 -0
  205. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_service_status.py +0 -0
  206. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_session_resume.py +0 -0
  207. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_app.py +0 -0
  208. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_coexistence.py +0 -0
  209. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_resume.py +0 -0
  210. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_terse.py +0 -0
  211. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_subprocess_utils.py +0 -0
  212. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_subscription.py +0 -0
  213. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_taskbar_menu.py +0 -0
  214. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_taskbar_obstacles.py +0 -0
  215. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_time_utils.py +0 -0
  216. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_tui.py +0 -0
  217. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_tui_sprite.py +0 -0
  218. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_checker.py +0 -0
  219. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_gate.py +0 -0
  220. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_release_notes.py +0 -0
  221. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_client.py +0 -0
  222. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_diagnosis_snapshot.py +0 -0
  223. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_dir_sweeper.py +0 -0
  224. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_lang.py +0 -0
  225. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_logging.py +0 -0
  226. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_notifications.py +0 -0
  227. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_rate.py +0 -0
  228. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_snapshot.py +0 -0
  229. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_snapshot_replay.py +0 -0
  230. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_agy.py +0 -0
  231. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_forwarder.py +0 -0
  232. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_grok.py +0 -0
  233. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_terse_mode.py +0 -0
  234. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_terse_reminder.py +0 -0
  235. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_web_panel_payload.py +0 -0
  236. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_whats_new.py +0 -0
  237. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_wheel_packaging.py +0 -0
  238. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_win_login_item.py +0 -0
  239. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_windows_manifest.py +0 -0
  240. {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_wintray.py +0 -0
  241. {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/__init__.py +0 -0
  242. {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/app.py +0 -0
  243. {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/sprite.py +0 -0
  244. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/__init__.py +0 -0
  245. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_daily_chart.py +0 -0
  246. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_ledger.py +0 -0
  247. {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_scripts.py +0 -0
  248. {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/__init__.py +0 -0
  249. {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/checker.py +0 -0
  250. {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/gate.py +0 -0
  251. {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/release_notes.py +0 -0
  252. {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/whats_new.py +0 -0
  253. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/__init__.py +0 -0
  254. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/doctor.py +0 -0
  255. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/dependency_links.txt +0 -0
  256. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/entry_points.txt +0 -0
  257. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/requires.txt +0 -0
  258. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/top_level.txt +0 -0
  259. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/__init__.py +0 -0
  260. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/app_version.py +0 -0
  261. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/i18n.py +0 -0
  262. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/prefs.py +0 -0
  263. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/project_resolver.py +0 -0
  264. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/service_status.py +0 -0
  265. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/subprocess_utils.py +0 -0
  266. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/time_utils.py +0 -0
  267. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_dir_sweeper.py +0 -0
  268. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_lang.py +0 -0
  269. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_logging.py +0 -0
  270. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/__init__.py +0 -0
  271. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_quota_aware.py +0 -0
  272. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_session_resume.py +0 -0
  273. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_agy.py +0 -0
  274. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_forwarder.py +0 -0
  275. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_grok.py +0 -0
  276. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_terse_mode.py +0 -0
  277. {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_terse_reminder.py +0 -0
  278. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/__init__.py +0 -0
  279. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/app.py +0 -0
  280. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/login_item.py +0 -0
  281. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/menu.py +0 -0
  282. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/prefs.py +0 -0
  283. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/quota_label.py +0 -0
  284. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_menu.py +0 -0
  285. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_obstacles.py +0 -0
  286. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_overlay.py +0 -0
  287. {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/watch.py +0 -0
  288. {usage_cli-0.34.10 → 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.10
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
@@ -51,9 +52,9 @@ def load_rate_limits() -> RateLimits | None:
51
52
  if data is None:
52
53
  return None
53
54
 
54
- rl = data.get("rate_limits") or {}
55
- five = rl.get("five_hour") or {}
56
- seven = rl.get("seven_day") or {}
55
+ rl = usage_client._as_dict(data.get("rate_limits"))
56
+ five = usage_client._as_dict(rl.get("five_hour"))
57
+ seven = usage_client._as_dict(rl.get("seven_day"))
57
58
 
58
59
  now_ts = datetime.now(timezone.utc).timestamp()
59
60
  five_pct = _as_finite_float(five.get("used_percentage"))
@@ -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
 
@@ -22,7 +22,8 @@ from usage_common.time_utils import parse_optional_iso8601_utc
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
 
25
- _CACHE_TTL_SECONDS = 300.0
25
+ # Habits move slowly, while a rescan reads every session file (~20s on heavy users).
26
+ _CACHE_TTL_SECONDS = 3600.0
26
27
  _cache: dict[int, tuple[float, PersonaProfile]] = {}
27
28
 
28
29
 
@@ -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,
@@ -1,5 +1,19 @@
1
1
  {
2
2
  "zh-TW": {
3
+ "report_title_tools_top": "{tool} 用得最多,佔 {pct}%",
4
+ "report_title_tools_single": "全部用量來自 {tool}",
5
+ "report_sub_tools": "各工具 token 佔比 · 共 {total} tokens",
6
+ "report_title_projects_top3": "前 3 個專案佔了 {pct}% 的用量",
7
+ "report_sub_projects": "各專案 token 佔比",
8
+ "report_title_trend_peak_week": "{date} 那週用量最高",
9
+ "report_sub_trend": "每日與每週 token 用量",
10
+ "report_title_composition_top": "{pct}% 的 token 是{kind}",
11
+ "report_sub_composition": "各類 token 佔比",
12
+ "report_title_sessions_top": "最大的一段會話用了 {tokens} tokens",
13
+ "report_title_streak": "已經連續 {days} 天每天都有用",
14
+ "report_source_note": "資料來源:本機 {tools} 使用紀錄 · {from} – {to}",
15
+ "report_source_pricing": "API 等值依公開 API 價目換算",
16
+ "report_weekly_chart_title": "每週用量",
3
17
  "panel_default_name": "預設",
4
18
  "panel_load_error": "面板載入失敗,請回報這個問題:",
5
19
  "panel_matrix": "駭客任務",
@@ -545,6 +559,7 @@
545
559
  "report_project_others": "其他專案",
546
560
  "report_ledger_lead": "Claude 派給 Codex、Antigravity 的工作,各吃掉多少週額度。",
547
561
  "report_trend_partial_week": "僅 {days} 天",
562
+ "report_trend_incomplete_week": "不完整週",
548
563
  "report_history_scope": "分工具紀錄從 {date} 起;更早的每日總量只在下方熱力圖",
549
564
  "report_composition_missing": "另有 {tokens} tokens 沒有輸入/輸出拆解",
550
565
  "report_wrapped_cost_scope": "只含有價格紀錄的日子({date} 起)",
@@ -558,6 +573,20 @@
558
573
  "report_ledger_all_reset": "最近 7 天的派工都在額度重置前,算在上週。"
559
574
  },
560
575
  "en": {
576
+ "report_title_tools_top": "{tool} leads with {pct}% of tokens",
577
+ "report_title_tools_single": "All usage came from {tool}",
578
+ "report_sub_tools": "Token share by tool · {total} tokens in total",
579
+ "report_title_projects_top3": "The top 3 projects account for {pct}% of tokens",
580
+ "report_sub_projects": "Token share by project",
581
+ "report_title_trend_peak_week": "The week of {date} was the busiest",
582
+ "report_sub_trend": "Daily and weekly token usage",
583
+ "report_title_composition_top": "{kind} makes up {pct}% of tokens",
584
+ "report_sub_composition": "Token share by type",
585
+ "report_title_sessions_top": "The largest session used {tokens} tokens",
586
+ "report_title_streak": "{days} days in a row with usage",
587
+ "report_source_note": "Source: local {tools} usage logs · {from} – {to}",
588
+ "report_source_pricing": "API-equivalent cost is based on public API prices",
589
+ "report_weekly_chart_title": "Weekly usage",
561
590
  "panel_default_name": "Default",
562
591
  "panel_load_error": "Panel failed to load. Please report this:",
563
592
  "panel_matrix": "Matrix",
@@ -1103,6 +1132,7 @@
1103
1132
  "report_project_others": "Other projects",
1104
1133
  "report_ledger_lead": "How much of each weekly quota went to work Claude handed to Codex and Antigravity.",
1105
1134
  "report_trend_partial_week": "only {days} days",
1135
+ "report_trend_incomplete_week": "Incomplete week",
1106
1136
  "report_history_scope": "Per-tool records start on {date}; earlier daily totals appear only in the heatmap below.",
1107
1137
  "report_composition_missing": "Another {tokens} tokens have no input/output breakdown.",
1108
1138
  "report_wrapped_cost_scope": "Only days with price records (from {date})",
@@ -1116,6 +1146,20 @@
1116
1146
  "report_ledger_all_reset": "All dispatched work in the last 7 days came before the quota reset, so it counts toward last week."
1117
1147
  },
1118
1148
  "zh-CN": {
1149
+ "report_title_tools_top": "{tool} 用得最多,占 {pct}%",
1150
+ "report_title_tools_single": "全部用量来自 {tool}",
1151
+ "report_sub_tools": "各工具 token 占比 · 共 {total} tokens",
1152
+ "report_title_projects_top3": "前 3 个项目占了 {pct}% 的用量",
1153
+ "report_sub_projects": "各项目 token 占比",
1154
+ "report_title_trend_peak_week": "{date} 那周用量最高",
1155
+ "report_sub_trend": "每日与每周 token 用量",
1156
+ "report_title_composition_top": "{pct}% 的 token 是{kind}",
1157
+ "report_sub_composition": "各类 token 占比",
1158
+ "report_title_sessions_top": "最大的一段会话用了 {tokens} tokens",
1159
+ "report_title_streak": "已经连续 {days} 天每天都有使用",
1160
+ "report_source_note": "数据来源:本机 {tools} 使用记录 · {from} – {to}",
1161
+ "report_source_pricing": "API 等值按公开 API 价格换算",
1162
+ "report_weekly_chart_title": "每周用量",
1119
1163
  "panel_default_name": "默认",
1120
1164
  "panel_load_error": "面板加载失败,请反馈这个问题:",
1121
1165
  "panel_matrix": "黑客任务",
@@ -1661,6 +1705,7 @@
1661
1705
  "report_project_others": "其他项目",
1662
1706
  "report_ledger_lead": "Claude 派给 Codex、Antigravity 的工作,各吃掉多少周额度。",
1663
1707
  "report_trend_partial_week": "仅 {days} 天",
1708
+ "report_trend_incomplete_week": "不完整周",
1664
1709
  "report_history_scope": "分工具记录从 {date} 起;更早的每日总量只在下方热力图",
1665
1710
  "report_composition_missing": "另有 {tokens} tokens 没有输入/输出拆解",
1666
1711
  "report_wrapped_cost_scope": "只含有价格记录的日子({date} 起)",
@@ -1674,6 +1719,20 @@
1674
1719
  "report_ledger_all_reset": "最近 7 天的派工都在额度重置前,算在上周。"
1675
1720
  },
1676
1721
  "ja": {
1722
+ "report_title_tools_top": "{tool} が最多で {pct}%",
1723
+ "report_title_tools_single": "すべての使用量は {tool} から",
1724
+ "report_sub_tools": "ツール別 token 比率 · 合計 {total} tokens",
1725
+ "report_title_projects_top3": "上位 3 プロジェクトが使用量の {pct}%",
1726
+ "report_sub_projects": "プロジェクト別 token 比率",
1727
+ "report_title_trend_peak_week": "{date} の週が使用量最多",
1728
+ "report_sub_trend": "日別・週別 token 使用量",
1729
+ "report_title_composition_top": "token の {pct}% は{kind}",
1730
+ "report_sub_composition": "種類別 token 比率",
1731
+ "report_title_sessions_top": "最大のセッションは {tokens} tokens",
1732
+ "report_title_streak": "{days} 日連続で毎日使用中",
1733
+ "report_source_note": "データ元:ローカルの {tools} 使用記録 · {from} – {to}",
1734
+ "report_source_pricing": "API 相当額は公開 API 料金で換算",
1735
+ "report_weekly_chart_title": "週別使用量",
1677
1736
  "panel_default_name": "デフォルト",
1678
1737
  "panel_load_error": "パネルの読み込みに失敗しました。報告してください:",
1679
1738
  "panel_matrix": "マトリックス",
@@ -2219,6 +2278,7 @@
2219
2278
  "report_project_others": "その他のプロジェクト",
2220
2279
  "report_ledger_lead": "Claude が Codex・Antigravity に任せた作業が、それぞれの週間クォータをどれだけ使ったか。",
2221
2280
  "report_trend_partial_week": "{days} 日分のみ",
2281
+ "report_trend_incomplete_week": "不完全な週",
2222
2282
  "report_history_scope": "ツール別記録は {date} から。それ以前の日別合計は下のヒートマップにのみ表示します。",
2223
2283
  "report_composition_missing": "ほかに {tokens} tokens は入力/出力の内訳がありません。",
2224
2284
  "report_wrapped_cost_scope": "価格記録のある日のみ({date} 以降)",
@@ -2232,6 +2292,20 @@
2232
2292
  "report_ledger_all_reset": "直近 7 日間に委任した作業はすべてクォータのリセット前のため、先週分に含まれます。"
2233
2293
  },
2234
2294
  "ko": {
2295
+ "report_title_tools_top": "{tool} 사용량이 {pct}%로 가장 많음",
2296
+ "report_title_tools_single": "모든 사용량은 {tool}에서 발생",
2297
+ "report_sub_tools": "도구별 token 비율 · 총 {total} tokens",
2298
+ "report_title_projects_top3": "상위 3개 프로젝트가 사용량의 {pct}%",
2299
+ "report_sub_projects": "프로젝트별 token 비율",
2300
+ "report_title_trend_peak_week": "{date} 주의 사용량이 가장 많음",
2301
+ "report_sub_trend": "일별 및 주별 token 사용량",
2302
+ "report_title_composition_top": "token의 {pct}%는 {kind}",
2303
+ "report_sub_composition": "유형별 token 비율",
2304
+ "report_title_sessions_top": "가장 큰 세션에서 {tokens} tokens 사용",
2305
+ "report_title_streak": "{days}일 연속 매일 사용 중",
2306
+ "report_source_note": "데이터 출처: 로컬 {tools} 사용 기록 · {from} – {to}",
2307
+ "report_source_pricing": "API 환산 비용은 공개 API 요금 기준",
2308
+ "report_weekly_chart_title": "주별 사용량",
2235
2309
  "panel_default_name": "기본",
2236
2310
  "panel_load_error": "패널을 불러오지 못했습니다. 제보해 주세요:",
2237
2311
  "panel_matrix": "매트릭스",
@@ -2777,6 +2851,7 @@
2777
2851
  "report_project_others": "기타 프로젝트",
2778
2852
  "report_ledger_lead": "Claude가 Codex와 Antigravity에 맡긴 작업이 각 주간 한도를 얼마나 썼는지 보여 줍니다.",
2779
2853
  "report_trend_partial_week": "{days}일만 포함",
2854
+ "report_trend_incomplete_week": "불완전한 주",
2780
2855
  "report_history_scope": "도구별 기록은 {date}부터입니다. 이전 일별 합계는 아래 히트맵에만 표시됩니다.",
2781
2856
  "report_composition_missing": "추가 {tokens} tokens에는 입력/출력 내역이 없습니다.",
2782
2857
  "report_wrapped_cost_scope": "가격 기록이 있는 날만 포함 ({date}부터)",
@@ -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")
@@ -86,7 +86,7 @@ PREV_SL_KEY = "previousStatusLine"
86
86
  # Where the status-line switch parks the live statusLine while it is off, so it
87
87
  # never overwrites the pre-install backup under PREV_SL_KEY.
88
88
  DISABLED_SL_KEY = "disabledStatusLine"
89
- HOOK_VERSION = "1.16"
89
+ HOOK_VERSION = "1.17"
90
90
  FORWARDER_VERSION = "1.1"
91
91
  # Antigravity's Go runner hands its status-line command to cmd.exe unquoted, so
92
92
  # every character cmd.exe treats specially has to be kept out of the path.
@@ -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:
@@ -303,9 +303,10 @@ def _has_rate_limits(data: dict[str, Any]) -> bool:
303
303
  rl = data.get("rate_limits")
304
304
  if not isinstance(rl, dict):
305
305
  return False
306
- windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if isinstance(w, dict)]
306
+ windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if w is not None]
307
307
  return bool(windows) and all(
308
- _as_finite_float(w.get("used_percentage")) is not None for w in windows
308
+ isinstance(w, dict) and _as_finite_float(w.get("used_percentage")) is not None
309
+ for w in windows
309
310
  )
310
311
 
311
312
 
@@ -347,7 +348,10 @@ def _build_snapshot(data: dict[str, Any], *, data_source: str = "hook") -> Usage
347
348
 
348
349
  return UsageSnapshot(
349
350
  current_percent=five_pct,
350
- current_reset_at=five_reset,
351
+ # Keep an unknown hook reset unknown so the keeper cannot remember a
352
+ # parse-time default as a real boundary. Display expiry logic above
353
+ # still uses the default to preserve the reported percentage.
354
+ current_reset_at=_as_finite_float(five.get("resets_at")),
351
355
  weekly_percent=seven_pct,
352
356
  weekly_reset_at=seven_reset,
353
357
  current_status=status,
@@ -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