usage-cli 0.34.7__tar.gz → 0.34.9__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 (281) hide show
  1. {usage_cli-0.34.7/usage_cli.egg-info → usage_cli-0.34.9}/PKG-INFO +1 -1
  2. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/session_hooks.py +7 -1
  3. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/setup_hook.py +11 -3
  4. {usage_cli-0.34.7 → usage_cli-0.34.9}/pyproject.toml +5 -1
  5. {usage_cli-0.34.7 → usage_cli-0.34.9}/setup_app.py +0 -1
  6. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_html_report_snapshot.py +15 -0
  7. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_packaged_resources.py +1 -1
  8. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_pricing.py +3 -1
  9. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_report_filter_js.py +19 -0
  10. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_report_share_mask.py +39 -8
  11. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_session_resume.py +42 -0
  12. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_setup_hook_terse.py +16 -0
  13. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_statusline.py +22 -18
  14. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/html_report.py +8 -2
  15. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_filter.py +2 -1
  16. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_scripts.py +22 -1
  17. {usage_cli-0.34.7 → usage_cli-0.34.9/usage_cli.egg-info}/PKG-INFO +1 -1
  18. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/pricing.py +7 -1
  19. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_session_resume.py +2 -2
  20. {usage_cli-0.34.7 → usage_cli-0.34.9}/LICENSE +0 -0
  21. {usage_cli-0.34.7 → usage_cli-0.34.9}/README.md +0 -0
  22. {usage_cli-0.34.7 → usage_cli-0.34.9}/THIRD_PARTY_NOTICES.md +0 -0
  23. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/__init__.py +0 -0
  24. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/agy.py +0 -0
  25. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/claude.py +0 -0
  26. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/codex.py +0 -0
  27. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/grok.py +0 -0
  28. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/muse.py +0 -0
  29. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/rate_limits.py +0 -0
  30. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/registry.py +0 -0
  31. {usage_cli-0.34.7 → usage_cli-0.34.9}/adapters/types.py +0 -0
  32. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/__init__.py +0 -0
  33. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/aggregator.py +0 -0
  34. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/blocks.py +0 -0
  35. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/diagnoser.py +0 -0
  36. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/diagnosis_snapshot.py +0 -0
  37. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/dispatch_ledger.py +0 -0
  38. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/history_persist.py +0 -0
  39. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/insights.py +0 -0
  40. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/ledger_backfill.py +0 -0
  41. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/persona_loader.py +0 -0
  42. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/reporter.py +0 -0
  43. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/subscription.py +0 -0
  44. {usage_cli-0.34.7 → usage_cli-0.34.9}/analyzer/usage_snapshot.py +0 -0
  45. {usage_cli-0.34.7 → usage_cli-0.34.9}/i18n.json +0 -0
  46. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/__init__.py +0 -0
  47. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/claude_pane.py +0 -0
  48. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/codex_hook_trust.py +0 -0
  49. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/login_item.py +0 -0
  50. {usage_cli-0.34.7 → usage_cli-0.34.9}/installer/statusline_settings.py +0 -0
  51. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/__init__.py +0 -0
  52. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/_shared_cache_file.py +0 -0
  53. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/agy_disk_cache.py +0 -0
  54. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/agy_loader.py +0 -0
  55. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/agy_quota_probe.py +0 -0
  56. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/cache_quarantine.py +0 -0
  57. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/chromium_cache.py +0 -0
  58. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/claude_desktop.py +0 -0
  59. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/claude_paths.py +0 -0
  60. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/claude_usage.py +0 -0
  61. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/codex_disk_cache.py +0 -0
  62. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/codex_events.py +0 -0
  63. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/codex_fork_replay.py +0 -0
  64. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/codex_loader.py +0 -0
  65. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/codex_paths.py +0 -0
  66. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/disk_cache_common.py +0 -0
  67. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/disk_cache_lifecycle.py +0 -0
  68. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/grok_loader.py +0 -0
  69. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/grok_quota_probe.py +0 -0
  70. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/history_disk_cache.py +0 -0
  71. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/history_loader.py +0 -0
  72. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/jsonl_limits.py +0 -0
  73. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/jsonl_utils.py +0 -0
  74. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/muse_loader.py +0 -0
  75. {usage_cli-0.34.7 → usage_cli-0.34.9}/loaders/muse_quota_probe.py +0 -0
  76. {usage_cli-0.34.7 → usage_cli-0.34.9}/main.py +0 -0
  77. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/__init__.py +0 -0
  78. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/actions.py +0 -0
  79. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/agy.py +0 -0
  80. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/app.py +0 -0
  81. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/chrome.py +0 -0
  82. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/fsevents_watch.py +0 -0
  83. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/grok.py +0 -0
  84. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/manual_refresh.py +0 -0
  85. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/menu.py +0 -0
  86. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/muse.py +0 -0
  87. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/new_badge.py +0 -0
  88. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/notify.py +0 -0
  89. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/popover.py +0 -0
  90. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/prefs.py +0 -0
  91. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/refresh.py +0 -0
  92. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/state.py +0 -0
  93. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/title.py +0 -0
  94. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/update.py +0 -0
  95. {usage_cli-0.34.7 → usage_cli-0.34.9}/menubar/whats_new.py +0 -0
  96. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/__init__.py +0 -0
  97. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/base.py +0 -0
  98. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/catalog.py +0 -0
  99. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/dynamic_height.py +0 -0
  100. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/panel_scale.py +0 -0
  101. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/panel_window.py +0 -0
  102. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/panel_window_state.py +0 -0
  103. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/payload.py +0 -0
  104. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/web_panel.py +0 -0
  105. {usage_cli-0.34.7 → usage_cli-0.34.9}/panels/window_drag.py +0 -0
  106. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/__init__.py +0 -0
  107. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/agy_window_keeper.py +0 -0
  108. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/burn_rate.py +0 -0
  109. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/codex_window_keeper.py +0 -0
  110. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/keeper_outcome.py +0 -0
  111. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/notifications.py +0 -0
  112. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/quota_snapshot.py +0 -0
  113. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/status_payload.py +0 -0
  114. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/usage_rate.py +0 -0
  115. {usage_cli-0.34.7 → usage_cli-0.34.9}/quota/window_keeper.py +0 -0
  116. {usage_cli-0.34.7 → usage_cli-0.34.9}/setup.cfg +0 -0
  117. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_adapters_claude.py +0 -0
  118. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_adapters_codex.py +0 -0
  119. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_adapters_rate_limits.py +0 -0
  120. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_aggregator.py +0 -0
  121. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_agy_adapter.py +0 -0
  122. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_agy_disk_cache.py +0 -0
  123. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_agy_loader.py +0 -0
  124. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_agy_quota_probe.py +0 -0
  125. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_agy_window_keeper.py +0 -0
  126. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_analyzer_blocks.py +0 -0
  127. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_analyzer_pipeline.py +0 -0
  128. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_app_version.py +0 -0
  129. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_burn_rate.py +0 -0
  130. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_cache_quarantine.py +0 -0
  131. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_check_upstream.py +0 -0
  132. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_chromium_cache.py +0 -0
  133. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_claude_desktop.py +0 -0
  134. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_claude_pane.py +0 -0
  135. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_claude_paths.py +0 -0
  136. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_codex_consistency.py +0 -0
  137. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_codex_hook_trust.py +0 -0
  138. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_codex_loader.py +0 -0
  139. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_codex_window_keeper.py +0 -0
  140. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_compact_cards.py +0 -0
  141. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_desktop_fresh_cache.py +0 -0
  142. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_desktop_quota_display.py +0 -0
  143. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_desktop_quota_review.py +0 -0
  144. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_diagnoser.py +0 -0
  145. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_disk_cache_shards.py +0 -0
  146. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_dispatch_ledger.py +0 -0
  147. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_doctor.py +0 -0
  148. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_dynamic_height.py +0 -0
  149. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_fsevents_watch.py +0 -0
  150. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_grok_loader.py +0 -0
  151. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_grok_quota_probe.py +0 -0
  152. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_history_loader.py +0 -0
  153. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_history_persist.py +0 -0
  154. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_hook_scripts_are_standalone.py +0 -0
  155. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_i18n_format_fallback.py +0 -0
  156. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_i18n_key_parity.py +0 -0
  157. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_i18n_packaged_path.py +0 -0
  158. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_insights.py +0 -0
  159. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_jsonl_utils.py +0 -0
  160. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_keeper_menu.py +0 -0
  161. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_keeper_outcome.py +0 -0
  162. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_ledger_backfill.py +0 -0
  163. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_login_item.py +0 -0
  164. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_main.py +0 -0
  165. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_manual_refresh.py +0 -0
  166. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar.py +0 -0
  167. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_agy.py +0 -0
  168. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_grok.py +0 -0
  169. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_muse.py +0 -0
  170. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_notify.py +0 -0
  171. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_prefs.py +0 -0
  172. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_state.py +0 -0
  173. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_menubar_update.py +0 -0
  174. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_muse_coverage.py +0 -0
  175. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_muse_loader.py +0 -0
  176. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_muse_quota_probe.py +0 -0
  177. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_muse_review_regressions.py +0 -0
  178. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_new_badge.py +0 -0
  179. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_packaging.py +0 -0
  180. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_panel_footer_layout.py +0 -0
  181. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_panel_scale.py +0 -0
  182. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_panel_window_state.py +0 -0
  183. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_panels.py +0 -0
  184. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_panels_lazy_import.py +0 -0
  185. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_persona_loader.py +0 -0
  186. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_project_resolver.py +0 -0
  187. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_quota_aware.py +0 -0
  188. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_quota_label.py +0 -0
  189. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_report_cold_read.py +0 -0
  190. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_report_daily_chart.py +0 -0
  191. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_reporter_build_data.py +0 -0
  192. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_service_status.py +0 -0
  193. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_setup_app.py +0 -0
  194. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_setup_hook.py +0 -0
  195. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_setup_hook_coexistence.py +0 -0
  196. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_setup_hook_resume.py +0 -0
  197. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_subprocess_utils.py +0 -0
  198. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_subscription.py +0 -0
  199. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_taskbar_menu.py +0 -0
  200. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_taskbar_obstacles.py +0 -0
  201. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_time_utils.py +0 -0
  202. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_tui.py +0 -0
  203. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_tui_sprite.py +0 -0
  204. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_ui_tables.py +0 -0
  205. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_update_checker.py +0 -0
  206. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_update_gate.py +0 -0
  207. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_update_release_notes.py +0 -0
  208. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_cli.py +0 -0
  209. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_client.py +0 -0
  210. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_diagnosis_snapshot.py +0 -0
  211. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_dir_sweeper.py +0 -0
  212. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_lang.py +0 -0
  213. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_logging.py +0 -0
  214. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_notifications.py +0 -0
  215. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_rate.py +0 -0
  216. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_snapshot.py +0 -0
  217. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_snapshot_replay.py +0 -0
  218. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_statusline_agy.py +0 -0
  219. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_statusline_forwarder.py +0 -0
  220. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_statusline_grok.py +0 -0
  221. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_terse_mode.py +0 -0
  222. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_usage_terse_reminder.py +0 -0
  223. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_web_panel_payload.py +0 -0
  224. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_whats_new.py +0 -0
  225. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_wheel_packaging.py +0 -0
  226. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_win_login_item.py +0 -0
  227. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_window_keeper.py +0 -0
  228. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_windows_manifest.py +0 -0
  229. {usage_cli-0.34.7 → usage_cli-0.34.9}/tests/test_wintray.py +0 -0
  230. {usage_cli-0.34.7 → usage_cli-0.34.9}/tui/__init__.py +0 -0
  231. {usage_cli-0.34.7 → usage_cli-0.34.9}/tui/app.py +0 -0
  232. {usage_cli-0.34.7 → usage_cli-0.34.9}/tui/sprite.py +0 -0
  233. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/__init__.py +0 -0
  234. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_charts.py +0 -0
  235. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_daily_chart.py +0 -0
  236. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_ledger.py +0 -0
  237. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/report_styles.py +0 -0
  238. {usage_cli-0.34.7 → usage_cli-0.34.9}/ui/tables.py +0 -0
  239. {usage_cli-0.34.7 → usage_cli-0.34.9}/updates/__init__.py +0 -0
  240. {usage_cli-0.34.7 → usage_cli-0.34.9}/updates/checker.py +0 -0
  241. {usage_cli-0.34.7 → usage_cli-0.34.9}/updates/gate.py +0 -0
  242. {usage_cli-0.34.7 → usage_cli-0.34.9}/updates/release_notes.py +0 -0
  243. {usage_cli-0.34.7 → usage_cli-0.34.9}/updates/whats_new.py +0 -0
  244. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_app/__init__.py +0 -0
  245. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_app/cli.py +0 -0
  246. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_app/doctor.py +0 -0
  247. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_cli.egg-info/SOURCES.txt +0 -0
  248. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_cli.egg-info/dependency_links.txt +0 -0
  249. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_cli.egg-info/entry_points.txt +0 -0
  250. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_cli.egg-info/requires.txt +0 -0
  251. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_cli.egg-info/top_level.txt +0 -0
  252. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/__init__.py +0 -0
  253. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/app_version.py +0 -0
  254. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/i18n.py +0 -0
  255. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/prefs.py +0 -0
  256. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/project_resolver.py +0 -0
  257. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/service_status.py +0 -0
  258. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/subprocess_utils.py +0 -0
  259. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/time_utils.py +0 -0
  260. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/usage_dir_sweeper.py +0 -0
  261. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/usage_lang.py +0 -0
  262. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_common/usage_logging.py +0 -0
  263. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/__init__.py +0 -0
  264. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_quota_aware.py +0 -0
  265. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_statusline.py +0 -0
  266. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_statusline_agy.py +0 -0
  267. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_statusline_forwarder.py +0 -0
  268. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_statusline_grok.py +0 -0
  269. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_terse_mode.py +0 -0
  270. {usage_cli-0.34.7 → usage_cli-0.34.9}/usage_hooks/usage_terse_reminder.py +0 -0
  271. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/__init__.py +0 -0
  272. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/app.py +0 -0
  273. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/login_item.py +0 -0
  274. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/menu.py +0 -0
  275. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/prefs.py +0 -0
  276. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/quota_label.py +0 -0
  277. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/taskbar_menu.py +0 -0
  278. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/taskbar_obstacles.py +0 -0
  279. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/taskbar_overlay.py +0 -0
  280. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/watch.py +0 -0
  281. {usage_cli-0.34.7 → usage_cli-0.34.9}/wintray/whats_new.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.34.7
