usage-cli 0.34.11__tar.gz → 0.34.12__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.11/usage_cli.egg-info → usage_cli-0.34.12}/PKG-INFO +1 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/claude.py +3 -4
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/rate_limits.py +2 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/blocks.py +4 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/reporter.py +4 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/session_hooks.py +5 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/setup_hook.py +47 -30
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_disk_cache.py +3 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_loader.py +25 -6
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/agy_quota_probe.py +3 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_loader.py +11 -4
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/muse_quota_probe.py +5 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/main.py +9 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/actions.py +3 -3
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/fsevents_watch.py +7 -5
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/state.py +3 -5
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/title.py +2 -2
- {usage_cli-0.34.11 → usage_cli-0.34.12}/pyproject.toml +1 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/agy_window_keeper.py +3 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/codex_window_keeper.py +3 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/window_keeper.py +3 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_claude.py +22 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_rate_limits.py +17 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_loader.py +22 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_quota_probe.py +19 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_window_keeper.py +44 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_analyzer_blocks.py +20 -0
- usage_cli-0.34.12/tests/test_check_release_version.py +47 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_check_upstream.py +8 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_pane.py +80 -1
- usage_cli-0.34.12/tests/test_cli_lookup.py +123 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_loader.py +44 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_window_keeper.py +20 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_fsevents_watch.py +27 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_html_report_snapshot.py +8 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar.py +147 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_state.py +38 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_quota_probe.py +20 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_filter_js.py +54 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_reporter_build_data.py +55 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook.py +172 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_ui_tables.py +99 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_cli.py +52 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_window_keeper.py +20 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/html_report.py +1 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_filter.py +7 -5
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/tables.py +17 -9
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/cli.py +12 -3
- {usage_cli-0.34.11 → usage_cli-0.34.12/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/SOURCES.txt +3 -0
- usage_cli-0.34.12/usage_common/cli_lookup.py +62 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/pricing.py +6 -1
- {usage_cli-0.34.11 → usage_cli-0.34.12}/LICENSE +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/README.md +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/agy.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/codex.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/grok.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/muse.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/registry.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/adapters/types.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/insights.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/i18n.json +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/login_item.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/agy.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/app.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/chrome.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/grok.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/menu.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/muse.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/notify.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/popover.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/prefs.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/refresh.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/update.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/base.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/catalog.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_window.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/payload.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/web_panel.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/panels/window_drag.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/notifications.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/status_payload.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/setup.cfg +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/setup_app.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_expired_windows.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_insights.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_main.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panels.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_chart_redesign.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_headings.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_tui.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tests/test_wintray.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/app.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/tui/sprite.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_charts.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/ui/report_styles.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/checker.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/gate.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/release_notes.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/updates/whats_new.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/__init__.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/app.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/login_item.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/menu.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/prefs.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/watch.py +0 -0
- {usage_cli-0.34.11 → usage_cli-0.34.12}/wintray/whats_new.py +0 -0
|
@@ -16,6 +16,7 @@ from typing import Any
|
|
|
16
16
|
from loaders.claude_paths import claude_config_dirs
|
|
17
17
|
from loaders.jsonl_utils import iter_jsonl_dicts
|
|
18
18
|
from usage_common.project_resolver import resolve_project_name
|
|
19
|
+
from usage_common.time_utils import parse_optional_iso8601_utc
|
|
19
20
|
|
|
20
21
|
from .types import AgentInfo
|
|
21
22
|
|
|
@@ -190,10 +191,8 @@ def _parse_assistant_entry(data: dict[str, Any], project: str) -> list[UsageEntr
|
|
|
190
191
|
else 0
|
|
191
192
|
)
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ts = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
196
|
-
except (ValueError, AttributeError):
|
|
194
|
+
ts = parse_optional_iso8601_utc(data.get("timestamp", ""))
|
|
195
|
+
if ts is None:
|
|
197
196
|
return []
|
|
198
197
|
|
|
199
198
|
message_id = message.get("id", "")
|
|
@@ -36,7 +36,8 @@ def _read_status() -> dict[str, Any] | None:
|
|
|
36
36
|
try:
|
|
37
37
|
with open(path, "r", encoding="utf-8") as f:
|
|
38
38
|
data: dict[str, Any] = json.load(f)
|
|
39
|
-
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
return data
|
|
40
41
|
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
41
42
|
continue
|
|
42
43
|
return None
|
|
@@ -56,7 +56,10 @@ def analyze_blocks(entries: list[UsageEntry]) -> list[SessionBlock]:
|
|
|
56
56
|
block.is_active = True
|
|
57
57
|
elapsed = (now - block.start_time).total_seconds() / 60
|
|
58
58
|
if elapsed > 0:
|
|
59
|
-
|
|
59
|
+
active_tokens = (
|
|
60
|
+
block.input_tokens + block.output_tokens + block.cache_creation_tokens
|
|
61
|
+
)
|
|
62
|
+
block.burn_rate = active_tokens / elapsed
|
|
60
63
|
|
|
61
64
|
return blocks
|
|
62
65
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import contextlib
|
|
10
|
+
from bisect import bisect_right
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -146,6 +147,7 @@ class CubeModelData(TypedDict):
|
|
|
146
147
|
|
|
147
148
|
|
|
148
149
|
class CubeReportData(TypedDict):
|
|
150
|
+
today: NotRequired[str]
|
|
149
151
|
dates: list[str]
|
|
150
152
|
agents: list[CubeAgentData]
|
|
151
153
|
models: list[CubeModelData]
|
|
@@ -1236,6 +1238,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1236
1238
|
)
|
|
1237
1239
|
cube_rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
|
|
1238
1240
|
cube: CubeReportData = {
|
|
1241
|
+
"today": today.isoformat(),
|
|
1239
1242
|
"dates": cube_dates,
|
|
1240
1243
|
"agents": cube_agents,
|
|
1241
1244
|
"models": cube_models,
|
|
@@ -1293,7 +1296,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1293
1296
|
start_time = _session_start_text(session.start_time)
|
|
1294
1297
|
session_rows.append(
|
|
1295
1298
|
{
|
|
1296
|
-
"date_idx":
|
|
1299
|
+
"date_idx": max(0, bisect_right(cube_dates, start_time[:10]) - 1),
|
|
1297
1300
|
"project_idx": project_indices[session.project or "unknown"],
|
|
1298
1301
|
"model_idx": model_indices[session.model or "unknown"],
|
|
1299
1302
|
"start_time": start_time,
|
|
@@ -1158,7 +1158,11 @@ def self_heal() -> None:
|
|
|
1158
1158
|
return
|
|
1159
1159
|
_migrate_bundled_python_commands_if_needed(settings)
|
|
1160
1160
|
setup_hook._migrate_windows_statusline_command_if_needed(settings)
|
|
1161
|
-
|
|
1161
|
+
usage_settings = settings.get(BACKUP_KEY)
|
|
1162
|
+
statusline_disabled = (
|
|
1163
|
+
isinstance(usage_settings, dict) and setup_hook.DISABLED_SL_KEY in usage_settings
|
|
1164
|
+
)
|
|
1165
|
+
if not is_setup() and "statusLine" not in settings and not statusline_disabled:
|
|
1162
1166
|
exit_code = _run_quietly(setup)
|
|
1163
1167
|
if exit_code == 0:
|
|
1164
1168
|
_append_self_heal_log("install_hook", "initial setup")
|
|
@@ -811,9 +811,7 @@ def _replace_grok_status_line(content: str, replacement: str) -> str | None:
|
|
|
811
811
|
else:
|
|
812
812
|
start, end = section
|
|
813
813
|
candidate = content[:start] + replacement + content[end:]
|
|
814
|
-
|
|
815
|
-
tomllib.loads(candidate)
|
|
816
|
-
except tomllib.TOMLDecodeError:
|
|
814
|
+
if _validated_table_edit(content, candidate, "ui", "status_line") != candidate:
|
|
817
815
|
return None
|
|
818
816
|
return candidate
|
|
819
817
|
|
|
@@ -887,8 +885,9 @@ def _unsetup_grok() -> bool:
|
|
|
887
885
|
restored = content[:start] + content[end:]
|
|
888
886
|
if start > 0 and restored[:start].endswith("\n"):
|
|
889
887
|
restored = restored[: start - 1] + restored[start:]
|
|
888
|
+
if _validated_table_edit(content, restored, "ui", "status_line") != restored:
|
|
889
|
+
return False
|
|
890
890
|
try:
|
|
891
|
-
tomllib.loads(restored)
|
|
892
891
|
_atomic_write_text(GROK_SETTINGS, restored)
|
|
893
892
|
GROK_PREVIOUS_STATUSLINE.unlink(missing_ok=True)
|
|
894
893
|
except (OSError, tomllib.TOMLDecodeError):
|
|
@@ -969,13 +968,31 @@ def _find_top_level_dotted_line(
|
|
|
969
968
|
return dotted_line.search(content, 0, top_level_end)
|
|
970
969
|
|
|
971
970
|
|
|
971
|
+
def _validated_table_edit(content: str, candidate: str, name: str, key: str) -> str:
|
|
972
|
+
"""Reject edits that change anything except the requested TOML key."""
|
|
973
|
+
try:
|
|
974
|
+
before, after = tomllib.loads(content), tomllib.loads(candidate)
|
|
975
|
+
except tomllib.TOMLDecodeError:
|
|
976
|
+
return content
|
|
977
|
+
|
|
978
|
+
def without_target(table: dict[str, Any], path: list[str]) -> None:
|
|
979
|
+
part = path[0]
|
|
980
|
+
if len(path) == 1:
|
|
981
|
+
table.pop(part, None)
|
|
982
|
+
elif isinstance(child := table.get(part), dict):
|
|
983
|
+
without_target(child, path[1:])
|
|
984
|
+
if not child:
|
|
985
|
+
table.pop(part)
|
|
986
|
+
|
|
987
|
+
path = [*name.split("."), key]
|
|
988
|
+
without_target(before, path)
|
|
989
|
+
without_target(after, path)
|
|
990
|
+
return candidate if before == after else content
|
|
991
|
+
|
|
992
|
+
|
|
972
993
|
def _insert_table_line(content: str, name: str, line: str) -> str:
|
|
973
994
|
def validated(candidate: str) -> str:
|
|
974
|
-
|
|
975
|
-
tomllib.loads(candidate)
|
|
976
|
-
except tomllib.TOMLDecodeError:
|
|
977
|
-
return content
|
|
978
|
-
return candidate
|
|
995
|
+
return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
|
|
979
996
|
|
|
980
997
|
table = _find_table(content, name)
|
|
981
998
|
if table is None:
|
|
@@ -1017,11 +1034,9 @@ def _replace_table_line(
|
|
|
1017
1034
|
content: str, name: str, line_regex: re.Pattern[str], replacement: str
|
|
1018
1035
|
) -> str:
|
|
1019
1036
|
def validated(candidate: str) -> str:
|
|
1020
|
-
|
|
1021
|
-
tomllib.loads(
|
|
1022
|
-
|
|
1023
|
-
return content
|
|
1024
|
-
return candidate
|
|
1037
|
+
return _validated_table_edit(
|
|
1038
|
+
content, candidate, name, next(iter(tomllib.loads(replacement)))
|
|
1039
|
+
)
|
|
1025
1040
|
|
|
1026
1041
|
table = _find_table(content, name)
|
|
1027
1042
|
if table is None:
|
|
@@ -1042,13 +1057,9 @@ def _replace_table_line(
|
|
|
1042
1057
|
return validated(candidate)
|
|
1043
1058
|
|
|
1044
1059
|
|
|
1045
|
-
def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str]) -> str:
|
|
1060
|
+
def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str], key: str) -> str:
|
|
1046
1061
|
def validated(candidate: str) -> str:
|
|
1047
|
-
|
|
1048
|
-
tomllib.loads(candidate)
|
|
1049
|
-
except tomllib.TOMLDecodeError:
|
|
1050
|
-
return content
|
|
1051
|
-
return candidate
|
|
1062
|
+
return _validated_table_edit(content, candidate, name, key)
|
|
1052
1063
|
|
|
1053
1064
|
table = _find_table(content, name)
|
|
1054
1065
|
if table is None:
|
|
@@ -1072,11 +1083,7 @@ def _ensure_table_line(content: str, name: str, line_regex: re.Pattern[str], lin
|
|
|
1072
1083
|
itself is absent."""
|
|
1073
1084
|
|
|
1074
1085
|
def validated(candidate: str) -> str:
|
|
1075
|
-
|
|
1076
|
-
tomllib.loads(candidate)
|
|
1077
|
-
except tomllib.TOMLDecodeError:
|
|
1078
|
-
return content
|
|
1079
|
-
return candidate
|
|
1086
|
+
return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
|
|
1080
1087
|
|
|
1081
1088
|
table = _find_table(content, name)
|
|
1082
1089
|
if table is None:
|
|
@@ -1097,7 +1104,7 @@ def _replace_tui_status_line(content: str, replacement: str) -> str:
|
|
|
1097
1104
|
|
|
1098
1105
|
|
|
1099
1106
|
def _remove_tui_status_line(content: str) -> str:
|
|
1100
|
-
return _remove_table_line(content, "tui", _SL_REGEX)
|
|
1107
|
+
return _remove_table_line(content, "tui", _SL_REGEX, "status_line")
|
|
1101
1108
|
|
|
1102
1109
|
|
|
1103
1110
|
def _read_codex_config() -> tuple[str, dict[str, Any]] | None:
|
|
@@ -1150,6 +1157,10 @@ def _setup_codex() -> None:
|
|
|
1150
1157
|
print(_t("setup_codex_config_unmodifiable"))
|
|
1151
1158
|
return
|
|
1152
1159
|
_atomic_write_text(CODEX_CONFIG, content)
|
|
1160
|
+
written = _read_codex_config()
|
|
1161
|
+
if written is None or _codex_status_line(written[1]) != CODEX_STATUS_LINE:
|
|
1162
|
+
print(_t("setup_codex_config_unmodifiable"))
|
|
1163
|
+
return
|
|
1153
1164
|
print(_t("setup_codex_configured"))
|
|
1154
1165
|
if old is not None and old not in LEGACY_CODEX_STATUS_LINES:
|
|
1155
1166
|
print(_t("setup_codex_backup_written", path=CODEX_BACKUP))
|
|
@@ -1335,15 +1346,20 @@ def unsetup() -> int:
|
|
|
1335
1346
|
settings = _load_settings()
|
|
1336
1347
|
sl = settings.get("statusLine")
|
|
1337
1348
|
backup = settings.get(BACKUP_KEY)
|
|
1338
|
-
parked = backup.
|
|
1339
|
-
if sl is None:
|
|
1340
|
-
sl = parked
|
|
1349
|
+
parked = backup.get(DISABLED_SL_KEY) if isinstance(backup, dict) else None
|
|
1341
1350
|
|
|
1342
|
-
if _is_usage_hook(sl) or _is_legacy_tt_hook(sl):
|
|
1351
|
+
if _is_usage_hook(sl) or _is_legacy_tt_hook(sl) or (sl is None and parked is not None):
|
|
1343
1352
|
legacy_backup = settings.get(LEGACY_TT_BACKUP_KEY)
|
|
1344
1353
|
prev = backup.get(PREV_SL_KEY) if isinstance(backup, dict) else None
|
|
1345
1354
|
if not isinstance(prev, dict) and isinstance(legacy_backup, dict):
|
|
1346
1355
|
prev = legacy_backup.get(PREV_SL_KEY)
|
|
1356
|
+
if (
|
|
1357
|
+
not isinstance(prev, dict)
|
|
1358
|
+
and isinstance(parked, dict)
|
|
1359
|
+
and not _is_usage_hook(parked)
|
|
1360
|
+
and not _is_legacy_tt_hook(parked)
|
|
1361
|
+
):
|
|
1362
|
+
prev = parked
|
|
1347
1363
|
|
|
1348
1364
|
if isinstance(prev, dict):
|
|
1349
1365
|
settings["statusLine"] = prev
|
|
@@ -1354,6 +1370,7 @@ def unsetup() -> int:
|
|
|
1354
1370
|
|
|
1355
1371
|
if isinstance(backup, dict):
|
|
1356
1372
|
backup.pop(PREV_SL_KEY, None)
|
|
1373
|
+
backup.pop(DISABLED_SL_KEY, None)
|
|
1357
1374
|
if not backup:
|
|
1358
1375
|
del settings[BACKUP_KEY]
|
|
1359
1376
|
if isinstance(legacy_backup, dict):
|
|
@@ -91,11 +91,13 @@ def seed_caches(
|
|
|
91
91
|
continue
|
|
92
92
|
if len(file_cache) >= maxsize:
|
|
93
93
|
file_cache.popitem(last=False)
|
|
94
|
+
wal = file_data.get("wal")
|
|
94
95
|
file_cache[Path(path_str)] = _FileCacheEntry(
|
|
95
96
|
mtime=float(file_data["mtime"]),
|
|
96
97
|
size=int(file_data["size"]),
|
|
97
98
|
entries=[_deserialize_entry(entry) for entry in entries_data],
|
|
98
99
|
skipped_missing_dedup_key=int(file_data["skipped_missing_dedup_key"]),
|
|
100
|
+
wal=None if wal is None else (int(wal[0]), int(wal[1])),
|
|
99
101
|
)
|
|
100
102
|
except (KeyError, TypeError, ValueError):
|
|
101
103
|
continue
|
|
@@ -118,6 +120,7 @@ def flush_caches(
|
|
|
118
120
|
str(path): {
|
|
119
121
|
"mtime": entry.mtime,
|
|
120
122
|
"size": entry.size,
|
|
123
|
+
"wal": entry.wal,
|
|
121
124
|
"entries": [_serialize_entry(item) for item in entry.entries],
|
|
122
125
|
"skipped_missing_dedup_key": entry.skipped_missing_dedup_key,
|
|
123
126
|
}
|
|
@@ -38,7 +38,7 @@ AGY_SESSIONS_DIR = Path(os.path.expanduser("~/.gemini/antigravity-cli/conversati
|
|
|
38
38
|
# below that count evicts databases parsed during the prior refresh, causing a
|
|
39
39
|
# permanent full SQLite/protobuf reparse on every load (LRU thrashing).
|
|
40
40
|
_FILE_CACHE_MAXSIZE = 4096
|
|
41
|
-
_AGY_DB_CACHE_SCHEMA =
|
|
41
|
+
_AGY_DB_CACHE_SCHEMA = 4
|
|
42
42
|
AGY_CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_db_cache.json"))
|
|
43
43
|
_disk_cache_seeded = False
|
|
44
44
|
_DISK_CACHE_FLUSH_INTERVAL_S = 300.0
|
|
@@ -92,6 +92,7 @@ class _FileCacheEntry:
|
|
|
92
92
|
size: int
|
|
93
93
|
entries: list[AgyUsageEntry]
|
|
94
94
|
skipped_missing_dedup_key: int
|
|
95
|
+
wal: tuple[int, int] | None = None
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
_file_cache: OrderedDict[Path, _FileCacheEntry] = OrderedDict()
|
|
@@ -120,22 +121,24 @@ def load_entries_with_stats(hours_back: int = 0) -> AgyLoadResult:
|
|
|
120
121
|
skipped_missing_dedup_key = 0
|
|
121
122
|
cutoff_ts = cutoff.timestamp() if cutoff else None
|
|
122
123
|
file_cache_snapshot = {
|
|
123
|
-
str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
|
|
124
|
+
str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
|
|
124
125
|
}
|
|
125
126
|
for path in AGY_SESSIONS_DIR.glob("*.db"):
|
|
126
127
|
if cutoff_ts is not None:
|
|
127
128
|
try:
|
|
128
|
-
|
|
129
|
-
continue
|
|
129
|
+
modified = path.stat().st_mtime
|
|
130
130
|
except OSError as exc:
|
|
131
131
|
logger.warning("failed to stat Antigravity database %s: %s", path, exc)
|
|
132
132
|
continue
|
|
133
|
+
wal = _wal_fingerprint(path)
|
|
134
|
+
if max(modified, wal[0] / 1e9 if wal else 0.0) < cutoff_ts:
|
|
135
|
+
continue
|
|
133
136
|
file_entries, skipped = _load_database(path, cutoff, seen_dedup_keys)
|
|
134
137
|
entries.extend(file_entries)
|
|
135
138
|
skipped_missing_dedup_key += skipped
|
|
136
139
|
|
|
137
140
|
if {
|
|
138
|
-
str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
|
|
141
|
+
str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
|
|
139
142
|
} != file_cache_snapshot:
|
|
140
143
|
_disk_cache_dirty = True
|
|
141
144
|
_flush_caches_to_disk()
|
|
@@ -176,6 +179,15 @@ def flush_caches_on_terminate() -> None:
|
|
|
176
179
|
_flush_caches_on_terminate(lambda: _flush_caches_to_disk(force=True))
|
|
177
180
|
|
|
178
181
|
|
|
182
|
+
def _wal_fingerprint(path: Path) -> tuple[int, int] | None:
|
|
183
|
+
"""Return the ``-wal`` file's (mtime_ns, size); WAL commits leave the main file untouched."""
|
|
184
|
+
try:
|
|
185
|
+
stat = Path(f"{path}-wal").stat()
|
|
186
|
+
except OSError:
|
|
187
|
+
return None
|
|
188
|
+
return (stat.st_mtime_ns, stat.st_size)
|
|
189
|
+
|
|
190
|
+
|
|
179
191
|
def _load_database(
|
|
180
192
|
path: Path,
|
|
181
193
|
cutoff: datetime | None,
|
|
@@ -187,8 +199,14 @@ def _load_database(
|
|
|
187
199
|
logger.warning("failed to stat Antigravity database %s: %s", path, exc)
|
|
188
200
|
return [], 0
|
|
189
201
|
|
|
202
|
+
wal = _wal_fingerprint(path)
|
|
190
203
|
cached = _file_cache.get(path)
|
|
191
|
-
if
|
|
204
|
+
if (
|
|
205
|
+
cached is not None
|
|
206
|
+
and cached.mtime == st.st_mtime
|
|
207
|
+
and cached.size == st.st_size
|
|
208
|
+
and cached.wal == wal
|
|
209
|
+
):
|
|
192
210
|
_file_cache.move_to_end(path)
|
|
193
211
|
return _filter_entries(
|
|
194
212
|
cached.entries,
|
|
@@ -208,6 +226,7 @@ def _load_database(
|
|
|
208
226
|
size=st.st_size,
|
|
209
227
|
entries=parsed_entries,
|
|
210
228
|
skipped_missing_dedup_key=skipped_missing_dedup_key,
|
|
229
|
+
wal=wal,
|
|
211
230
|
)
|
|
212
231
|
return _filter_entries(parsed_entries, skipped_missing_dedup_key, cutoff, seen_dedup_keys)
|
|
213
232
|
|
|
@@ -18,7 +18,6 @@ from __future__ import annotations
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
20
|
import platform
|
|
21
|
-
import shutil
|
|
22
21
|
import subprocess
|
|
23
22
|
import sys
|
|
24
23
|
import tempfile
|
|
@@ -35,6 +34,7 @@ from typing import Any
|
|
|
35
34
|
from urllib.error import HTTPError, URLError
|
|
36
35
|
from urllib.request import Request, urlopen
|
|
37
36
|
|
|
37
|
+
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
38
38
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
39
39
|
|
|
40
40
|
CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
|
|
@@ -168,6 +168,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
168
168
|
capture_output=True,
|
|
169
169
|
timeout=timeout,
|
|
170
170
|
stdin=subprocess.DEVNULL,
|
|
171
|
+
env=cli_env(agy),
|
|
171
172
|
**hidden_console_kwargs(),
|
|
172
173
|
)
|
|
173
174
|
except (OSError, subprocess.SubprocessError):
|
|
@@ -189,14 +190,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
189
190
|
|
|
190
191
|
def find_agy() -> str | None:
|
|
191
192
|
"""Find the Antigravity CLI in PATH or common user installation paths."""
|
|
192
|
-
|
|
193
|
-
if path is not None:
|
|
194
|
-
return path
|
|
195
|
-
for candidate in ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"):
|
|
196
|
-
expanded = os.path.expanduser(candidate)
|
|
197
|
-
if os.access(expanded, os.X_OK):
|
|
198
|
-
return expanded
|
|
199
|
-
return None
|
|
193
|
+
return resolve_cli("agy", ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"))
|
|
200
194
|
|
|
201
195
|
|
|
202
196
|
def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
|
|
@@ -652,16 +652,23 @@ def _rate_limits_from_websocket_error(
|
|
|
652
652
|
headers = _as_dict(event.get("headers"))
|
|
653
653
|
primary_reset = _as_optional_float(headers.get("X-Codex-Primary-Reset-At"))
|
|
654
654
|
secondary_reset = _as_optional_float(headers.get("X-Codex-Secondary-Reset-At"))
|
|
655
|
-
|
|
655
|
+
recorded_at = _timestamp_from_log_ts(ts)
|
|
656
|
+
recorded_ts = recorded_at.timestamp() if recorded_at is not None else None
|
|
656
657
|
if primary_reset is None:
|
|
657
658
|
primary_reset_after = _as_optional_float(headers.get("X-Codex-Primary-Reset-After-Seconds"))
|
|
658
|
-
primary_reset =
|
|
659
|
+
primary_reset = (
|
|
660
|
+
recorded_ts + primary_reset_after
|
|
661
|
+
if recorded_ts is not None and primary_reset_after is not None
|
|
662
|
+
else None
|
|
663
|
+
)
|
|
659
664
|
if secondary_reset is None:
|
|
660
665
|
secondary_reset_after = _as_optional_float(
|
|
661
666
|
headers.get("X-Codex-Secondary-Reset-After-Seconds")
|
|
662
667
|
)
|
|
663
668
|
secondary_reset = (
|
|
664
|
-
|
|
669
|
+
recorded_ts + secondary_reset_after
|
|
670
|
+
if recorded_ts is not None and secondary_reset_after is not None
|
|
671
|
+
else None
|
|
665
672
|
)
|
|
666
673
|
return _build_rate_limits(
|
|
667
674
|
primary_pct=_as_optional_float(headers.get("X-Codex-Primary-Used-Percent")),
|
|
@@ -669,7 +676,7 @@ def _rate_limits_from_websocket_error(
|
|
|
669
676
|
secondary_pct=_as_optional_float(headers.get("X-Codex-Secondary-Used-Percent")),
|
|
670
677
|
secondary_reset=secondary_reset,
|
|
671
678
|
model=_event_value(body, "model") or "unknown",
|
|
672
|
-
updated_at=
|
|
679
|
+
updated_at=recorded_at,
|
|
673
680
|
)
|
|
674
681
|
|
|
675
682
|
|
|
@@ -11,7 +11,6 @@ import logging
|
|
|
11
11
|
import math
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
|
-
import shutil
|
|
15
14
|
import subprocess
|
|
16
15
|
import tempfile
|
|
17
16
|
import threading
|
|
@@ -22,6 +21,7 @@ from pathlib import Path
|
|
|
22
21
|
from typing import TypedDict, TypeGuard
|
|
23
22
|
|
|
24
23
|
from loaders import muse_loader
|
|
24
|
+
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
25
25
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
26
26
|
|
|
27
27
|
logger = logging.getLogger(__name__)
|
|
@@ -54,14 +54,9 @@ class MuseQuotaResult(TypedDict):
|
|
|
54
54
|
|
|
55
55
|
def find_muse() -> str | None:
|
|
56
56
|
"""Find Muse even when the app inherits a limited GUI PATH."""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for candidate in ("~/.local/bin/muse", "/opt/homebrew/bin/muse", "/usr/local/bin/muse"):
|
|
61
|
-
expanded = os.path.expanduser(candidate)
|
|
62
|
-
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
63
|
-
return expanded
|
|
64
|
-
return None
|
|
57
|
+
return resolve_cli(
|
|
58
|
+
"muse", ("~/.local/bin/muse", "/opt/homebrew/bin/muse", "/usr/local/bin/muse")
|
|
59
|
+
)
|
|
65
60
|
|
|
66
61
|
|
|
67
62
|
def _number(value: object) -> TypeGuard[int | float]:
|
|
@@ -239,6 +234,7 @@ def _probe_worker(executable: str) -> None:
|
|
|
239
234
|
stderr=subprocess.PIPE,
|
|
240
235
|
encoding="utf-8",
|
|
241
236
|
cwd=PROBE_DIR,
|
|
237
|
+
env=cli_env(executable),
|
|
242
238
|
**hidden_console_kwargs(),
|
|
243
239
|
)
|
|
244
240
|
_probe_process = process
|
|
@@ -231,11 +231,19 @@ def main() -> None:
|
|
|
231
231
|
session_hooks._migrate_bundled_python_commands_if_needed()
|
|
232
232
|
raise SystemExit(exit_code)
|
|
233
233
|
if args.unsetup:
|
|
234
|
-
from installer.
|
|
234
|
+
from installer.claude_pane import disable_claude_beginner, disable_claude_pane
|
|
235
|
+
from installer.session_hooks import (
|
|
236
|
+
disable_quota_aware,
|
|
237
|
+
disable_session_resume,
|
|
238
|
+
disable_terse_mode,
|
|
239
|
+
)
|
|
235
240
|
from installer.setup_hook import unsetup
|
|
236
241
|
|
|
237
242
|
disable_session_resume()
|
|
238
243
|
disable_terse_mode()
|
|
244
|
+
disable_quota_aware()
|
|
245
|
+
disable_claude_pane()
|
|
246
|
+
disable_claude_beginner()
|
|
239
247
|
raise SystemExit(unsetup())
|
|
240
248
|
preferences_snapshot = _load_preferences()
|
|
241
249
|
_self_heal()
|
|
@@ -177,7 +177,7 @@ def show_forwarder_mode_prompt_if_needed(language: str | None = None) -> None:
|
|
|
177
177
|
)
|
|
178
178
|
if dismissed or setup_hook._detect_current_state(settings) != "external":
|
|
179
179
|
return
|
|
180
|
-
except Exception:
|
|
180
|
+
except (Exception, SystemExit):
|
|
181
181
|
if os.environ.get("USAGE_DEBUG") == "1":
|
|
182
182
|
logger.warning("forwarder prompt check failed", exc_info=True)
|
|
183
183
|
return
|
|
@@ -194,13 +194,13 @@ def show_forwarder_mode_prompt_if_needed(language: str | None = None) -> None:
|
|
|
194
194
|
if result == 1000:
|
|
195
195
|
setup_hook.setup(force_forwarder=True)
|
|
196
196
|
session_hooks._migrate_bundled_python_commands_if_needed()
|
|
197
|
-
except Exception:
|
|
197
|
+
except (Exception, SystemExit):
|
|
198
198
|
if os.environ.get("USAGE_DEBUG") == "1":
|
|
199
199
|
logger.warning("forwarder setup failed", exc_info=True)
|
|
200
200
|
finally:
|
|
201
201
|
try:
|
|
202
202
|
_set_forwarder_mode_prompt_dismissed()
|
|
203
|
-
except Exception:
|
|
203
|
+
except (Exception, SystemExit):
|
|
204
204
|
if os.environ.get("USAGE_DEBUG") == "1":
|
|
205
205
|
logger.warning("forwarder prompt dismissal failed", exc_info=True)
|
|
206
206
|
|
|
@@ -15,6 +15,7 @@ from dataclasses import dataclass
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
+
from loaders.claude_paths import claude_config_dirs
|
|
18
19
|
from loaders.codex_paths import codex_home
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
@@ -115,14 +116,15 @@ def classify_file_events(paths: Iterable[str], flags: Iterable[int]) -> FileEven
|
|
|
115
116
|
|
|
116
117
|
def usage_watch_paths() -> list[Path]:
|
|
117
118
|
"""Return existing agent directories that contain usage history records."""
|
|
118
|
-
home = Path.home()
|
|
119
119
|
codex_dir = codex_home()
|
|
120
120
|
return [
|
|
121
121
|
path
|
|
122
|
-
for path in (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
for path in dict.fromkeys(
|
|
123
|
+
(
|
|
124
|
+
*(directory / "projects" for directory in claude_config_dirs()),
|
|
125
|
+
codex_dir / "sessions",
|
|
126
|
+
codex_dir / "archived_sessions",
|
|
127
|
+
)
|
|
126
128
|
)
|
|
127
129
|
if path.exists()
|
|
128
130
|
]
|
|
@@ -571,14 +571,12 @@ def project_rows_for_windows(
|
|
|
571
571
|
current_time = datetime.now(UTC) if now is None else now
|
|
572
572
|
local_now = current_time.astimezone()
|
|
573
573
|
local_today = local_now.date()
|
|
574
|
-
|
|
575
|
-
assert local_tz is not None
|
|
576
|
-
today_start = datetime.combine(local_today, datetime_time.min, tzinfo=local_tz).astimezone(UTC)
|
|
574
|
+
today_start = datetime.combine(local_today, datetime_time.min).astimezone(UTC)
|
|
577
575
|
tomorrow_start = datetime.combine(
|
|
578
|
-
local_today + timedelta(days=1), datetime_time.min
|
|
576
|
+
local_today + timedelta(days=1), datetime_time.min
|
|
579
577
|
).astimezone(UTC)
|
|
580
578
|
yesterday_start = datetime.combine(
|
|
581
|
-
local_today - timedelta(days=1), datetime_time.min
|
|
579
|
+
local_today - timedelta(days=1), datetime_time.min
|
|
582
580
|
).astimezone(UTC)
|
|
583
581
|
cutoff_7d = current_time - timedelta(hours=168)
|
|
584
582
|
cutoff_30d = current_time - timedelta(hours=720)
|
|
@@ -35,7 +35,7 @@ from menubar.state import PopoverState, QuotaRowState, _format_percent
|
|
|
35
35
|
class _TitleApp(Protocol):
|
|
36
36
|
_menubar_text_cache: dict[str, Any]
|
|
37
37
|
codex_5h_pct: float | None
|
|
38
|
-
_last_button_title_key: tuple[
|
|
38
|
+
_last_button_title_key: tuple[Any, ...] | None
|
|
39
39
|
_last_plain_title_key: tuple[str] | None
|
|
40
40
|
status_item: Any
|
|
41
41
|
interval: float
|
|
@@ -166,7 +166,7 @@ def _menubar_attributed_title(app: _TitleApp, state: PopoverState) -> Any:
|
|
|
166
166
|
def _set_button_title(app: _TitleApp, state: PopoverState) -> None:
|
|
167
167
|
app._reschedule_poll_timer(menubar_poll_interval(app, state))
|
|
168
168
|
title = _compose_title(app, state)
|
|
169
|
-
title_key = (title,)
|
|
169
|
+
title_key = (title, tuple(icon for icon, _text in _menubar_items(app, state)))
|
|
170
170
|
if app._last_button_title_key == title_key:
|
|
171
171
|
return
|
|
172
172
|
|
|
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
import shutil
|
|
14
13
|
import subprocess
|
|
15
14
|
import threading
|
|
16
15
|
import time
|
|
@@ -19,6 +18,7 @@ from pathlib import Path
|
|
|
19
18
|
from menubar.agy import AgyRefreshResult
|
|
20
19
|
from menubar.prefs import _agy_window_keeper_enabled
|
|
21
20
|
from quota import keeper_outcome
|
|
21
|
+
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
22
22
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
@@ -63,14 +63,7 @@ def _load_last_ping(path: Path | None = None) -> float | None:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _resolve_agy_bin() -> str | None:
|
|
66
|
-
|
|
67
|
-
if found:
|
|
68
|
-
return found
|
|
69
|
-
for candidate in _AGY_BIN_FALLBACKS:
|
|
70
|
-
resolved = os.path.expanduser(candidate)
|
|
71
|
-
if os.path.isfile(resolved) and os.access(resolved, os.X_OK):
|
|
72
|
-
return resolved
|
|
73
|
-
return None
|
|
66
|
+
return resolve_cli("agy", _AGY_BIN_FALLBACKS)
|
|
74
67
|
|
|
75
68
|
|
|
76
69
|
def _try_acquire() -> bool:
|
|
@@ -98,6 +91,7 @@ def _run_agy_ping(agy_bin: str) -> subprocess.CompletedProcess[str]:
|
|
|
98
91
|
timeout=PING_TIMEOUT_SECONDS,
|
|
99
92
|
cwd=os.path.expanduser("~"),
|
|
100
93
|
check=False,
|
|
94
|
+
env=cli_env(agy_bin),
|
|
101
95
|
**hidden_console_kwargs(),
|
|
102
96
|
)
|
|
103
97
|
|