usage-cli 0.33.5__tar.gz → 0.33.7__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 (273) hide show
  1. {usage_cli-0.33.5/usage_cli.egg-info → usage_cli-0.33.7}/PKG-INFO +2 -1
  2. {usage_cli-0.33.5 → usage_cli-0.33.7}/README.md +1 -0
  3. usage_cli-0.33.7/analyzer/history_persist.py +116 -0
  4. usage_cli-0.33.7/analyzer/ledger_backfill.py +110 -0
  5. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/reporter.py +62 -16
  6. {usage_cli-0.33.5 → usage_cli-0.33.7}/i18n.json +30 -20
  7. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_quota_probe.py +31 -0
  8. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/agy.py +3 -0
  9. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/refresh.py +2 -0
  10. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/state.py +5 -1
  11. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/update.py +2 -1
  12. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/payload.py +1 -0
  13. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/web_panel.py +1 -0
  14. {usage_cli-0.33.5 → usage_cli-0.33.7}/pyproject.toml +1 -1
  15. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_quota_probe.py +65 -0
  16. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_pane.py +4 -1
  17. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_dispatch_ledger.py +22 -0
  18. usage_cli-0.33.7/tests/test_history_persist.py +172 -0
  19. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_html_report_snapshot.py +32 -5
  20. usage_cli-0.33.7/tests/test_ledger_backfill.py +136 -0
  21. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar.py +5 -0
  22. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_agy.py +6 -0
  23. usage_cli-0.33.7/tests/test_panel_footer_layout.py +88 -0
  24. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_report_daily_chart.py +25 -1
  25. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_report_filter_js.py +77 -7
  26. usage_cli-0.33.7/tests/test_report_share_mask.py +51 -0
  27. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_reporter_build_data.py +113 -0
  28. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_snapshot.py +21 -0
  29. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_web_panel_payload.py +3 -0
  30. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_wintray.py +7 -1
  31. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/html_report.py +41 -13
  32. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_daily_chart.py +61 -12
  33. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_filter.py +18 -7
  34. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_ledger.py +1 -0
  35. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_scripts.py +2 -2
  36. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_styles.py +7 -5
  37. {usage_cli-0.33.5 → usage_cli-0.33.7/usage_cli.egg-info}/PKG-INFO +2 -1
  38. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/SOURCES.txt +5 -0
  39. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/app.py +5 -1
  40. usage_cli-0.33.5/tests/test_panel_footer_layout.py +0 -44
  41. {usage_cli-0.33.5 → usage_cli-0.33.7}/LICENSE +0 -0
  42. {usage_cli-0.33.5 → usage_cli-0.33.7}/THIRD_PARTY_NOTICES.md +0 -0
  43. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/__init__.py +0 -0
  44. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/agy.py +0 -0
  45. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/claude.py +0 -0
  46. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/codex.py +0 -0
  47. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/grok.py +0 -0
  48. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/muse.py +0 -0
  49. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/rate_limits.py +0 -0
  50. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/registry.py +0 -0
  51. {usage_cli-0.33.5 → usage_cli-0.33.7}/adapters/types.py +0 -0
  52. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/__init__.py +0 -0
  53. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/aggregator.py +0 -0
  54. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/blocks.py +0 -0
  55. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/diagnoser.py +0 -0
  56. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/diagnosis_snapshot.py +0 -0
  57. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/dispatch_ledger.py +0 -0
  58. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/insights.py +0 -0
  59. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/persona_loader.py +0 -0
  60. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/subscription.py +0 -0
  61. {usage_cli-0.33.5 → usage_cli-0.33.7}/analyzer/usage_snapshot.py +0 -0
  62. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/__init__.py +0 -0
  63. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/claude_pane.py +0 -0
  64. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/codex_hook_trust.py +0 -0
  65. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/login_item.py +0 -0
  66. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/session_hooks.py +0 -0
  67. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/setup_hook.py +0 -0
  68. {usage_cli-0.33.5 → usage_cli-0.33.7}/installer/statusline_settings.py +0 -0
  69. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/__init__.py +0 -0
  70. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/_shared_cache_file.py +0 -0
  71. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_disk_cache.py +0 -0
  72. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/agy_loader.py +0 -0
  73. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/cache_quarantine.py +0 -0
  74. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/chromium_cache.py +0 -0
  75. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_desktop.py +0 -0
  76. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_paths.py +0 -0
  77. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/claude_usage.py +0 -0
  78. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_disk_cache.py +0 -0
  79. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_events.py +0 -0
  80. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_fork_replay.py +0 -0
  81. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_loader.py +0 -0
  82. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/codex_paths.py +0 -0
  83. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/disk_cache_common.py +0 -0
  84. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/disk_cache_lifecycle.py +0 -0
  85. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/grok_loader.py +0 -0
  86. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/grok_quota_probe.py +0 -0
  87. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/history_disk_cache.py +0 -0
  88. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/history_loader.py +0 -0
  89. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/jsonl_limits.py +0 -0
  90. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/jsonl_utils.py +0 -0
  91. {usage_cli-0.33.5 → usage_cli-0.33.7}/loaders/muse_loader.py +0 -0
  92. {usage_cli-0.33.5 → usage_cli-0.33.7}/main.py +0 -0
  93. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/__init__.py +0 -0
  94. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/actions.py +0 -0
  95. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/app.py +0 -0
  96. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/chrome.py +0 -0
  97. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/fsevents_watch.py +0 -0
  98. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/grok.py +0 -0
  99. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/manual_refresh.py +0 -0
  100. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/menu.py +0 -0
  101. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/new_badge.py +0 -0
  102. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/notify.py +0 -0
  103. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/popover.py +0 -0
  104. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/prefs.py +0 -0
  105. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/title.py +0 -0
  106. {usage_cli-0.33.5 → usage_cli-0.33.7}/menubar/whats_new.py +0 -0
  107. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/__init__.py +0 -0
  108. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/base.py +0 -0
  109. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/catalog.py +0 -0
  110. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/dynamic_height.py +0 -0
  111. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_scale.py +0 -0
  112. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_window.py +0 -0
  113. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/panel_window_state.py +0 -0
  114. {usage_cli-0.33.5 → usage_cli-0.33.7}/panels/window_drag.py +0 -0
  115. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/__init__.py +0 -0
  116. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/agy_window_keeper.py +0 -0
  117. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/burn_rate.py +0 -0
  118. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/codex_window_keeper.py +0 -0
  119. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/keeper_outcome.py +0 -0
  120. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/notifications.py +0 -0
  121. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/quota_snapshot.py +0 -0
  122. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/status_payload.py +0 -0
  123. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/usage_rate.py +0 -0
  124. {usage_cli-0.33.5 → usage_cli-0.33.7}/quota/window_keeper.py +0 -0
  125. {usage_cli-0.33.5 → usage_cli-0.33.7}/setup.cfg +0 -0
  126. {usage_cli-0.33.5 → usage_cli-0.33.7}/setup_app.py +0 -0
  127. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_claude.py +0 -0
  128. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_codex.py +0 -0
  129. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_adapters_rate_limits.py +0 -0
  130. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_aggregator.py +0 -0
  131. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_adapter.py +0 -0
  132. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_disk_cache.py +0 -0
  133. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_loader.py +0 -0
  134. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_agy_window_keeper.py +0 -0
  135. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_analyzer_blocks.py +0 -0
  136. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_analyzer_pipeline.py +0 -0
  137. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_app_version.py +0 -0
  138. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_burn_rate.py +0 -0
  139. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_cache_quarantine.py +0 -0
  140. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_check_upstream.py +0 -0
  141. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_chromium_cache.py +0 -0
  142. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_desktop.py +0 -0
  143. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_claude_paths.py +0 -0
  144. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_consistency.py +0 -0
  145. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_hook_trust.py +0 -0
  146. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_loader.py +0 -0
  147. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_codex_window_keeper.py +0 -0
  148. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_fresh_cache.py +0 -0
  149. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_quota_display.py +0 -0
  150. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_desktop_quota_review.py +0 -0
  151. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_diagnoser.py +0 -0
  152. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_disk_cache_shards.py +0 -0
  153. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_doctor.py +0 -0
  154. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_dynamic_height.py +0 -0
  155. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_fsevents_watch.py +0 -0
  156. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_grok_loader.py +0 -0
  157. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_grok_quota_probe.py +0 -0
  158. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_history_loader.py +0 -0
  159. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_hook_scripts_are_standalone.py +0 -0
  160. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_format_fallback.py +0 -0
  161. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_key_parity.py +0 -0
  162. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_i18n_packaged_path.py +0 -0
  163. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_insights.py +0 -0
  164. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_jsonl_utils.py +0 -0
  165. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_keeper_menu.py +0 -0
  166. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_keeper_outcome.py +0 -0
  167. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_login_item.py +0 -0
  168. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_main.py +0 -0
  169. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_manual_refresh.py +0 -0
  170. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_grok.py +0 -0
  171. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_notify.py +0 -0
  172. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_prefs.py +0 -0
  173. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_state.py +0 -0
  174. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_menubar_update.py +0 -0
  175. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_muse_loader.py +0 -0
  176. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_new_badge.py +0 -0
  177. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_packaged_resources.py +0 -0
  178. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_packaging.py +0 -0
  179. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panel_scale.py +0 -0
  180. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panel_window_state.py +0 -0
  181. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panels.py +0 -0
  182. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_panels_lazy_import.py +0 -0
  183. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_persona_loader.py +0 -0
  184. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_pricing.py +0 -0
  185. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_project_resolver.py +0 -0
  186. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_quota_aware.py +0 -0
  187. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_quota_label.py +0 -0
  188. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_service_status.py +0 -0
  189. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_session_resume.py +0 -0
  190. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_app.py +0 -0
  191. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook.py +0 -0
  192. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_coexistence.py +0 -0
  193. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_resume.py +0 -0
  194. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_setup_hook_terse.py +0 -0
  195. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_subprocess_utils.py +0 -0
  196. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_subscription.py +0 -0
  197. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_taskbar_menu.py +0 -0
  198. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_time_utils.py +0 -0
  199. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_tui.py +0 -0
  200. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_tui_sprite.py +0 -0
  201. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_ui_tables.py +0 -0
  202. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_checker.py +0 -0
  203. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_gate.py +0 -0
  204. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_update_release_notes.py +0 -0
  205. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_cli.py +0 -0
  206. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_client.py +0 -0
  207. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_diagnosis_snapshot.py +0 -0
  208. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_dir_sweeper.py +0 -0
  209. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_lang.py +0 -0
  210. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_logging.py +0 -0
  211. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_notifications.py +0 -0
  212. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_rate.py +0 -0
  213. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_snapshot_replay.py +0 -0
  214. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline.py +0 -0
  215. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_agy.py +0 -0
  216. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_forwarder.py +0 -0
  217. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_statusline_grok.py +0 -0
  218. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_terse_mode.py +0 -0
  219. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_usage_terse_reminder.py +0 -0
  220. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_whats_new.py +0 -0
  221. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_wheel_packaging.py +0 -0
  222. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_win_login_item.py +0 -0
  223. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_window_keeper.py +0 -0
  224. {usage_cli-0.33.5 → usage_cli-0.33.7}/tests/test_windows_manifest.py +0 -0
  225. {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/__init__.py +0 -0
  226. {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/app.py +0 -0
  227. {usage_cli-0.33.5 → usage_cli-0.33.7}/tui/sprite.py +0 -0
  228. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/__init__.py +0 -0
  229. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/report_charts.py +0 -0
  230. {usage_cli-0.33.5 → usage_cli-0.33.7}/ui/tables.py +0 -0
  231. {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/__init__.py +0 -0
  232. {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/checker.py +0 -0
  233. {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/gate.py +0 -0
  234. {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/release_notes.py +0 -0
  235. {usage_cli-0.33.5 → usage_cli-0.33.7}/updates/whats_new.py +0 -0
  236. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/__init__.py +0 -0
  237. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/cli.py +0 -0
  238. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_app/doctor.py +0 -0
  239. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/dependency_links.txt +0 -0
  240. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/entry_points.txt +0 -0
  241. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/requires.txt +0 -0
  242. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_cli.egg-info/top_level.txt +0 -0
  243. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/__init__.py +0 -0
  244. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/app_version.py +0 -0
  245. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/i18n.py +0 -0
  246. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/prefs.py +0 -0
  247. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/pricing.py +0 -0
  248. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/project_resolver.py +0 -0
  249. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/service_status.py +0 -0
  250. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/subprocess_utils.py +0 -0
  251. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/time_utils.py +0 -0
  252. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_dir_sweeper.py +0 -0
  253. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_lang.py +0 -0
  254. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_common/usage_logging.py +0 -0
  255. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/__init__.py +0 -0
  256. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_quota_aware.py +0 -0
  257. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_session_resume.py +0 -0
  258. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline.py +0 -0
  259. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_agy.py +0 -0
  260. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_forwarder.py +0 -0
  261. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_statusline_grok.py +0 -0
  262. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_terse_mode.py +0 -0
  263. {usage_cli-0.33.5 → usage_cli-0.33.7}/usage_hooks/usage_terse_reminder.py +0 -0
  264. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/__init__.py +0 -0
  265. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/login_item.py +0 -0
  266. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/menu.py +0 -0
  267. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/prefs.py +0 -0
  268. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/quota_label.py +0 -0
  269. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_menu.py +0 -0
  270. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_obstacles.py +0 -0
  271. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/taskbar_overlay.py +0 -0
  272. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/watch.py +0 -0
  273. {usage_cli-0.33.5 → usage_cli-0.33.7}/wintray/whats_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.33.5
3
+ Version: 0.33.7
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
@@ -274,6 +274,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
274
274
  - **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
275
275
  - **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
276
276
  - **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
277
+ - **Long-term history:** Claude Code deletes transcripts older than 30 days by default. While the app runs, `usage` saves daily totals to `~/.usage/usage_snapshot.json` and `~/.usage/year_ledger.json` every 6 hours, so reports keep older days. Nothing leaves your machine. When you move to a new computer, copy the whole `~/.usage/` folder too.
277
278
 
278
279
  <details>
279
280
  <summary>How Claude Desktop quota is read</summary>
@@ -252,6 +252,7 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
252
252
  - **Antigravity**, only if you use it, needs network access: quota is fetched from Google's official quota endpoint with the OAuth credential the Antigravity CLI already stored after sign-in — read from macOS Keychain, Windows Credential Manager, or a local token file depending on CLI version. `usage` never writes that credential back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
253
253
  - **Other background network activity:** Public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (built-in prices are used offline), and an occasional GitHub check for a new version.
254
254
  - **Beginner Mode**, only if you turn it on, sends Claude Code's latest answer to Claude Haiku through your own Claude Code sign-in to pick terms. Your term list stays in `~/.usage/glossary.json`.
255
+ - **Long-term history:** Claude Code deletes transcripts older than 30 days by default. While the app runs, `usage` saves daily totals to `~/.usage/usage_snapshot.json` and `~/.usage/year_ledger.json` every 6 hours, so reports keep older days. Nothing leaves your machine. When you move to a new computer, copy the whole `~/.usage/` folder too.
255
256
 
256
257
  <details>
257
258
  <summary>How Claude Desktop quota is read</summary>
@@ -0,0 +1,116 @@
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
+ """Periodically preserve real usage without scanning on the app thread."""
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import threading
14
+ from datetime import UTC, datetime, timedelta
15
+ from pathlib import Path
16
+
17
+ from adapters.registry import detect_agents
18
+ from analyzer import diagnosis_snapshot, reporter, usage_snapshot
19
+ from usage_common.time_utils import parse_optional_iso8601_utc
20
+
21
+ STATE_PATH = Path.home() / ".usage" / "history_persist_state.json"
22
+ _INTERVAL = timedelta(hours=6)
23
+ _LOOKBACK_DAYS = 45
24
+ _lock = threading.Lock()
25
+ _refresh_in_flight = False
26
+ _timer: threading.Timer | None = None
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _remaining(now: datetime) -> float:
31
+ try:
32
+ state = json.loads(STATE_PATH.read_text(encoding="utf-8"))
33
+ last = (
34
+ parse_optional_iso8601_utc(state.get("last_success"))
35
+ if isinstance(state, dict)
36
+ else None
37
+ )
38
+ except (OSError, ValueError):
39
+ last = None
40
+ return max(0.0, (_INTERVAL - (now - last)).total_seconds()) if last else 0.0
41
+
42
+
43
+ def _schedule_next_locked(delay: float) -> None:
44
+ global _timer
45
+ if _timer is None:
46
+ _timer = threading.Timer(delay, _on_timer)
47
+ _timer.daemon = True
48
+ try:
49
+ _timer.start()
50
+ except Exception:
51
+ _timer = None
52
+ raise
53
+
54
+
55
+ def _on_timer() -> None:
56
+ global _timer
57
+ with _lock:
58
+ _timer = None
59
+ maybe_schedule_refresh()
60
+
61
+
62
+ def maybe_schedule_refresh() -> None:
63
+ global _refresh_in_flight, _timer
64
+ try:
65
+ with _lock:
66
+ if _refresh_in_flight:
67
+ return
68
+ remaining = _remaining(datetime.now(UTC))
69
+ if remaining > 0:
70
+ _schedule_next_locked(remaining)
71
+ return
72
+ if _timer is not None:
73
+ _timer.cancel()
74
+ _timer = None
75
+ _refresh_in_flight = True
76
+ threading.Thread(target=_refresh_in_background, daemon=True).start()
77
+ except Exception:
78
+ with _lock:
79
+ _refresh_in_flight = False
80
+ logger.warning("Failed to schedule usage history persistence", exc_info=True)
81
+
82
+
83
+ def _refresh_in_background() -> None:
84
+ global _refresh_in_flight
85
+ try:
86
+ refresh_history()
87
+ except Exception:
88
+ logger.warning("Failed to persist usage history", exc_info=True)
89
+ finally:
90
+ with _lock:
91
+ _refresh_in_flight = False
92
+ try:
93
+ _schedule_next_locked(_INTERVAL.total_seconds())
94
+ except Exception:
95
+ logger.warning("Failed to reschedule usage history persistence", exc_info=True)
96
+
97
+
98
+ def refresh_history(now: datetime | None = None) -> bool:
99
+ current = now or datetime.now(UTC)
100
+ if _remaining(current) > 0:
101
+ return False
102
+ today = current.astimezone().date()
103
+ entries = reporter.load_history_entries(
104
+ detect_agents(), today - timedelta(days=_LOOKBACK_DAYS - 1), today
105
+ )
106
+ snapshot = usage_snapshot.record_entries(entries)
107
+ ledger = reporter.persist_year_entries(entries, today)
108
+ # Existing writers swallow I/O errors. Read back before marking success.
109
+ for path, expected in (
110
+ (usage_snapshot.SNAPSHOT_PATH, snapshot),
111
+ (reporter.YEAR_LEDGER_PATH, ledger),
112
+ ):
113
+ if json.loads(path.read_text(encoding="utf-8")) != expected:
114
+ raise OSError(f"Usage history write verification failed: {path}")
115
+ diagnosis_snapshot._atomic_write_json(STATE_PATH, {"last_success": current.isoformat()})
116
+ return True
@@ -0,0 +1,110 @@
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
+ """Rehydrate only the daily token gap left after snapshot replay."""
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections import defaultdict
12
+ from collections.abc import Mapping, Sequence
13
+ from datetime import date, datetime
14
+ from typing import TYPE_CHECKING
15
+
16
+ from adapters.types import UsageEntry
17
+ from usage_common.pricing import calculate_cost
18
+
19
+ if TYPE_CHECKING:
20
+ from analyzer.reporter import _YearDay
21
+
22
+
23
+ def is_ledger_entry(entry: UsageEntry) -> bool:
24
+ return entry.session_id.startswith("usage-ledger:")
25
+
26
+
27
+ def _allocate(
28
+ ledger: Mapping[str, int], current: Mapping[str, int], gap: int
29
+ ) -> list[tuple[str, int]]:
30
+ weights = {key: max(tokens - current.get(key, 0), 0) for key, tokens in ledger.items()}
31
+ if not any(weights.values()):
32
+ weights = {key: max(tokens, 0) for key, tokens in ledger.items()}
33
+ total = sum(weights.values())
34
+ if total == 0:
35
+ return [("unknown", gap)]
36
+ # Integer largest-remainder allocation keeps every dimension's sum exact.
37
+ shares = {key: gap * weight // total for key, weight in weights.items()}
38
+ ranked = sorted(weights, key=lambda key: (-(gap * weights[key] % total), key))
39
+ for key in ranked[: gap - sum(shares.values())]:
40
+ shares[key] += 1
41
+ return [(key, shares[key]) for key in sorted(shares) if shares[key] > 0]
42
+
43
+
44
+ def merge_with_ledger(
45
+ entries: Sequence[UsageEntry],
46
+ entry_dates: Mapping[int, date],
47
+ days: Mapping[str, _YearDay],
48
+ date_from: date,
49
+ date_to: date,
50
+ ) -> tuple[list[UsageEntry], dict[int, date]]:
51
+ merged = list(entries)
52
+ dates = dict(entry_dates)
53
+ by_day: dict[date, list[UsageEntry]] = defaultdict(list)
54
+ for entry in entries:
55
+ by_day[entry_dates[id(entry)]].append(entry)
56
+ for day_key, ledger in sorted(days.items()):
57
+ day = date.fromisoformat(day_key)
58
+ if not date_from <= day <= date_to or not any(
59
+ tokens > 0 for tokens in ledger["agent_tokens"].values()
60
+ ):
61
+ continue
62
+ current = by_day.get(day, [])
63
+ gap = ledger["total_tokens"] - sum(entry.total_tokens for entry in current)
64
+ if gap <= 0:
65
+ continue
66
+ dimensions: list[list[tuple[str, int]]] = []
67
+ for attribute, ledger_tokens in (
68
+ ("agent_id", ledger["agent_tokens"]),
69
+ ("project", ledger["project_tokens"]),
70
+ ("model", ledger["model_tokens"]),
71
+ ):
72
+ totals: dict[str, int] = defaultdict(int)
73
+ for entry in current:
74
+ totals[getattr(entry, attribute) or "unknown"] += entry.total_tokens
75
+ dimensions.append(_allocate(ledger_tokens, totals, gap))
76
+ cost_gap = max(ledger["cost"] - sum(calculate_cost(entry) for entry in current), 0.0)
77
+ # Intersect the three allocations without claiming historical correlations.
78
+ indices = [0, 0, 0]
79
+ remaining = [dimension[0][1] for dimension in dimensions]
80
+ allocated = 0
81
+ while allocated < gap:
82
+ tokens = min(remaining)
83
+ agent, project, model = (
84
+ dimension[index][0] for dimension, index in zip(dimensions, indices, strict=True)
85
+ )
86
+ synthetic_id = f"usage-ledger:{day_key}:{allocated}"
87
+ entry = UsageEntry(
88
+ timestamp=datetime.combine(day, datetime.min.time()),
89
+ session_id=f"usage-ledger:{day_key}",
90
+ message_id=synthetic_id,
91
+ request_id=synthetic_id,
92
+ agent_id=agent,
93
+ project=project,
94
+ model=model,
95
+ input_tokens=tokens,
96
+ output_tokens=0,
97
+ cache_creation_tokens=0,
98
+ cache_read_tokens=0,
99
+ cost_usd=cost_gap * tokens / gap,
100
+ message_count=0,
101
+ )
102
+ merged.append(entry)
103
+ dates[id(entry)] = day
104
+ allocated += tokens
105
+ for axis in range(3):
106
+ remaining[axis] -= tokens
107
+ if remaining[axis] == 0 and allocated < gap:
108
+ indices[axis] += 1
109
+ remaining[axis] = dimensions[axis][indices[axis]][1]
110
+ return merged, dates
@@ -27,7 +27,7 @@ from adapters.types import AgentInfo, UsageEntry
27
27
  from usage_common.pricing import calculate_cost, is_model_priced
28
28
 
29
29
  from .aggregator import aggregate_sessions
30
- from . import diagnoser, dispatch_ledger, usage_snapshot
30
+ from . import diagnoser, dispatch_ledger, ledger_backfill, usage_snapshot
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -317,6 +317,18 @@ def _load_agent_entries(
317
317
  return entries
318
318
 
319
319
 
320
+ def load_history_entries(
321
+ agents: list[AgentInfo], date_from: date, date_to: date
322
+ ) -> list[UsageEntry]:
323
+ hours_back = ((date_to - date_from).days + 2) * 24
324
+ return [
325
+ entry
326
+ for agent in agents
327
+ for entry in _load_agent_entries(agent, hours_back)
328
+ if date_from <= _entry_date(entry) <= date_to and not ledger_backfill.is_ledger_entry(entry)
329
+ ]
330
+
331
+
320
332
  def _load_recent_claude_entries(hours_back: int) -> list[UsageEntry]:
321
333
  entries: list[UsageEntry] = []
322
334
  seen: set[str] = set()
@@ -798,6 +810,15 @@ def build_year_data(agents: list[AgentInfo]) -> YearReportData:
798
810
  )
799
811
 
800
812
 
813
+ def persist_year_entries(entries: list[UsageEntry], today: date) -> _YearLedger:
814
+ grid_end = today + timedelta(days=(5 - today.weekday()) % 7)
815
+ grid_start = grid_end - timedelta(days=_YEAR_WEEKS * 7 - 1)
816
+ return _merge_year_ledger(
817
+ _aggregate_year_days(entries),
818
+ trim_before=grid_start - timedelta(days=_YEAR_LEDGER_TRIM_BUFFER_DAYS),
819
+ )
820
+
821
+
801
822
  def _load_year_data_cached(agents: list[AgentInfo]) -> YearReportData:
802
823
  cached = _read_year_cache()
803
824
  if cached is not None:
@@ -923,10 +944,21 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
923
944
  raw_entries.extend(_load_agent_entries(agent, hours_back))
924
945
  entry_dates = {id(entry): _entry_date(entry) for entry in raw_entries}
925
946
 
947
+ ledger_days = _read_year_ledger()["days"]
926
948
  if date_from is None:
927
949
  earliest_live = min(entry_dates.values()) if entry_dates else None
928
950
  earliest_snap = usage_snapshot.earliest_date()
929
- earliest_candidates = [day for day in (earliest_live, earliest_snap) if day is not None]
951
+ earliest_ledger = min(
952
+ (
953
+ date.fromisoformat(day)
954
+ for day, value in ledger_days.items()
955
+ if any(tokens > 0 for tokens in value["agent_tokens"].values())
956
+ ),
957
+ default=None,
958
+ )
959
+ earliest_candidates = [
960
+ day for day in (earliest_live, earliest_snap, earliest_ledger) if day is not None
961
+ ]
930
962
  date_from = min(earliest_candidates) if earliest_candidates else date_to
931
963
 
932
964
  span_from = date_from
@@ -940,12 +972,16 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
940
972
  raw_entries, entry_dates = usage_snapshot.merge_live_with_snapshot(
941
973
  raw_entries, entry_dates, span_from, span_to
942
974
  )
975
+ raw_entries, entry_dates = ledger_backfill.merge_with_ledger(
976
+ raw_entries, entry_dates, ledger_days, span_from, span_to
977
+ )
943
978
 
944
979
  entries = [entry for entry in raw_entries if date_from <= entry_dates[id(entry)] <= date_to]
945
980
 
981
+ real_entries = [entry for entry in entries if not ledger_backfill.is_ledger_entry(entry)]
946
982
  total_tokens = sum(entry.total_tokens for entry in entries)
947
983
  total_cost = 0.0
948
- session_ids = {entry.session_id for entry in entries}
984
+ session_ids = {entry.session_id for entry in real_entries}
949
985
  active_dates = {entry_dates[id(entry)] for entry in entries}
950
986
  total_days = (date_to - date_from).days + 1
951
987
  comparison = _build_comparison(raw_entries, entry_dates, period, date_from, date_to)
@@ -974,7 +1010,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
974
1010
  lambda: defaultdict(lambda: defaultdict(int))
975
1011
  )
976
1012
  daily_totals: dict[date, dict[str, Any]] = defaultdict(lambda: {"tokens": 0, "cost": 0.0})
977
- cube_totals: dict[tuple[date, str, str, str], dict[str, int | float]] = defaultdict(
1013
+ cube_totals: dict[tuple[date, str, str, str, bool], dict[str, int | float]] = defaultdict(
978
1014
  lambda: {
979
1015
  "input_tokens": 0,
980
1016
  "output_tokens": 0,
@@ -990,18 +1026,20 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
990
1026
  total_cost += cost
991
1027
  agent_totals = by_agent_totals[entry.agent_id or "unknown"]
992
1028
  agent_totals["tokens"] += entry.total_tokens
993
- agent_totals["input_tokens"] += entry.input_tokens
994
- agent_totals["output_tokens"] += entry.output_tokens
995
- agent_totals["cache_creation_tokens"] += entry.cache_creation_tokens
996
- agent_totals["cache_read_tokens"] += entry.cache_read_tokens
1029
+ if not ledger_backfill.is_ledger_entry(entry):
1030
+ agent_totals["input_tokens"] += entry.input_tokens
1031
+ agent_totals["output_tokens"] += entry.output_tokens
1032
+ agent_totals["cache_creation_tokens"] += entry.cache_creation_tokens
1033
+ agent_totals["cache_read_tokens"] += entry.cache_read_tokens
1034
+ agent_totals["sessions"].add(entry.session_id)
997
1035
  agent_totals["cost"] += cost
998
- agent_totals["sessions"].add(entry.session_id)
999
1036
  agent_totals["messages"] += entry.message_count
1000
1037
 
1001
1038
  project = by_project_totals[entry.project or "unknown"]
1002
1039
  project["tokens"] += entry.total_tokens
1003
1040
  project["cost"] += cost
1004
- project["sessions"].add(entry.session_id)
1041
+ if not ledger_backfill.is_ledger_entry(entry):
1042
+ project["sessions"].add(entry.session_id)
1005
1043
 
1006
1044
  model = by_model_totals[entry.model or "unknown"]
1007
1045
  model["tokens"] += entry.total_tokens
@@ -1026,6 +1064,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1026
1064
  entry.agent_id or "unknown",
1027
1065
  entry.model or "unknown",
1028
1066
  entry.project or "unknown",
1067
+ ledger_backfill.is_ledger_entry(entry),
1029
1068
  )
1030
1069
  ]
1031
1070
  cube_row["input_tokens"] += entry.input_tokens
@@ -1118,6 +1157,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1118
1157
  cube_agent_id,
1119
1158
  cube_model_name,
1120
1159
  cube_project_name,
1160
+ _cube_is_ledger,
1121
1161
  ), values in cube_totals.items():
1122
1162
  row_tokens = (
1123
1163
  int(values["input_tokens"])
@@ -1162,6 +1202,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1162
1202
  cube_agent_id,
1163
1203
  cube_model_name,
1164
1204
  cube_project_name,
1205
+ cube_is_ledger,
1165
1206
  ), values in cube_totals.items():
1166
1207
  cube_rows.append(
1167
1208
  [
@@ -1175,6 +1216,8 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1175
1216
  int(values["cache_read_tokens"]),
1176
1217
  _round_cost(float(values["cost"])),
1177
1218
  int(values["message_count"]),
1219
+ # Optional index 10 marks ledger totals; legacy real rows stay unchanged.
1220
+ *([1] if cube_is_ledger else []),
1178
1221
  ]
1179
1222
  )
1180
1223
  cube_rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
@@ -1202,10 +1245,13 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1202
1245
  top_sessions: list[TopSessionReportRow] = []
1203
1246
  now = datetime.now().astimezone(timezone.utc)
1204
1247
  weekly_entries = raw_entries if weekly_preloaded is None else weekly_preloaded
1248
+ weekly_entries = [
1249
+ entry for entry in weekly_entries if not ledger_backfill.is_ledger_entry(entry)
1250
+ ]
1205
1251
  windows = dispatch_ledger.load_windows() if weekly_entries else {}
1206
1252
  estimates = dispatch_ledger.estimate_sessions(weekly_entries, windows, now)
1207
1253
  sessions_by_cost = sorted(
1208
- aggregate_sessions(entries), key=lambda session: session.cost_usd, reverse=True
1254
+ aggregate_sessions(real_entries), key=lambda session: session.cost_usd, reverse=True
1209
1255
  )
1210
1256
  sessions_by_tokens = sorted(
1211
1257
  sessions_by_cost, key=lambda session: session.total_tokens, reverse=True
@@ -1238,7 +1284,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1238
1284
  )
1239
1285
 
1240
1286
  year_data = _load_year_data_cached(agents)
1241
- usage_snapshot.record_entries(entries, entry_dates=entry_dates)
1287
+ usage_snapshot.record_entries(real_entries, entry_dates=entry_dates)
1242
1288
 
1243
1289
  return {
1244
1290
  "period": period,
@@ -1247,10 +1293,10 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
1247
1293
  "date_to": date_to.isoformat(),
1248
1294
  "summary": {
1249
1295
  "total_tokens": total_tokens,
1250
- "input_tokens": sum(entry.input_tokens for entry in entries),
1251
- "output_tokens": sum(entry.output_tokens for entry in entries),
1252
- "cache_creation_tokens": sum(entry.cache_creation_tokens for entry in entries),
1253
- "cache_read_tokens": sum(entry.cache_read_tokens for entry in entries),
1296
+ "input_tokens": sum(entry.input_tokens for entry in real_entries),
1297
+ "output_tokens": sum(entry.output_tokens for entry in real_entries),
1298
+ "cache_creation_tokens": sum(entry.cache_creation_tokens for entry in real_entries),
1299
+ "cache_read_tokens": sum(entry.cache_read_tokens for entry in real_entries),
1254
1300
  "cost_usd": _round_cost(total_cost),
1255
1301
  "sessions": len(session_ids),
1256
1302
  # by_project 只留前 10 名,敘述句要報真實專案數就得另外帶
@@ -260,8 +260,8 @@
260
260
  "report_fixed_range": "固定區間",
261
261
  "report_fixed_group_note": "以下區塊不受上方日期篩選影響",
262
262
  "report_model": "模型",
263
- "report_appendix_title": "數字怎麼算的",
264
- "report_appendix_desc": "Token 組成 · 快取命中率 · 花費可信度",
263
+ "report_appendix_title": "花費怎麼算的",
264
+ "report_appendix_desc": "公開 API 價目換算 · 花費可信度",
265
265
  "report_narrative": "過去這段時間你燒了 {tokens} tokens。{peak_date} 是高峰({peak_tokens}/天),主力 LLM 是 {top_model}。",
266
266
  "report_period": "區間",
267
267
  "report_project": "專案",
@@ -270,7 +270,7 @@
270
270
  "report_persona_active_hours": "活躍時段",
271
271
  "report_persona_caption": "你最常在 {h1}{h2} 與 AI 協作",
272
272
  "report_persona_caption_second": " 和 {h2}",
273
- "report_persona_peak": "峰值 {count} 則",
273
+ "report_persona_peak": "峰值 {count} 則訊息(含工具呼叫)",
274
274
  "report_persona_empty": "這段期間還沒有足夠資料",
275
275
  "report_recent_titles_heading": "最近在做什麼",
276
276
  "report_persona_section": "使用習慣",
@@ -539,7 +539,9 @@
539
539
  "quota_aware_action_failed": "無法切換額度感知模式。",
540
540
  "claude_beginner_error": "新手模式:{error}",
541
541
  "claude_desktop_updated": "Claude 桌面版 · {minutes} 分鐘前更新",
542
- "claude_desktop_stale": "Claude 桌面版 · 資料過期 · {minutes} 分鐘前更新"
542
+ "claude_desktop_stale": "Claude 桌面版 · 資料過期 · {minutes} 分鐘前更新",
543
+ "report_project_others": "其他專案",
544
+ "report_ledger_pct_note": "Claude、Codex、Antigravity 三欄是約佔該工具週額度的百分比。"
543
545
  },
544
546
  "en": {
545
547
  "panel_default_name": "Default",
@@ -802,8 +804,8 @@
802
804
  "report_fixed_range": "Fixed range",
803
805
  "report_fixed_group_note": "The sections below don't follow the date filter above",
804
806
  "report_model": "Model",
805
- "report_appendix_title": "How these numbers are calculated",
806
- "report_appendix_desc": "Token mix · Cache hit rate · Cost confidence",
807
+ "report_appendix_title": "How cost is calculated",
808
+ "report_appendix_desc": "Public API price conversion · Cost confidence",
807
809
  "report_narrative": "You burned {tokens} tokens. Peak was {peak_date} ({peak_tokens}/day). Top model: {top_model}.",
808
810
  "report_period": "Period",
809
811
  "report_project": "Project",
@@ -812,7 +814,7 @@
812
814
  "report_persona_active_hours": "Active hours",
813
815
  "report_persona_caption": "You most often collaborate with AI at {h1}{h2}",
814
816
  "report_persona_caption_second": " and {h2}",
815
- "report_persona_peak": "Peak {count} messages",
817
+ "report_persona_peak": "Peak: {count} messages (incl. tool calls)",
816
818
  "report_persona_empty": "Not enough data in this period yet",
817
819
  "report_recent_titles_heading": "What you worked on",
818
820
  "report_persona_section": "Usage habits",
@@ -1081,7 +1083,9 @@
1081
1083
  "quota_aware_action_failed": "Could not change Quota-Aware Mode.",
1082
1084
  "claude_beginner_error": "Beginner Mode: {error}",
1083
1085
  "claude_desktop_updated": "Claude Desktop · updated {minutes}m ago",
1084
- "claude_desktop_stale": "Claude Desktop · stale · updated {minutes}m ago"
1086
+ "claude_desktop_stale": "Claude Desktop · stale · updated {minutes}m ago",
1087
+ "report_project_others": "Other projects",
1088
+ "report_ledger_pct_note": "The Claude, Codex, and Antigravity columns are approximate shares of each tool’s weekly quota."
1085
1089
  },
1086
1090
  "zh-CN": {
1087
1091
  "panel_default_name": "默认",
@@ -1344,8 +1348,8 @@
1344
1348
  "report_fixed_range": "Fixed range",
1345
1349
  "report_fixed_group_note": "以下区块不受上方日期筛选影响",
1346
1350
  "report_model": "模型",
1347
- "report_appendix_title": "数字怎么算的",
1348
- "report_appendix_desc": "Token 组成 · 缓存命中率 · 花费可信度",
1351
+ "report_appendix_title": "花费怎么算的",
1352
+ "report_appendix_desc": "公开 API 价格换算 · 花费可信度",
1349
1353
  "report_narrative": "过去这段时间你烧了 {tokens} tokens。{peak_date} 是高峰({peak_tokens}/天),主力 LLM 是 {top_model}。",
1350
1354
  "report_period": "区间",
1351
1355
  "report_project": "项目",
@@ -1354,7 +1358,7 @@
1354
1358
  "report_persona_active_hours": "活跃时段",
1355
1359
  "report_persona_caption": "你最常在 {h1}{h2} 与 AI 协作",
1356
1360
  "report_persona_caption_second": " 和 {h2}",
1357
- "report_persona_peak": "峰值 {count} 条",
1361
+ "report_persona_peak": "峰值 {count} 条消息(含工具调用)",
1358
1362
  "report_persona_empty": "这段期间还没有足够数据",
1359
1363
  "report_recent_titles_heading": "最近在做什么",
1360
1364
  "report_persona_section": "使用习惯",
@@ -1623,7 +1627,9 @@
1623
1627
  "quota_aware_action_failed": "无法切换额度感知模式。",
1624
1628
  "claude_beginner_error": "新手模式:{error}",
1625
1629
  "claude_desktop_updated": "Claude 桌面版 · {minutes} 分钟前更新",
1626
- "claude_desktop_stale": "Claude 桌面版 · 数据过期 · {minutes} 分钟前更新"
1630
+ "claude_desktop_stale": "Claude 桌面版 · 数据过期 · {minutes} 分钟前更新",
1631
+ "report_project_others": "其他项目",
1632
+ "report_ledger_pct_note": "Claude、Codex、Antigravity 三列是约占该工具周额度的百分比。"
1627
1633
  },
1628
1634
  "ja": {
1629
1635
  "panel_default_name": "デフォルト",
@@ -1886,8 +1892,8 @@
1886
1892
  "report_fixed_range": "Fixed range",
1887
1893
  "report_fixed_group_note": "以下のセクションは上の日付フィルターの影響を受けません",
1888
1894
  "report_model": "モデル",
1889
- "report_appendix_title": "数値の算出方法",
1890
- "report_appendix_desc": "トークン構成 · キャッシュヒット率 · コストの信頼度",
1895
+ "report_appendix_title": "コストの算出方法",
1896
+ "report_appendix_desc": "公開 API 料金での換算 · コストの信頼度",
1891
1897
  "report_narrative": "この期間に {tokens} tokens を使いました。ピークは {peak_date}({peak_tokens}/日)。主力モデル:{top_model}。",
1892
1898
  "report_period": "期間",
1893
1899
  "report_project": "プロジェクト",
@@ -1896,7 +1902,7 @@
1896
1902
  "report_persona_active_hours": "アクティブな時間帯",
1897
1903
  "report_persona_caption": "{h1}{h2} の時間帯に AI との共同作業が最も多い傾向があります",
1898
1904
  "report_persona_caption_second": " と {h2}",
1899
- "report_persona_peak": "ピーク {count} 件",
1905
+ "report_persona_peak": "ピーク: {count} 件のメッセージ(ツール呼び出しを含む)",
1900
1906
  "report_persona_empty": "この期間はまだ十分なデータがありません",
1901
1907
  "report_recent_titles_heading": "最近の作業",
1902
1908
  "report_persona_section": "利用習慣",
@@ -2165,7 +2171,9 @@
2165
2171
  "quota_aware_action_failed": "クォータ認識モードを切り替えられませんでした。",
2166
2172
  "claude_beginner_error": "初心者モード:{error}",
2167
2173
  "claude_desktop_updated": "Claude デスクトップ · {minutes}分前に更新",
2168
- "claude_desktop_stale": "Claude デスクトップ · 古いデータ · {minutes}分前に更新"
2174
+ "claude_desktop_stale": "Claude デスクトップ · 古いデータ · {minutes}分前に更新",
2175
+ "report_project_others": "その他のプロジェクト",
2176
+ "report_ledger_pct_note": "Claude、Codex、Antigravity の各列は、各ツールの週間上限に対する推定使用率です。"
2169
2177
  },
2170
2178
  "ko": {
2171
2179
  "panel_default_name": "기본",
@@ -2428,8 +2436,8 @@
2428
2436
  "report_fixed_range": "Fixed range",
2429
2437
  "report_fixed_group_note": "아래 섹션은 위의 날짜 필터와 관계없이 표시됩니다",
2430
2438
  "report_model": "모델",
2431
- "report_appendix_title": "수치 계산 방식",
2432
- "report_appendix_desc": "토큰 구성 · 캐시 적중률 · 비용 신뢰도",
2439
+ "report_appendix_title": "비용 계산 방식",
2440
+ "report_appendix_desc": "공개 API 가격 환산 · 비용 신뢰도",
2433
2441
  "report_narrative": "이 기간 동안 {tokens} tokens을 사용했습니다. 최고점은 {peak_date}({peak_tokens}/일). 주력 모델: {top_model}.",
2434
2442
  "report_period": "기간",
2435
2443
  "report_project": "프로젝트",
@@ -2438,7 +2446,7 @@
2438
2446
  "report_persona_active_hours": "활동 시간대",
2439
2447
  "report_persona_caption": "주로 {h1}{h2}에 AI와 협업합니다",
2440
2448
  "report_persona_caption_second": " 및 {h2}",
2441
- "report_persona_peak": "최고 {count}건",
2449
+ "report_persona_peak": "최고 {count}개 메시지(도구 호출 포함)",
2442
2450
  "report_persona_empty": "이 기간에는 아직 충분한 데이터가 없습니다",
2443
2451
  "report_recent_titles_heading": "최근 작업",
2444
2452
  "report_persona_section": "사용 습관",
@@ -2707,6 +2715,8 @@
2707
2715
  "quota_aware_action_failed": "할당량 인식 모드를 변경할 수 없습니다.",
2708
2716
  "claude_beginner_error": "초보자 모드: {error}",
2709
2717
  "claude_desktop_updated": "Claude 데스크톱 · {minutes}분 전 업데이트",
2710
- "claude_desktop_stale": "Claude 데스크톱 · 오래된 데이터 · {minutes}분 전 업데이트"
2718
+ "claude_desktop_stale": "Claude 데스크톱 · 오래된 데이터 · {minutes}분 전 업데이트",
2719
+ "report_project_others": "기타 프로젝트",
2720
+ "report_ledger_pct_note": "Claude, Codex, Antigravity 열은 각 도구의 주간 한도 대비 대략적인 사용 비율입니다."
2711
2721
  }
2712
2722
  }
@@ -76,6 +76,9 @@ _CLIENT_SECRET = "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"
76
76
  _DEFAULT_RETRY_AFTER_SECONDS = 60.0
77
77
  _MAX_RETRY_AFTER_SECONDS = 3600.0
78
78
  _rate_limit_until_monotonic = 0.0
79
+ # When each group last appeared in a real API response; in memory only, reset on restart.
80
+ _group_seen_at: dict[str, datetime] = {}
81
+ _MISSING_GROUP_GRACE_MINUTES = 30
79
82
 
80
83
 
81
84
  def _user_agent() -> str:
@@ -160,10 +163,38 @@ def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
160
163
  probed = probe_quota()
161
164
  if probed is None:
162
165
  return cached
166
+ probed = _carry_missing_groups(probed, cached)
163
167
  _write_cache(probed)
164
168
  return probed
165
169
 
166
170
 
171
+ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
172
+ """Keep a group the API skipped this once, so the group toggle has something to show.
173
+
174
+ A group is carried only while the API last returned it within the grace window, so a
175
+ group Google really removes drops out instead of living on in the cache forever.
176
+ """
177
+ now = datetime.now(UTC)
178
+ for group in probed.groups:
179
+ _group_seen_at[group.name] = now
180
+ if cached is None:
181
+ return probed
182
+ names = {group.name for group in probed.groups}
183
+ grace = timedelta(minutes=_MISSING_GROUP_GRACE_MINUTES)
184
+ missing = [
185
+ group
186
+ for group in cached.groups
187
+ if group.name not in names
188
+ and group.name in _group_seen_at
189
+ and now - _group_seen_at[group.name] <= grace
190
+ ]
191
+ if not missing:
192
+ return probed
193
+ order = {group.name: index for index, group in enumerate(cached.groups)}
194
+ groups = sorted([*probed.groups, *missing], key=lambda group: order.get(group.name, 0))
195
+ return AgyQuotaResult(groups=groups, fetched_at=probed.fetched_at)
196
+
197
+
167
198
  def _resolve_access_token(timeout: float) -> str | None:
168
199
  """Return a usable access token, refreshing when stale.
169
200