3
+ Version: 0.34.9
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
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
  import contextlib
12
12
  import io
13
13
  import json
14
+ import logging
14
15
  import os
15
16
  import re
16
17
  import shutil
@@ -59,7 +60,7 @@ CODEX_CONFIG = setup_hook.CODEX_CONFIG
59
60
  # session. Off by default: enabled only via the menu toggle, never by self_heal.
60
61
  RESUME_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-session-resume.py"))
61
62
  RESUME_PROMPT_SIDECAR = Path(os.path.expanduser("~/.claude/usage-resume-prompt.json"))
62
- RESUME_HOOK_VERSION = "1.11"
63
+ RESUME_HOOK_VERSION = "1.12"
63
64
  RESUME_MATCHER = "startup|clear|resume"
64
65
  RESUME_LANGS = ("zh-TW", "zh-CN", "en", "ja", "ko")
65
66
  _RESUME_MARKER = "usage-session-resume"
@@ -545,6 +546,11 @@ def _setup_codex_terse() -> None:
545
546
  new_content = _ensure_table_line(content, "features", _FEATURES_HOOKS_REGEX, "hooks = true")
546
547
  if new_content != content:
547
548
  _atomic_write_text(CODEX_CONFIG, new_content)
549
+ else:
550
+ logging.getLogger(__name__).warning(
551
+ "Could not enable Codex hooks in %s: config update failed TOML validation",
552
+ CODEX_CONFIG,
553
+ )
548
554
 
