usage-cli 0.34.7__tar.gz → 0.34.8__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.
- {usage_cli-0.34.7/usage_cli.egg-info → usage_cli-0.34.8}/PKG-INFO +1 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/session_hooks.py +7 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/setup_hook.py +11 -3
- {usage_cli-0.34.7 → usage_cli-0.34.8}/pyproject.toml +5 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/setup_app.py +0 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_html_report_snapshot.py +15 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_packaged_resources.py +1 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_pricing.py +3 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_report_filter_js.py +19 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_report_share_mask.py +39 -8
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_session_resume.py +42 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_setup_hook_terse.py +16 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_statusline.py +22 -18
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/html_report.py +8 -2
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_filter.py +2 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_scripts.py +22 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/pricing.py +7 -1
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_session_resume.py +2 -2
- {usage_cli-0.34.7 → usage_cli-0.34.8}/LICENSE +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/README.md +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/agy.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/claude.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/codex.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/grok.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/muse.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/rate_limits.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/registry.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/adapters/types.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/blocks.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/insights.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/reporter.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/i18n.json +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/login_item.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/agy_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/agy_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/codex_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/loaders/muse_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/main.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/actions.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/agy.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/app.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/chrome.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/grok.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/menu.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/muse.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/notify.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/popover.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/prefs.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/refresh.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/state.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/title.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/update.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/base.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/catalog.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/panel_window.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/payload.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/web_panel.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/panels/window_drag.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/notifications.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/status_payload.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/quota/window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/setup.cfg +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_agy_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_check_upstream.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_insights.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_main.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_muse_quota_probe.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_panels.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_tui.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tests/test_wintray.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tui/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tui/app.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/tui/sprite.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_charts.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/report_styles.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/ui/tables.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/updates/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/updates/checker.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/updates/gate.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/updates/release_notes.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/updates/whats_new.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_app/cli.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/__init__.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/app.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/login_item.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/menu.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/prefs.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/watch.py +0 -0
- {usage_cli-0.34.7 → usage_cli-0.34.8}/wintray/whats_new.py +0 -0
|
@@ -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.
|
|
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.
|
|
3
|
+
version = "0.34.8"
|
|
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]
|
|
@@ -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/
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
241
|
+
if (item.attribute) item.el.setAttribute(item.attribute, item.original);
|
|
242
|
+
else item.el.textContent = item.original;
|
|
222
243
|
});
|
|
223
244
|
}
|
|
224
245
|
}
|
|
@@ -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-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|