usage-cli 0.34.12__tar.gz → 0.34.14__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 (294) hide show
  1. {usage_cli-0.34.12/usage_cli.egg-info → usage_cli-0.34.14}/PKG-INFO +1 -1
  2. usage_cli-0.34.14/analyzer/ledger_legacy_backfill.py +79 -0
  3. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/reporter.py +11 -3
  4. {usage_cli-0.34.12 → usage_cli-0.34.14}/i18n.json +64 -29
  5. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/login_item.py +3 -2
  6. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/session_hooks.py +16 -9
  7. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/setup_hook.py +17 -3
  8. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/statusline_settings.py +8 -3
  9. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/agy_quota_probe.py +18 -0
  10. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/muse_quota_probe.py +78 -23
  11. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/actions.py +20 -4
  12. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/app.py +13 -11
  13. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/menu.py +62 -9
  14. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/notify.py +29 -0
  15. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/prefs.py +24 -0
  16. usage_cli-0.34.14/menubar/quota_countdown.py +34 -0
  17. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/title.py +3 -23
  18. {usage_cli-0.34.12 → usage_cli-0.34.14}/pyproject.toml +1 -1
  19. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/agy_window_keeper.py +5 -0
  20. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/notifications.py +13 -3
  21. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_agy_quota_probe.py +92 -0
  22. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_agy_window_keeper.py +14 -0
  23. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_check_upstream.py +1 -0
  24. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_claude_expired_windows.py +18 -0
  25. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_html_report_snapshot.py +3 -1
  26. usage_cli-0.34.14/tests/test_ledger_legacy_backfill.py +216 -0
  27. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_login_item.py +4 -2
  28. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar.py +137 -8
  29. usage_cli-0.34.14/tests/test_menubar_permission.py +136 -0
  30. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_muse_quota_probe.py +79 -0
  31. usage_cli-0.34.14/tests/test_network.py +41 -0
  32. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_quota_aware.py +67 -0
  33. usage_cli-0.34.14/tests/test_quota_threshold_menu.py +202 -0
  34. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_daily_chart.py +21 -0
  35. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_session_resume.py +139 -0
  36. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_setup_hook.py +42 -0
  37. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_notifications.py +31 -0
  38. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_statusline_agy.py +5 -1
  39. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_wintray.py +129 -2
  40. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/html_report.py +10 -4
  41. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_daily_chart.py +6 -4
  42. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_filter.py +1 -1
  43. {usage_cli-0.34.12 → usage_cli-0.34.14/usage_cli.egg-info}/PKG-INFO +1 -1
  44. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_cli.egg-info/SOURCES.txt +7 -0
  45. usage_cli-0.34.14/usage_common/network.py +29 -0
  46. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_quota_aware.py +38 -17
  47. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_session_resume.py +36 -5
  48. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_statusline.py +12 -2
  49. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/app.py +57 -10
  50. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/menu.py +23 -0
  51. {usage_cli-0.34.12 → usage_cli-0.34.14}/LICENSE +0 -0
  52. {usage_cli-0.34.12 → usage_cli-0.34.14}/README.md +0 -0
  53. {usage_cli-0.34.12 → usage_cli-0.34.14}/THIRD_PARTY_NOTICES.md +0 -0
  54. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/__init__.py +0 -0
  55. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/agy.py +0 -0
  56. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/claude.py +0 -0
  57. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/codex.py +0 -0
  58. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/grok.py +0 -0
  59. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/muse.py +0 -0
  60. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/rate_limits.py +0 -0
  61. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/registry.py +0 -0
  62. {usage_cli-0.34.12 → usage_cli-0.34.14}/adapters/types.py +0 -0
  63. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/__init__.py +0 -0
  64. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/aggregator.py +0 -0
  65. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/blocks.py +0 -0
  66. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/diagnoser.py +0 -0
  67. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/diagnosis_snapshot.py +0 -0
  68. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/dispatch_ledger.py +0 -0
  69. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/history_persist.py +0 -0
  70. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/insights.py +0 -0
  71. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/ledger_backfill.py +0 -0
  72. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/persona_loader.py +0 -0
  73. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/subscription.py +0 -0
  74. {usage_cli-0.34.12 → usage_cli-0.34.14}/analyzer/usage_snapshot.py +0 -0
  75. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/__init__.py +0 -0
  76. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/claude_pane.py +0 -0
  77. {usage_cli-0.34.12 → usage_cli-0.34.14}/installer/codex_hook_trust.py +0 -0
  78. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/__init__.py +0 -0
  79. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/_shared_cache_file.py +0 -0
  80. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/agy_disk_cache.py +0 -0
  81. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/agy_loader.py +0 -0
  82. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/cache_quarantine.py +0 -0
  83. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/chromium_cache.py +0 -0
  84. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/claude_desktop.py +0 -0
  85. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/claude_paths.py +0 -0
  86. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/claude_usage.py +0 -0
  87. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/codex_disk_cache.py +0 -0
  88. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/codex_events.py +0 -0
  89. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/codex_fork_replay.py +0 -0
  90. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/codex_loader.py +0 -0
  91. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/codex_paths.py +0 -0
  92. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/disk_cache_common.py +0 -0
  93. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/disk_cache_lifecycle.py +0 -0
  94. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/grok_loader.py +0 -0
  95. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/grok_quota_probe.py +0 -0
  96. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/history_disk_cache.py +0 -0
  97. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/history_loader.py +0 -0
  98. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/jsonl_limits.py +0 -0
  99. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/jsonl_utils.py +0 -0
  100. {usage_cli-0.34.12 → usage_cli-0.34.14}/loaders/muse_loader.py +0 -0
  101. {usage_cli-0.34.12 → usage_cli-0.34.14}/main.py +0 -0
  102. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/__init__.py +0 -0
  103. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/agy.py +0 -0
  104. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/chrome.py +0 -0
  105. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/fsevents_watch.py +0 -0
  106. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/grok.py +0 -0
  107. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/manual_refresh.py +0 -0
  108. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/muse.py +0 -0
  109. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/new_badge.py +0 -0
  110. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/popover.py +0 -0
  111. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/refresh.py +0 -0
  112. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/state.py +0 -0
  113. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/update.py +0 -0
  114. {usage_cli-0.34.12 → usage_cli-0.34.14}/menubar/whats_new.py +0 -0
  115. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/__init__.py +0 -0
  116. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/base.py +0 -0
  117. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/catalog.py +0 -0
  118. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/dynamic_height.py +0 -0
  119. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/panel_scale.py +0 -0
  120. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/panel_window.py +0 -0
  121. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/panel_window_state.py +0 -0
  122. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/payload.py +0 -0
  123. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/web_panel.py +0 -0
  124. {usage_cli-0.34.12 → usage_cli-0.34.14}/panels/window_drag.py +0 -0
  125. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/__init__.py +0 -0
  126. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/burn_rate.py +0 -0
  127. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/codex_window_keeper.py +0 -0
  128. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/keeper_outcome.py +0 -0
  129. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/quota_snapshot.py +0 -0
  130. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/status_payload.py +0 -0
  131. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/usage_rate.py +0 -0
  132. {usage_cli-0.34.12 → usage_cli-0.34.14}/quota/window_keeper.py +0 -0
  133. {usage_cli-0.34.12 → usage_cli-0.34.14}/setup.cfg +0 -0
  134. {usage_cli-0.34.12 → usage_cli-0.34.14}/setup_app.py +0 -0
  135. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_adapters_claude.py +0 -0
  136. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_adapters_codex.py +0 -0
  137. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_adapters_rate_limits.py +0 -0
  138. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_aggregator.py +0 -0
  139. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_agy_adapter.py +0 -0
  140. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_agy_disk_cache.py +0 -0
  141. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_agy_loader.py +0 -0
  142. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_analyzer_blocks.py +0 -0
  143. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_analyzer_pipeline.py +0 -0
  144. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_app_version.py +0 -0
  145. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_burn_rate.py +0 -0
  146. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_cache_quarantine.py +0 -0
  147. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_check_release_version.py +0 -0
  148. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_chromium_cache.py +0 -0
  149. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_claude_desktop.py +0 -0
  150. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_claude_pane.py +0 -0
  151. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_claude_paths.py +0 -0
  152. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_cli_lookup.py +0 -0
  153. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_codex_consistency.py +0 -0
  154. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_codex_hook_trust.py +0 -0
  155. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_codex_loader.py +0 -0
  156. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_codex_window_keeper.py +0 -0
  157. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_compact_cards.py +0 -0
  158. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_desktop_fresh_cache.py +0 -0
  159. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_desktop_quota_display.py +0 -0
  160. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_desktop_quota_review.py +0 -0
  161. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_diagnoser.py +0 -0
  162. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_disk_cache_shards.py +0 -0
  163. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_dispatch_ledger.py +0 -0
  164. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_doctor.py +0 -0
  165. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_dynamic_height.py +0 -0
  166. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_fsevents_watch.py +0 -0
  167. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_grok_loader.py +0 -0
  168. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_grok_quota_probe.py +0 -0
  169. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_history_loader.py +0 -0
  170. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_history_persist.py +0 -0
  171. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_hook_scripts_are_standalone.py +0 -0
  172. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_i18n_format_fallback.py +0 -0
  173. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_i18n_key_parity.py +0 -0
  174. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_i18n_packaged_path.py +0 -0
  175. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_insights.py +0 -0
  176. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_jsonl_utils.py +0 -0
  177. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_keeper_menu.py +0 -0
  178. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_keeper_outcome.py +0 -0
  179. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_ledger_backfill.py +0 -0
  180. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_main.py +0 -0
  181. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_manual_refresh.py +0 -0
  182. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_agy.py +0 -0
  183. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_grok.py +0 -0
  184. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_muse.py +0 -0
  185. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_notify.py +0 -0
  186. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_prefs.py +0 -0
  187. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_state.py +0 -0
  188. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_menubar_update.py +0 -0
  189. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_muse_coverage.py +0 -0
  190. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_muse_loader.py +0 -0
  191. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_muse_review_regressions.py +0 -0
  192. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_new_badge.py +0 -0
  193. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_packaged_resources.py +0 -0
  194. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_packaging.py +0 -0
  195. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_panel_footer_layout.py +0 -0
  196. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_panel_scale.py +0 -0
  197. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_panel_window_state.py +0 -0
  198. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_panels.py +0 -0
  199. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_panels_lazy_import.py +0 -0
  200. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_persona_loader.py +0 -0
  201. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_pricing.py +0 -0
  202. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_project_resolver.py +0 -0
  203. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_quota_label.py +0 -0
  204. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_chart_redesign.py +0 -0
  205. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_cold_read.py +0 -0
  206. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_filter_js.py +0 -0
  207. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_headings.py +0 -0
  208. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_report_share_mask.py +0 -0
  209. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_reporter_build_data.py +0 -0
  210. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_service_status.py +0 -0
  211. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_setup_app.py +0 -0
  212. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_setup_hook_coexistence.py +0 -0
  213. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_setup_hook_resume.py +0 -0
  214. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_setup_hook_terse.py +0 -0
  215. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_subprocess_utils.py +0 -0
  216. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_subscription.py +0 -0
  217. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_taskbar_menu.py +0 -0
  218. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_taskbar_obstacles.py +0 -0
  219. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_time_utils.py +0 -0
  220. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_tui.py +0 -0
  221. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_tui_sprite.py +0 -0
  222. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_ui_tables.py +0 -0
  223. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_update_checker.py +0 -0
  224. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_update_gate.py +0 -0
  225. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_update_release_notes.py +0 -0
  226. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_cli.py +0 -0
  227. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_client.py +0 -0
  228. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_diagnosis_snapshot.py +0 -0
  229. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_dir_sweeper.py +0 -0
  230. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_lang.py +0 -0
  231. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_logging.py +0 -0
  232. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_rate.py +0 -0
  233. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_snapshot.py +0 -0
  234. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_snapshot_replay.py +0 -0
  235. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_statusline.py +0 -0
  236. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_statusline_forwarder.py +0 -0
  237. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_statusline_grok.py +0 -0
  238. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_terse_mode.py +0 -0
  239. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_usage_terse_reminder.py +0 -0
  240. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_web_panel_payload.py +0 -0
  241. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_whats_new.py +0 -0
  242. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_wheel_packaging.py +0 -0
  243. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_win_login_item.py +0 -0
  244. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_window_keeper.py +0 -0
  245. {usage_cli-0.34.12 → usage_cli-0.34.14}/tests/test_windows_manifest.py +0 -0
  246. {usage_cli-0.34.12 → usage_cli-0.34.14}/tui/__init__.py +0 -0
  247. {usage_cli-0.34.12 → usage_cli-0.34.14}/tui/app.py +0 -0
  248. {usage_cli-0.34.12 → usage_cli-0.34.14}/tui/sprite.py +0 -0
  249. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/__init__.py +0 -0
  250. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_charts.py +0 -0
  251. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_ledger.py +0 -0
  252. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_scripts.py +0 -0
  253. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/report_styles.py +0 -0
  254. {usage_cli-0.34.12 → usage_cli-0.34.14}/ui/tables.py +0 -0
  255. {usage_cli-0.34.12 → usage_cli-0.34.14}/updates/__init__.py +0 -0
  256. {usage_cli-0.34.12 → usage_cli-0.34.14}/updates/checker.py +0 -0
  257. {usage_cli-0.34.12 → usage_cli-0.34.14}/updates/gate.py +0 -0
  258. {usage_cli-0.34.12 → usage_cli-0.34.14}/updates/release_notes.py +0 -0
  259. {usage_cli-0.34.12 → usage_cli-0.34.14}/updates/whats_new.py +0 -0
  260. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_app/__init__.py +0 -0
  261. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_app/cli.py +0 -0
  262. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_app/doctor.py +0 -0
  263. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_cli.egg-info/dependency_links.txt +0 -0
  264. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_cli.egg-info/entry_points.txt +0 -0
  265. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_cli.egg-info/requires.txt +0 -0
  266. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_cli.egg-info/top_level.txt +0 -0
  267. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/__init__.py +0 -0
  268. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/app_version.py +0 -0
  269. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/cli_lookup.py +0 -0
  270. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/i18n.py +0 -0
  271. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/prefs.py +0 -0
  272. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/pricing.py +0 -0
  273. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/project_resolver.py +0 -0
  274. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/service_status.py +0 -0
  275. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/subprocess_utils.py +0 -0
  276. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/time_utils.py +0 -0
  277. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/usage_dir_sweeper.py +0 -0
  278. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/usage_lang.py +0 -0
  279. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_common/usage_logging.py +0 -0
  280. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/__init__.py +0 -0
  281. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_statusline_agy.py +0 -0
  282. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_statusline_forwarder.py +0 -0
  283. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_statusline_grok.py +0 -0
  284. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_terse_mode.py +0 -0
  285. {usage_cli-0.34.12 → usage_cli-0.34.14}/usage_hooks/usage_terse_reminder.py +0 -0
  286. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/__init__.py +0 -0
  287. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/login_item.py +0 -0
  288. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/prefs.py +0 -0
  289. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/quota_label.py +0 -0
  290. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/taskbar_menu.py +0 -0
  291. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/taskbar_obstacles.py +0 -0
  292. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/taskbar_overlay.py +0 -0
  293. {usage_cli-0.34.12 → usage_cli-0.34.14}/wintray/watch.py +0 -0
  294. {usage_cli-0.34.12 → usage_cli-0.34.14}/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.12
