usage-cli 0.33.0__tar.gz → 0.33.1__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 (261) hide show
  1. {usage_cli-0.33.0 → usage_cli-0.33.1}/PKG-INFO +5 -2
  2. {usage_cli-0.33.0 → usage_cli-0.33.1}/README.md +3 -1
  3. usage_cli-0.33.1/THIRD_PARTY_NOTICES.md +96 -0
  4. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/rate_limits.py +23 -2
  5. {usage_cli-0.33.0 → usage_cli-0.33.1}/i18n.json +20 -10
  6. usage_cli-0.33.1/loaders/_shared_cache_file.py +67 -0
  7. usage_cli-0.33.1/loaders/chromium_cache.py +215 -0
  8. usage_cli-0.33.1/loaders/claude_desktop.py +160 -0
  9. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/state.py +12 -1
  10. {usage_cli-0.33.0 → usage_cli-0.33.1}/pyproject.toml +2 -1
  11. usage_cli-0.33.1/tests/test_chromium_cache.py +249 -0
  12. usage_cli-0.33.1/tests/test_claude_desktop.py +166 -0
  13. usage_cli-0.33.1/tests/test_desktop_quota_display.py +232 -0
  14. usage_cli-0.33.1/tests/test_desktop_quota_review.py +269 -0
  15. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_quota_label.py +113 -6
  16. usage_cli-0.33.1/tests/test_taskbar_menu.py +189 -0
  17. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_wintray.py +86 -3
  18. {usage_cli-0.33.0 → usage_cli-0.33.1}/tui/app.py +4 -2
  19. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/PKG-INFO +5 -2
  20. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/SOURCES.txt +9 -0
  21. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/requires.txt +1 -0
  22. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_client.py +58 -15
  23. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/app.py +102 -2
  24. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/menu.py +16 -1
  25. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/quota_label.py +12 -2
  26. usage_cli-0.33.1/wintray/taskbar_menu.py +86 -0
  27. usage_cli-0.33.0/THIRD_PARTY_NOTICES.md +0 -28
  28. {usage_cli-0.33.0 → usage_cli-0.33.1}/LICENSE +0 -0
  29. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/__init__.py +0 -0
  30. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/agy.py +0 -0
  31. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/claude.py +0 -0
  32. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/codex.py +0 -0
  33. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/grok.py +0 -0
  34. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/muse.py +0 -0
  35. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/registry.py +0 -0
  36. {usage_cli-0.33.0 → usage_cli-0.33.1}/adapters/types.py +0 -0
  37. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/__init__.py +0 -0
  38. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/aggregator.py +0 -0
  39. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/blocks.py +0 -0
  40. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/diagnoser.py +0 -0
  41. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/dispatch_ledger.py +0 -0
  42. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/insights.py +0 -0
  43. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/persona_loader.py +0 -0
  44. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/reporter.py +0 -0
  45. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/subscription.py +0 -0
  46. {usage_cli-0.33.0 → usage_cli-0.33.1}/analyzer/usage_snapshot.py +0 -0
  47. {usage_cli-0.33.0 → usage_cli-0.33.1}/doctor.py +0 -0
  48. {usage_cli-0.33.0 → usage_cli-0.33.1}/fsevents_watch.py +0 -0
  49. {usage_cli-0.33.0 → usage_cli-0.33.1}/i18n.py +0 -0
  50. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/__init__.py +0 -0
  51. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/claude_pane.py +0 -0
  52. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/codex_hook_trust.py +0 -0
  53. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/login_item.py +0 -0
  54. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/session_hooks.py +0 -0
  55. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/setup_hook.py +0 -0
  56. {usage_cli-0.33.0 → usage_cli-0.33.1}/installer/statusline_settings.py +0 -0
  57. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/__init__.py +0 -0
  58. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/agy_disk_cache.py +0 -0
  59. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/agy_loader.py +0 -0
  60. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/agy_quota_probe.py +0 -0
  61. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/cache_quarantine.py +0 -0
  62. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/claude_paths.py +0 -0
  63. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/codex_disk_cache.py +0 -0
  64. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/codex_events.py +0 -0
  65. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/codex_fork_replay.py +0 -0
  66. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/codex_loader.py +0 -0
  67. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/codex_paths.py +0 -0
  68. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/disk_cache_common.py +0 -0
  69. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/disk_cache_lifecycle.py +0 -0
  70. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/grok_loader.py +0 -0
  71. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/grok_quota_probe.py +0 -0
  72. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/history_disk_cache.py +0 -0
  73. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/history_loader.py +0 -0
  74. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/jsonl_limits.py +0 -0
  75. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/jsonl_utils.py +0 -0
  76. {usage_cli-0.33.0 → usage_cli-0.33.1}/loaders/muse_loader.py +0 -0
  77. {usage_cli-0.33.0 → usage_cli-0.33.1}/main.py +0 -0
  78. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/__init__.py +0 -0
  79. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/actions.py +0 -0
  80. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/agy.py +0 -0
  81. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/app.py +0 -0
  82. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/chrome.py +0 -0
  83. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/grok.py +0 -0
  84. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/manual_refresh.py +0 -0
  85. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/menu.py +0 -0
  86. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/new_badge.py +0 -0
  87. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/notify.py +0 -0
  88. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/popover.py +0 -0
  89. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/prefs.py +0 -0
  90. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/refresh.py +0 -0
  91. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/title.py +0 -0
  92. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/update.py +0 -0
  93. {usage_cli-0.33.0 → usage_cli-0.33.1}/menubar/whats_new.py +0 -0
  94. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/__init__.py +0 -0
  95. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/base.py +0 -0
  96. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/dynamic_height.py +0 -0
  97. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/panel_scale.py +0 -0
  98. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/panel_window.py +0 -0
  99. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/panel_window_state.py +0 -0
  100. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/payload.py +0 -0
  101. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/web_panel.py +0 -0
  102. {usage_cli-0.33.0 → usage_cli-0.33.1}/panels/window_drag.py +0 -0
  103. {usage_cli-0.33.0 → usage_cli-0.33.1}/prefs.py +0 -0
  104. {usage_cli-0.33.0 → usage_cli-0.33.1}/pricing.py +0 -0
  105. {usage_cli-0.33.0 → usage_cli-0.33.1}/project_resolver.py +0 -0
  106. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/__init__.py +0 -0
  107. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/agy_window_keeper.py +0 -0
  108. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/burn_rate.py +0 -0
  109. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/codex_window_keeper.py +0 -0
  110. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/keeper_outcome.py +0 -0
  111. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/quota_snapshot.py +0 -0
  112. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/usage_rate.py +0 -0
  113. {usage_cli-0.33.0 → usage_cli-0.33.1}/quota/window_keeper.py +0 -0
  114. {usage_cli-0.33.0 → usage_cli-0.33.1}/service_status.py +0 -0
  115. {usage_cli-0.33.0 → usage_cli-0.33.1}/setup.cfg +0 -0
  116. {usage_cli-0.33.0 → usage_cli-0.33.1}/setup_app.py +0 -0
  117. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_adapters_claude.py +0 -0
  118. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_adapters_codex.py +0 -0
  119. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_adapters_rate_limits.py +0 -0
  120. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_aggregator.py +0 -0
  121. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_agy_adapter.py +0 -0
  122. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_agy_disk_cache.py +0 -0
  123. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_agy_loader.py +0 -0
  124. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_agy_quota_probe.py +0 -0
  125. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_agy_window_keeper.py +0 -0
  126. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_analyzer_blocks.py +0 -0
  127. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_analyzer_pipeline.py +0 -0
  128. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_burn_rate.py +0 -0
  129. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_cache_quarantine.py +0 -0
  130. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_check_upstream.py +0 -0
  131. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_claude_pane.py +0 -0
  132. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_claude_paths.py +0 -0
  133. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_codex_consistency.py +0 -0
  134. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_codex_hook_trust.py +0 -0
  135. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_codex_loader.py +0 -0
  136. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_codex_window_keeper.py +0 -0
  137. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_diagnoser.py +0 -0
  138. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_disk_cache_shards.py +0 -0
  139. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_dispatch_ledger.py +0 -0
  140. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_doctor.py +0 -0
  141. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_dynamic_height.py +0 -0
  142. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_fsevents_watch.py +0 -0
  143. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_grok_loader.py +0 -0
  144. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_grok_quota_probe.py +0 -0
  145. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_history_loader.py +0 -0
  146. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_hook_scripts_are_standalone.py +0 -0
  147. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_html_report_snapshot.py +0 -0
  148. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_i18n_format_fallback.py +0 -0
  149. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_i18n_key_parity.py +0 -0
  150. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_i18n_packaged_path.py +0 -0
  151. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_insights.py +0 -0
  152. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_jsonl_utils.py +0 -0
  153. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_keeper_menu.py +0 -0
  154. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_keeper_outcome.py +0 -0
  155. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_login_item.py +0 -0
  156. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_main.py +0 -0
  157. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_manual_refresh.py +0 -0
  158. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar.py +0 -0
  159. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_agy.py +0 -0
  160. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_grok.py +0 -0
  161. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_notify.py +0 -0
  162. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_prefs.py +0 -0
  163. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_state.py +0 -0
  164. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_menubar_update.py +0 -0
  165. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_muse_loader.py +0 -0
  166. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_new_badge.py +0 -0
  167. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_packaged_resources.py +0 -0
  168. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_packaging.py +0 -0
  169. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_panel_footer_layout.py +0 -0
  170. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_panel_scale.py +0 -0
  171. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_panel_window_state.py +0 -0
  172. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_panels.py +0 -0
  173. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_panels_lazy_import.py +0 -0
  174. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_persona_loader.py +0 -0
  175. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_pricing.py +0 -0
  176. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_project_resolver.py +0 -0
  177. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_quota_aware.py +0 -0
  178. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_report_daily_chart.py +0 -0
  179. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_report_filter_js.py +0 -0
  180. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_reporter_build_data.py +0 -0
  181. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_service_status.py +0 -0
  182. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_session_resume.py +0 -0
  183. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_setup_app.py +0 -0
  184. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_setup_hook.py +0 -0
  185. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_setup_hook_coexistence.py +0 -0
  186. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_setup_hook_resume.py +0 -0
  187. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_setup_hook_terse.py +0 -0
  188. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_subprocess_utils.py +0 -0
  189. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_subscription.py +0 -0
  190. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_time_utils.py +0 -0
  191. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_tui.py +0 -0
  192. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_tui_sprite.py +0 -0
  193. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_ui_tables.py +0 -0
  194. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_update_checker.py +0 -0
  195. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_update_gate.py +0 -0
  196. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_update_release_notes.py +0 -0
  197. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_cli.py +0 -0
  198. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_client.py +0 -0
  199. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_diagnosis_snapshot.py +0 -0
  200. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_dir_sweeper.py +0 -0
  201. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_lang.py +0 -0
  202. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_logging.py +0 -0
  203. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_notifications.py +0 -0
  204. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_rate.py +0 -0
  205. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_snapshot.py +0 -0
  206. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_snapshot_replay.py +0 -0
  207. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_statusline.py +0 -0
  208. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_statusline_agy.py +0 -0
  209. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_statusline_forwarder.py +0 -0
  210. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_statusline_grok.py +0 -0
  211. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_terse_mode.py +0 -0
  212. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_usage_terse_reminder.py +0 -0
  213. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_web_panel_payload.py +0 -0
  214. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_whats_new.py +0 -0
  215. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_wheel_packaging.py +0 -0
  216. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_win_login_item.py +0 -0
  217. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_window_keeper.py +0 -0
  218. {usage_cli-0.33.0 → usage_cli-0.33.1}/tests/test_windows_manifest.py +0 -0
  219. {usage_cli-0.33.0 → usage_cli-0.33.1}/tui/__init__.py +0 -0
  220. {usage_cli-0.33.0 → usage_cli-0.33.1}/tui/sprite.py +0 -0
  221. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/__init__.py +0 -0
  222. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/html_report.py +0 -0
  223. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_charts.py +0 -0
  224. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_daily_chart.py +0 -0
  225. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_filter.py +0 -0
  226. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_ledger.py +0 -0
  227. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_scripts.py +0 -0
  228. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/report_styles.py +0 -0
  229. {usage_cli-0.33.0 → usage_cli-0.33.1}/ui/tables.py +0 -0
  230. {usage_cli-0.33.0 → usage_cli-0.33.1}/updates/__init__.py +0 -0
  231. {usage_cli-0.33.0 → usage_cli-0.33.1}/updates/checker.py +0 -0
  232. {usage_cli-0.33.0 → usage_cli-0.33.1}/updates/gate.py +0 -0
  233. {usage_cli-0.33.0 → usage_cli-0.33.1}/updates/release_notes.py +0 -0
  234. {usage_cli-0.33.0 → usage_cli-0.33.1}/updates/whats_new.py +0 -0
  235. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/dependency_links.txt +0 -0
  236. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/entry_points.txt +0 -0
  237. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.egg-info/top_level.txt +0 -0
  238. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_cli.py +0 -0
  239. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/__init__.py +0 -0
  240. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/subprocess_utils.py +0 -0
  241. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/time_utils.py +0 -0
  242. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/usage_dir_sweeper.py +0 -0
  243. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/usage_lang.py +0 -0
  244. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_common/usage_logging.py +0 -0
  245. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_diagnosis_snapshot.py +0 -0
  246. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_notifications.py +0 -0
  247. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_quota_aware.py +0 -0
  248. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_session_resume.py +0 -0
  249. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_statusline.py +0 -0
  250. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_statusline_agy.py +0 -0
  251. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_statusline_forwarder.py +0 -0
  252. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_statusline_grok.py +0 -0
  253. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_terse_mode.py +0 -0
  254. {usage_cli-0.33.0 → usage_cli-0.33.1}/usage_terse_reminder.py +0 -0
  255. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/__init__.py +0 -0
  256. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/login_item.py +0 -0
  257. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/prefs.py +0 -0
  258. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/taskbar_obstacles.py +0 -0
  259. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/taskbar_overlay.py +0 -0
  260. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/watch.py +0 -0
  261. {usage_cli-0.33.0 → usage_cli-0.33.1}/wintray/whats_new.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.33.0