549
555
  hooks = data.get("hooks")
550
556
  if not isinstance(hooks, dict):
@@ -1070,14 +1070,22 @@ def _ensure_table_line(content: str, name: str, line_regex: re.Pattern[str], lin
1070
1070
  """Make sure ``line`` is in table ``name``: replace an existing ``line_regex`` match if
1071
1071
  present, else insert ``line`` fresh. Appends a new ``[name]`` table at EOF when the table
1072
1072
  itself is absent."""
1073
+
1074
+ def validated(candidate: str) -> str:
1075
+ try:
1076
+ tomllib.loads(candidate)
1077
+ except tomllib.TOMLDecodeError:
1078
+ return content
1079
+ return candidate
1080
+
1073
1081
  table = _find_table(content, name)
1074
1082
  if table is None:
1075
- return content.rstrip() + f"\n\n[{name}]\n{line}\n"
1083
+ return validated(content.rstrip() + f"\n\n[{name}]\n{line}\n")
1076
1084
  start, end = _table_section(content, table)
1077
1085
  section = content[start:end]
1078
1086
  if line_regex.search(section):
1079
- return content[:start] + line_regex.sub(line, section, count=1) + content[end:]
1080
- return content[:start] + f"\n{line}" + content[start:]
1087
+ return validated(content[:start] + line_regex.sub(line, section, count=1) + content[end:])
1088
+ return validated(content[:start] + f"\n{line}" + content[start:])
1081
1089
 
1082
1090
 
1083
1091
  def _insert_tui_status_line(content: str, replacement: str) -> str:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usage-cli"
3
- version = "0.34.7"
3
+ version = "0.34.9"
4
4
  description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -64,6 +64,10 @@ dev = [
64
64
  ]
65
65
  build = [
66
66
  "py2app>=0.28; sys_platform == 'darwin'",
67
+ "pyinstaller>=6.22.3; sys_platform == 'win32'",
68
+ # The [build-system] backend, locked here so release.yml can build the
69
+ # PyPI wheel with --no-build-isolation instead of fetching an unpinned one.
70
+ "setuptools>=84.0.0",
67
71
  ]
68
72
 
69
73
  [tool.ruff]
@@ -65,7 +65,6 @@ if __name__ == "__main__":
65
65
  "assets/usage_uhoh.wav",
66
66
  "assets/critters",
67
67
  "assets/panels",
68
- "assets/windows",
69
68
  "i18n.json",
70
69
  "claude_pane",
71
70
  "pyproject.toml",
@@ -7,6 +7,8 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import base64
10
+ import csv
11
+ import io
10
12
  import os
11
13
  import re
12
14
  import sys
@@ -799,6 +801,19 @@ def test_build_csv_data_contains_projects_and_models() -> None:
799
801
  assert "model,claude-sonnet-4,52.4,1229345,23.91\r\n" in csv_text
800
802
 
801
803
 
804
+ @pytest.mark.parametrize("name", ["=1+1", "-cmd", "+cmd", "@cmd", "\tcmd", "\rcmd", '=1,"x"'])
805
+ def test_build_csv_data_escapes_formula_names_only(name: str) -> None:
806
+ data = {
807
+ "by_project": [{"project": name, "pct": -1.5, "tokens": -2, "cost": -3.0}],
808
+ "by_model": [{"model": name, "pct": -1.5, "tokens": -2, "cost": -3.0}],
809
+ }
810
+
811
+ rows = list(csv.reader(io.StringIO(html_report._build_csv_data(data, "en"))))
812
+
813
+ assert rows[1] == ["project", "'" + name, "-1.5", "-2", "-3.00"]
814
+ assert rows[2] == ["model", "'" + name, "-1.5", "-2", "-3.00"]
815
+
816
+
802
817
  def test_build_csv_data_masks_project_names() -> None:
803
818
  csv_text = html_report._build_csv_data(_full_report_data(), "en", mask_projects=True)
804
819
 
@@ -126,7 +126,7 @@ def test_parse_setup_app_resources() -> None:
126
126
  resources = parse_setup_app_resources()
127
127
 
128
128
  assert "i18n.json" in resources
129
- assert "assets/windows" in resources
129
+ assert "assets/panels" in resources
130
130
 
131
131
 
132
132
  def test_local_data_files_are_declared_as_py2app_resources() -> None:
@@ -467,6 +467,8 @@ def test_compare_fallback_pricing_reports_all_difference_categories() -> None:
467
467
  },