3
+ Version: 0.34.14
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
@@ -0,0 +1,79 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
3
+ #
4
+ # Part of "usage". Free software licensed under the GNU Affero General Public
5
+ # License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
6
+
7
+ """Restore dimensions of total-only ledger days from read-only Codex state."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import sqlite3
13
+ from contextlib import closing
14
+ from datetime import datetime
15
+ from typing import TYPE_CHECKING
16
+
17
+ from loaders import codex_loader
18
+
19
+ from .ledger_backfill import _allocate
20
+
21
+ if TYPE_CHECKING:
22
+ from .reporter import _YearDay
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def backfill_days(days: dict[str, _YearDay]) -> bool:
28
+ """Mutate eligible days only after a successful source read; report changes."""
29
+ eligible = {
30
+ key: day
31
+ for key, day in days.items()
32
+ if day["total_tokens"] > 0 and all(tokens == 0 for tokens in day["agent_tokens"].values())
33
+ }
34
+ if not eligible:
35
+ return False
36
+ try:
37
+ with closing(
38
+ sqlite3.connect(codex_loader._readonly_sqlite_uri(codex_loader.STATE_DB), uri=True)
39
+ ) as connection:
40
+ rows = connection.execute(
41
+ "SELECT created_at, tokens_used, model, cwd FROM threads WHERE tokens_used > 0"
42
+ ).fetchall()
43
+ except (OSError, sqlite3.Error) as exc:
44
+ logger.warning("Legacy ledger backfill could not read %s: %s", codex_loader.STATE_DB, exc)
45
+ return False
46
+
47
+ models: dict[str, dict[str, int]] = {key: {} for key in eligible}
48
+ projects: dict[str, dict[str, int]] = {key: {} for key in eligible}
49
+ for created_at, tokens, model, cwd in rows:
50
+ if not isinstance(created_at, int | float) or not isinstance(tokens, int) or tokens <= 0:
51
+ logger.warning("Legacy ledger backfill skipped invalid thread counts or timestamp")
52
+ continue
53
+ try:
54
+ day_key = datetime.fromtimestamp(created_at).date().isoformat()
55
+ except (OSError, OverflowError, ValueError):
56
+ logger.warning("Legacy ledger backfill skipped invalid thread timestamp %r", created_at)
57
+ continue
58
+ if day_key not in eligible:
59
+ continue
60
+ model = model if isinstance(model, str) and model else "unknown"
61
+ project = codex_loader._project_from_cwd(cwd) if isinstance(cwd, str) and cwd else "unknown"
62
+ models[day_key][model] = models[day_key].get(model, 0) + tokens
63
+ projects[day_key][project] = projects[day_key].get(project, 0) + tokens
64
+
65
+ for day_key, day in eligible.items():
66
+ source_total = sum(models[day_key].values())
67
+ codex_tokens = min(source_total, day["total_tokens"])
68
+ unknown_tokens = day["total_tokens"] - codex_tokens
69
+ day["agent_tokens"] = {"codex": codex_tokens} if codex_tokens else {}
70
+ day["model_tokens"] = (
71
+ dict(_allocate(models[day_key], {}, codex_tokens)) if codex_tokens else {}
72
+ )
73
+ day["project_tokens"] = (
74
+ dict(_allocate(projects[day_key], {}, codex_tokens)) if codex_tokens else {}
75
+ )
76
+ if unknown_tokens:
77
+ for dimension in (day["agent_tokens"], day["model_tokens"], day["project_tokens"]):
78
+ dimension["unknown"] = dimension.get("unknown", 0) + unknown_tokens
79
+ return True
@@ -29,7 +29,7 @@ from adapters.types import AgentInfo, UsageEntry
29
29
  from usage_common.pricing import calculate_cost, is_model_priced
30
30
 
31
31
  from .aggregator import aggregate_sessions
32
- from . import diagnoser, dispatch_ledger, ledger_backfill, usage_snapshot
32
+ from . import diagnoser, dispatch_ledger, ledger_backfill, ledger_legacy_backfill, usage_snapshot
33
33
 
34
34
  logger = logging.getLogger(__name__)
35
35
 
@@ -45,9 +45,9 @@ _YEAR_WEEKS = 53
45
45
  YEAR_CACHE_PATH = Path(os.path.expanduser("~/.usage/year_cache.json"))
46
46
  YEAR_LEDGER_PATH = YEAR_CACHE_PATH.with_name("year_ledger.json")
47
47
  YEAR_CACHE_TTL_SECONDS = 6 * 3600
48
- _YEAR_CACHE_SCHEMA = 6
48
+ _YEAR_CACHE_SCHEMA = 7
49
49
  _YEAR_LEDGER_SCHEMA = 1
50
- _year_ledger_lock = threading.Lock()
50
+ _year_ledger_lock = threading.RLock()
51
51
  _YEAR_LEDGER_TRIM_BUFFER_DAYS = 60
52
52
 
53
53
 
@@ -572,6 +572,14 @@ def _year_day_from_json(value: object) -> _YearDay | None:
572
572
 
573
573
 
574
574
  def _read_year_ledger() -> _YearLedger:
575
+ with _year_ledger_lock:
576
+ ledger = _read_year_ledger_unlocked()
577
+ if ledger_legacy_backfill.backfill_days(ledger["days"]):
578
+ _write_year_ledger(ledger)
579
+ return ledger
580
+
581
+
582
+ def _read_year_ledger_unlocked() -> _YearLedger:
575
583
  try:
576
584
  with YEAR_LEDGER_PATH.open(encoding="utf-8") as file:
577
585
  ledger = json.load(file)
@@ -37,13 +37,19 @@
37
37
  "statusline_sync_menu": "狀態列同步額度",
38
38
  "statusline_sync_tooltip": "從 Claude Code 的狀態列讀取額度。關掉後,Claude 的額度就不會再更新。",
39
39
  "launch_at_login": "開機自動啟動",
40
+ "launch_at_login_failed": "無法設定開機自動啟動:{error}",
40
41
  "reset_panel_position": "重設面板位置",
41
42
  "hide_sections_menu": "隱藏區塊",
42
43
  "tray_provider_menu": "系統匣顯示來源",
43
44
  "quota_label_menu": "顯示工作列額度",
44
45
  "quota_label_format": "{provider}: {value}%",
45
46
  "quota_label_unknown": "{provider}: --",
47
+ "quota_label_countdown": "{provider}: {value}",
46
48
  "quota_notifications_menu": "用量提醒通知",
49
+ "quota_notifications_toggle": "開啟通知",
50
+ "quota_notifications_permission_denied": "系統已關閉 usage 的通知 — 打開系統設定…",
51
+ "quota_threshold_header": "提醒門檻",
52
+ "quota_threshold_item": "用到 {percent}%",
47
53
  "window_keeper_menu": "自動啟動 5 小時計時",
48
54
  "window_keeper_tooltip": "5 小時額度重置後,自動各送一則極小訊息(Claude 用 Haiku、agy 用 Gemini 3.8 Flash Low、Codex 用最省成本模型),讓下一輪 5 小時立刻開始計時(會消耗極少量額度)",
49
55
  "window_keeper_sleep_title": "🐾 自動啟動 5 小時計時已開啟",
@@ -130,8 +136,8 @@
130
136
  "grok_stale_minutes": "約 {minutes} 分鐘前",
131
137
  "grok_stale_hours": "約 {hours} 小時前",
132
138
  "grok_stale_tooltip": "資料來自 Grok CLI 本機 log,不是即時 API;請最近執行過 grok 指令後再查看,才會更新。",
133
- "codex_stale_minutes": "約 {minutes} 分鐘前",
134
- "codex_stale_hours": "約 {hours} 小時前",
139
+ "codex_stale_minutes": "約 {minutes} 分鐘前更新",
140
+ "codex_stale_hours": "約 {hours} 小時前更新",
135
141
  "codex_stale_tooltip": "Claude 有即時回報所以很準;Codex 沒有,usage 會讀它本機偶爾留下的紀錄,可能比實際略舊。繼續在終端機跑 Codex 就會自動更新。",
136
142
  "history_load_error_file": "讀不到使用紀錄檔",
137
143
  "history_load_error_parse": "使用紀錄格式異常",
@@ -143,7 +149,7 @@
143
149
  "project_range_7d": "7 日",
144
150
  "project_range_30d": "月",
145
151
  "project_range_all": "全部",
146
- "session_label": "Session",
152
+ "session_label": "5 小時",
147
153
  "weekly_label": "本週",
148
154
  "monthly_label": "本月",
149
155
  "projects_empty": "無記錄",
@@ -256,12 +262,12 @@
256
262
  "report_wrapped_books_hint": "含重複的快取讀取,不等於實際產出的文字量。",
257
263
  "report_kpi_active_value": "{active} / {total} 天",
258
264
  "report_kpi_cost": "API 等值",
259
- "report_kpi_cost_unpriced": "其中 {tokens} tokens 無公開價格,未計入",
265
+ "report_kpi_cost_unpriced": "其中 {tokens} tokens 沒有價格資料,未計入",
260
266
  "report_pricing_section": "API 價格覆蓋率(依 token 數)",
261
267
  "report_pricing_priced": "可換算 API 等值",
262
- "report_pricing_unpriced": "無公開價格",
268
+ "report_pricing_unpriced": "沒有價格資料",
263
269
  "report_pricing_all_priced": "全部用量都有公開價格。",
264
- "report_pricing_hint": "API 等值是用公開 API 價目表換算的等值金額,不是你的訂閱帳單;無公開價格的用量不計入 API 等值。 沒有公開價格不代表免費,只是無法換算。",
270
+ "report_pricing_hint": "API 等值是用公開 API 價目表換算的等值金額,不是你的訂閱帳單;沒有價格資料的用量不計入 API 等值,包括沒有公開價格的模型,以及只存了總量的舊紀錄。沒有價格資料不代表免費,只是無法換算。",
265
271
  "report_kpi_peak_day": "燒最兇的一天",
266
272
  "report_kpi_tokens": "燒掉 Tokens",
267
273
  "report_kpi_vs_prev_period": "較前期",
@@ -515,6 +521,7 @@
515
521
  "claude_pane_remaining": "剩",
516
522
  "claude_pane_reset_done": "已重置",
517
523
  "claude_pane_hours_ago": "{count}時前",
524
+ "claude_pane_updated_ago": "{ago}更新",
518
525
  "update_check_failed_offline": "連不上 GitHub,請確認網路後再試。",
519
526
  "update_check_failed_rate_limited": "GitHub 暫時拒絕請求,可能太頻繁了,稍後再試。",
520
527
  "update_check_failed_server": "GitHub 回應錯誤,稍後再試。",
@@ -610,13 +617,19 @@
610
617
  "statusline_sync_menu": "Sync quota via status line",
611
618
  "statusline_sync_tooltip": "Reads the Claude quota from Claude Code's status line. Turn it off and the Claude quota stops updating.",
612
619
  "launch_at_login": "Launch at Login",
620
+ "launch_at_login_failed": "Unable to set launch at login: {error}",
613
621
  "reset_panel_position": "Reset panel position",
614
622
  "hide_sections_menu": "Hide Sections",
615
623
  "tray_provider_menu": "Tray Display Source",
616
624
  "quota_label_menu": "Show Taskbar Quota",
617
625
  "quota_label_format": "{provider}: {value}%",
618
626
  "quota_label_unknown": "{provider}: --",
627
+ "quota_label_countdown": "{provider}: {value}",
619
628
  "quota_notifications_menu": "Usage Alert Notifications",
629
+ "quota_notifications_toggle": "Enable notifications",
630
+ "quota_notifications_permission_denied": "System notifications for usage are off — Open System Settings…",
631
+ "quota_threshold_header": "Alert thresholds",
632
+ "quota_threshold_item": "At {percent}%",
620
633
  "window_keeper_menu": "Auto-start 5-hour Session",
621
634
  "window_keeper_tooltip": "After your 5-hour quotas reset, automatically send one tiny message to each tool (Claude with Haiku, agy with Gemini 3.8 Flash Low, Codex with its cheapest model) so the next 5-hour sessions start counting right away (uses a tiny bit of quota).",
622
635
  "window_keeper_sleep_title": "🐾 Auto-start 5-hour Session is on",
@@ -703,8 +716,8 @@
703
716
  "grok_stale_minutes": "about {minutes} minutes ago",
704
717
  "grok_stale_hours": "about {hours} hours ago",
705
718
  "grok_stale_tooltip": "Data comes from Grok CLI's local log, not a live API. Run a grok command recently for it to update.",
706
- "codex_stale_minutes": "about {minutes} minutes ago",
707
- "codex_stale_hours": "about {hours} hours ago",
719
+ "codex_stale_minutes": "updated about {minutes} min ago",
720
+ "codex_stale_hours": "updated about {hours} hr ago",
708
721
  "codex_stale_tooltip": "Claude is accurate because it reports in real time; Codex does not, so usage reads the local records it leaves occasionally, which may be slightly older than reality. Keep running Codex in the terminal and it will update automatically.",
709
722
  "history_load_error_file": "Couldn't read usage files",
710
723
  "history_load_error_parse": "Usage data format issue",
@@ -829,12 +842,12 @@
829
842
  "report_wrapped_books_hint": "Includes repeated cache reads; this is not the amount of text actually generated.",
830
843
  "report_kpi_active_value": "{active} / {total} days",
831
844
  "report_kpi_cost": "API equivalent",
832
- "report_kpi_cost_unpriced": "Includes {tokens} tokens without public pricing, excluded from the API equivalent",
845
+ "report_kpi_cost_unpriced": "Includes {tokens} tokens without pricing data, excluded from the API equivalent",
833
846
  "report_pricing_section": "API pricing coverage (by token count)",
834
847
  "report_pricing_priced": "Convertible to API equivalent",
835
- "report_pricing_unpriced": "No public price",
848
+ "report_pricing_unpriced": "No pricing data",
836
849
  "report_pricing_all_priced": "All usage has a public price.",
837
- "report_pricing_hint": "API equivalent is the API-equivalent value from public price lists, not your subscription bill. Usage without a public price is left out of API equivalent. No public price does not mean free; the API equivalent cannot be calculated.",
850
+ "report_pricing_hint": "API equivalent is the API-equivalent value from public price lists, not your subscription bill. Usage without pricing data is left out of API equivalent: models with no public price, and older records that kept only totals. Missing pricing data does not mean free; the API equivalent cannot be calculated.",
838
851
  "report_kpi_peak_day": "Peak Day",
839
852
  "report_kpi_tokens": "Burned Tokens",
840
853
  "report_kpi_vs_prev_period": "vs previous period",
@@ -1088,6 +1101,7 @@
1088
1101
  "claude_pane_remaining": "left",
1089
1102
  "claude_pane_reset_done": "Reset",
1090
1103
  "claude_pane_hours_ago": "{count}h ago",
1104
+ "claude_pane_updated_ago": "updated {ago}",
1091
1105
  "update_check_failed_offline": "Cannot reach GitHub. Check your connection and try again.",
1092
1106
  "update_check_failed_rate_limited": "GitHub is limiting requests. Try again later.",
1093
1107
  "update_check_failed_server": "GitHub returned an error. Try again later.",
@@ -1183,13 +1197,19 @@
1183
1197
  "statusline_sync_menu": "状态栏同步额度",
1184
1198
  "statusline_sync_tooltip": "从 Claude Code 的状态栏读取额度。关闭后,Claude 的额度就不会再更新。",
1185
1199
  "launch_at_login": "开机自动启动",
1200
+ "launch_at_login_failed": "无法设置开机自动启动:{error}",
1186
1201
  "reset_panel_position": "重设面板位置",
1187
1202
  "hide_sections_menu": "隐藏区块",
1188
1203
  "tray_provider_menu": "托盘显示来源",
1189
1204
  "quota_label_menu": "显示任务栏额度",
1190
1205
  "quota_label_format": "{provider}: {value}%",
1191
1206
  "quota_label_unknown": "{provider}: --",
1207
+ "quota_label_countdown": "{provider}: {value}",
1192
1208
  "quota_notifications_menu": "用量提醒通知",
1209
+ "quota_notifications_toggle": "开启通知",
1210
+ "quota_notifications_permission_denied": "系统已关闭 usage 的通知 — 打开系统设置…",
1211
+ "quota_threshold_header": "提醒阈值",
1212
+ "quota_threshold_item": "用到 {percent}%",
1193
1213
  "window_keeper_menu": "自动启动 5 小时计时",
1194
1214
  "window_keeper_tooltip": "5 小时额度重置后,自动各发送一则极小消息(Claude 用 Haiku、agy 用 Gemini 3.8 Flash Low、Codex 用最省成本模型),让下一轮 5 小时立刻开始计时(会消耗极少量额度)",
1195
1215
  "window_keeper_sleep_title": "🐾 已开启自动启动 5 小时计时",
@@ -1276,8 +1296,8 @@
1276
1296
  "grok_stale_minutes": "约 {minutes} 分钟前",
1277
1297
  "grok_stale_hours": "约 {hours} 小时前",
1278
1298
  "grok_stale_tooltip": "资料来自 Grok CLI 本地日志,不是即时 API;请最近运行过 grok 指令后再查看,才会更新。",
1279
- "codex_stale_minutes": "约 {minutes} 分钟前",
1280
- "codex_stale_hours": "约 {hours} 小时前",
1299
+ "codex_stale_minutes": "约 {minutes} 分钟前更新",
1300
+ "codex_stale_hours": "约 {hours} 小时前更新",
1281
1301
  "codex_stale_tooltip": "Claude 有实时回报所以很准;Codex 没有,usage 会读取它本机偶尔留下的记录,可能比实际略旧。继续在终端运行 Codex 就会自动更新。",
1282
1302
  "history_load_error_file": "读不到使用记录文件",
1283
1303
  "history_load_error_parse": "使用记录格式异常",
@@ -1289,7 +1309,7 @@
1289
1309
  "project_range_7d": "7 日",
1290
1310
  "project_range_30d": "月",
1291
1311
  "project_range_all": "全部",
1292
- "session_label": "Session",
1312
+ "session_label": "5 小时",
1293
1313
  "weekly_label": "本周",
1294
1314
  "monthly_label": "本月",
1295
1315
  "projects_empty": "无记录",
@@ -1402,12 +1422,12 @@
1402
1422
  "report_wrapped_books_hint": "含重复的缓存读取,不等于实际生成的文字量。",
1403
1423
  "report_kpi_active_value": "{active} / {total} 天",
1404
1424
  "report_kpi_cost": "API 等值",
1405
- "report_kpi_cost_unpriced": "其中 {tokens} tokens 无公开价格,未计入",
1425
+ "report_kpi_cost_unpriced": "其中 {tokens} tokens 没有价格数据,未计入",
1406
1426
  "report_pricing_section": "API 价格覆盖率(按 token 数)",
1407
1427
  "report_pricing_priced": "可换算 API 等值",
1408
- "report_pricing_unpriced": "无公开价格",
1428
+ "report_pricing_unpriced": "没有价格数据",
1409
1429
  "report_pricing_all_priced": "全部用量都有公开价格。",
1410
- "report_pricing_hint": "API 等值是用公开 API 价目表换算的等值金额,不是你的订阅账单;无公开价格的用量不计入 API 等值。 没有公开价格不代表免费,只是无法换算。",
1430
+ "report_pricing_hint": "API 等值是用公开 API 价目表换算的等值金额,不是你的订阅账单;没有价格数据的用量不计入 API 等值,包括没有公开价格的模型,以及只存了总量的旧记录。没有价格数据不代表免费,只是无法换算。",
1411
1431
  "report_kpi_peak_day": "烧最猛的一天",
1412
1432
  "report_kpi_tokens": "烧掉 Tokens",
1413
1433
  "report_kpi_vs_prev_period": "较前期",
@@ -1661,6 +1681,7 @@
1661
1681
  "claude_pane_remaining": "剩",
1662
1682
  "claude_pane_reset_done": "已重置",
1663
1683
  "claude_pane_hours_ago": "{count}时前",
1684
+ "claude_pane_updated_ago": "{ago}更新",
1664
1685
  "update_check_failed_offline": "连不上 GitHub,请确认网络后再试。",
1665
1686
  "update_check_failed_rate_limited": "GitHub 暂时拒绝请求,可能太频繁了,稍后再试。",
1666
1687
  "update_check_failed_server": "GitHub 响应错误,稍后再试。",
@@ -1756,13 +1777,19 @@
1756
1777
  "statusline_sync_menu": "ステータスラインで使用量を同期",
1757
1778
  "statusline_sync_tooltip": "Claude Code のステータスラインから使用量を読み取ります。オフにすると Claude の使用量は更新されなくなります。",
1758
1779
  "launch_at_login": "ログイン時に起動",
1780
+ "launch_at_login_failed": "ログイン時の起動を設定できません:{error}",
1759
1781
  "reset_panel_position": "パネルの位置をリセット",
1760
1782
  "hide_sections_menu": "セクションを非表示",
1761
1783
  "tray_provider_menu": "トレイの表示元",
1762
1784
  "quota_label_menu": "タスクバーにクォータを表示",
1763
1785
  "quota_label_format": "{provider}: {value}%",
1764
1786
  "quota_label_unknown": "{provider}: --",
1787
+ "quota_label_countdown": "{provider}: {value}",
1765
1788
  "quota_notifications_menu": "使用量アラート通知",
1789
+ "quota_notifications_toggle": "通知を有効にする",
1790
+ "quota_notifications_permission_denied": "usage の通知はシステムで無効です — システム設定を開く…",
1791
+ "quota_threshold_header": "通知のしきい値",
1792
+ "quota_threshold_item": "使用量 {percent}%",
1766
1793
  "window_keeper_menu": "5 時間セッションを自動開始",
1767
1794
  "window_keeper_tooltip": "5 時間枠のリセット後、各ツールにごく小さなメッセージを自動送信します(Claude は Haiku、agy は Gemini 3.8 Flash Low、Codex は最も低コストなモデル)。次の 5 時間のカウントをすぐに開始します(ごくわずかな枠を消費します)。",
1768
1795
  "window_keeper_sleep_title": "🐾 5 時間セッションの自動開始をオンにしました",
@@ -1849,8 +1876,8 @@
1849
1876
  "grok_stale_minutes": "約 {minutes} 分前",
1850
1877
  "grok_stale_hours": "約 {hours} 時間前",
1851
1878
  "grok_stale_tooltip": "データは Grok CLI のローカルログから読み取るもので、リアルタイム API ではありません。更新するには最近 grok コマンドを実行してください。",
1852
- "codex_stale_minutes": "約 {minutes} 分前",
1853
- "codex_stale_hours": "約 {hours} 時間前",
1879
+ "codex_stale_minutes": "約 {minutes} 分前に更新",
1880
+ "codex_stale_hours": "約 {hours} 時間前に更新",
1854
1881
  "codex_stale_tooltip": "Claude はリアルタイムに報告するので正確です。Codex にはそれがないため、usage は Codex がローカルに時々残す記録を読み取り、実際より少し古い場合があります。ターミナルで Codex を使い続けると自動的に更新されます。",
1855
1882
  "history_load_error_file": "使用履歴ファイルを読み込めません",
1856
1883
  "history_load_error_parse": "使用履歴データの形式に問題があります",
@@ -1862,7 +1889,7 @@
1862
1889
  "project_range_7d": "7日間",
1863
1890
  "project_range_30d": "月",
1864
1891
  "project_range_all": "全期間",
1865
- "session_label": "セッション",
1892
+ "session_label": "5時間",
1866
1893
  "weekly_label": "週間",
1867
1894
  "monthly_label": "月間",
1868
1895
  "projects_empty": "記録なし",
@@ -1975,12 +2002,12 @@
1975
2002
  "report_wrapped_books_hint": "繰り返しのキャッシュ読み込みを含むため、実際に生成した文章量とは異なります。",
1976
2003
  "report_kpi_active_value": "{active} / {total} 日",
1977
2004
  "report_kpi_cost": "API 換算",
1978
- "report_kpi_cost_unpriced": "うち {tokens} tokens は公開価格がなく、API 換算に含みません",
2005
+ "report_kpi_cost_unpriced": "うち {tokens} tokens は価格データがなく、API 換算に含みません",
1979
2006
  "report_pricing_section": "API 価格カバー率(トークン数ベース)",
1980
2007
  "report_pricing_priced": "API 相当額に換算可能",
1981
- "report_pricing_unpriced": "公開価格なし",
2008
+ "report_pricing_unpriced": "価格データなし",
1982
2009
  "report_pricing_all_priced": "すべての使用量に公開価格があります。",
1983
- "report_pricing_hint": "API 換算は公開 API 価格表で換算した相当額で、サブスクリプションの請求額ではありません。公開価格のない使用量はAPI 換算に含まれません。 公開価格がないことは無料を意味しません。API 相当額に換算できないだけです。",
2010
+ "report_pricing_hint": "API 換算は公開 API 価格表で換算した相当額で、サブスクリプションの請求額ではありません。価格データのない使用量(公開価格のないモデルや、合計だけが残っている古い記録)は API 換算に含まれません。価格データがないことは無料を意味しません。API 相当額に換算できないだけです。",
1984
2011
  "report_kpi_peak_day": "最も使った日",
1985
2012
  "report_kpi_tokens": "使った Tokens",
1986
2013
  "report_kpi_vs_prev_period": "前期比",
@@ -2234,6 +2261,7 @@
2234
2261
  "claude_pane_remaining": "残り",
2235
2262
  "claude_pane_reset_done": "リセット済み",
2236
2263
  "claude_pane_hours_ago": "{count}時間前",
2264
+ "claude_pane_updated_ago": "{ago}に更新",
2237
2265
  "update_check_failed_offline": "GitHub に接続できません。ネット接続を確認して再試行してください。",
2238
2266
  "update_check_failed_rate_limited": "GitHub がリクエストを制限しています。後でもう一度お試しください。",
2239
2267
  "update_check_failed_server": "GitHub でエラーが発生しました。後でもう一度お試しください。",
@@ -2329,13 +2357,19 @@
2329
2357
  "statusline_sync_menu": "상태 표시줄로 사용량 동기화",
2330
2358
  "statusline_sync_tooltip": "Claude Code 상태 표시줄에서 사용량을 읽습니다. 끄면 Claude 사용량이 더 이상 업데이트되지 않습니다.",
2331
2359
  "launch_at_login": "로그인 시 시작",
2360
+ "launch_at_login_failed": "로그인 시 시작을 설정할 수 없습니다: {error}",
2332
2361
  "reset_panel_position": "패널 위치 재설정",
2333
2362
  "hide_sections_menu": "섹션 숨기기",
2334
2363
  "tray_provider_menu": "트레이 표시 소스",
2335
2364
  "quota_label_menu": "작업 표시줄 할당량 표시",
2336
2365
  "quota_label_format": "{provider}: {value}%",
2337
2366
  "quota_label_unknown": "{provider}: --",
2367
+ "quota_label_countdown": "{provider}: {value}",
2338
2368
  "quota_notifications_menu": "사용량 알림",
2369
+ "quota_notifications_toggle": "알림 켜기",
2370
+ "quota_notifications_permission_denied": "시스템에서 usage 알림이 꺼져 있습니다 — 시스템 설정 열기…",
2371
+ "quota_threshold_header": "알림 기준",
2372
+ "quota_threshold_item": "사용량 {percent}%",
2339
2373
  "window_keeper_menu": "5시간 세션 자동 시작",
2340
2374
  "window_keeper_tooltip": "5시간 할당량이 리셋되면 각 도구에 아주 작은 메시지를 자동 전송합니다 (Claude는 Haiku, agy는 Gemini 3.8 Flash Low, Codex는 가장 저렴한 모델). 다음 5시간 카운트를 바로 시작합니다 (극소량의 할당량을 소모합니다).",
2341
2375
  "window_keeper_sleep_title": "🐾 5시간 세션 자동 시작이 켜졌어요",
@@ -2422,8 +2456,8 @@
2422
2456
  "grok_stale_minutes": "약 {minutes}분 전",
2423
2457
  "grok_stale_hours": "약 {hours}시간 전",
2424
2458
  "grok_stale_tooltip": "데이터는 Grok CLI의 로컬 로그에서 읽으며 실시간 API가 아닙니다. 업데이트하려면 최근에 grok 명령을 실행하세요.",
2425
- "codex_stale_minutes": "약 {minutes}분 전",
2426
- "codex_stale_hours": "약 {hours}시간 전",
2459
+ "codex_stale_minutes": "약 {minutes}분 전 업데이트",
2460
+ "codex_stale_hours": "약 {hours}시간 전 업데이트",
2427
2461
  "codex_stale_tooltip": "Claude는 실시간으로 보고하므로 정확합니다. Codex는 그렇지 않아 usage가 Codex가 로컬에 가끔 남기는 기록을 읽으며, 실제보다 조금 오래된 값일 수 있습니다. 터미널에서 Codex를 계속 실행하면 자동으로 업데이트됩니다.",
2428
2462
  "history_load_error_file": "사용 기록 파일을 읽을 수 없습니다",
2429
2463
  "history_load_error_parse": "사용 기록 형식에 문제가 있습니다",
@@ -2435,7 +2469,7 @@
2435
2469
  "project_range_7d": "7일",
2436
2470
  "project_range_30d": "월",
2437
2471
  "project_range_all": "전체",
2438
- "session_label": "세션",
2472
+ "session_label": "5시간",
2439
2473
  "weekly_label": "주간",
2440
2474
  "monthly_label": "월간",
2441
2475
  "projects_empty": "기록 없음",
@@ -2548,12 +2582,12 @@
2548
2582
  "report_wrapped_books_hint": "반복된 캐시 읽기를 포함하므로 실제로 생성한 글의 양과는 다릅니다.",
2549
2583
  "report_kpi_active_value": "{active} / {total}일",
2550
2584
  "report_kpi_cost": "API 환산",
2551
- "report_kpi_cost_unpriced": "이 중 {tokens} tokens는 공개 가격이 없어 API 환산에서 제외됨",
2585
+ "report_kpi_cost_unpriced": "이 중 {tokens} tokens는 가격 데이터가 없어 API 환산에서 제외됨",
2552
2586
  "report_pricing_section": "API 가격 적용률 (토큰 수 기준)",
2553
2587
  "report_pricing_priced": "API 환산액 계산 가능",
2554
- "report_pricing_unpriced": "공개 가격 없음",
2588
+ "report_pricing_unpriced": "가격 데이터 없음",
2555
2589
  "report_pricing_all_priced": "모든 사용량에 공개 가격이 있습니다.",
2556
- "report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 공개 가격이 없는 사용량은 API 환산에서 제외됩니다. 공개 가격이 없다고 무료인 것은 아닙니다. API 환산액을 계산할 수 없다는 뜻입니다.",
2590
+ "report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 가격 데이터가 없는 사용량(공개 가격이 없는 모델, 합계만 남은 이전 기록)은 API 환산에서 제외됩니다. 가격 데이터가 없다고 무료인 것은 아닙니다. API 환산액을 계산할 수 없다는 뜻입니다.",
2557
2591
  "report_kpi_peak_day": "가장 많이 쓴 날",
2558
2592
  "report_kpi_tokens": "쓴 Tokens",
2559
2593
  "report_kpi_vs_prev_period": "이전 기간 대비",
@@ -2807,6 +2841,7 @@
2807
2841
  "claude_pane_remaining": "남음",
2808
2842
  "claude_pane_reset_done": "리셋됨",
2809
2843
  "claude_pane_hours_ago": "{count}시간 전",
2844
+ "claude_pane_updated_ago": "{ago} 업데이트",
2810
2845
  "update_check_failed_offline": "GitHub에 연결할 수 없어요. 인터넷 연결을 확인하고 다시 시도하세요.",
2811
2846
  "update_check_failed_rate_limited": "GitHub가 요청을 제한하고 있어요. 잠시 후 다시 시도하세요.",
2812
2847
  "update_check_failed_server": "GitHub에서 오류가 발생했어요. 잠시 후 다시 시도하세요.",
@@ -113,14 +113,15 @@ def _launchctl_bootstrap() -> None:
113
113
  )
114
114
  except FileNotFoundError as exc:
115
115
  _log_launchctl_error("bootstrap", str(exc))
116
- return
116
+ raise
117
117
  except subprocess.TimeoutExpired as exc:
118
118
  _log_launchctl_error("bootstrap", f"timed out after {exc.timeout} seconds")
119
- return
119
+ raise
120
120
 
121
121
  if result.returncode not in (0, 17):
122
122
  stderr = _stderr_summary(result.stderr)
123
123
  _log_launchctl_error("bootstrap", f"returncode={result.returncode} stderr={stderr!r}")
124
+ raise RuntimeError(stderr or f"launchctl: {result.returncode}")
124
125
 
125
126
 
126
127
  def enable() -> None:
@@ -60,7 +60,7 @@ CODEX_CONFIG = setup_hook.CODEX_CONFIG
60
60
  # session. Off by default: enabled only via the menu toggle, never by self_heal.
61
61
  RESUME_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-session-resume.py"))
62
62
  RESUME_PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-resume-prompt.json"))
63
- RESUME_HOOK_VERSION = "1.12"
63
+ RESUME_HOOK_VERSION = "1.13"
64
64
  RESUME_MATCHER = "startup|clear|resume"
65
65
  RESUME_LANGS = ("zh-TW", "zh-CN", "en", "ja", "ko")
66
66
  _RESUME_MARKER = "usage-session-resume"
@@ -95,7 +95,7 @@ _TERSE_REMINDER_MARKER = "usage-terse-reminder"
95
95
  _TERSE_REMINDER_MARKERS = (_TERSE_REMINDER_MARKER, "usage_terse_reminder")
96
96
  # Independent, opt-in Claude Code UserPromptSubmit hook.
97
97
  QUOTA_AWARE_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-quota-aware.py"))
98
- QUOTA_AWARE_HOOK_VERSION = "1.3"
98
+ QUOTA_AWARE_HOOK_VERSION = "1.5"
99
99
  QUOTA_AWARE_MATCHER = ""
100
100
  _QUOTA_AWARE_MARKER = "usage-quota-aware"
101
101
  _QUOTA_AWARE_MARKERS = (_QUOTA_AWARE_MARKER, "usage_quota_aware")
@@ -622,7 +622,9 @@ def _self_heal_codex_terse() -> None:
622
622
  script_missing = not CODEX_TERSE_HOOK_TARGET.exists()
623
623
  entry_missing = not _is_codex_terse_installed()
624
624
  old_version = _installed_codex_terse_version()
625
- version_stale = CODEX_TERSE_HOOK_TARGET.exists() and old_version != TERSE_HOOK_VERSION
625
+ version_stale = CODEX_TERSE_HOOK_TARGET.exists() and setup_hook.is_older(
626
+ old_version, TERSE_HOOK_VERSION
627
+ )
626
628
 
627
629
  if not (script_missing or entry_missing or version_stale or not hooks_enabled):
628
630
  return
@@ -859,7 +861,7 @@ def _self_heal_resume() -> None:
859
861
  _copy_resume_script()
860
862
  _write_resume_sidecar()
861
863
  _append_self_heal_log("restore_resume_hook", detail)
862
- elif _installed_resume_version() != RESUME_HOOK_VERSION:
864
+ elif setup_hook.is_older(_installed_resume_version(), RESUME_HOOK_VERSION):
863
865
  old = _installed_resume_version()
864
866
  _copy_resume_script()
865
867
  _write_resume_sidecar()
@@ -896,7 +898,7 @@ def _self_heal_terse_mode() -> None:
896
898
  _append_self_heal_log("restore_terse_hook", f"missing={','.join(missing)}")
897
899
  else:
898
900
  old = _installed_terse_version()
899
- if old != TERSE_HOOK_VERSION:
901
+ if setup_hook.is_older(old, TERSE_HOOK_VERSION):
900
902
  _copy_terse_script()
901
903
  _write_terse_sidecar()
902
904
  _append_self_heal_log(
@@ -926,7 +928,7 @@ def _self_heal_terse_reminder() -> None:
926
928
  _append_self_heal_log("restore_terse_reminder_hook", f"missing={','.join(parts)}")
927
929
  return
928
930
  old = _installed_terse_reminder_version()
929
- if old != TERSE_REMINDER_HOOK_VERSION:
931
+ if setup_hook.is_older(old, TERSE_REMINDER_HOOK_VERSION):
930
932
  _copy_terse_reminder_script()
931
933
  _append_self_heal_log(
932
934
  "update_terse_reminder_hook",
@@ -1149,8 +1151,8 @@ def _debug_self_heal_failure(action: str, exc: BaseException) -> None:
1149
1151
  print(f"usage self-heal {action} failed: {type(exc).__name__}: {exc}", file=sys.stderr)
1150
1152
 
1151
1153
 
1152
- def self_heal() -> None:
1153
- """Best-effort startup repair for usage-owned Claude statusLine hooks."""
1154
+ def _self_heal_statuslines() -> None:
1155
+ """Best-effort startup repair for usage-owned status lines."""
1154
1156
  try:
1155
1157
  settings = _load_settings()
1156
1158
  state = _detect_current_state(settings)
@@ -1261,6 +1263,11 @@ def self_heal() -> None:
1261
1263
  raise
1262
1264
  _debug_self_heal_failure("setup_codex", exc)
1263
1265
 
1266
+
1267
+ def self_heal() -> None:
1268
+ """Repair status lines and independently repair opt-in session hooks."""
1269
+ _self_heal_statuslines()
1270
+
1264
1271
  try:
1265
1272
  _self_heal_resume()
1266
1273
  except BaseException as exc:
@@ -1379,7 +1386,7 @@ def _self_heal_quota_aware() -> None:
1379
1386
  if not is_quota_aware_enabled():
1380
1387
  return
1381
1388
  old = _installed_quota_aware_version()
1382
- if old != QUOTA_AWARE_HOOK_VERSION:
1389
+ if setup_hook.is_older(old, QUOTA_AWARE_HOOK_VERSION):
1383
1390
  _copy_quota_aware_script()
1384
1391
  _append_self_heal_log(
1385
1392
  "update_quota_aware_hook", f"{old or 'unknown'} -> {QUOTA_AWARE_HOOK_VERSION}"
@@ -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.17"
89
+ HOOK_VERSION = "1.18"
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.
@@ -1231,16 +1231,30 @@ def _installed_forwarder_version() -> str | None:
1231
1231
  return None
1232
1232
 
1233
1233
 
1234
+ def is_older(installed: str | None, current: str) -> bool:
1235
+ """True when an installed hook should be replaced: missing, unreadable, or older.
1236
+
1237
+ Never downgrade: an older usage build would otherwise swap back a hook that a newer
1238
+ build installed, and the two would keep replacing each other on every self-heal.
1239
+ """
1240
+ if installed is None:
1241
+ return True
1242
+ try:
1243
+ return tuple(map(int, installed.split("."))) < tuple(map(int, current.split(".")))
1244
+ except ValueError:
1245
+ return True
1246
+
1247
+
1234
1248
  def needs_update() -> bool:
1235
1249
  if not HOOK_TARGET.parent.exists():
1236
1250
  return False
1237
- return _installed_hook_version() != HOOK_VERSION
1251
+ return is_older(_installed_hook_version(), HOOK_VERSION)
1238
1252
 
1239
1253
 
1240
1254
  def forwarder_needs_update() -> bool:
1241
1255
  if not FORWARDER_TARGET.exists():
1242
1256
  return False
1243
- return _installed_forwarder_version() != FORWARDER_VERSION
1257
+ return is_older(_installed_forwarder_version(), FORWARDER_VERSION)
1244
1258
 
1245
1259
 
1246
1260
  def update_hook() -> None:
@@ -110,7 +110,8 @@ def _disable_statusline_settings() -> int:
110
110
  _sync_agy_statusline(False)
111
111
  _sync_grok_statusline(False)
112
112
  settings = _load_claude_settings()
113
- if "statusLine" not in settings:
113
+ # The switch can be on for agy alone; a user's own Claude status line is not ours to stash.
114
+ if "statusLine" not in settings or setup_hook._detect_current_state(settings) == "external":
114
115
  return 0
115
116
  usage_settings = settings.setdefault("usage", {})
116
117
  if not isinstance(usage_settings, dict):
@@ -129,7 +130,9 @@ def _enable_statusline_settings() -> int:
129
130
  _sync_grok_statusline(True)
130
131
  settings = _load_claude_settings()
131
132
  if "statusLine" in settings:
132
- return 0
133
+ if setup_hook._detect_current_state(settings) in {"us-direct", "us-forwarder"}:
134
+ return 0
135
+ return setup_hook.setup()
133
136
  raw_usage_settings = settings.get("usage")
134
137
  usage_settings = raw_usage_settings if isinstance(raw_usage_settings, dict) else None
135
138
  # Older versions parked the switch's statusLine under previousStatusLine.
@@ -169,7 +172,9 @@ def _statusline_enabled() -> bool:
169
172
  settings = _load_claude_settings()
170
173
  except (OSError, ValueError, json.JSONDecodeError):
171
174
  return False
172
- if "statusLine" in settings:
175
+ from installer import setup_hook
176
+
177
+ if setup_hook._detect_current_state(settings) in {"us-direct", "us-forwarder"}:
173
178
  return True
174
179
  if sys.platform not in {"darwin", "win32"}:
175
180
  return False