usage-cli 0.34.13__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.13/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.13 → usage_cli-0.34.14}/analyzer/reporter.py +11 -3
  4. {usage_cli-0.34.13 → usage_cli-0.34.14}/i18n.json +20 -15
  5. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/agy_quota_probe.py +18 -0
  6. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/app.py +4 -1
  7. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/notify.py +5 -0
  8. usage_cli-0.34.14/menubar/quota_countdown.py +34 -0
  9. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/title.py +3 -23
  10. {usage_cli-0.34.13 → usage_cli-0.34.14}/pyproject.toml +1 -1
  11. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/agy_window_keeper.py +5 -0
  12. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/notifications.py +9 -3
  13. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_agy_quota_probe.py +92 -0
  14. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_agy_window_keeper.py +14 -0
  15. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_check_upstream.py +1 -0
  16. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_html_report_snapshot.py +3 -1
  17. usage_cli-0.34.14/tests/test_ledger_legacy_backfill.py +216 -0
  18. usage_cli-0.34.14/tests/test_network.py +41 -0
  19. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_quota_threshold_menu.py +2 -2
  20. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_daily_chart.py +21 -0
  21. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_notifications.py +31 -0
  22. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_wintray.py +92 -2
  23. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/html_report.py +10 -4
  24. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_daily_chart.py +6 -4
  25. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_filter.py +1 -1
  26. {usage_cli-0.34.13 → usage_cli-0.34.14/usage_cli.egg-info}/PKG-INFO +1 -1
  27. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_cli.egg-info/SOURCES.txt +5 -0
  28. usage_cli-0.34.14/usage_common/network.py +29 -0
  29. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/app.py +20 -3
  30. {usage_cli-0.34.13 → usage_cli-0.34.14}/LICENSE +0 -0
  31. {usage_cli-0.34.13 → usage_cli-0.34.14}/README.md +0 -0
  32. {usage_cli-0.34.13 → usage_cli-0.34.14}/THIRD_PARTY_NOTICES.md +0 -0
  33. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/__init__.py +0 -0
  34. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/agy.py +0 -0
  35. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/claude.py +0 -0
  36. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/codex.py +0 -0
  37. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/grok.py +0 -0
  38. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/muse.py +0 -0
  39. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/rate_limits.py +0 -0
  40. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/registry.py +0 -0
  41. {usage_cli-0.34.13 → usage_cli-0.34.14}/adapters/types.py +0 -0
  42. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/__init__.py +0 -0
  43. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/aggregator.py +0 -0
  44. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/blocks.py +0 -0
  45. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/diagnoser.py +0 -0
  46. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/diagnosis_snapshot.py +0 -0
  47. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/dispatch_ledger.py +0 -0
  48. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/history_persist.py +0 -0
  49. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/insights.py +0 -0
  50. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/ledger_backfill.py +0 -0
  51. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/persona_loader.py +0 -0
  52. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/subscription.py +0 -0
  53. {usage_cli-0.34.13 → usage_cli-0.34.14}/analyzer/usage_snapshot.py +0 -0
  54. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/__init__.py +0 -0
  55. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/claude_pane.py +0 -0
  56. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/codex_hook_trust.py +0 -0
  57. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/login_item.py +0 -0
  58. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/session_hooks.py +0 -0
  59. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/setup_hook.py +0 -0
  60. {usage_cli-0.34.13 → usage_cli-0.34.14}/installer/statusline_settings.py +0 -0
  61. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/__init__.py +0 -0
  62. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/_shared_cache_file.py +0 -0
  63. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/agy_disk_cache.py +0 -0
  64. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/agy_loader.py +0 -0
  65. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/cache_quarantine.py +0 -0
  66. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/chromium_cache.py +0 -0
  67. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/claude_desktop.py +0 -0
  68. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/claude_paths.py +0 -0
  69. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/claude_usage.py +0 -0
  70. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/codex_disk_cache.py +0 -0
  71. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/codex_events.py +0 -0
  72. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/codex_fork_replay.py +0 -0
  73. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/codex_loader.py +0 -0
  74. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/codex_paths.py +0 -0
  75. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/disk_cache_common.py +0 -0
  76. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/disk_cache_lifecycle.py +0 -0
  77. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/grok_loader.py +0 -0
  78. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/grok_quota_probe.py +0 -0
  79. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/history_disk_cache.py +0 -0
  80. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/history_loader.py +0 -0
  81. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/jsonl_limits.py +0 -0
  82. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/jsonl_utils.py +0 -0
  83. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/muse_loader.py +0 -0
  84. {usage_cli-0.34.13 → usage_cli-0.34.14}/loaders/muse_quota_probe.py +0 -0
  85. {usage_cli-0.34.13 → usage_cli-0.34.14}/main.py +0 -0
  86. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/__init__.py +0 -0
  87. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/actions.py +0 -0
  88. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/agy.py +0 -0
  89. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/chrome.py +0 -0
  90. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/fsevents_watch.py +0 -0
  91. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/grok.py +0 -0
  92. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/manual_refresh.py +0 -0
  93. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/menu.py +0 -0
  94. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/muse.py +0 -0
  95. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/new_badge.py +0 -0
  96. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/popover.py +0 -0
  97. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/prefs.py +0 -0
  98. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/refresh.py +0 -0
  99. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/state.py +0 -0
  100. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/update.py +0 -0
  101. {usage_cli-0.34.13 → usage_cli-0.34.14}/menubar/whats_new.py +0 -0
  102. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/__init__.py +0 -0
  103. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/base.py +0 -0
  104. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/catalog.py +0 -0
  105. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/dynamic_height.py +0 -0
  106. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/panel_scale.py +0 -0
  107. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/panel_window.py +0 -0
  108. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/panel_window_state.py +0 -0
  109. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/payload.py +0 -0
  110. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/web_panel.py +0 -0
  111. {usage_cli-0.34.13 → usage_cli-0.34.14}/panels/window_drag.py +0 -0
  112. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/__init__.py +0 -0
  113. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/burn_rate.py +0 -0
  114. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/codex_window_keeper.py +0 -0
  115. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/keeper_outcome.py +0 -0
  116. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/quota_snapshot.py +0 -0
  117. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/status_payload.py +0 -0
  118. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/usage_rate.py +0 -0
  119. {usage_cli-0.34.13 → usage_cli-0.34.14}/quota/window_keeper.py +0 -0
  120. {usage_cli-0.34.13 → usage_cli-0.34.14}/setup.cfg +0 -0
  121. {usage_cli-0.34.13 → usage_cli-0.34.14}/setup_app.py +0 -0
  122. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_adapters_claude.py +0 -0
  123. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_adapters_codex.py +0 -0
  124. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_adapters_rate_limits.py +0 -0
  125. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_aggregator.py +0 -0
  126. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_agy_adapter.py +0 -0
  127. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_agy_disk_cache.py +0 -0
  128. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_agy_loader.py +0 -0
  129. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_analyzer_blocks.py +0 -0
  130. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_analyzer_pipeline.py +0 -0
  131. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_app_version.py +0 -0
  132. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_burn_rate.py +0 -0
  133. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_cache_quarantine.py +0 -0
  134. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_check_release_version.py +0 -0
  135. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_chromium_cache.py +0 -0
  136. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_claude_desktop.py +0 -0
  137. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_claude_expired_windows.py +0 -0
  138. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_claude_pane.py +0 -0
  139. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_claude_paths.py +0 -0
  140. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_cli_lookup.py +0 -0
  141. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_codex_consistency.py +0 -0
  142. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_codex_hook_trust.py +0 -0
  143. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_codex_loader.py +0 -0
  144. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_codex_window_keeper.py +0 -0
  145. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_compact_cards.py +0 -0
  146. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_desktop_fresh_cache.py +0 -0
  147. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_desktop_quota_display.py +0 -0
  148. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_desktop_quota_review.py +0 -0
  149. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_diagnoser.py +0 -0
  150. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_disk_cache_shards.py +0 -0
  151. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_dispatch_ledger.py +0 -0
  152. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_doctor.py +0 -0
  153. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_dynamic_height.py +0 -0
  154. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_fsevents_watch.py +0 -0
  155. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_grok_loader.py +0 -0
  156. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_grok_quota_probe.py +0 -0
  157. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_history_loader.py +0 -0
  158. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_history_persist.py +0 -0
  159. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_hook_scripts_are_standalone.py +0 -0
  160. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_i18n_format_fallback.py +0 -0
  161. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_i18n_key_parity.py +0 -0
  162. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_i18n_packaged_path.py +0 -0
  163. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_insights.py +0 -0
  164. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_jsonl_utils.py +0 -0
  165. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_keeper_menu.py +0 -0
  166. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_keeper_outcome.py +0 -0
  167. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_ledger_backfill.py +0 -0
  168. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_login_item.py +0 -0
  169. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_main.py +0 -0
  170. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_manual_refresh.py +0 -0
  171. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar.py +0 -0
  172. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_agy.py +0 -0
  173. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_grok.py +0 -0
  174. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_muse.py +0 -0
  175. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_notify.py +0 -0
  176. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_permission.py +0 -0
  177. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_prefs.py +0 -0
  178. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_state.py +0 -0
  179. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_menubar_update.py +0 -0
  180. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_muse_coverage.py +0 -0
  181. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_muse_loader.py +0 -0
  182. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_muse_quota_probe.py +0 -0
  183. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_muse_review_regressions.py +0 -0
  184. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_new_badge.py +0 -0
  185. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_packaged_resources.py +0 -0
  186. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_packaging.py +0 -0
  187. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_panel_footer_layout.py +0 -0
  188. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_panel_scale.py +0 -0
  189. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_panel_window_state.py +0 -0
  190. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_panels.py +0 -0
  191. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_panels_lazy_import.py +0 -0
  192. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_persona_loader.py +0 -0
  193. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_pricing.py +0 -0
  194. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_project_resolver.py +0 -0
  195. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_quota_aware.py +0 -0
  196. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_quota_label.py +0 -0
  197. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_chart_redesign.py +0 -0
  198. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_cold_read.py +0 -0
  199. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_filter_js.py +0 -0
  200. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_headings.py +0 -0
  201. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_report_share_mask.py +0 -0
  202. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_reporter_build_data.py +0 -0
  203. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_service_status.py +0 -0
  204. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_session_resume.py +0 -0
  205. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_setup_app.py +0 -0
  206. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_setup_hook.py +0 -0
  207. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_setup_hook_coexistence.py +0 -0
  208. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_setup_hook_resume.py +0 -0
  209. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_setup_hook_terse.py +0 -0
  210. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_subprocess_utils.py +0 -0
  211. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_subscription.py +0 -0
  212. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_taskbar_menu.py +0 -0
  213. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_taskbar_obstacles.py +0 -0
  214. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_time_utils.py +0 -0
  215. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_tui.py +0 -0
  216. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_tui_sprite.py +0 -0
  217. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_ui_tables.py +0 -0
  218. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_update_checker.py +0 -0
  219. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_update_gate.py +0 -0
  220. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_update_release_notes.py +0 -0
  221. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_cli.py +0 -0
  222. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_client.py +0 -0
  223. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_diagnosis_snapshot.py +0 -0
  224. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_dir_sweeper.py +0 -0
  225. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_lang.py +0 -0
  226. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_logging.py +0 -0
  227. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_rate.py +0 -0
  228. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_snapshot.py +0 -0
  229. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_snapshot_replay.py +0 -0
  230. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_statusline.py +0 -0
  231. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_statusline_agy.py +0 -0
  232. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_statusline_forwarder.py +0 -0
  233. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_statusline_grok.py +0 -0
  234. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_terse_mode.py +0 -0
  235. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_usage_terse_reminder.py +0 -0
  236. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_web_panel_payload.py +0 -0
  237. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_whats_new.py +0 -0
  238. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_wheel_packaging.py +0 -0
  239. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_win_login_item.py +0 -0
  240. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_window_keeper.py +0 -0
  241. {usage_cli-0.34.13 → usage_cli-0.34.14}/tests/test_windows_manifest.py +0 -0
  242. {usage_cli-0.34.13 → usage_cli-0.34.14}/tui/__init__.py +0 -0
  243. {usage_cli-0.34.13 → usage_cli-0.34.14}/tui/app.py +0 -0
  244. {usage_cli-0.34.13 → usage_cli-0.34.14}/tui/sprite.py +0 -0
  245. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/__init__.py +0 -0
  246. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_charts.py +0 -0
  247. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_ledger.py +0 -0
  248. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_scripts.py +0 -0
  249. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/report_styles.py +0 -0
  250. {usage_cli-0.34.13 → usage_cli-0.34.14}/ui/tables.py +0 -0
  251. {usage_cli-0.34.13 → usage_cli-0.34.14}/updates/__init__.py +0 -0
  252. {usage_cli-0.34.13 → usage_cli-0.34.14}/updates/checker.py +0 -0
  253. {usage_cli-0.34.13 → usage_cli-0.34.14}/updates/gate.py +0 -0
  254. {usage_cli-0.34.13 → usage_cli-0.34.14}/updates/release_notes.py +0 -0
  255. {usage_cli-0.34.13 → usage_cli-0.34.14}/updates/whats_new.py +0 -0
  256. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_app/__init__.py +0 -0
  257. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_app/cli.py +0 -0
  258. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_app/doctor.py +0 -0
  259. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_cli.egg-info/dependency_links.txt +0 -0
  260. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_cli.egg-info/entry_points.txt +0 -0
  261. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_cli.egg-info/requires.txt +0 -0
  262. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_cli.egg-info/top_level.txt +0 -0
  263. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/__init__.py +0 -0
  264. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/app_version.py +0 -0
  265. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/cli_lookup.py +0 -0
  266. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/i18n.py +0 -0
  267. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/prefs.py +0 -0
  268. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/pricing.py +0 -0
  269. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/project_resolver.py +0 -0
  270. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/service_status.py +0 -0
  271. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/subprocess_utils.py +0 -0
  272. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/time_utils.py +0 -0
  273. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/usage_dir_sweeper.py +0 -0
  274. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/usage_lang.py +0 -0
  275. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_common/usage_logging.py +0 -0
  276. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/__init__.py +0 -0
  277. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_quota_aware.py +0 -0
  278. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_session_resume.py +0 -0
  279. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_statusline.py +0 -0
  280. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_statusline_agy.py +0 -0
  281. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_statusline_forwarder.py +0 -0
  282. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_statusline_grok.py +0 -0
  283. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_terse_mode.py +0 -0
  284. {usage_cli-0.34.13 → usage_cli-0.34.14}/usage_hooks/usage_terse_reminder.py +0 -0
  285. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/__init__.py +0 -0
  286. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/login_item.py +0 -0
  287. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/menu.py +0 -0
  288. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/prefs.py +0 -0
  289. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/quota_label.py +0 -0
  290. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/taskbar_menu.py +0 -0
  291. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/taskbar_obstacles.py +0 -0
  292. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/taskbar_overlay.py +0 -0
  293. {usage_cli-0.34.13 → usage_cli-0.34.14}/wintray/watch.py +0 -0
  294. {usage_cli-0.34.13 → 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.13
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)
@@ -44,6 +44,7 @@
44
44
  "quota_label_menu": "顯示工作列額度",