3
+ Version: 0.33.1
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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  License-File: THIRD_PARTY_NOTICES.md
10
+ Requires-Dist: zstandard<1.0.0,>=0.25.0
10
11
  Requires-Dist: pyobjc-framework-Cocoa>=11.0; sys_platform == "darwin"
11
12
  Requires-Dist: pyobjc-framework-Quartz>=11.0; sys_platform == "darwin"
12
13
  Requires-Dist: pyobjc-framework-UserNotifications>=11.0; sys_platform == "darwin"
@@ -101,6 +102,8 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
101
102
 
102
103
  ## Privacy & Data Sources
103
104
 
105
+ Claude Desktop chat quota is also supported without installing the standalone Claude Code CLI or configuring a status line. Keep Claude Desktop open: when Claude Code quota files are unavailable, usage reads its local `plan-usage-history.json` (including Microsoft Store installations on Windows). When a recent response in its local Chromium block-file HTTP cache matches the organization, usage also reads the exact session and weekly reset times. Newer cached observations take precedence over throttled history samples; older cache must match the percentages. Missing, unsupported, expired, or inconsistent cache data leaves the countdown unknown; times are never estimated. Desktop samples normally update every 5–15 minutes. The panel shows the observation age, marks it stale after 30 minutes, and stops displaying it after two hours. The most recent organization sample is used; custom desktop profiles are not discovered. No cookies, login tokens, or API calls are needed. These quota caches contain no per-project token counts. Desktop sessions that also write compatible Claude Code logs under `~/.claude/projects/` are counted by the existing project/token reports.
106
+
104
107
  - Claude Code and Codex numbers are read from local log files on your machine.
