usage-cli 0.34.13__tar.gz → 0.34.15__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.15}/PKG-INFO +1 -1
  2. usage_cli-0.34.15/analyzer/ledger_legacy_backfill.py +79 -0
  3. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/reporter.py +11 -3
  4. {usage_cli-0.34.13 → usage_cli-0.34.15}/i18n.json +20 -15
  5. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_quota_probe.py +38 -2
  6. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/agy.py +5 -0
  7. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/app.py +4 -1
  8. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/notify.py +5 -0
  9. usage_cli-0.34.15/menubar/quota_countdown.py +34 -0
  10. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/title.py +3 -23
  11. {usage_cli-0.34.13 → usage_cli-0.34.15}/pyproject.toml +1 -1
  12. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/agy_window_keeper.py +6 -1
  13. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/notifications.py +9 -3
  14. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_quota_probe.py +160 -1
  15. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_window_keeper.py +49 -1
  16. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_check_upstream.py +1 -0
  17. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_html_report_snapshot.py +3 -1
  18. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_keeper_outcome.py +4 -2
  19. usage_cli-0.34.15/tests/test_ledger_legacy_backfill.py +216 -0
  20. usage_cli-0.34.15/tests/test_network.py +97 -0
  21. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_threshold_menu.py +2 -2
  22. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_daily_chart.py +21 -0
  23. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_cli.py +2 -1
  24. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_notifications.py +31 -0
  25. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_wintray.py +166 -2
  26. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/html_report.py +10 -4
  27. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_daily_chart.py +6 -4
  28. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_filter.py +1 -1
  29. {usage_cli-0.34.13 → usage_cli-0.34.15/usage_cli.egg-info}/PKG-INFO +1 -1
  30. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/SOURCES.txt +5 -0
  31. usage_cli-0.34.15/usage_common/network.py +51 -0
  32. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/app.py +31 -5
  33. {usage_cli-0.34.13 → usage_cli-0.34.15}/LICENSE +0 -0
  34. {usage_cli-0.34.13 → usage_cli-0.34.15}/README.md +0 -0
  35. {usage_cli-0.34.13 → usage_cli-0.34.15}/THIRD_PARTY_NOTICES.md +0 -0
  36. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/__init__.py +0 -0
  37. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/agy.py +0 -0
  38. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/claude.py +0 -0
  39. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/codex.py +0 -0
  40. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/grok.py +0 -0
  41. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/muse.py +0 -0
  42. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/rate_limits.py +0 -0
  43. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/registry.py +0 -0
  44. {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/types.py +0 -0
  45. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/__init__.py +0 -0
  46. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/aggregator.py +0 -0
  47. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/blocks.py +0 -0
  48. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/diagnoser.py +0 -0
  49. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/diagnosis_snapshot.py +0 -0
  50. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/dispatch_ledger.py +0 -0
  51. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/history_persist.py +0 -0
  52. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/insights.py +0 -0
  53. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/ledger_backfill.py +0 -0
  54. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/persona_loader.py +0 -0
  55. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/subscription.py +0 -0
  56. {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/usage_snapshot.py +0 -0
  57. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/__init__.py +0 -0
  58. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/claude_pane.py +0 -0
  59. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/codex_hook_trust.py +0 -0
  60. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/login_item.py +0 -0
  61. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/session_hooks.py +0 -0
  62. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/setup_hook.py +0 -0
  63. {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/statusline_settings.py +0 -0
  64. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/__init__.py +0 -0
  65. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/_shared_cache_file.py +0 -0
  66. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_disk_cache.py +0 -0
  67. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_loader.py +0 -0
  68. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/cache_quarantine.py +0 -0
  69. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/chromium_cache.py +0 -0
  70. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_desktop.py +0 -0
  71. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_paths.py +0 -0
  72. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_usage.py +0 -0
  73. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_disk_cache.py +0 -0
  74. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_events.py +0 -0
  75. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_fork_replay.py +0 -0
  76. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_loader.py +0 -0
  77. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_paths.py +0 -0
  78. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/disk_cache_common.py +0 -0
  79. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/disk_cache_lifecycle.py +0 -0
  80. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/grok_loader.py +0 -0
  81. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/grok_quota_probe.py +0 -0
  82. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/history_disk_cache.py +0 -0
  83. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/history_loader.py +0 -0
  84. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/jsonl_limits.py +0 -0
  85. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/jsonl_utils.py +0 -0
  86. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/muse_loader.py +0 -0
  87. {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/muse_quota_probe.py +0 -0
  88. {usage_cli-0.34.13 → usage_cli-0.34.15}/main.py +0 -0
  89. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/__init__.py +0 -0
  90. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/actions.py +0 -0
  91. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/chrome.py +0 -0
  92. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/fsevents_watch.py +0 -0
  93. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/grok.py +0 -0
  94. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/manual_refresh.py +0 -0
  95. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/menu.py +0 -0
  96. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/muse.py +0 -0
  97. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/new_badge.py +0 -0
  98. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/popover.py +0 -0
  99. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/prefs.py +0 -0
  100. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/refresh.py +0 -0
  101. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/state.py +0 -0
  102. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/update.py +0 -0
  103. {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/whats_new.py +0 -0
  104. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/__init__.py +0 -0
  105. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/base.py +0 -0
  106. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/catalog.py +0 -0
  107. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/dynamic_height.py +0 -0
  108. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_scale.py +0 -0
  109. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_window.py +0 -0
  110. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_window_state.py +0 -0
  111. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/payload.py +0 -0
  112. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/web_panel.py +0 -0
  113. {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/window_drag.py +0 -0
  114. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/__init__.py +0 -0
  115. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/burn_rate.py +0 -0
  116. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/codex_window_keeper.py +0 -0
  117. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/keeper_outcome.py +0 -0
  118. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/quota_snapshot.py +0 -0
  119. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/status_payload.py +0 -0
  120. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/usage_rate.py +0 -0
  121. {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/window_keeper.py +0 -0
  122. {usage_cli-0.34.13 → usage_cli-0.34.15}/setup.cfg +0 -0
  123. {usage_cli-0.34.13 → usage_cli-0.34.15}/setup_app.py +0 -0
  124. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_claude.py +0 -0
  125. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_codex.py +0 -0
  126. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_rate_limits.py +0 -0
  127. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_aggregator.py +0 -0
  128. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_adapter.py +0 -0
  129. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_disk_cache.py +0 -0
  130. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_loader.py +0 -0
  131. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_analyzer_blocks.py +0 -0
  132. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_analyzer_pipeline.py +0 -0
  133. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_app_version.py +0 -0
  134. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_burn_rate.py +0 -0
  135. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_cache_quarantine.py +0 -0
  136. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_check_release_version.py +0 -0
  137. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_chromium_cache.py +0 -0
  138. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_desktop.py +0 -0
  139. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_expired_windows.py +0 -0
  140. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_pane.py +0 -0
  141. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_paths.py +0 -0
  142. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_cli_lookup.py +0 -0
  143. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_consistency.py +0 -0
  144. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_hook_trust.py +0 -0
  145. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_loader.py +0 -0
  146. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_window_keeper.py +0 -0
  147. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_compact_cards.py +0 -0
  148. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_fresh_cache.py +0 -0
  149. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_quota_display.py +0 -0
  150. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_quota_review.py +0 -0
  151. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_diagnoser.py +0 -0
  152. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_disk_cache_shards.py +0 -0
  153. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_dispatch_ledger.py +0 -0
  154. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_doctor.py +0 -0
  155. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_dynamic_height.py +0 -0
  156. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_fsevents_watch.py +0 -0
  157. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_grok_loader.py +0 -0
  158. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_grok_quota_probe.py +0 -0
  159. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_history_loader.py +0 -0
  160. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_history_persist.py +0 -0
  161. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_hook_scripts_are_standalone.py +0 -0
  162. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_format_fallback.py +0 -0
  163. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_key_parity.py +0 -0
  164. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_packaged_path.py +0 -0
  165. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_insights.py +0 -0
  166. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_jsonl_utils.py +0 -0
  167. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_keeper_menu.py +0 -0
  168. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_ledger_backfill.py +0 -0
  169. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_login_item.py +0 -0
  170. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_main.py +0 -0
  171. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_manual_refresh.py +0 -0
  172. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar.py +0 -0
  173. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_agy.py +0 -0
  174. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_grok.py +0 -0
  175. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_muse.py +0 -0
  176. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_notify.py +0 -0
  177. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_permission.py +0 -0
  178. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_prefs.py +0 -0
  179. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_state.py +0 -0
  180. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_update.py +0 -0
  181. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_coverage.py +0 -0
  182. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_loader.py +0 -0
  183. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_quota_probe.py +0 -0
  184. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_review_regressions.py +0 -0
  185. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_new_badge.py +0 -0
  186. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_packaged_resources.py +0 -0
  187. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_packaging.py +0 -0
  188. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_footer_layout.py +0 -0
  189. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_scale.py +0 -0
  190. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_window_state.py +0 -0
  191. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panels.py +0 -0
  192. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panels_lazy_import.py +0 -0
  193. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_persona_loader.py +0 -0
  194. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_pricing.py +0 -0
  195. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_project_resolver.py +0 -0
  196. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_aware.py +0 -0
  197. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_label.py +0 -0
  198. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_chart_redesign.py +0 -0
  199. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_cold_read.py +0 -0
  200. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_filter_js.py +0 -0
  201. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_headings.py +0 -0
  202. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_share_mask.py +0 -0
  203. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_reporter_build_data.py +0 -0
  204. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_service_status.py +0 -0
  205. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_session_resume.py +0 -0
  206. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_app.py +0 -0
  207. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook.py +0 -0
  208. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_coexistence.py +0 -0
  209. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_resume.py +0 -0
  210. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_terse.py +0 -0
  211. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_subprocess_utils.py +0 -0
  212. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_subscription.py +0 -0
  213. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_taskbar_menu.py +0 -0
  214. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_taskbar_obstacles.py +0 -0
  215. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_time_utils.py +0 -0
  216. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_tui.py +0 -0
  217. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_tui_sprite.py +0 -0
  218. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_ui_tables.py +0 -0
  219. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_checker.py +0 -0
  220. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_gate.py +0 -0
  221. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_release_notes.py +0 -0
  222. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_client.py +0 -0
  223. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_diagnosis_snapshot.py +0 -0
  224. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_dir_sweeper.py +0 -0
  225. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_lang.py +0 -0
  226. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_logging.py +0 -0
  227. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_rate.py +0 -0
  228. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_snapshot.py +0 -0
  229. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_snapshot_replay.py +0 -0
  230. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline.py +0 -0
  231. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_agy.py +0 -0
  232. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_forwarder.py +0 -0
  233. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_grok.py +0 -0
  234. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_terse_mode.py +0 -0
  235. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_terse_reminder.py +0 -0
  236. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_web_panel_payload.py +0 -0
  237. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_whats_new.py +0 -0
  238. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_wheel_packaging.py +0 -0
  239. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_win_login_item.py +0 -0
  240. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_window_keeper.py +0 -0
  241. {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_windows_manifest.py +0 -0
  242. {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/__init__.py +0 -0
  243. {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/app.py +0 -0
  244. {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/sprite.py +0 -0
  245. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/__init__.py +0 -0
  246. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_charts.py +0 -0
  247. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_ledger.py +0 -0
  248. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_scripts.py +0 -0
  249. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_styles.py +0 -0
  250. {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/tables.py +0 -0
  251. {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/__init__.py +0 -0
  252. {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/checker.py +0 -0
  253. {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/gate.py +0 -0
  254. {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/release_notes.py +0 -0
  255. {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/whats_new.py +0 -0
  256. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/__init__.py +0 -0
  257. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/cli.py +0 -0
  258. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/doctor.py +0 -0
  259. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/dependency_links.txt +0 -0
  260. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/entry_points.txt +0 -0
  261. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/requires.txt +0 -0
  262. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/top_level.txt +0 -0
  263. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/__init__.py +0 -0
  264. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/app_version.py +0 -0
  265. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/cli_lookup.py +0 -0
  266. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/i18n.py +0 -0
  267. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/prefs.py +0 -0
  268. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/pricing.py +0 -0
  269. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/project_resolver.py +0 -0
  270. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/service_status.py +0 -0
  271. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/subprocess_utils.py +0 -0
  272. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/time_utils.py +0 -0
  273. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_dir_sweeper.py +0 -0
  274. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_lang.py +0 -0
  275. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_logging.py +0 -0
  276. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/__init__.py +0 -0
  277. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_quota_aware.py +0 -0
  278. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_session_resume.py +0 -0
  279. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline.py +0 -0
  280. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_agy.py +0 -0
  281. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_forwarder.py +0 -0
  282. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_grok.py +0 -0
  283. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_terse_mode.py +0 -0
  284. {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_terse_reminder.py +0 -0
  285. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/__init__.py +0 -0
  286. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/login_item.py +0 -0
  287. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/menu.py +0 -0
  288. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/prefs.py +0 -0
  289. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/quota_label.py +0 -0
  290. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_menu.py +0 -0
  291. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_obstacles.py +0 -0
  292. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_overlay.py +0 -0
  293. {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/watch.py +0 -0
  294. {usage_cli-0.34.13 → usage_cli-0.34.15}/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.15
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": "이전 기간 대비",
@@ -34,7 +34,9 @@ from typing import Any
34
34
  from urllib.error import HTTPError, URLError
35
35
  from urllib.request import Request, urlopen
36
36
 
37
+ from loaders.cache_quarantine import quarantine
37
38
  from usage_common.cli_lookup import cli_env, resolve_cli
39
+ from usage_common.network import google_auth_reachable
38
40
  from usage_common.subprocess_utils import hidden_console_kwargs
39
41
 
40
42
  CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
@@ -79,9 +81,15 @@ _CLIENT_SECRET = "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"
79
81
  _DEFAULT_RETRY_AFTER_SECONDS = 60.0
80
82
  _MAX_RETRY_AFTER_SECONDS = 3600.0
81
83
  _rate_limit_until_monotonic = 0.0
84
+ # A failed launch may open a browser sign-in page; don't repeat it every refresh.
85
+ _CLI_FAILURE_COOLDOWN_SECONDS = 1800.0
86
+ _cli_retry_after_monotonic = 0.0
82
87
  # When each group last appeared in a real API response; in memory only, reset on restart.
83
88
  _group_seen_at: dict[str, datetime] = {}
84
89
  _MISSING_GROUP_GRACE_MINUTES = 30
90
+ # Models omit the period; keep known weekly resets weekly near expiry.
91
+ # In memory only: after restart, unseen resets use the time heuristic again.
92
+ _weekly_reset_times: set[str] = set()
85
93
 
86
94
 
87
95
  def _user_agent() -> str:
@@ -157,9 +165,23 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
157
165
  The slash command makes no model call. ``--log-file`` reuses one file instead of
158
166
  leaving a new log in the CLI's log directory on every probe.
159
167
  """
168
+ global _cli_retry_after_monotonic
160
169
  agy = find_agy()
161
170
  if agy is None:
162
171
  return None
172
+ if time.monotonic() < _cli_retry_after_monotonic:
173
+ return None
174
+ # Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page.
175
+ if not google_auth_reachable():
176
+ return None
177
+ result = _run_usage_command(agy, timeout)
178
+ _cli_retry_after_monotonic = (
179
+ 0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
180
+ )
181
+ return result
182
+
183
+
184
+ def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
163
185
  command = [agy, "-p", "/usage", "--output-format", "json", "--log-file", str(_CLI_LOG_PATH)]
164
186
  try:
165
187
  completed = subprocess.run(
@@ -509,6 +531,7 @@ def _post_json(url: str, access_token: str, body: dict[str, object], timeout: fl
509
531
  except HTTPError as exc:
510
532
  _handle_http_error(exc)
511
533
  if exc.code == 401:
534
+ _token_cache.clear()
512
535
  _mark_auth_expired()
513
536
  return None
514
537
  except (URLError, OSError, ValueError):
@@ -619,7 +642,13 @@ def _gemini_group_from_models(raw: object) -> AgyQuotaGroup | None:
619
642
  window = _bucket_to_window(bucket)
620
643
  reset = _bucket_reset_time(bucket)
621
644
  assert reset is not None
622
- session = _seconds_until(reset) <= 5 * 3600 + 5 * 60
645
+ _weekly_reset_times.difference_update(
646
+ [seen for seen in _weekly_reset_times if _seconds_until(seen) <= 0]
647
+ )
648
+ session = reset not in _weekly_reset_times and _seconds_until(reset) <= 5 * 3600 + 5 * 60
649
+ # A full bucket's reset time slides with every response, so only a spent one is remembered.
650
+ if not session and window is not None and window.remaining_percent < 100.0:
651
+ _weekly_reset_times.add(reset)
623
652
  template = _assemble_groups({"gemini": {}})[0]
624
653
  return AgyQuotaGroup(
625
654
  name=template.name,
@@ -807,7 +836,14 @@ def _read_cache() -> AgyQuotaResult | None:
807
836
  try:
808
837
  with CACHE_PATH.open(encoding="utf-8") as cache_file:
809
838
  payload: object = json.load(cache_file)
810
- except (OSError, json.JSONDecodeError):
839
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
840
+ quarantine(
841
+ CACHE_PATH, "decode-error" if isinstance(exc, UnicodeDecodeError) else "json-error"
842
+ )
843
+ with suppress(OSError):
844
+ CACHE_PATH.unlink()
845
+ return None
846
+ except OSError:
811
847
  return None
812
848
  return _result_from_payload(payload)
813
849
 
@@ -59,6 +59,7 @@ class AgyQuotaProjection:
59
59
  five_hour: AgyQuotaWindow | None
60
60
  group_switchable: bool = False
61
61
  weekly_only: bool = False
62
+ gemini_five_hour: AgyQuotaWindow | None = None
62
63
 
63
64
 
64
65
  @dataclass(frozen=True, slots=True)
@@ -162,6 +163,10 @@ def project_quota(
162
163
  weekly=weekly,
163
164
  stale=stale,
164
165
  five_hour=selected.five_hour,
166
+ gemini_five_hour=next(
167
+ (group.five_hour for group in quota.groups if _matches_group(group.name, "gemini")),
168
+ None,
169
+ ),
165
170
  )
166
171
 
167
172
 
@@ -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.15"
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__)
@@ -117,7 +118,7 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
117
118
  if not enabled:
118
119
  return
119
120
  projection = result.projection
120
- five_hour = projection.five_hour if projection is not None else None
121
+ five_hour = projection.gemini_five_hour if projection is not None else None
121
122
  now = time.time()
122
123
  last_ping_at = _load_last_ping()
123
124
  state = keeper_outcome.read_state(AGY_WINDOW_KEEPER_STATE_PATH)
@@ -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