45
45
  "quota_label_format": "{provider}: {value}%",
46
46
  "quota_label_unknown": "{provider}: --",
47
+ "quota_label_countdown": "{provider}: {value}",
47
48
  "quota_notifications_menu": "用量提醒通知",
48
49
  "quota_notifications_toggle": "開啟通知",
49
50
  "quota_notifications_permission_denied": "系統已關閉 usage 的通知 — 打開系統設定…",
@@ -261,12 +262,12 @@
261
262
  "report_wrapped_books_hint": "含重複的快取讀取,不等於實際產出的文字量。",
262
263
  "report_kpi_active_value": "{active} / {total} 天",
263
264
  "report_kpi_cost": "API 等值",
264
- "report_kpi_cost_unpriced": "其中 {tokens} tokens 無公開價格,未計入",
265
+ "report_kpi_cost_unpriced": "其中 {tokens} tokens 沒有價格資料,未計入",
265
266
  "report_pricing_section": "API 價格覆蓋率(依 token 數)",
266
267
  "report_pricing_priced": "可換算 API 等值",
267
- "report_pricing_unpriced": "無公開價格",
268
+ "report_pricing_unpriced": "沒有價格資料",
268
269
  "report_pricing_all_priced": "全部用量都有公開價格。",
269
- "report_pricing_hint": "API 等值是用公開 API 價目表換算的等值金額,不是你的訂閱帳單;無公開價格的用量不計入 API 等值。 沒有公開價格不代表免費,只是無法換算。",
270
+ "report_pricing_hint": "API 等值是用公開 API 價目表換算的等值金額,不是你的訂閱帳單;沒有價格資料的用量不計入 API 等值,包括沒有公開價格的模型,以及只存了總量的舊紀錄。沒有價格資料不代表免費,只是無法換算。",
270
271
  "report_kpi_peak_day": "燒最兇的一天",
271
272
  "report_kpi_tokens": "燒掉 Tokens",
272
273
  "report_kpi_vs_prev_period": "較前期",
@@ -623,6 +624,7 @@
623
624
  "quota_label_menu": "Show Taskbar Quota",
624
625
  "quota_label_format": "{provider}: {value}%",
625
626
  "quota_label_unknown": "{provider}: --",
627
+ "quota_label_countdown": "{provider}: {value}",
626
628
  "quota_notifications_menu": "Usage Alert Notifications",
627
629
  "quota_notifications_toggle": "Enable notifications",
628
630
  "quota_notifications_permission_denied": "System notifications for usage are off — Open System Settings…",
@@ -840,12 +842,12 @@
840
842
  "report_wrapped_books_hint": "Includes repeated cache reads; this is not the amount of text actually generated.",
841
843
  "report_kpi_active_value": "{active} / {total} days",
842
844
  "report_kpi_cost": "API equivalent",
843
- "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",
844
846
  "report_pricing_section": "API pricing coverage (by token count)",
845
847
  "report_pricing_priced": "Convertible to API equivalent",
846
- "report_pricing_unpriced": "No public price",
848
+ "report_pricing_unpriced": "No pricing data",
847
849
  "report_pricing_all_priced": "All usage has a public price.",