105
108
  - Antigravity quota requires network access, and only if you use it: quota is fetched from Google's official quota endpoint using 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` reads that credential without writing it back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
106
109
  - Background network activity: the Antigravity quota/token endpoints above, public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (falls back to built-in prices offline), and occasionally checking GitHub for a new version. Claude Code and Codex log contents are never uploaded.
@@ -109,7 +112,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
109
112
  ## Requirements
110
113
 
111
114
  - macOS 12 (Monterey) or newer, or Windows 10/11
112
- - Claude Code, Codex, Antigravity, or Grok CLI has been used at least once (so local usage data exists).
115
+ - Local usage data from Claude Code, Codex, Antigravity, or Grok CLI, or a running Claude Desktop app with plan-usage history.
113
116
  - (Source runs only) Python 3.13.
114
117
 
115
118
  ## Install
@@ -80,6 +80,8 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
80
80
 
81
81
  ## Privacy & Data Sources
82
82
 
83
+ Claude Desktop chat quota is also supported without installing the standalone Claude Code CLI or configuring a status line. Keep Claude Desktop open: when Claude Code quota files are unavailable, usage reads its local `plan-usage-history.json` (including Microsoft Store installations on Windows). When a recent response in its local Chromium block-file HTTP cache matches the organization, usage also reads the exact session and weekly reset times. Newer cached observations take precedence over throttled history samples; older cache must match the percentages. Missing, unsupported, expired, or inconsistent cache data leaves the countdown unknown; times are never estimated. Desktop samples normally update every 5–15 minutes. The panel shows the observation age, marks it stale after 30 minutes, and stops displaying it after two hours. The most recent organization sample is used; custom desktop profiles are not discovered. No cookies, login tokens, or API calls are needed. These quota caches contain no per-project token counts. Desktop sessions that also write compatible Claude Code logs under `~/.claude/projects/` are counted by the existing project/token reports.
84
+
83
85
  - Claude Code and Codex numbers are read from local log files on your machine.
84
86
  - Antigravity quota requires network access, and only if you use it: quota is fetched from Google's official quota endpoint using 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` reads that credential without writing it back and keeps any refreshed access token in memory only; the call itself reads quota metadata.
85
87
  - Background network activity: the Antigravity quota/token endpoints above, public Claude and Codex status pages to flag outages, a public model-pricing table to estimate cost (falls back to built-in prices offline), and occasionally checking GitHub for a new version. Claude Code and Codex log contents are never uploaded.
@@ -88,7 +90,7 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
88
90
  ## Requirements
89
91
 
90
92
  - macOS 12 (Monterey) or newer, or Windows 10/11
91
- - Claude Code, Codex, Antigravity, or Grok CLI has been used at least once (so local usage data exists).
93
+ - Local usage data from Claude Code, Codex, Antigravity, or Grok CLI, or a running Claude Desktop app with plan-usage history.
92
94
  - (Source runs only) Python 3.13.
93
95
 
94
96
  ## Install
@@ -0,0 +1,96 @@
1
+ # Third-party notices
2
+
3
+ The Windows taskbar label anchoring in `wintray/taskbar_overlay.py` is adapted
4
+ from [Codex Usage Widget](https://github.com/ognjeeen/codex-usage-widget),
5
+ commit `940d722d4044242bbe73f8056014449c2ef827a7`, particularly
6
+ `Infrastructure/Windows/TaskbarWindowInterop.cs`.
7
+
8
+ MIT License
9
+
10
+ Copyright (c) 2026 Ognjen Marinković
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+
31
+ ## Python Zstandard
32
+
33
+ The local Claude HTTP cache reader uses python-zstandard, including its bundled
34
+ Zstandard library, under their BSD licenses.
35
+
36
+ Copyright (c) 2016, Gregory Szorc
37
+ All rights reserved.
38
+
39
+ Redistribution and use in source and binary forms, with or without modification,
40
+ are permitted provided that the following conditions are met:
41
+
42
+ 1. Redistributions of source code must retain the above copyright notice, this
43
+ list of conditions and the following disclaimer.
44
+
45
+ 2. Redistributions in binary form must reproduce the above copyright notice,
46
+ this list of conditions and the following disclaimer in the documentation
47
+ and/or other materials provided with the distribution.
48
+
49
+ 3. Neither the name of the copyright holder nor the names of its contributors
50
+ may be used to endorse or promote products derived from this software without
51
+ specific prior written permission.
52
+
53
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
54
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
55
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
56
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
57
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
58
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
59
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
60
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
61
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
62
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
63
+
64
+
65
+ ## Zstandard
66
+
67
+ BSD License
68
+
69
+ For Zstandard software
70
+
71
+ Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
72
+
73
+ Redistribution and use in source and binary forms, with or without modification,
74
+ are permitted provided that the following conditions are met:
75
+
76
+ * Redistributions of source code must retain the above copyright notice, this
77
+ list of conditions and the following disclaimer.
78
+
79
+ * Redistributions in binary form must reproduce the above copyright notice,
80
+ this list of conditions and the following disclaimer in the documentation
81
+ and/or other materials provided with the distribution.
82
+
83
+ * Neither the name Facebook, nor Meta, nor the names of its contributors may
84
+ be used to endorse or promote products derived from this software without
85
+ specific prior written permission.
86
+
87
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
88
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
89
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
90
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
91
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
92
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
93
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
94
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
95
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
96
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -10,6 +10,8 @@ import os
10
10
  from datetime import datetime, timezone
11
11
  from typing import Any
12
12
 
13
+ import usage_client
14
+
13
15
  from .types import RateLimits
14
16
 
15
17
  STATUS_FILE = os.path.expanduser("~/.claude/usage-status.json")
@@ -42,8 +44,12 @@ def _read_status() -> dict[str, Any] | None:
42
44
 
43
45
  def load_rate_limits() -> RateLimits | None:
44
46
  data = _read_status()
45
- if data is None:
46
- return None
47
+ if data is None or not usage_client._has_complete_rate_limits(data):
48
+ snapshot = usage_client.read_quota_fallback()
49
+ if snapshot is not None:
50
+ return _snapshot_rate_limits(snapshot)
51
+ if data is None:
52
+ return None
47
53
 
48
54
  rl = data.get("rate_limits") or {}
49
55
  five = rl.get("five_hour") or {}
@@ -74,3 +80,18 @@ def load_rate_limits() -> RateLimits | None:
74
80
  model=model_name,
75
81
  updated_at=data.get("_received_at", ""),
76
82
  )
83
+
84
+
85
+ def _snapshot_rate_limits(quota: usage_client.UsageSnapshot) -> RateLimits:
86
+ return RateLimits(
87
+ five_hour_pct=quota.current_percent,
88
+ seven_day_pct=quota.weekly_percent,
89
+ five_hour_resets_at=int(quota.current_reset_at)
90
+ if quota.current_reset_at is not None
91
+ else None,
92
+ seven_day_resets_at=int(quota.weekly_reset_at)
93
+ if quota.weekly_reset_at is not None
94
+ else None,
95
+ model="",
96
+ updated_at=datetime.fromtimestamp(quota.polled_at, timezone.utc).isoformat(),
97
+ )
@@ -69,7 +69,7 @@
69
69
  "awaiting_rate_limits": "請對 Claude Code 發送一句訊息以同步配額",
70
70
  "hook_broken_not_installed": "⚠ 狀態列沒在更新,請按「設定狀態列」重新安裝",
71
71
  "hook_broken_restart": "⚠ 狀態列沒在運作,請重開一次 Claude Code",
72
- "usage_status_missing": "⚠ 找不到狀態檔,請按「設定狀態列」,再打開一次 Claude Code",
72
+ "usage_status_missing": "尚無額度資料。請保持 Claude 桌面版開啟,或設定狀態列後啟動終端機的 Claude Code。",
73
73
  "usage_status_missing_active": "⚠ 配額資料只能靠終端機的狀態列同步,桌面版/背景工作階段不會觸發,請開一次終端機的 Claude Code 來同步",
74
74
  "usage_status_no_quota": "⚠ 狀態檔尚無配額資料,等 Claude Code 再刷新一次 statusLine",
75
75
  "status_error": "錯誤 ({message})",
@@ -542,7 +542,9 @@
542
542
  "quota_aware_enabled_msg": "已開啟額度感知模式。請重啟 Claude Code 套用。",
543
543
  "quota_aware_disabled_msg": "已關閉額度感知模式。請重啟 Claude Code 套用。",
544
544
  "quota_aware_action_failed": "無法切換額度感知模式。",
545
- "claude_beginner_error": "新手模式:{error}"
545
+ "claude_beginner_error": "新手模式:{error}",
546
+ "claude_desktop_updated": "Claude 桌面版 · {minutes} 分鐘前更新",
547
+ "claude_desktop_stale": "Claude 桌面版 · 資料過期 · {minutes} 分鐘前更新"
546
548
  },
547
549
  "en": {
548
550
  "panel_default_name": "Default",
@@ -614,7 +616,7 @@
614
616
  "awaiting_rate_limits": "Send a message in Claude Code to sync your quota",
615
617
  "hook_broken_not_installed": "⚠ Status line is not updating. Click \"Set Up Status Line\" to reinstall it.",
616
618
  "hook_broken_restart": "⚠ Status line hook is not running. Restart Claude Code once.",
617
- "usage_status_missing": "⚠ Status file not found. Click \"Set Up Status Line\", then open Claude Code once.",
619
+ "usage_status_missing": "No quota data yet. Keep Claude Desktop open, or set up the status line and start Claude Code in a terminal.",
618
620
  "usage_status_missing_active": "⚠ Quota data only syncs via the terminal status line — the desktop app and headless sessions don't trigger it. Open Claude Code in a terminal once to sync.",
619
621
  "usage_status_no_quota": "⚠ Status file has no quota data yet. Wait for Claude Code to refresh statusLine once.",
620
622
  "status_error": "Error ({message})",
@@ -1087,7 +1089,9 @@
1087
1089
  "quota_aware_enabled_msg": "Quota-Aware Mode enabled. Restart Claude Code to apply.",
1088
1090
  "quota_aware_disabled_msg": "Quota-Aware Mode disabled. Restart Claude Code to apply.",
1089
1091
  "quota_aware_action_failed": "Could not change Quota-Aware Mode.",
1090
- "claude_beginner_error": "Beginner Mode: {error}"
1092
+ "claude_beginner_error": "Beginner Mode: {error}",
1093
+ "claude_desktop_updated": "Claude Desktop · updated {minutes}m ago",
1094
+ "claude_desktop_stale": "Claude Desktop · stale · updated {minutes}m ago"
1091
1095
  },
1092
1096
  "zh-CN": {
1093
1097
  "panel_default_name": "默认",
@@ -1159,7 +1163,7 @@
1159
1163
  "awaiting_rate_limits": "请对 Claude Code 发送一句消息以同步配额",
1160
1164
  "hook_broken_not_installed": "⚠ 状态栏没有更新,请点「设置状态栏」重新安装",
1161
1165
  "hook_broken_restart": "⚠ 状态栏没有运行,请重开一次 Claude Code",
1162
- "usage_status_missing": "⚠ 找不到状态文件,请点「设置状态栏」,再打开一次 Claude Code",
1166
+ "usage_status_missing": "尚无额度数据。请保持 Claude 桌面版打开,或设置状态栏后启动终端中的 Claude Code。",
1163
1167
  "usage_status_missing_active": "⚠ 配额数据只能靠终端的状态栏同步,桌面版/后台会话不会触发,请打开一次终端的 Claude Code 来同步",
1164
1168
  "usage_status_no_quota": "⚠ 状态文件还没有配额数据,等 Claude Code 再刷新一次 statusLine",
1165
1169
  "status_error": "错误 ({message})",
@@ -1632,7 +1636,9 @@
1632
1636
  "quota_aware_enabled_msg": "已开启额度感知模式。请重启 Claude Code 应用。",
1633
1637
  "quota_aware_disabled_msg": "已关闭额度感知模式。请重启 Claude Code 应用。",
1634
1638
  "quota_aware_action_failed": "无法切换额度感知模式。",
1635
- "claude_beginner_error": "新手模式:{error}"
1639
+ "claude_beginner_error": "新手模式:{error}",
1640
+ "claude_desktop_updated": "Claude 桌面版 · {minutes} 分钟前更新",
1641
+ "claude_desktop_stale": "Claude 桌面版 · 数据过期 · {minutes} 分钟前更新"
1636
1642
  },
1637
1643
  "ja": {
1638
1644
  "panel_default_name": "デフォルト",
@@ -1704,7 +1710,7 @@
1704
1710
  "awaiting_rate_limits": "Claude Code にメッセージを送信してクォータを同期してください",
1705
1711
  "hook_broken_not_installed": "⚠ ステータスラインが更新されていません。「ステータスラインを設定」を押して再インストールしてください。",
1706
1712
  "hook_broken_restart": "⚠ ステータスライン hook が動作していません。Claude Code を一度再起動してください。",
1707
- "usage_status_missing": "⚠ ステータスファイルが見つかりません。「ステータスラインを設定」を押してから、Claude Code を一度起動してください。",
1713
+ "usage_status_missing": "利用枠データがありません。Claude デスクトップを開いたままにするか、ステータスラインを設定してターミナルの Claude Code を起動してください。",
1708
1714
  "usage_status_missing_active": "⚠ クォータ情報はターミナルのステータスラインでのみ同期されます。デスクトップアプリやヘッドレスセッションでは同期されません。ターミナルで Claude Code を一度開いてください。",
1709
1715
  "usage_status_no_quota": "⚠ ステータスファイルにクォータデータがまだありません。Claude Code が statusLine をもう一度更新するまでお待ちください。",
1710
1716
  "status_error": "エラー ({message})",
@@ -2177,7 +2183,9 @@
2177
2183
  "quota_aware_enabled_msg": "クォータ認識モードを有効にしました。Claude Code を再起動してください。",
2178
2184
  "quota_aware_disabled_msg": "クォータ認識モードを無効にしました。Claude Code を再起動してください。",
2179
2185
  "quota_aware_action_failed": "クォータ認識モードを切り替えられませんでした。",
2180
- "claude_beginner_error": "初心者モード:{error}"
2186
+ "claude_beginner_error": "初心者モード:{error}",
2187
+ "claude_desktop_updated": "Claude デスクトップ · {minutes}分前に更新",
2188
+ "claude_desktop_stale": "Claude デスクトップ · 古いデータ · {minutes}分前に更新"
2181
2189
  },
2182
2190
  "ko": {
2183
2191
  "panel_default_name": "기본",
@@ -2249,7 +2257,7 @@
2249
2257
  "awaiting_rate_limits": "Claude Code에 메시지를 보내 할당량을 동기화하세요",
2250
2258
  "hook_broken_not_installed": "⚠ 상태 표시줄이 업데이트되지 않습니다. \"상태 표시줄 설정\"을 눌러 다시 설치하세요.",
2251
2259
  "hook_broken_restart": "⚠ 상태 표시줄 hook이 작동하지 않습니다. Claude Code를 한 번 다시 시작하세요.",
2252
- "usage_status_missing": "⚠ 상태 파일을 찾을 수 없습니다. \"상태 표시줄 설정\"을 누른 뒤 Claude Code를 한 번 여세요.",
2260
+ "usage_status_missing": "사용 한도 데이터가 없습니다. Claude 데스크톱을 열어 두거나 상태 표시줄을 설정한 뒤 터미널에서 Claude Code를 실행하세요.",
2253
2261
  "usage_status_missing_active": "⚠ 할당량 데이터는 터미널 상태 표시줄로만 동기화됩니다. 데스크톱 앱이나 헤드리스 세션에서는 동기화되지 않습니다. 터미널에서 Claude Code를 한 번 열어주세요.",
2254
2262
  "usage_status_no_quota": "⚠ 상태 파일에 아직 할당량 데이터가 없습니다. Claude Code가 statusLine을 한 번 더 새로 고칠 때까지 기다리세요.",
2255
2263
  "status_error": "오류 ({message})",
@@ -2722,6 +2730,8 @@
2722
2730
  "quota_aware_enabled_msg": "할당량 인식 모드를 켰습니다. Claude Code를 다시 시작해 주세요.",
2723
2731
  "quota_aware_disabled_msg": "할당량 인식 모드를 껐습니다. Claude Code를 다시 시작해 주세요.",
2724
2732
  "quota_aware_action_failed": "할당량 인식 모드를 변경할 수 없습니다.",
2725
- "claude_beginner_error": "초보자 모드: {error}"
2733
+ "claude_beginner_error": "초보자 모드: {error}",
2734
+ "claude_desktop_updated": "Claude 데스크톱 · {minutes}분 전 업데이트",
2735
+ "claude_desktop_stale": "Claude 데스크톱 · 오래된 데이터 · {minutes}분 전 업데이트"
2726
2736
  }