468
468
  "claude-sonnet-5": {"input_cost_per_token": 1.0},
469
469
  "anthropic/claude-haiku-5": {"input_cost_per_token": 1.0},
470
+ "gpt-6.1-terra": {"input_cost_per_token": 1.0},
471
+ "azure/gpt-6.1-terra": {"input_cost_per_token": 1.0},
470
472
  }
471
473
 
472
474
  result = CHECK_FALLBACK_PRICING["compare_pricing"](fallback, upstream)
@@ -476,7 +478,7 @@ def test_compare_fallback_pricing_reports_all_difference_categories() -> None:
476
478
  for mismatch in result.mismatches
477
479
  ] == [("claude-opus-5", "output_cost_per_token", 9.0, 2.0)]
478
480
  assert result.missing_upstream == ("claude-fable-5",)
479
- assert result.missing_fallback == ("claude-sonnet-5",)
481
+ assert result.missing_fallback == ("claude-sonnet-5", "gpt-6.1-terra")
480
482
  assert result.has_differences is True
481
483
 
482
484
 
@@ -851,6 +851,25 @@ for (const [peak, ceiling] of [
851
851
  assert.equal(axisEnv.trendSection.querySelector('.daily-y-label').textContent, ceiling);
852
852
  }
853
853
  console.log('daily chart: date range rebuilds pricing; tokens/cost toggle redraws');
854
+
855
+ // Name fields must stay text in spreadsheet apps; numeric fields remain numeric.
856
+ for (const name of ['=1+1', '-cmd', '+cmd', '@cmd', '\tcmd', '\rcmd', '=1,"x"']) {
857
+ const formulaCube = {
858
+ dates: ['2026-05-21'],
859
+ agents: [{id: 'codex', name: 'Codex'}],
860
+ models: [{name, cost_known: true}],
861
+ projects: [name],
862
+ rows: [[0, 0, 0, 0, 10, 0, 0, 0, -3, 1]],
863
+ };
864
+ run(makeEnvironment(formulaCube, {}));
865
+ const safeName = "'" + name;
866
+ const field = /[",\r\n]/.test(safeName)
867
+ ? `"${safeName.replace(/"/g, '""')}"` : safeName;
868
+ assert.equal(
869
+ window.usageReportFilter.buildCsv(false),
870
+ `type,name,share_pct,tokens,cost_usd\r\nproject,${field},100.0,10,-3.00\r\nmodel,${field},100.0,10,-3.00\r\n`
871
+ );
872
+ }
854
873
  """
855
874
 
856
875
 
@@ -11,23 +11,47 @@ from ui.report_scripts import REPORT_JS_TEMPLATE
11
11
 
12
12
 
13
13
  @pytest.mark.skipif(shutil.which("node") is None, reason="Node.js is unavailable")
14
- def test_project_mask_skips_other_without_consuming_a_number(tmp_path: Path) -> None:
14
+ def test_project_mask_covers_accessibility_attributes_and_restores_them(tmp_path: Path) -> None:
15
15
  start = REPORT_JS_TEMPLATE.index("async function withShareableReport(")
16
16
  end = REPORT_JS_TEMPLATE.index("\nasync function downloadHtml", start)
17
17
  script = r"""
18
18
  const assert = require('node:assert/strict');
19
- const make = (text, other = false) => ({
19
+ const make = (text, other = false, attributes = {}) => ({
20
20
  textContent: text,
21
+ attributes,
22
+ getAttribute: (name) => attributes[name] ?? null,
23
+ setAttribute: (name, value) => { attributes[name] = value; },
21
24
  closest: (selector) => selector === '[data-project-other]' && other ? {} : null,
22
25
  });
23
- const names = [make('alpha'), make('Other projects', true), make('beta')];
24
- const legend = [make('alpha'), make('Other projects', true), make('beta')];
26
+ const names = [make(' alpha '), make('Other projects', true), make('beta[.]')];
27
+ const legend = [make('alpha'), make('Other projects', true), make('beta[.]')];
28
+ const toggle = make('', false, {
29
+ 'aria-label': 'Expand alpha; alpha / beta[.]',
30
+ 'aria-description': 'Details for beta[.]',
31
+ title: 'alpha and beta[.]',
32
+ alt: 'alpha logo',
33
+ class: 'alpha',
34
+ id: 'beta',
35
+ 'data-project-name': 'alpha',
36
+ });
37
+ const untouched = make('', false, {title: 'Other projects betaX'});
38
+ const originalAttributes = {...toggle.attributes};
39
+ const assertMasked = () => {
40
+ assert.deepEqual(toggle.attributes, {
41
+ ...originalAttributes,
42
+ 'aria-label': 'Expand Project 1; Project 1 / Project 2',
43
+ 'aria-description': 'Details for Project 2',
44
+ title: 'Project 1 and Project 2',
45
+ alt: 'Project 1 logo',
46
+ });
47
+ assert.deepEqual(untouched.attributes, {title: 'Other projects betaX'});
48
+ };
25
49
  const document = {
26
50
  querySelectorAll: (selector) => selector === '.project-section .rank-line .name'
27
51
  ? names : selector === '.project-section .lg-name' ? legend : [],
28
52
  querySelector: () => null,
29
53
  };
30
- const reportRoot = {};
54
+ const reportRoot = {querySelectorAll: () => [toggle, untouched]};
31
55
  function closeShareModal() {}
32
56
  function stopSnakeAndRestore() {}
33
57
  eval(__FUNCTION__);
@@ -36,12 +60,19 @@ eval(__FUNCTION__);
36
60
  await withShareableReport(true, async () => {
37
61
  assert.deepEqual(names.map((el) => el.textContent), expected);
38
62
  assert.deepEqual(legend.map((el) => el.textContent), expected);
63
+ assertMasked();
64
+ });
65
+ assert.deepEqual(names.map((el) => el.textContent), [' alpha ', 'Other projects', 'beta[.]']);
66
+ assert.deepEqual(toggle.attributes, originalAttributes);
67
+ await withShareableReport(false, async () => {
68
+ assert.deepEqual(toggle.attributes, originalAttributes);
39
69
  });
40
- assert.deepEqual(names.map((el) => el.textContent), ['alpha', 'Other projects', 'beta']);
41
70
  await assert.rejects(withShareableReport(true, async () => {
71
+ assertMasked();
42
72
  throw new Error('export failed');
43
- }));
44
- assert.deepEqual(legend.map((el) => el.textContent), ['alpha', 'Other projects', 'beta']);
73
+ }), /export failed/);
74
+ assert.deepEqual(legend.map((el) => el.textContent), ['alpha', 'Other projects', 'beta[.]']);
75
+ assert.deepEqual(toggle.attributes, originalAttributes);
45
76
  })();