848
- "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.",
849
851
  "report_kpi_peak_day": "Peak Day",
850
852
  "report_kpi_tokens": "Burned Tokens",
851
853
  "report_kpi_vs_prev_period": "vs previous period",
@@ -1202,6 +1204,7 @@
1202
1204
  "quota_label_menu": "显示任务栏额度",
1203
1205
  "quota_label_format": "{provider}: {value}%",
1204
1206
  "quota_label_unknown": "{provider}: --",
1207
+ "quota_label_countdown": "{provider}: {value}",
1205
1208
  "quota_notifications_menu": "用量提醒通知",
1206
1209
  "quota_notifications_toggle": "开启通知",
1207
1210
  "quota_notifications_permission_denied": "系统已关闭 usage 的通知 — 打开系统设置…",
@@ -1419,12 +1422,12 @@
1419
1422
  "report_wrapped_books_hint": "含重复的缓存读取,不等于实际生成的文字量。",
1420
1423
  "report_kpi_active_value": "{active} / {total} 天",
1421
1424
  "report_kpi_cost": "API 等值",
1422
- "report_kpi_cost_unpriced": "其中 {tokens} tokens 无公开价格,未计入",
1425
+ "report_kpi_cost_unpriced": "其中 {tokens} tokens 没有价格数据,未计入",
1423
1426
  "report_pricing_section": "API 价格覆盖率(按 token 数)",
1424
1427
  "report_pricing_priced": "可换算 API 等值",
1425
- "report_pricing_unpriced": "无公开价格",
1428
+ "report_pricing_unpriced": "没有价格数据",
1426
1429
  "report_pricing_all_priced": "全部用量都有公开价格。",
1427
- "report_pricing_hint": "API 等值是用公开 API 价目表换算的等值金额,不是你的订阅账单;无公开价格的用量不计入 API 等值。 没有公开价格不代表免费,只是无法换算。",
1430
+ "report_pricing_hint": "API 等值是用公开 API 价目表换算的等值金额,不是你的订阅账单;没有价格数据的用量不计入 API 等值,包括没有公开价格的模型,以及只存了总量的旧记录。没有价格数据不代表免费,只是无法换算。",
1428
1431
  "report_kpi_peak_day": "烧最猛的一天",
1429
1432
  "report_kpi_tokens": "烧掉 Tokens",
1430
1433
  "report_kpi_vs_prev_period": "较前期",
@@ -1781,6 +1784,7 @@
1781
1784
  "quota_label_menu": "タスクバーにクォータを表示",
1782
1785
  "quota_label_format": "{provider}: {value}%",
1783
1786
  "quota_label_unknown": "{provider}: --",
1787
+ "quota_label_countdown": "{provider}: {value}",
1784
1788
  "quota_notifications_menu": "使用量アラート通知",
1785
1789
  "quota_notifications_toggle": "通知を有効にする",
1786
1790
  "quota_notifications_permission_denied": "usage の通知はシステムで無効です — システム設定を開く…",
@@ -1998,12 +2002,12 @@
1998
2002
  "report_wrapped_books_hint": "繰り返しのキャッシュ読み込みを含むため、実際に生成した文章量とは異なります。",
1999
2003
  "report_kpi_active_value": "{active} / {total} 日",
2000
2004
  "report_kpi_cost": "API 換算",
2001
- "report_kpi_cost_unpriced": "うち {tokens} tokens は公開価格がなく、API 換算に含みません",
2005
+ "report_kpi_cost_unpriced": "うち {tokens} tokens は価格データがなく、API 換算に含みません",
2002
2006
  "report_pricing_section": "API 価格カバー率(トークン数ベース)",
2003
2007
  "report_pricing_priced": "API 相当額に換算可能",
2004
- "report_pricing_unpriced": "公開価格なし",
2008
+ "report_pricing_unpriced": "価格データなし",
2005
2009
  "report_pricing_all_priced": "すべての使用量に公開価格があります。",
2006
- "report_pricing_hint": "API 換算は公開 API 価格表で換算した相当額で、サブスクリプションの請求額ではありません。公開価格のない使用量はAPI 換算に含まれません。 公開価格がないことは無料を意味しません。API 相当額に換算できないだけです。",
2010
+ "report_pricing_hint": "API 換算は公開 API 価格表で換算した相当額で、サブスクリプションの請求額ではありません。価格データのない使用量(公開価格のないモデルや、合計だけが残っている古い記録)は API 換算に含まれません。価格データがないことは無料を意味しません。API 相当額に換算できないだけです。",
2007
2011
  "report_kpi_peak_day": "最も使った日",
2008
2012
  "report_kpi_tokens": "使った Tokens",
2009
2013
  "report_kpi_vs_prev_period": "前期比",
@@ -2360,6 +2364,7 @@
2360
2364
  "quota_label_menu": "작업 표시줄 할당량 표시",
2361
2365
  "quota_label_format": "{provider}: {value}%",
2362
2366
  "quota_label_unknown": "{provider}: --",
2367
+ "quota_label_countdown": "{provider}: {value}",
2363
2368
  "quota_notifications_menu": "사용량 알림",
2364
2369
  "quota_notifications_toggle": "알림 켜기",
2365
2370
  "quota_notifications_permission_denied": "시스템에서 usage 알림이 꺼져 있습니다 — 시스템 설정 열기…",
@@ -2577,12 +2582,12 @@
2577
2582
  "report_wrapped_books_hint": "반복된 캐시 읽기를 포함하므로 실제로 생성한 글의 양과는 다릅니다.",
2578
2583
  "report_kpi_active_value": "{active} / {total}일",
2579
2584
  "report_kpi_cost": "API 환산",
2580
- "report_kpi_cost_unpriced": "이 중 {tokens} tokens는 공개 가격이 없어 API 환산에서 제외됨",
2585
+ "report_kpi_cost_unpriced": "이 중 {tokens} tokens는 가격 데이터가 없어 API 환산에서 제외됨",
2581
2586
  "report_pricing_section": "API 가격 적용률 (토큰 수 기준)",
2582
2587
  "report_pricing_priced": "API 환산액 계산 가능",
2583
- "report_pricing_unpriced": "공개 가격 없음",
2588
+ "report_pricing_unpriced": "가격 데이터 없음",
2584
2589
  "report_pricing_all_priced": "모든 사용량에 공개 가격이 있습니다.",
2585
- "report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 공개 가격이 없는 사용량은 API 환산에서 제외됩니다. 공개 가격이 없다고 무료인 것은 아닙니다. API 환산액을 계산할 수 없다는 뜻입니다.",
2590
+ "report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 가격 데이터가 없는 사용량(공개 가격이 없는 모델, 합계만 남은 이전 기록)은 API 환산에서 제외됩니다. 가격 데이터가 없다고 무료인 것은 아닙니다. API 환산액을 계산할 수 없다는 뜻입니다.",
2586
2591
  "report_kpi_peak_day": "가장 많이 쓴 날",
2587
2592
  "report_kpi_tokens": "쓴 Tokens",
2588
2593
  "report_kpi_vs_prev_period": "이전 기간 대비",
@@ -35,6 +35,7 @@ from urllib.error import HTTPError, URLError
35
35
  from urllib.request import Request, urlopen
36
36
 
37
37
  from usage_common.cli_lookup import cli_env, resolve_cli
38
+ from usage_common.network import google_auth_reachable
38
39
  from usage_common.subprocess_utils import hidden_console_kwargs
39
40
 
40
41
  CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
@@ -79,6 +80,9 @@ _CLIENT_SECRET = "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"
79
80
  _DEFAULT_RETRY_AFTER_SECONDS = 60.0
80
81
  _MAX_RETRY_AFTER_SECONDS = 3600.0
81
82
  _rate_limit_until_monotonic = 0.0
83
+ # A failed launch may open a browser sign-in page; don't repeat it every refresh.
84
+ _CLI_FAILURE_COOLDOWN_SECONDS = 1800.0
85
+ _cli_retry_after_monotonic = 0.0
82
86
  # When each group last appeared in a real API response; in memory only, reset on restart.
83
87
  _group_seen_at: dict[str, datetime] = {}
84
88
  _MISSING_GROUP_GRACE_MINUTES = 30
@@ -157,9 +161,23 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
157
161
  The slash command makes no model call. ``--log-file`` reuses one file instead of
158
162
  leaving a new log in the CLI's log directory on every probe.
159
163
  """
164
+ global _cli_retry_after_monotonic
160
165
  agy = find_agy()
161
166
  if agy is None:
162
167
  return None
168
+ if time.monotonic() < _cli_retry_after_monotonic:
169
+ return None
170
+ # Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page.
171
+ if not google_auth_reachable():
172
+ return None
173
+ result = _run_usage_command(agy, timeout)
174
+ _cli_retry_after_monotonic = (
175
+ 0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
176
+ )
177
+ return result
178
+
179
+
180
+ def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
163
181
  command = [agy, "-p", "/usage", "--output-format", "json", "--log-file", str(_CLI_LOG_PATH)]
164
182
  try:
165
183
  completed = subprocess.run(
@@ -843,7 +843,10 @@ class AppDelegate(NSObject):
843
843
 
844
844
  def _process_quota_notifications(self, state: PopoverState) -> None:
845
845
  try:
846
- events = self._quota_notifier.update(menubar_notify.notification_channels(state))
846
+ events = self._quota_notifier.update(
847
+ menubar_notify.notification_channels(state),
848
+ menubar_notify.notification_scopes(state),
849
+ )
847
850
  for event in events:
848
851
  if _quota_notifications_enabled() and not self.mock:
849
852
  self._send_quota_notification(event, state)
@@ -170,6 +170,11 @@ def notification_channels(state: PopoverState) -> dict[str, tuple[float | None,
170
170
  }
171
171
 
172
172
 
173
+ def notification_scopes(state: PopoverState) -> dict[str, str]:
174
+ """Antigravity rows follow the selected model group; track each group apart."""
175
+ return {"agy_session": state.agy_group_name, "agy_weekly": state.agy_group_name}
176
+
177
+
173
178
  # Keep the instance alive: UNUserNotificationCenter holds its delegate weakly.
174
179
  _notification_delegate: Any = None
175
180
 
@@ -0,0 +1,34 @@
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
+ """Shared quota recovery countdown formatting, without platform UI imports."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ from menubar.state import QuotaRowState
14
+
15
+
16
+ def _format_countdown(seconds: float) -> str:
17
+ minutes = math.ceil(seconds / 60)
18
+ if minutes >= 1440:
19
+ return f"{minutes // 1440}d{minutes % 1440 // 60}h"
20
+ if minutes >= 60:
21
+ return f"{minutes // 60}h{minutes % 60}m"
22
+ return f"{minutes}m"
23
+
24
+
25
+ def _quota_countdown(rows: tuple[QuotaRowState, ...], now: float) -> str | None:
26
+ full = [row for row in rows if row.display_percent is not None and row.display_percent >= 100]
27
+ if not full:
28
+ return None
29
+ resets = []
30
+ for row in full:
31
+ if row.resets_at is None or row.resets_at <= now:
32
+ return None
33
+ resets.append(row.resets_at)
34
+ return _format_countdown(max(resets) - now)
@@ -8,7 +8,6 @@
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import math
12
11
  import time
13
12
  from collections.abc import Callable
14
13
  from typing import Any, Protocol
@@ -29,7 +28,9 @@ from menubar.chrome import (
29
28
  _menubar_icon_attachment_string,
30
29
  _muse_menubar_icon,
31
30
  )
32
- from menubar.state import PopoverState, QuotaRowState, _format_percent
31
+ from menubar.quota_countdown import _format_countdown as _format_countdown
32
+ from menubar.quota_countdown import _quota_countdown
33
+ from menubar.state import PopoverState, _format_percent
33
34
 
34
35
 
35
36
  class _TitleApp(Protocol):
@@ -75,27 +76,6 @@ _MENUBAR_MAX_ITEMS = 3
75
76
  SLOW_POLL_INTERVAL_S = 300.0
76
77
 
77
78
 
78
- def _format_countdown(seconds: float) -> str:
79
- minutes = math.ceil(seconds / 60)
80
- if minutes >= 1440:
81
- return f"{minutes // 1440}d{minutes % 1440 // 60}h"
82
- if minutes >= 60:
83
- return f"{minutes // 60}h{minutes % 60}m"
84
- return f"{minutes}m"
85
-
86
-
87
- def _quota_countdown(rows: tuple[QuotaRowState, ...], now: float) -> str | None:
88
- full = [row for row in rows if row.display_percent is not None and row.display_percent >= 100]
89
- if not full:
90
- return None
91
- resets = []
92
- for row in full:
93
- if row.resets_at is None or row.resets_at <= now:
94
- return None
95
- resets.append(row.resets_at)
96
- return _format_countdown(max(resets) - now)
97
-
98
-
99
79
  def menubar_poll_interval(app: _TitleApp, state: PopoverState) -> float:
100
80
  if app._panel_window_is_visible() or any(
101
81
  text != "--" and not text.endswith("%") for _icon, text in _menubar_items(app, state)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.34.13"
3
+ version = "0.34.14"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -19,6 +19,7 @@ from menubar.agy import AgyRefreshResult
19
19
  from menubar.prefs import _agy_window_keeper_enabled
20
20
  from quota import keeper_outcome
21
21
  from usage_common.cli_lookup import cli_env, resolve_cli
22
+ from usage_common.network import google_auth_reachable
22
23
  from usage_common.subprocess_utils import hidden_console_kwargs
23
24
 
24
25
  logger = logging.getLogger(__name__)
@@ -137,6 +138,10 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
137
138
  retry = not fresh and keeper_outcome.should_retry(state, now, None, gate(None))
138
139
  if not (fresh or retry):
139
140
  return
141
+ # Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page;
142
+ # leave the state untouched so the next refresh tries again.
143
+ if not google_auth_reachable():
144
+ return
140
145
  if not _try_acquire():
141
146
  return
142
147
  keeper_outcome.update_state(
@@ -45,7 +45,10 @@ class QuotaNotifier:
45
45
  def __init__(self, thresholds: list[float] | None = None) -> None:
46
46
  values = [90.0] if thresholds is None else thresholds
47
47
  self.set_thresholds(values)
48
- self._channels = {channel: _ChannelState() for channel in VALID_CHANNELS}
48
+ # Keyed by (channel, scope): one channel can show different quota pools,
49
+ # e.g. Antigravity's Gemini and Argon groups. Switching the shown pool
50
+ # must not read as a reset of the other one.
51
+ self._channels: dict[tuple[str, str], _ChannelState] = {}
49
52
 
50
53
  def set_thresholds(self, values: list[float]) -> None:
51
54
  """Replace thresholds without resetting channel observations or warning latches."""
@@ -54,21 +57,24 @@ class QuotaNotifier:
54
57
  def update(
55
58
  self,
56
59
  channels: dict[str, tuple[float | None, bool]],
60
+ scopes: dict[str, str] | None = None,
57
61
  ) -> list[NotificationEvent]:
58
62
  events: list[NotificationEvent] = []
59
63
  for channel, (percent, available) in channels.items():
60
64
  if channel not in VALID_CHANNELS:
61
65
  continue
62
- events.extend(self._update_channel(channel, percent, available))
66
+ scope = (scopes or {}).get(channel, "")
67
+ events.extend(self._update_channel(channel, scope, percent, available))
63
68
  return events
64
69
 
65
70
  def _update_channel(
66
71
  self,
67
72
  channel: str,
73
+ scope: str,
68
74
  percent: float | None,
69
75
  available: bool,
70
76
  ) -> list[NotificationEvent]:
71
- state = self._channels[channel]
77
+ state = self._channels.setdefault((channel, scope), _ChannelState())
72
78
  events: list[NotificationEvent] = []
73
79
  if not available:
74
80
  return events
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import platform
9
9
  import shutil
10
+ import subprocess
10
11
  import sys
11
12
  import time
12
13
  from base64 import b64encode
@@ -108,11 +109,13 @@ def _write_token(
108
109
  def _reset_token_cache(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
109
110
  agy_quota_probe._token_cache.clear()
110
111
  agy_quota_probe._rate_limit_until_monotonic = 0.0
112
+ agy_quota_probe._cli_retry_after_monotonic = 0.0
111
113
  agy_quota_probe._group_seen_at.clear()
112
114
  monkeypatch.setattr(agy_quota_probe, "_read_macos_credential", lambda: None)
113
115
  yield
114
116
  agy_quota_probe._token_cache.clear()
115
117
  agy_quota_probe._rate_limit_until_monotonic = 0.0
118
+ agy_quota_probe._cli_retry_after_monotonic = 0.0
116
119
  agy_quota_probe._group_seen_at.clear()
117
120
 
118
121
 
@@ -1002,10 +1005,97 @@ def _fake_cli(
1002
1005
  return type("Completed", (), {"returncode": returncode, "stdout": stdout})()
1003
1006
 
1004
1007
  monkeypatch.setattr(agy_quota_probe, "find_agy", lambda: "/bin/agy")
1008
+ monkeypatch.setattr(agy_quota_probe, "google_auth_reachable", lambda: True)
1005
1009
  monkeypatch.setattr("loaders.agy_quota_probe.subprocess.run", run)
1006
1010
  return calls
1007
1011
 
1008
1012
 
1013
+ def test_cli_probe_is_skipped_while_offline(monkeypatch: pytest.MonkeyPatch) -> None:
1014
+ # Offline, the CLI treats a failed token refresh as "not logged in" and opens a
1015
+ # browser sign-in page, so the probe must not launch it at all.
1016
+ calls = _fake_cli(monkeypatch, _usage_command_output(datetime.now(UTC)))
1017
+ monkeypatch.setattr(agy_quota_probe, "google_auth_reachable", lambda: False)
1018
+
1019
+ assert _real_probe_via_cli(15) is None
1020
+ assert calls == []
1021
+ assert agy_quota_probe._cli_retry_after_monotonic == 0.0
1022
+
1023
+
1024
+ def test_cli_probe_still_runs_when_auth_expired(monkeypatch: pytest.MonkeyPatch) -> None:
1025
+ # The marker can outlive a re-sign-in (e.g. a stale legacy token on Windows), so it
1026
+ # must not veto the CLI; only the failure cooldown limits repeat launches.
1027
+ calls = _fake_cli(monkeypatch, _usage_command_output(datetime.now(UTC)))
1028
+ agy_quota_probe.AUTH_EXPIRED_PATH.touch()
1029
+
1030
+ assert _real_probe_via_cli(15) is not None
1031
+ assert len(calls) == 1
1032
+
1033
+
1034
+ def test_cli_failure_cooldown_and_success_reset(monkeypatch: pytest.MonkeyPatch) -> None:
1035
+ monkeypatch.setattr(time, "monotonic", lambda: 100.0)
1036
+ calls = _fake_cli(monkeypatch, b"{}", returncode=1)
1037
+ assert _real_probe_via_cli(15) is None
1038
+ assert agy_quota_probe._cli_retry_after_monotonic == 1900.0
1039
+ with monkeypatch.context() as patch:
1040
+ patch.setattr(time, "monotonic", lambda: 1899.0)
1041
+ patch.setattr(
1042
+ agy_quota_probe, "google_auth_reachable", lambda: pytest.fail("must not probe network")
1043
+ )
1044
+ assert _real_probe_via_cli(15) is None
1045
+ assert len(calls) == 1
1046
+
1047
+ monkeypatch.setattr(time, "monotonic", lambda: 1901.0)
1048
+ assert _real_probe_via_cli(15) is None
1049
+ assert len(calls) == 2
1050
+ assert agy_quota_probe._cli_retry_after_monotonic == 3701.0
1051
+
1052
+ monkeypatch.setattr(time, "monotonic", lambda: 3702.0)
1053
+ calls = _fake_cli(monkeypatch, _usage_command_output(datetime.now(UTC)))
1054
+ assert _real_probe_via_cli(15) is not None
1055
+ assert agy_quota_probe._cli_retry_after_monotonic == 0.0
1056
+ assert _real_probe_via_cli(15) is not None
1057
+ assert len(calls) == 2
1058
+
1059
+ calls = _fake_cli(monkeypatch, b"{}", returncode=1)
1060
+ assert _real_probe_via_cli(15) is None
1061
+ assert agy_quota_probe._cli_retry_after_monotonic == 5502.0
1062
+ assert _real_probe_via_cli(15) is None
1063
+ assert len(calls) == 1
1064
+
1065
+
1066
+ def test_missing_cli_does_not_start_cooldown(monkeypatch: pytest.MonkeyPatch) -> None:
1067
+ calls = _fake_cli(monkeypatch, b"{}")
1068
+ monkeypatch.setattr(agy_quota_probe, "find_agy", lambda: None)
1069
+
1070
+ assert _real_probe_via_cli(15) is None
1071
+ assert calls == []
1072
+ assert agy_quota_probe._cli_retry_after_monotonic == 0.0
1073
+
1074
+
1075
+ @pytest.mark.parametrize("error", [OSError("failed"), subprocess.TimeoutExpired("agy", 15)])
1076
+ def test_cli_launch_error_starts_cooldown(
1077
+ monkeypatch: pytest.MonkeyPatch, error: Exception
1078
+ ) -> None:
1079
+ _fake_cli(monkeypatch, b"{}")
1080
+ monkeypatch.setattr(time, "monotonic", lambda: 100.0)
1081
+
1082
+ def fail(*args: object, **kwargs: object) -> None:
1083
+ raise error
1084
+
1085
+ monkeypatch.setattr(subprocess, "run", fail)
1086
+ assert _real_probe_via_cli(15) is None
1087
+ assert agy_quota_probe._cli_retry_after_monotonic == 1900.0
1088
+
1089
+
1090
+ def test_cli_unbuildable_result_starts_cooldown(monkeypatch: pytest.MonkeyPatch) -> None:
1091
+ _fake_cli(monkeypatch, _usage_command_output(datetime.now(UTC)))
1092
+ monkeypatch.setattr(time, "monotonic", lambda: 100.0)
1093
+ monkeypatch.setattr(agy_quota_probe, "_build_result", lambda groups: None)
1094
+
1095
+ assert _real_probe_via_cli(15) is None
1096
+ assert agy_quota_probe._cli_retry_after_monotonic == 1900.0
1097
+
1098
+
1009
1099
  def test_cli_probe_reads_three_groups_with_argon_weekly_only(
1010
1100
  monkeypatch: pytest.MonkeyPatch,
1011
1101
  ) -> None:
@@ -1044,7 +1134,9 @@ def test_cli_probe_returns_none_on_unusable_output(
1044
1134
  monkeypatch: pytest.MonkeyPatch, stdout: bytes, returncode: int
1045
1135
  ) -> None:
1046
1136
  _fake_cli(monkeypatch, stdout, returncode)
1137
+ monkeypatch.setattr(time, "monotonic", lambda: 100.0)
1047
1138
  assert _real_probe_via_cli(15) is None
1139
+ assert agy_quota_probe._cli_retry_after_monotonic == 1900.0
1048
1140
 
1049
1141
 
1050
1142
  def test_probe_quota_prefers_cli_over_api(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -167,9 +167,23 @@ def _arm_ping(monkeypatch: pytest.MonkeyPatch, *, enabled: bool = True) -> list[
167
167
  monkeypatch.setattr(agy_window_keeper, "_agy_window_keeper_enabled", lambda: enabled)
168
168
  monkeypatch.setattr(agy_window_keeper, "_resolve_agy_bin", lambda: "/fake/agy")
169
169
  monkeypatch.setattr(agy_window_keeper, "_run_agy_ping", calls.append)
170
+ monkeypatch.setattr(agy_window_keeper, "google_auth_reachable", lambda: True)
170
171
  return calls
171
172
 
172
173
 
174
+ def test_maybe_ping_waits_while_offline_and_leaves_state_untouched(
175
+ monkeypatch: pytest.MonkeyPatch, isolated_state: Path
176
+ ) -> None:
177
+ calls = _arm_ping(monkeypatch)
178
+ monkeypatch.setattr(agy_window_keeper, "google_auth_reachable", lambda: False)
179
+
180
+ agy_window_keeper.maybe_ping(_refresh_result(), mock=False)
181
+
182
+ assert calls == []
183
+ assert agy_window_keeper._load_last_ping() is None
184
+ assert _SyncThread.instances == []
185
+
186
+
173
187
  def test_maybe_ping_fires_and_stamps_dispatch(
174
188
  monkeypatch: pytest.MonkeyPatch, isolated_state: Path
175
189
  ) -> None:
@@ -107,6 +107,7 @@ def status() -> dict[str, Any]:
107
107
 
108
108
 
109
109
  def test_claude_statusline_ok(status: dict[str, Any]) -> None:
110
+ status["_rate_limits_session_id"] = "session"
110
111
  write_json(check.CLAUDE_STATUS_PATH, status)
111
112
  assert check.check_claude_statusline()[0] == "OK"
112
113
 
@@ -501,6 +501,8 @@ def test_generate_html_omits_unpriced_cost_note_when_all_models_are_priced() ->
501
501
 
502
502
  def test_generate_html_shows_formatted_unpriced_cost_note() -> None:
503
503
  data = _full_report_data()
504
+ # Without a cube the card falls back to by_model; the cube path is in the snapshots.
505
+ data.pop("cube", None)
504
506
  data["by_model"].append(
505
507
  {
506
508
  "model": "grok-4",
@@ -513,7 +515,7 @@ def test_generate_html_shows_formatted_unpriced_cost_note() -> None:
513
515
 
514
516
  html = html_report.generate_html(data, language="zh-TW")
515
517
 
516
- assert "其中 1.2M tokens 無公開價格,未計入" in html
518
+ assert "其中 1.2M tokens 沒有價格資料,未計入" in html
517
519
  assert "另有 1234567 tokens 無公開價格" not in html
518
520
 
519
521