2727
2737
  }
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+
3
+ """Read a cache file while Chromium holds it open, without changing the file."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes as C
8
+ import sys
9
+ from ctypes import wintypes as W
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+
13
+
14
+ @lru_cache(maxsize=1)
15
+ def _kernel() -> C.CDLL:
16
+ if sys.platform != "win32":
17
+ raise OSError("Windows shared cache reader is unavailable")
18
+ kernel = C.WinDLL("kernel32", use_last_error=True)
19
+ kernel.CreateFileW.argtypes = [
20
+ W.LPCWSTR,
21
+ W.DWORD,
22
+ W.DWORD,
23
+ C.c_void_p,
24
+ W.DWORD,
25
+ W.DWORD,
26
+ C.c_void_p,
27
+ ]
28
+ kernel.CreateFileW.restype = W.HANDLE
29
+ kernel.SetFilePointerEx.argtypes = [W.HANDLE, C.c_longlong, C.c_void_p, W.DWORD]
30
+ kernel.SetFilePointerEx.restype = W.BOOL
31
+ kernel.ReadFile.argtypes = [W.HANDLE, C.c_void_p, W.DWORD, C.POINTER(W.DWORD), C.c_void_p]
32
+ kernel.ReadFile.restype = W.BOOL
33
+ kernel.CloseHandle.argtypes = [W.HANDLE]
34
+ kernel.CloseHandle.restype = W.BOOL
35
+ return kernel
36
+
37
+
38
+ def read_range(path: Path, offset: int, length: int) -> bytes:
39
+ if offset < 0 or length < 0 or length > 16 * 1024 * 1024:
40
+ raise ValueError("Invalid cache read bounds")
41
+ if sys.platform != "win32":
42
+ with path.open("rb") as file:
43
+ file.seek(offset)
44
+ return file.read(length)
45
+ kernel = _kernel()
46
+ # GENERIC_READ, FILE_SHARE_READ | WRITE | DELETE, OPEN_EXISTING. Chromium
47
+ # requests delete access; ordinary CRT opens do not always share that access.
48
+ handle = kernel.CreateFileW(str(path), 0x80000000, 7, None, 3, 0, None)
49
+ if handle == C.c_void_p(-1).value:
50
+ raise OSError("Cannot open the cache file for reading")
51
+ try:
52
+ if not kernel.SetFilePointerEx(handle, offset, None, 0):
53
+ raise OSError("Cannot seek in the cache file")
54
+ buffer = C.create_string_buffer(length)
55
+ count = W.DWORD()
56
+ if not kernel.ReadFile(handle, buffer, length, C.byref(count), None):
57
+ raise OSError("Cannot read the cache file")
58
+ return buffer.raw[: count.value]
59
+ finally:
60
+ kernel.CloseHandle(handle)
61
+
62
+
63
+ def read_bounded(path: Path, limit: int) -> bytes:
64
+ size = path.stat().st_size
65
+ if size > limit:
66
+ raise ValueError("Cache file exceeds the size limit")
67
+ return read_range(path, 0, size)
@@ -0,0 +1,215 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+
3
+ """Read one local JSON response from Chromium's block-file HTTP cache.
4
+
5
+ Formats: Chromium net/disk_cache/blockfile/{disk_format,addr}.h and
6
+ net/http/http_response_info.cc. No cookies, credentials, or requests are used.
7
+ Unsupported formats and incomplete/in-flight entries return no response.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import struct
15
+ import zlib
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import cast
19
+
20
+ from loaders._shared_cache_file import read_bounded, read_range
21
+
22
+ MAX_BODY = 1024 * 1024
23
+ MAX_WINDOW = 8 * 1024 * 1024
24
+ MAX_INDEX = 4 * 1024 * 1024
25
+ MAX_ENTRIES = 16 * 1024 * 1024
26
+ BLOCK_SIZES = {1: 36, 2: 256, 3: 1024, 4: 4096, 5: 8, 6: 256, 7: 256}
27
+ INDEX_HEADER = 368
28
+ BLOCK_HEADER = 8192
29
+ TIME_EPOCH = 11644473600
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CachedResponse:
34
+ data: dict[str, object]
35
+ fetched_at: float
36
+
37
+
38
+ def _response_info(raw: bytes) -> tuple[float, bytes]:
39
+ if len(raw) < 28 or struct.unpack_from("<I", raw)[0] != len(raw) - 4:
40
+ raise ValueError("Invalid response info")
41
+ flags = struct.unpack_from("<I", raw, 4)[0]
42
+ if flags & 255 != 3 or flags & (1 << 12):
43
+ raise ValueError("Unsupported or truncated response")
44
+ extra = struct.unpack_from("<I", raw, 8)[0] if flags & (1 << 31) else 0
45
+ offset = 12 if flags & (1 << 31) else 8
46
+ fetched_at = struct.unpack_from("<q", raw, offset + 8)[0] / 1e6 - TIME_EPOCH
47
+ offset += 16 + (8 if extra & 4 else 0)
48
+ length = struct.unpack_from("<I", raw, offset)[0]
49
+ headers = raw[offset + 4 : offset + 4 + length]
50
+ if len(headers) != length or not re.match(rb"^HTTP/\S+ 200(?: |\x00)", headers):
51
+ raise ValueError("No successful response headers")
52
+ if b"application/json" not in headers.lower():
53
+ raise ValueError("Not a JSON response")
54
+ return fetched_at, headers
55
+
56
+
57
+ def _decode(raw: bytes, headers: bytes) -> dict[str, object]:
58
+ # Chrome may store a zstd frame even when the original HTTP response used
59
+ # another encoding. Detect the on-disk frame first.
60
+ if raw.startswith(b"\x28\xb5\x2f\xfd"):
61
+ try:
62
+ import zstandard
63
+ except (ImportError, OSError) as exc:
64
+ raise ValueError("Zstandard decoder is unavailable") from exc
65
+ try:
66
+ size = zstandard.frame_content_size(raw)
67
+ if size != zstandard.CONTENTSIZE_UNKNOWN and size > MAX_BODY:
68
+ raise ValueError("Response body exceeds the size limit")
69
+ if zstandard.get_frame_parameters(raw).window_size > MAX_WINDOW:
70
+ raise ValueError("Response compression window exceeds the size limit")
71
+ decoder = zstandard.ZstdDecompressor(max_window_size=MAX_WINDOW)
72
+ raw = decoder.decompress(raw, max_output_size=MAX_BODY, allow_extra_data=False)
73
+ except zstandard.ZstdError as exc:
74
+ raise ValueError("Invalid Zstandard response") from exc
75
+ elif raw.startswith(b"\x1f\x8b") or b"content-encoding: deflate" in headers.lower():
76
+ decoder_z = zlib.decompressobj(31 if raw.startswith(b"\x1f\x8b") else 15)
77
+ raw = decoder_z.decompress(raw, MAX_BODY + 1)
78
+ if not decoder_z.eof:
79
+ raise ValueError("Incomplete or oversized compressed response")
80
+ if len(raw) > MAX_BODY:
81
+ raise ValueError("Response body exceeds the size limit")
82
+ data = json.loads(raw)
83
+ if not isinstance(data, dict):
84
+ raise ValueError("Not a JSON object")
85
+ return cast(dict[str, object], data)
86
+
87
+
88
+ class _BlockCache:
89
+ def __init__(self, path: Path) -> None:
90
+ self.path = path
91
+ self.index = read_bounded(path / "index", MAX_INDEX)
92
+ magic, version = struct.unpack_from("<II", self.index)
93
+ if magic != 0xC103CAC3 or version not in {0x20000, 0x20001, 0x30000}:
94
+ raise ValueError("Unsupported cache index")
95
+ self.table_len = struct.unpack_from("<i", self.index, 28)[0] or 65536
96
+ if (
97
+ self.table_len <= 0
98
+ or self.table_len & (self.table_len - 1)
99
+ or len(self.index) != INDEX_HEADER + 4 * self.table_len
100
+ ):
101
+ raise ValueError("Invalid cache index size")
102
+
103
+ def stream(self, address: int, length: int) -> bytes:
104
+ if not address & 0x80000000 or not 0 < length <= MAX_BODY:
105
+ raise ValueError("Invalid cache stream")
106
+ kind = (address >> 28) & 7
107
+ if kind == 0:
108
+ raw = read_range(self.path / f"f_{address & 0x0FFFFFFF:06x}", 0, length)
109
+ else:
110
+ size = BLOCK_SIZES[kind]
111
+ if length > size * (((address >> 24) & 3) + 1):
112
+ raise ValueError("Invalid block allocation")
113
+ offset = BLOCK_HEADER + (address & 65535) * size
114
+ raw = read_range(self.path / f"data_{(address >> 16) & 255}", offset, length)
115
+ if len(raw) != length:
116
+ raise ValueError("Incomplete cache stream")
117
+ return raw
118
+
119
+ def _active(self, entry: bytes, file: int, block: int) -> bool:
120
+ bucket = struct.unpack_from("<I", entry)[0] & (self.table_len - 1)
121
+ address = struct.unpack_from("<I", self.index, INDEX_HEADER + bucket * 4)[0]
122
+ visited: set[int] = set()
123
+ for _ in range(128):
124
+ if not address or address in visited or (address >> 28) & 7 not in {2, 6}:
125
+ return False
126
+ visited.add(address)
127
+ if ((address >> 16) & 255, address & 65535) == (file, block):
128
+ return True
129
+ address = struct.unpack_from("<I", self.stream(address, 256), 4)[0]
130
+ return False
131
+
132
+ def responses(self, url: bytes, earliest: float, now: float) -> list[CachedResponse]:
133
+ files = {
134
+ (address >> 16) & 255
135
+ for (address,) in struct.iter_unpack("<I", self.index[INDEX_HEADER:])
136
+ if (address >> 28) & 7 in {2, 6}
137
+ }
138
+ results = []
139
+ for file in sorted(files):
140
+ try:
141
+ raw = read_bounded(self.path / f"data_{file}", MAX_ENTRIES)
142
+ except (OSError, ValueError):
143
+ continue
144
+ if (
145
+ len(raw) < BLOCK_HEADER
146
+ or struct.unpack_from("<II", raw) != (0xC104CAC3, 0x20000)
147
+ or struct.unpack_from("<H", raw, 8)[0] != file
148
+ or struct.unpack_from("<i", raw, 12)[0] != 256
149
+ ):
150
+ continue
151
+ for offset in range(BLOCK_HEADER, len(raw) - 255, 256):
152
+ entry = raw[offset : offset + 256]
153
+ try:
154
+ response = self._response(raw, offset, entry, file, url, earliest, now)
155
+ except (
156
+ OSError,
157
+ ValueError,
158
+ struct.error,
159
+ zlib.error,
160
+ RecursionError,
161
+ ):
162
+ continue
163
+ if response is not None:
164
+ results.append(response)
165
+ return results
166
+
167
+ def _response(
168
+ self,
169
+ raw: bytes,
170
+ offset: int,
171
+ entry: bytes,
172
+ file: int,
173
+ url: bytes,
174
+ earliest: float,
175
+ now: float,
176
+ ) -> CachedResponse | None:
177
+ if struct.unpack_from("<i", entry, 20)[0] != 0:
178
+ return None
179
+ key_len, long_key = struct.unpack_from("<iI", entry, 32)
180
+ if not 0 < key_len <= 4096:
181
+ return None
182
+ key = (
183
+ self.stream(long_key, key_len) if long_key else raw[offset + 96 : offset + 96 + key_len]
184
+ )
185
+ # Chromium prefixes keys with upload identifiers (e.g. "1/0/") and
186
+ # may also add a network-isolation key before the request URL.
187
+ if key[key.rfind(b"https://") :].partition(b"?")[0] != url:
188
+ return None
189
+ if not self._active(entry, file, (offset - BLOCK_HEADER) // 256):
190
+ return None
191
+ ranking = self.stream(struct.unpack_from("<I", entry, 8)[0], 36)
192
+ if struct.unpack_from("<i", ranking, 28)[0] != 0:
193
+ return None
194
+ sizes = struct.unpack_from("<4i", entry, 40)
195
+ addresses = struct.unpack_from("<4I", entry, 56)
196
+ metadata = self.stream(addresses[0], sizes[0])
197
+ fetched_at, headers = _response_info(metadata)
198
+ if not max(earliest, now - 7200) <= fetched_at <= now + 60:
199
+ return None
200
+ body = self.stream(addresses[1], sizes[1])
201
+ # Cache updates are concurrent; refuse entries whose metadata changed
202
+ # while reading. No files are copied, modified, or locked exclusively.
203
+ if read_range(self.path / f"data_{file}", offset, 96) != entry[:96]:
204
+ return None
205
+ if self.stream(addresses[0], sizes[0]) != metadata:
206
+ return None
207
+ return CachedResponse(_decode(body, headers), fetched_at)
208
+
209
+
210
+ def load_cached_json(path: Path, url: bytes, earliest: float, now: float) -> CachedResponse | None:
211
+ try:
212
+ responses = _BlockCache(path).responses(url, earliest, now)
213
+ except (OSError, ValueError, struct.error):
214
+ return None
215
+ return max(responses, key=lambda response: response.fetched_at, default=None)