46
77
  """.replace("__FUNCTION__", json.dumps(REPORT_JS_TEMPLATE[start:end]))
47
78
  script_path = tmp_path / "mask.cjs"
@@ -296,6 +296,48 @@ def test_git_dirty_includes_untracked_directory_and_keeps_other_paths(tmp_path:
296
296
  assert "new-feature" in files
297
297
 
298
298
 
299
+ def test_git_dirty_disables_repository_fsmonitor(
300
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
301
+ ) -> None:
302
+ monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
303
+ monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
304
+ repo = tmp_path / "repo"
305
+ repo.mkdir()
306
+ subprocess.run(["git", "init", str(repo)], check=True, capture_output=True)
307
+ (repo / "tracked.py").write_text("before\n", encoding="utf-8")
308
+ subprocess.run(["git", "-C", str(repo), "add", "."], check=True)
309
+ subprocess.run(
310
+ [
311
+ "git",
312
+ "-C",
313
+ str(repo),
314
+ "-c",
315
+ "user.name=Test User",
316
+ "-c",
317
+ "user.email=test@example.com",
318
+ "-c",
319
+ "commit.gpgsign=false",
320
+ "commit",
321
+ "-m",
322
+ "initial",
323
+ ],
324
+ check=True,
325
+ capture_output=True,
326
+ )
327
+ monitor = tmp_path / "fsmonitor"
328
+ marker = tmp_path / "fsmonitor.marker"
329
+ monitor.write_text('#!/bin/sh\n: > "$0.marker"\n', encoding="utf-8")
330
+ monitor.chmod(0o700)
331
+ subprocess.run(["git", "-C", str(repo), "config", "core.fsmonitor", str(monitor)], check=True)
332
+ (repo / "tracked.py").write_text("after\n", encoding="utf-8")
333
+
334
+ dirty = mod._git_dirty(str(repo))
335
+
336
+ assert not marker.exists()
337
+ assert dirty is not None
338
+ assert dirty[1:] == (1, ["tracked.py"])
339
+
340
+
299
341
  def test_build_prompt_reads_previous_session(
300
342
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
301
343
  ) -> None:
@@ -361,6 +361,22 @@ def test_setup_codex_terse_keeps_unreadable_hooks_json_unchanged(
361
361
  assert not terse_paths.codex_terse_target.exists()
362
362
 
363
363
 
364
+ @pytest.mark.parametrize("content", ["features = { hooks = false }\n", "features.hooks = false\n"])
365
+ def test_setup_codex_terse_preserves_unsupported_features_syntax(
366
+ terse_paths: TerseHookPaths, caplog: pytest.LogCaptureFixture, content: str
367
+ ) -> None:
368
+ terse_paths.codex_config.write_text(content, encoding="utf-8")
369
+
370
+ session_hooks._setup_codex_terse()
371
+
372
+ actual = terse_paths.codex_config.read_text(encoding="utf-8")
373
+ assert actual == content
374
+ assert tomllib.loads(actual)["features"]["hooks"] is False
375
+ assert "Could not enable Codex hooks" in caplog.text
376
+ assert len(caplog.records) == 1
377
+ assert caplog.records[0].levelname == "WARNING"
378
+
379
+
364
380
  def test_setup_codex_terse_creates_missing_hooks_json(terse_paths: TerseHookPaths) -> None:
365
381
  terse_paths.codex_config.write_text('model = "gpt-5"\n', encoding="utf-8")
366
382
 
@@ -10,6 +10,7 @@ import errno
10
10
  import io
11
11
  import json
12
12
  import os
13
+ import subprocess
13
14
  import sys
14
15
  import tempfile
15
16
  import time
@@ -226,23 +227,25 @@ def test_exclusive_lock_times_out_when_another_process_holds_it(
226
227
  tmp_path: Path,
227
228
  ) -> None:
228
229
  lock_path = tmp_path / "usage-status.lock"
229
- fcntl = vars(usage_statusline).get("fcntl")
230
- assert fcntl is not None
231
- ready_read, ready_write = os.pipe()
232
- # os.fork is POSIX-only; this test is skipped on Windows, but mypy still
233
- # type-checks the line there.
234
- child_pid = os.fork() # type: ignore[attr-defined]
235
- if child_pid == 0:
236
- os.close(ready_read)
237
- lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
238
- fcntl.flock(lock_fd, fcntl.LOCK_EX)
239
- os.write(ready_write, b"1")
240
- time.sleep(0.3)
241
- os._exit(0)
242
-
243
- os.close(ready_write)
230
+ # A fresh interpreter holds the lock until we close its stdin. os.fork()
231
+ # here would warn: pytest's process is already multi-threaded.
232
+ holder = subprocess.Popen(
233
+ [
234
+ sys.executable,
235
+ "-c",
236
+ "import fcntl, os, sys\n"
237
+ "fd = os.open(sys.argv[1], os.O_CREAT | os.O_RDWR, 0o600)\n"
238
+ "fcntl.flock(fd, fcntl.LOCK_EX)\n"
239
+ "print('1', flush=True)\n"
240
+ "sys.stdin.read()\n",
241
+ str(lock_path),
242
+ ],
243
+ stdin=subprocess.PIPE,
244
+ stdout=subprocess.PIPE,
245
+ )
246
+ assert holder.stdin is not None and holder.stdout is not None
244
247
  try:
245
- assert os.read(ready_read, 1) == b"1"
248
+ assert holder.stdout.readline() == b"1\n"
246
249
  monkeypatch.setattr(usage_statusline, "_LOCK_TIMEOUT_S", 0.01)
247
250
  lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR, 0o600)
248
251
  try:
@@ -253,8 +256,9 @@ def test_exclusive_lock_times_out_when_another_process_holds_it(
253
256
  finally:
254
257
  os.close(lock_fd)
255
258
  finally:
256
- os.close(ready_read)
257
- os.waitpid(child_pid, 0)
259
+ holder.stdin.close()
260
+ holder.wait(timeout=5)
261
+ holder.stdout.close()
258
262
 
259
263
 
260
264
  def test_acquire_msvcrt_lock_waits_out_a_contended_lock(
@@ -1277,23 +1277,29 @@ def _build_csv_data(data: Mapping[str, Any], lang: str, *, mask_projects: bool =
1277
1277
  writer = csv.writer(out, lineterminator="\r\n")
1278
1278
  writer.writerow(["type", "name", "share_pct", "tokens", "cost_usd"])
1279
1279
  for idx, item in enumerate(data.get("by_project", []), start=1):
1280
+ name = f"Project {idx}" if mask_projects else _display_name(item.get("project"), lang)
1281
+ if name.startswith(("=", "+", "-", "@", "\t", "\r")):
1282
+ name = "'" + name
1280
1283
  writer.writerow(
1281
1284
  [
1282
1285
  "project",
1283
- f"Project {idx}" if mask_projects else _display_name(item.get("project"), lang),
1286
+ name,
1284
1287
  f"{float(item.get('pct', 0.0)):.1f}",
1285
1288
  str(int(item.get("tokens", 0))),
1286
1289
  _csv_cost(float(item.get("cost", 0.0))),
1287
1290
  ]
1288
1291
  )
1289
1292
  for model_item in data.get("by_model", []):
1293
+ name = _display_name(model_item.get("model"), lang)
1294
+ if name.startswith(("=", "+", "-", "@", "\t", "\r")):
1295
+ name = "'" + name
1290
1296
  cost_val = (
1291
1297
  None if not model_item.get("cost_known", True) else float(model_item.get("cost", 0.0))
1292
1298
  )
1293
1299
  writer.writerow(
1294
1300
  [
1295
1301
  "model",
1296
- _display_name(model_item.get("model"), lang),
1302
+ name,
1297
1303
  f"{float(model_item.get('pct', 0.0)):.1f}",
1298
1304
  str(int(model_item.get("tokens", 0))),
1299
1305
  _csv_cost(cost_val),
@@ -1003,7 +1003,8 @@ REPORT_FILTER_JS = r"""(() => {
1003
1003
  }
1004
1004
 
1005
1005
  function csvField(value) {
1006
- const text = String(value);
1006
+ let text = String(value);
1007
+ if (/^[=+\-@\t\r]/.test(text)) text = "'" + text;
1007
1008
  if (/[",\r\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
1008
1009
  return text;
1009
1010
  }
@@ -199,6 +199,26 @@ async function withShareableReport(maskProjects, callback) {
199
199
  restores.push({el, original: el.textContent});
200
200
  el.textContent = '—';
201
201
  });
202
+ const masks = new Map();
203
+ restores.forEach(({el, original}) => {
204
+ const name = (original || '').trim();
205
+ if (name && original !== el.textContent && !masks.has(name)) masks.set(name, el.textContent);
206
+ });
207
+ const names = Array.from(masks.keys()).sort((a, b) => b.length - a.length);
208
+ if (names.length) {
209
+ const pattern = new RegExp(names.map((name) => name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('|'), 'g');
210
+ reportRoot.querySelectorAll('*').forEach((el) => {
211
+ ['aria-label', 'aria-description', 'title', 'alt'].forEach((attribute) => {
212
+ const original = el.getAttribute(attribute);
213
+ if (original === null) return;
214
+ const masked = original.replace(pattern, (name) => masks.get(name));
215
+ if (masked !== original) {
216
+ restores.push({el, attribute, original});
217
+ el.setAttribute(attribute, masked);
218
+ }
219
+ });
220
+ });
221
+ }
202
222
  ['#usage-csv-data', '#usage-cube-data', '#usage-session-data'].forEach((selector) => {
203
223
  const dataNode = document.querySelector(selector);
204
224
  if (dataNode) {
@@ -218,7 +238,8 @@ async function withShareableReport(maskProjects, callback) {
218
238
  item.parent.insertBefore(item.el, item.next);
219
239
  });
220
240
  restores.forEach((item) => {
221
- item.el.textContent = item.original;
241
+ if (item.attribute) item.el.setAttribute(item.attribute, item.original);
242
+ else item.el.textContent = item.original;
222
243
  });
223
244
  }
224
245
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usage-cli
3
- Version: 0.34.7
3
+ Version: 0.34.9
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
@@ -33,7 +33,7 @@ FALLBACK_RETRY_SECONDS = 600
33
33
  MISSING_MODEL_REFRESH_SECONDS = FALLBACK_RETRY_SECONDS
34
34
  USER_AGENT = "usage/0.9"
35
35
  # The date when the hard-coded fallback table was last manually checked upstream.
36
- FALLBACK_PRICING_AS_OF: str = "2026-10-12"
36
+ FALLBACK_PRICING_AS_OF: str = "2026-10-30"
37
37
  # Anthropic's official cache write/read prices relative to input pricing. These
38
38
  # are only fallbacks for missing upstream fields and may be wrong for other providers.
39
39
  CACHE_WRITE_COST_MULTIPLIER = 1.25
@@ -539,6 +539,12 @@ def _fallback_pricing() -> PricingTable:
539
539
  "cache_creation_input_token_cost": 2.5e-6,
540
540
  "cache_read_input_token_cost": 0.1e-6,
541
541
  },
542
+ "gpt-6.1-terra": {
543
+ "input_cost_per_token": 1e-6,
544
+ "output_cost_per_token": 5e-6,
545
+ "cache_creation_input_token_cost": 1.25e-6,
546
+ "cache_read_input_token_cost": 0.05e-6,
547
+ },
542
548
  "gpt-6-luna": {
543
549
  "input_cost_per_token": 0.1e-6,
544
550
  "output_cost_per_token": 0.5e-6,
@@ -50,7 +50,7 @@ from datetime import datetime, timedelta, timezone
50
50
  from pathlib import Path
51
51
  from typing import Any, TypedDict, cast
52
52
 
53
- __version__ = "1.11"
53
+ __version__ = "1.12"
54
54
 
55
55
 
56
56
  class _HiddenConsoleKwargs(TypedDict, total=False):
@@ -496,7 +496,7 @@ def _git_dirty(cwd: str) -> tuple[str, int, list[str]] | None:
496
496
  if not branch:
497
497
  return None
498
498
  status_proc = subprocess.run(
499
- ["git", "-C", cwd, "status", "--porcelain"],
499
+ ["git", "-C", cwd, "-c", "core.fsmonitor=false", "status", "--porcelain"],
500
500
  stdout=subprocess.PIPE,
501
501
  stderr=subprocess.DEVNULL,
502
502
  timeout=2,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes