usage-cli 0.34.16__tar.gz → 0.34.17__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.16/usage_cli.egg-info → usage_cli-0.34.17}/PKG-INFO +3 -3
- {usage_cli-0.34.16 → usage_cli-0.34.17}/README.md +2 -2
- {usage_cli-0.34.16 → usage_cli-0.34.17}/i18n.json +5 -5
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/session_hooks.py +1 -1
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/agy_quota_probe.py +201 -19
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/agy.py +10 -3
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/refresh.py +3 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/pyproject.toml +1 -1
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/agy_window_keeper.py +51 -12
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/status_payload.py +11 -1
- usage_cli-0.34.17/tests/test_agy_background_auth.py +536 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_agy_quota_probe.py +165 -1
- usage_cli-0.34.17/tests/test_quota_aware.py +1772 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_service_status.py +52 -0
- usage_cli-0.34.17/tests/test_status_payload.py +64 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_statusline_agy.py +13 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17/usage_cli.egg-info}/PKG-INFO +3 -3
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_cli.egg-info/SOURCES.txt +2 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/service_status.py +34 -0
- usage_cli-0.34.17/usage_hooks/usage_quota_aware.py +590 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_statusline_agy.py +10 -1
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/app.py +2 -0
- usage_cli-0.34.16/tests/test_quota_aware.py +0 -473
- usage_cli-0.34.16/usage_hooks/usage_quota_aware.py +0 -217
- {usage_cli-0.34.16 → usage_cli-0.34.17}/LICENSE +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/agy.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/claude.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/codex.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/grok.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/muse.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/rate_limits.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/registry.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/adapters/types.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/blocks.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/insights.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/ledger_legacy_backfill.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/reporter.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/login_item.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/setup_hook.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/agy_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/codex_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/loaders/muse_quota_probe.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/main.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/actions.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/app.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/chrome.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/grok.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/muse.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/notify.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/popover.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/prefs.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/quota_countdown.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/state.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/title.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/update.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/base.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/catalog.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/panel_window.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/payload.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/web_panel.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/panels/window_drag.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/notifications.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/quota/window_keeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/setup.cfg +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/setup_app.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_check_release_version.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_check_upstream.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_claude_expired_windows.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_cli_lookup.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_insights.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_ledger_legacy_backfill.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_main.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_permission.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_muse_quota_probe.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_network.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_panels.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_quota_threshold_menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_chart_redesign.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_headings.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_tui.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tests/test_wintray.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tui/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tui/app.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/tui/sprite.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/html_report.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_charts.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_filter.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/report_styles.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/ui/tables.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/updates/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/updates/checker.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/updates/gate.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/updates/release_notes.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/updates/whats_new.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_app/cli.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/cli_lookup.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/network.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/pricing.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/__init__.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/login_item.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/prefs.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/watch.py +0 -0
- {usage_cli-0.34.16 → usage_cli-0.34.17}/wintray/whats_new.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usage-cli
|
|
3
|
-
Version: 0.34.
|
|
3
|
+
Version: 0.34.17
|
|
4
4
|
Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -123,7 +123,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
123
123
|
| **Token Saver** | Asks Claude Code and Codex for shorter, plainer replies while keeping code and error messages byte-exact. In an A/B test on real sessions, late replies stayed **~40% shorter** instead of drifting 84% longer. | Off · no extra model calls |
|
|
124
124
|
| **Side pane** | Quotas, other conversations, and background jobs next to your work. [See the side pane](#claude-code-side-pane). | Off · no extra model calls |
|
|
125
125
|
| **Beginner Mode** | Explains up to three technical terms from each answer in one plain line, then brings them back for review. | Off · a little Claude quota |
|
|
126
|
-
| **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. | Off · no extra model calls |
|
|
126
|
+
| **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. Claude also gets a short quota overview at the start of each conversation. | Off · no extra model calls |
|
|
127
127
|
| **Auto-start 5-hour session** | Right after a 5-hour quota resets, sends each tool one tiny message so the next window starts counting right away. | Off · negligible quota |
|
|
128
128
|
| **Token-waste health check** | A daily scan of your logs for repeated file reads and noisy output. Say "show me" and the AI walks you through fixes. | Comes with Progress Concierge · no extra model calls |
|
|
129
129
|
|
|
@@ -146,7 +146,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
146
146
|
- **Linux:** CI verifies `usage setup` on Ubuntu.
|
|
147
147
|
- **Progress Concierge:** When you `/resume` a conversation that sat long enough for its cache to expire, it warns how many tokens the next message will re-send and suggests `/compact` first.
|
|
148
148
|
- **Beginner Mode (macOS and Windows):** Terms appear above the prompt in your UI language. Press 9 to mark them understood; skipped terms come back after 1, 3, then 7 days. Understood terms return as a one-question multiple-choice quiz 7, 21, then 60 days later, at most one a day, and a wrong answer puts the term back in the hints. `/terms` opens your term history. Picking terms asks Claude Haiku through your Claude Code, and pauses while your 5-hour quota is at 90% or more.
|
|
149
|
-
- **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, and treats each quota and model group separately, so a used-up one only matters for work that runs on it.
|
|
149
|
+
- **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, tells Claude once when a quota it warned about resets, and treats each quota and model group separately, so a used-up one only matters for work that runs on it. Official Claude Code / Claude API or Codex API incidents and recovery also notify Claude, once per status change in each conversation. Claude also gets a short quota overview at the start of each conversation. After `/login` changes accounts, Claude hears once that earlier Claude Code quota notices no longer apply.
|
|
150
150
|
- **Auto-start 5-hour session:** Claude gets Haiku, Antigravity gets Gemini 3.8 Flash Low, and Codex gets its cheapest model. The quota used is negligible. The Claude and Codex pings skip your own hooks and stay out of `claude --resume` and `codex resume`.
|
|
151
151
|
- **Panel:** It stays put when another app takes focus; a second click on the menu bar icon, or Escape, closes it. Card order is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
|
|
152
152
|
- **HTML reports:** A "What you worked on" section lists the names Claude Code gave your recent conversations, and masking covers those titles too.
|
|
@@ -101,7 +101,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
101
101
|
| **Token Saver** | Asks Claude Code and Codex for shorter, plainer replies while keeping code and error messages byte-exact. In an A/B test on real sessions, late replies stayed **~40% shorter** instead of drifting 84% longer. | Off · no extra model calls |
|
|
102
102
|
| **Side pane** | Quotas, other conversations, and background jobs next to your work. [See the side pane](#claude-code-side-pane). | Off · no extra model calls |
|
|
103
103
|
| **Beginner Mode** | Explains up to three technical terms from each answer in one plain line, then brings them back for review. | Off · a little Claude quota |
|
|
104
|
-
| **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. | Off · no extra model calls |
|
|
104
|
+
| **Quota-Aware Mode** | When a quota runs low, Claude tells you before a big task that would use it, and lets you do a smaller part or wait for the reset. Claude also gets a short quota overview at the start of each conversation. | Off · no extra model calls |
|
|
105
105
|
| **Auto-start 5-hour session** | Right after a 5-hour quota resets, sends each tool one tiny message so the next window starts counting right away. | Off · negligible quota |
|
|
106
106
|
| **Token-waste health check** | A daily scan of your logs for repeated file reads and noisy output. Say "show me" and the AI walks you through fixes. | Comes with Progress Concierge · no extra model calls |
|
|
107
107
|
|
|
@@ -124,7 +124,7 @@ Once set up, the bottom of the Claude Code window will show a status line like t
|
|
|
124
124
|
- **Linux:** CI verifies `usage setup` on Ubuntu.
|
|
125
125
|
- **Progress Concierge:** When you `/resume` a conversation that sat long enough for its cache to expire, it warns how many tokens the next message will re-send and suggests `/compact` first.
|
|
126
126
|
- **Beginner Mode (macOS and Windows):** Terms appear above the prompt in your UI language. Press 9 to mark them understood; skipped terms come back after 1, 3, then 7 days. Understood terms return as a one-question multiple-choice quiz 7, 21, then 60 days later, at most one a day, and a wrong answer puts the term back in the hints. `/terms` opens your term history. Picking terms asks Claude Haiku through your Claude Code, and pauses while your 5-hour quota is at 90% or more.
|
|
127
|
-
- **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, and treats each quota and model group separately, so a used-up one only matters for work that runs on it.
|
|
127
|
+
- **Quota-Aware Mode (macOS and Windows):** Claude Code gets one line with what is left and when it resets when a 5-hour quota passes 80%, 90%, or 95%, or a weekly quota passes 95%. It covers Claude Code, Codex, Antigravity, and Muse, says each level once per conversation, tells Claude once when a quota it warned about resets, and treats each quota and model group separately, so a used-up one only matters for work that runs on it. Official Claude Code / Claude API or Codex API incidents and recovery also notify Claude, once per status change in each conversation. Claude also gets a short quota overview at the start of each conversation. After `/login` changes accounts, Claude hears once that earlier Claude Code quota notices no longer apply.
|
|
128
128
|
- **Auto-start 5-hour session:** Claude gets Haiku, Antigravity gets Gemini 3.8 Flash Low, and Codex gets its cheapest model. The quota used is negligible. The Claude and Codex pings skip your own hooks and stay out of `claude --resume` and `codex resume`.
|
|
129
129
|
- **Panel:** It stays put when another app takes focus; a second click on the menu bar icon, or Escape, closes it. Card order is shared across every theme with quota cards (all except World Cup 2026) and survives restarts.
|
|
130
130
|
- **HTML reports:** A "What you worked on" section lists the names Claude Code gave your recent conversations, and masking covers those titles too.
|
|
@@ -556,7 +556,7 @@
|
|
|
556
556
|
"claude_beginner_disabled_msg": "已關閉新手模式。重開 Claude Code 後生效。",
|
|
557
557
|
"claude_beginner_action_failed": "無法切換新手模式",
|
|
558
558
|
"quota_aware_menu": "額度感知模式",
|
|
559
|
-
"quota_aware_tooltip": "額度快用完時,Claude 開大任務前會先告訴你剩多少、幾點重置,讓你選先做小的還是等重置。",
|
|
559
|
+
"quota_aware_tooltip": "額度快用完時,Claude 開大任務前會先告訴你剩多少、幾點重置,讓你選先做小的還是等重置。Claude Code 或 Codex 官方服務出狀況時,也會告訴 Claude。每個對話開始時,也會給 Claude 一份各家額度的簡短總覽。",
|
|
560
560
|
"quota_aware_enabled_msg": "已開啟額度感知模式。請重啟 Claude Code 套用。",
|
|
561
561
|
"quota_aware_disabled_msg": "已關閉額度感知模式。請重啟 Claude Code 套用。",
|
|
562
562
|
"quota_aware_action_failed": "無法切換額度感知模式。",
|
|
@@ -1136,7 +1136,7 @@
|
|
|
1136
1136
|
"claude_beginner_disabled_msg": "Beginner Mode disabled. Restart Claude Code to apply.",
|
|
1137
1137
|
"claude_beginner_action_failed": "Could not change Beginner Mode",
|
|
1138
1138
|
"quota_aware_menu": "Quota-Aware Mode",
|
|
1139
|
-
"quota_aware_tooltip": "When quota is running low, Claude tells you how much remains and when it resets before starting a large task, so you can choose a smaller part now or wait for the reset.",
|
|
1139
|
+
"quota_aware_tooltip": "When quota is running low, Claude tells you how much remains and when it resets before starting a large task, so you can choose a smaller part now or wait for the reset. Claude is also notified of official Claude Code or Codex service incidents. Claude also gets a short quota overview at the start of each conversation.",
|
|
1140
1140
|
"quota_aware_enabled_msg": "Quota-Aware Mode enabled. Restart Claude Code to apply.",
|
|
1141
1141
|
"quota_aware_disabled_msg": "Quota-Aware Mode disabled. Restart Claude Code to apply.",
|
|
1142
1142
|
"quota_aware_action_failed": "Could not change Quota-Aware Mode.",
|
|
@@ -1716,7 +1716,7 @@
|
|
|
1716
1716
|
"claude_beginner_disabled_msg": "已关闭新手模式。重启 Claude Code 后生效。",
|
|
1717
1717
|
"claude_beginner_action_failed": "无法切换新手模式",
|
|
1718
1718
|
"quota_aware_menu": "额度感知模式",
|
|
1719
|
-
"quota_aware_tooltip": "额度快用完时,Claude 开大任务前会先告诉你还剩多少、几点重置,让你选先做小的还是等重置。",
|
|
1719
|
+
"quota_aware_tooltip": "额度快用完时,Claude 开大任务前会先告诉你还剩多少、几点重置,让你选先做小的还是等重置。Claude Code 或 Codex 官方服务出状况时,也会告诉 Claude。每个对话开始时,也会给 Claude 一份各家额度的简短总览。",
|
|
1720
1720
|
"quota_aware_enabled_msg": "已开启额度感知模式。请重启 Claude Code 应用。",
|
|
1721
1721
|
"quota_aware_disabled_msg": "已关闭额度感知模式。请重启 Claude Code 应用。",
|
|
1722
1722
|
"quota_aware_action_failed": "无法切换额度感知模式。",
|
|
@@ -2296,7 +2296,7 @@
|
|
|
2296
2296
|
"claude_beginner_disabled_msg": "初心者モードを無効にしました。Claude Code を再起動してください。",
|
|
2297
2297
|
"claude_beginner_action_failed": "初心者モードを切り替えられません",
|
|
2298
2298
|
"quota_aware_menu": "クォータ認識モード",
|
|
2299
|
-
"quota_aware_tooltip": "クォータが少なくなると、大きなタスクを始める前に Claude が残量とリセット時刻を伝えます。小さな部分を先に進めるか、リセットを待つか選べます。",
|
|
2299
|
+
"quota_aware_tooltip": "クォータが少なくなると、大きなタスクを始める前に Claude が残量とリセット時刻を伝えます。小さな部分を先に進めるか、リセットを待つか選べます。Claude Code または Codex の公式サービスに障害が起きたときも Claude に伝えます。会話の開始時にも、各ツールのクォータの短い概要を Claude に伝えます。",
|
|
2300
2300
|
"quota_aware_enabled_msg": "クォータ認識モードを有効にしました。Claude Code を再起動してください。",
|
|
2301
2301
|
"quota_aware_disabled_msg": "クォータ認識モードを無効にしました。Claude Code を再起動してください。",
|
|
2302
2302
|
"quota_aware_action_failed": "クォータ認識モードを切り替えられませんでした。",
|
|
@@ -2876,7 +2876,7 @@
|
|
|
2876
2876
|
"claude_beginner_disabled_msg": "초보자 모드를 껐습니다. Claude Code를 다시 시작하세요.",
|
|
2877
2877
|
"claude_beginner_action_failed": "초보자 모드를 전환할 수 없습니다",
|
|
2878
2878
|
"quota_aware_menu": "할당량 인식 모드",
|
|
2879
|
-
"quota_aware_tooltip": "할당량이 부족해지면 Claude가 큰 작업을 시작하기 전에 남은 양과 초기화 시간을 알려 줍니다. 작은 부분을 먼저 진행하거나 초기화를 기다릴 수 있습니다.",
|
|
2879
|
+
"quota_aware_tooltip": "할당량이 부족해지면 Claude가 큰 작업을 시작하기 전에 남은 양과 초기화 시간을 알려 줍니다. 작은 부분을 먼저 진행하거나 초기화를 기다릴 수 있습니다. Claude Code 또는 Codex 공식 서비스에 장애가 생겨도 Claude에게 알려 줍니다. 대화가 시작될 때도 Claude에게 각 도구의 할당량을 간단히 요약해 줍니다.",
|
|
2880
2880
|
"quota_aware_enabled_msg": "할당량 인식 모드를 켰습니다. Claude Code를 다시 시작해 주세요.",
|
|
2881
2881
|
"quota_aware_disabled_msg": "할당량 인식 모드를 껐습니다. Claude Code를 다시 시작해 주세요.",
|
|
2882
2882
|
"quota_aware_action_failed": "할당량 인식 모드를 변경할 수 없습니다.",
|
|
@@ -95,7 +95,7 @@ _TERSE_REMINDER_MARKER = "usage-terse-reminder"
|
|
|
95
95
|
_TERSE_REMINDER_MARKERS = (_TERSE_REMINDER_MARKER, "usage_terse_reminder")
|
|
96
96
|
# Independent, opt-in Claude Code UserPromptSubmit hook.
|
|
97
97
|
QUOTA_AWARE_HOOK_TARGET = Path(os.path.expanduser("~/.claude/usage-quota-aware.py"))
|
|
98
|
-
QUOTA_AWARE_HOOK_VERSION = "1.
|
|
98
|
+
QUOTA_AWARE_HOOK_VERSION = "1.12"
|
|
99
99
|
QUOTA_AWARE_MATCHER = ""
|
|
100
100
|
_QUOTA_AWARE_MARKER = "usage-quota-aware"
|
|
101
101
|
_QUOTA_AWARE_MARKERS = (_QUOTA_AWARE_MARKER, "usage_quota_aware")
|
|
@@ -15,12 +15,15 @@ the internal ``retrieveUserQuotaSummary`` endpoint for the quota summary.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import hashlib
|
|
18
19
|
import json
|
|
20
|
+
import math
|
|
19
21
|
import os
|
|
20
22
|
import platform
|
|
21
23
|
import subprocess
|
|
22
24
|
import sys
|
|
23
25
|
import tempfile
|
|
26
|
+
import threading
|
|
24
27
|
import time
|
|
25
28
|
import urllib.parse
|
|
26
29
|
from base64 import b64decode
|
|
@@ -41,7 +44,9 @@ from usage_common.subprocess_utils import hidden_console_kwargs
|
|
|
41
44
|
|
|
42
45
|
CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
|
|
43
46
|
AUTH_EXPIRED_PATH = CACHE_PATH.parent / "agy_auth_expired"
|
|
47
|
+
CLI_SIGNED_OUT_PATH = CACHE_PATH.parent / "agy_cli_signed_out"
|
|
44
48
|
_CLI_LOG_PATH = CACHE_PATH.parent / "agy_usage_cli.log"
|
|
49
|
+
BACKGROUND_AGY_LOCK = threading.Lock()
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def auth_expired() -> bool:
|
|
@@ -54,6 +59,38 @@ def _mark_auth_expired() -> None:
|
|
|
54
59
|
AUTH_EXPIRED_PATH.touch()
|
|
55
60
|
|
|
56
61
|
|
|
62
|
+
def mark_cli_signed_out(before: str | None) -> None:
|
|
63
|
+
after = _credential_fingerprint()
|
|
64
|
+
if after != before:
|
|
65
|
+
return
|
|
66
|
+
with suppress(OSError):
|
|
67
|
+
CLI_SIGNED_OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
CLI_SIGNED_OUT_PATH.write_text(
|
|
69
|
+
json.dumps({"fingerprint": after, "marked_at": time.time()}),
|
|
70
|
+
encoding="utf-8",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def agy_cli_signed_in(timeout: float) -> bool:
|
|
75
|
+
"""Open the gate for a readable credential; bypass cooldown only for a known change."""
|
|
76
|
+
global _cli_retry_after_monotonic
|
|
77
|
+
if not CLI_SIGNED_OUT_PATH.exists():
|
|
78
|
+
return True
|
|
79
|
+
try:
|
|
80
|
+
marker = json.loads(CLI_SIGNED_OUT_PATH.read_text(encoding="utf-8"))
|
|
81
|
+
except (OSError, ValueError, UnicodeError):
|
|
82
|
+
marker = {}
|
|
83
|
+
stored = marker.get("fingerprint") if isinstance(marker, dict) else None
|
|
84
|
+
current = _credential_fingerprint()
|
|
85
|
+
if current is None or current == stored:
|
|
86
|
+
return False
|
|
87
|
+
with suppress(OSError):
|
|
88
|
+
CLI_SIGNED_OUT_PATH.unlink(missing_ok=True)
|
|
89
|
+
if isinstance(stored, str):
|
|
90
|
+
_cli_retry_after_monotonic = 0.0
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
57
94
|
# Legacy OAuth token written by older Antigravity CLI versions. Read-only: we
|
|
58
95
|
# never write back here (that is the CLI's home and could corrupt its login).
|
|
59
96
|
_TOKEN_PATH = Path(os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token"))
|
|
@@ -121,6 +158,8 @@ class AgyQuotaGroup:
|
|
|
121
158
|
models: list[str]
|
|
122
159
|
weekly: AgyQuotaWindow | None
|
|
123
160
|
five_hour: AgyQuotaWindow | None
|
|
161
|
+
weekly_fetched_at: str | None = None
|
|
162
|
+
weekly_resets_at: str | None = None
|
|
124
163
|
|
|
125
164
|
|
|
126
165
|
@dataclass(frozen=True, slots=True)
|
|
@@ -169,19 +208,40 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
169
208
|
agy = find_agy()
|
|
170
209
|
if agy is None:
|
|
171
210
|
return None
|
|
172
|
-
if
|
|
173
|
-
return None
|
|
174
|
-
# Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page.
|
|
175
|
-
if not google_auth_reachable():
|
|
211
|
+
if not BACKGROUND_AGY_LOCK.acquire(blocking=False):
|
|
176
212
|
return None
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
213
|
+
try:
|
|
214
|
+
if not agy_cli_signed_in(timeout) or time.monotonic() < _cli_retry_after_monotonic:
|
|
215
|
+
return None
|
|
216
|
+
# Offline, the CLI would open a browser sign-in page.
|
|
217
|
+
if not google_auth_reachable():
|
|
218
|
+
return None
|
|
219
|
+
before = _credential_fingerprint()
|
|
220
|
+
log_offset = 0
|
|
221
|
+
with suppress(OSError):
|
|
222
|
+
log_offset = _CLI_LOG_PATH.stat().st_size
|
|
223
|
+
result = _run_usage_command(agy, timeout, before)
|
|
224
|
+
if result is None:
|
|
225
|
+
with suppress(OSError):
|
|
226
|
+
with _CLI_LOG_PATH.open("rb") as log:
|
|
227
|
+
size = log.seek(0, os.SEEK_END)
|
|
228
|
+
start = log_offset if size >= log_offset else 0
|
|
229
|
+
log.seek(max(start, size - 65536))
|
|
230
|
+
tail = log.read()
|
|
231
|
+
if (
|
|
232
|
+
b"Print mode: not authenticated" in tail
|
|
233
|
+
or b"triggering interactive OAuth" in tail
|
|
234
|
+
):
|
|
235
|
+
mark_cli_signed_out(before)
|
|
236
|
+
_cli_retry_after_monotonic = (
|
|
237
|
+
0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
|
|
238
|
+
)
|
|
239
|
+
return result
|
|
240
|
+
finally:
|
|
241
|
+
BACKGROUND_AGY_LOCK.release()
|
|
182
242
|
|
|
183
243
|
|
|
184
|
-
def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
|
|
244
|
+
def _run_usage_command(agy: str, timeout: float, before: str | None) -> AgyQuotaResult | None:
|
|
185
245
|
command = [agy, "-p", "/usage", "--output-format", "json", "--log-file", str(_CLI_LOG_PATH)]
|
|
186
246
|
try:
|
|
187
247
|
completed = subprocess.run(
|
|
@@ -193,8 +253,19 @@ def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
|
|
|
193
253
|
env=cli_env(agy),
|
|
194
254
|
**hidden_console_kwargs(),
|
|
195
255
|
)
|
|
256
|
+
except subprocess.TimeoutExpired as exc:
|
|
257
|
+
if "Authentication required" in str(exc.stdout) or "Authentication required" in str(
|
|
258
|
+
exc.stderr
|
|
259
|
+
):
|
|
260
|
+
mark_cli_signed_out(before)
|
|
261
|
+
return None
|
|
196
262
|
except (OSError, subprocess.SubprocessError):
|
|
197
263
|
return None
|
|
264
|
+
if "Authentication required" in str(completed.stdout) or "Authentication required" in str(
|
|
265
|
+
getattr(completed, "stderr", "")
|
|
266
|
+
):
|
|
267
|
+
mark_cli_signed_out(before)
|
|
268
|
+
return None
|
|
198
269
|
if completed.returncode != 0:
|
|
199
270
|
return None
|
|
200
271
|
try:
|
|
@@ -226,11 +297,57 @@ def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
|
|
|
226
297
|
probed = probe_quota()
|
|
227
298
|
if probed is None:
|
|
228
299
|
return cached
|
|
300
|
+
probed = _carry_missing_weekly(probed, cached)
|
|
229
301
|
probed = _carry_missing_groups(probed, cached)
|
|
230
302
|
_write_cache(probed)
|
|
231
303
|
return probed
|
|
232
304
|
|
|
233
305
|
|
|
306
|
+
def _carry_missing_weekly(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
|
|
307
|
+
"""Retain a missing weekly window until its cached reset, without refreshing its age."""
|
|
308
|
+
if cached is None:
|
|
309
|
+
return probed
|
|
310
|
+
cached_at = _parse_timestamp(cached.fetched_at)
|
|
311
|
+
probed_at = _parse_timestamp(probed.fetched_at)
|
|
312
|
+
if cached_at is None or probed_at is None:
|
|
313
|
+
return probed
|
|
314
|
+
previous = {group.name: group for group in cached.groups}
|
|
315
|
+
groups = []
|
|
316
|
+
for group in probed.groups:
|
|
317
|
+
old = previous.get(group.name)
|
|
318
|
+
window = old.weekly if old is not None else None
|
|
319
|
+
if (
|
|
320
|
+
group.weekly is None
|
|
321
|
+
and old is not None
|
|
322
|
+
and window is not None
|
|
323
|
+
and window.resets_in_minutes is not None
|
|
324
|
+
):
|
|
325
|
+
resets_at = (
|
|
326
|
+
_parse_timestamp(old.weekly_resets_at)
|
|
327
|
+
if old.weekly_resets_at is not None
|
|
328
|
+
else cached_at + timedelta(minutes=window.resets_in_minutes)
|
|
329
|
+
)
|
|
330
|
+
if resets_at is not None and resets_at > probed_at:
|
|
331
|
+
minutes = max(0, math.ceil((resets_at - probed_at).total_seconds() / 60))
|
|
332
|
+
group = replace(
|
|
333
|
+
group,
|
|
334
|
+
weekly=replace(
|
|
335
|
+
window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes)
|
|
336
|
+
),
|
|
337
|
+
weekly_fetched_at=old.weekly_fetched_at or cached.fetched_at,
|
|
338
|
+
weekly_resets_at=resets_at.isoformat(),
|
|
339
|
+
)
|
|
340
|
+
groups.append(group)
|
|
341
|
+
return replace(probed, groups=groups)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _age_window(window: AgyQuotaWindow | None, elapsed: int) -> AgyQuotaWindow | None:
|
|
345
|
+
if window is None or window.resets_in_minutes is None or elapsed == 0:
|
|
346
|
+
return window
|
|
347
|
+
minutes = max(0, window.resets_in_minutes - elapsed)
|
|
348
|
+
return replace(window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes))
|
|
349
|
+
|
|
350
|
+
|
|
234
351
|
def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None) -> AgyQuotaResult:
|
|
235
352
|
"""Keep a group the API skipped this once, so the group toggle has something to show.
|
|
236
353
|
|
|
@@ -261,14 +378,12 @@ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None)
|
|
|
261
378
|
else 0
|
|
262
379
|
)
|
|
263
380
|
|
|
264
|
-
def age_window(window: AgyQuotaWindow | None) -> AgyQuotaWindow | None:
|
|
265
|
-
if window is None or window.resets_in_minutes is None or elapsed == 0:
|
|
266
|
-
return window
|
|
267
|
-
minutes = max(0, window.resets_in_minutes - elapsed)
|
|
268
|
-
return replace(window, resets_in_minutes=minutes, resets_in=_format_resets_in(minutes))
|
|
269
|
-
|
|
270
381
|
missing = [
|
|
271
|
-
replace(
|
|
382
|
+
replace(
|
|
383
|
+
group,
|
|
384
|
+
weekly=_age_window(group.weekly, elapsed),
|
|
385
|
+
five_hour=_age_window(group.five_hour, elapsed),
|
|
386
|
+
)
|
|
272
387
|
for group in missing
|
|
273
388
|
]
|
|
274
389
|
order = {group.name: index for index, group in enumerate(cached.groups)}
|
|
@@ -276,6 +391,21 @@ def _carry_missing_groups(probed: AgyQuotaResult, cached: AgyQuotaResult | None)
|
|
|
276
391
|
return AgyQuotaResult(groups=groups, fetched_at=probed.fetched_at)
|
|
277
392
|
|
|
278
393
|
|
|
394
|
+
def _credential_fingerprint() -> str | None:
|
|
395
|
+
"""Hash only the stored credential, without resolving or refreshing a token."""
|
|
396
|
+
token_data: object = None
|
|
397
|
+
if sys.platform == "darwin":
|
|
398
|
+
token_data = _read_macos_credential()
|
|
399
|
+
if not _has_usable_token(token_data):
|
|
400
|
+
token_data = _read_token_file()
|
|
401
|
+
if not _has_usable_token(token_data) and sys.platform == "win32":
|
|
402
|
+
token_data = _read_windows_credential()
|
|
403
|
+
token = token_data.get("token") if isinstance(token_data, dict) else None
|
|
404
|
+
if not isinstance(token, dict):
|
|
405
|
+
return None
|
|
406
|
+
return hashlib.sha256(json.dumps(token, sort_keys=True).encode()).hexdigest()
|
|
407
|
+
|
|
408
|
+
|
|
279
409
|
def _resolve_access_token(timeout: float) -> str | None:
|
|
280
410
|
"""Return a usable access token, refreshing when stale.
|
|
281
411
|
|
|
@@ -845,7 +975,31 @@ def _read_cache() -> AgyQuotaResult | None:
|
|
|
845
975
|
return None
|
|
846
976
|
except OSError:
|
|
847
977
|
return None
|
|
848
|
-
|
|
978
|
+
result = _result_from_payload(payload)
|
|
979
|
+
if result is None:
|
|
980
|
+
return None
|
|
981
|
+
fetched_at = _parse_timestamp(result.fetched_at)
|
|
982
|
+
assert fetched_at is not None
|
|
983
|
+
now = datetime.now(UTC)
|
|
984
|
+
|
|
985
|
+
def weekly_expired(group: AgyQuotaGroup) -> bool:
|
|
986
|
+
if group.weekly_resets_at is not None:
|
|
987
|
+
resets_at = _parse_timestamp(group.weekly_resets_at)
|
|
988
|
+
if resets_at is not None:
|
|
989
|
+
return resets_at <= now
|
|
990
|
+
return (
|
|
991
|
+
group.weekly is not None
|
|
992
|
+
and group.weekly.resets_in_minutes is not None
|
|
993
|
+
and fetched_at + timedelta(minutes=group.weekly.resets_in_minutes) <= now
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
groups = [
|
|
997
|
+
replace(group, weekly=None, weekly_fetched_at=None, weekly_resets_at=None)
|
|
998
|
+
if group.weekly_fetched_at is not None and weekly_expired(group)
|
|
999
|
+
else group
|
|
1000
|
+
for group in result.groups
|
|
1001
|
+
]
|
|
1002
|
+
return replace(result, groups=groups)
|
|
849
1003
|
|
|
850
1004
|
|
|
851
1005
|
def _write_cache(result: AgyQuotaResult) -> None:
|
|
@@ -874,6 +1028,12 @@ def _result_to_payload(result: AgyQuotaResult) -> dict[str, object]:
|
|
|
874
1028
|
"models": group.models,
|
|
875
1029
|
"weekly": _window_to_payload(group.weekly),
|
|
876
1030
|
"five_hour": _window_to_payload(group.five_hour),
|
|
1031
|
+
**(
|
|
1032
|
+
{"weekly_fetched_at": group.weekly_fetched_at}
|
|
1033
|
+
if group.weekly_fetched_at
|
|
1034
|
+
else {}
|
|
1035
|
+
),
|
|
1036
|
+
**({"weekly_resets_at": group.weekly_resets_at} if group.weekly_resets_at else {}),
|
|
877
1037
|
}
|
|
878
1038
|
for group in result.groups
|
|
879
1039
|
],
|
|
@@ -917,6 +1077,8 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
|
|
|
917
1077
|
models = payload.get("models")
|
|
918
1078
|
weekly = _window_from_payload(payload.get("weekly"))
|
|
919
1079
|
five_hour = _window_from_payload(payload.get("five_hour"))
|
|
1080
|
+
weekly_fetched_at = payload.get("weekly_fetched_at")
|
|
1081
|
+
weekly_resets_at = payload.get("weekly_resets_at")
|
|
920
1082
|
if (
|
|
921
1083
|
not isinstance(name, str)
|
|
922
1084
|
or not name
|
|
@@ -926,9 +1088,29 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
|
|
|
926
1088
|
or "five_hour" not in payload
|
|
927
1089
|
or (payload["weekly"] is not None and weekly is None)
|
|
928
1090
|
or (payload["five_hour"] is not None and five_hour is None)
|
|
1091
|
+
or (
|
|
1092
|
+
weekly_fetched_at is not None
|
|
1093
|
+
and (
|
|
1094
|
+
not isinstance(weekly_fetched_at, str)
|
|
1095
|
+
or _parse_timestamp(weekly_fetched_at) is None
|
|
1096
|
+
)
|
|
1097
|
+
)
|
|
1098
|
+
or (
|
|
1099
|
+
weekly_resets_at is not None
|
|
1100
|
+
and (
|
|
1101
|
+
not isinstance(weekly_resets_at, str) or _parse_timestamp(weekly_resets_at) is None
|
|
1102
|
+
)
|
|
1103
|
+
)
|
|
929
1104
|
):
|
|
930
1105
|
return None
|
|
931
|
-
return AgyQuotaGroup(
|
|
1106
|
+
return AgyQuotaGroup(
|
|
1107
|
+
name=name,
|
|
1108
|
+
models=models,
|
|
1109
|
+
weekly=weekly,
|
|
1110
|
+
five_hour=five_hour,
|
|
1111
|
+
weekly_fetched_at=weekly_fetched_at,
|
|
1112
|
+
weekly_resets_at=weekly_resets_at,
|
|
1113
|
+
)
|
|
932
1114
|
|
|
933
1115
|
|
|
934
1116
|
def _window_from_payload(payload: object) -> AgyQuotaWindow | None:
|
|
@@ -90,7 +90,12 @@ def project_quota(
|
|
|
90
90
|
)
|
|
91
91
|
current_time = time.time() if now is None else now
|
|
92
92
|
age_minutes = _cache_age_minutes(quota.fetched_at, current_time)
|
|
93
|
-
stale = _stale_state(
|
|
93
|
+
stale = _stale_state(
|
|
94
|
+
selected.weekly_fetched_at or quota.fetched_at,
|
|
95
|
+
current_time,
|
|
96
|
+
language,
|
|
97
|
+
force=selected.weekly_fetched_at is not None,
|
|
98
|
+
)
|
|
94
99
|
session_forecast: float | None = None
|
|
95
100
|
weekly_forecast: float | None = None
|
|
96
101
|
if stale is None and burn_rate_trackers is not None:
|
|
@@ -363,7 +368,9 @@ def _window_row(
|
|
|
363
368
|
)
|
|
364
369
|
|
|
365
370
|
|
|
366
|
-
def _stale_state(
|
|
371
|
+
def _stale_state(
|
|
372
|
+
fetched_at: str, now: float, language: str, *, force: bool = False
|
|
373
|
+
) -> AgyStaleState | None:
|
|
367
374
|
if auth_expired():
|
|
368
375
|
return {
|
|
369
376
|
"ageText": _t(language, "agy_signin_expired"),
|
|
@@ -373,7 +380,7 @@ def _stale_state(fetched_at: str, now: float, language: str) -> AgyStaleState |
|
|
|
373
380
|
age_seconds = now - parse_iso8601_utc_or_raise(fetched_at).timestamp()
|
|
374
381
|
except (TypeError, ValueError):
|
|
375
382
|
return None
|
|
376
|
-
if age_seconds <= AGY_STALE_SECONDS:
|
|
383
|
+
if age_seconds <= AGY_STALE_SECONDS and not force:
|
|
377
384
|
return None
|
|
378
385
|
if age_seconds < 3600:
|
|
379
386
|
return cast(
|
|
@@ -36,6 +36,7 @@ from menubar.prefs import (
|
|
|
36
36
|
from quota import quota_snapshot
|
|
37
37
|
from quota.burn_rate import BurnRateTracker
|
|
38
38
|
from quota.usage_rate import UsageRateTracker
|
|
39
|
+
from usage_common import service_status
|
|
39
40
|
from usage_common.service_status import CLAUDE_STATUS, CODEX_STATUS, get_service_status
|
|
40
41
|
|
|
41
42
|
logger = logging.getLogger(__name__)
|
|
@@ -197,6 +198,8 @@ def build_result(app: _RefreshApp, sources: RefreshSources) -> dict[str, Any]:
|
|
|
197
198
|
get_service_status(CLAUDE_STATUS),
|
|
198
199
|
get_service_status(CODEX_STATUS),
|
|
199
200
|
)
|
|
201
|
+
if not app.mock:
|
|
202
|
+
service_status.write_snapshot(service_statuses)
|
|
200
203
|
group = int(app.tracker.group())
|
|
201
204
|
state = menubar_state.build_popover_state(
|
|
202
205
|
outcome=outcome,
|
|
@@ -15,6 +15,7 @@ import threading
|
|
|
15
15
|
import time
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
+
from loaders import agy_quota_probe
|
|
18
19
|
from menubar.agy import AgyRefreshResult
|
|
19
20
|
from menubar.prefs import _agy_window_keeper_enabled
|
|
20
21
|
from quota import keeper_outcome
|
|
@@ -37,6 +38,10 @@ _lock = threading.Lock()
|
|
|
37
38
|
_ping_in_flight = False
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
class _PingSkipped(Exception):
|
|
42
|
+
"""The background safety gate refused this launch."""
|
|
43
|
+
|
|
44
|
+
|
|
40
45
|
def should_ping(
|
|
41
46
|
now: float,
|
|
42
47
|
enabled: bool,
|
|
@@ -83,18 +88,32 @@ def _release() -> None:
|
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
def _run_agy_ping(agy_bin: str) -> subprocess.CompletedProcess[str]:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
if not agy_quota_probe.BACKGROUND_AGY_LOCK.acquire(blocking=False):
|
|
92
|
+
raise _PingSkipped("background agy is busy")
|
|
93
|
+
try:
|
|
94
|
+
before = agy_quota_probe._credential_fingerprint()
|
|
95
|
+
if not agy_quota_probe.agy_cli_signed_in(PING_TIMEOUT_SECONDS):
|
|
96
|
+
raise _PingSkipped("CLI is signed out")
|
|
97
|
+
result = subprocess.run( # noqa: S603 - resolved local Antigravity CLI
|
|
98
|
+
[agy_bin, "-p", "ok", "--model", AGY_MODEL],
|
|
99
|
+
stdin=subprocess.DEVNULL,
|
|
100
|
+
capture_output=True,
|
|
101
|
+
encoding="utf-8",
|
|
102
|
+
errors="replace",
|
|
103
|
+
timeout=PING_TIMEOUT_SECONDS,
|
|
104
|
+
cwd=os.path.expanduser("~"),
|
|
105
|
+
check=False,
|
|
106
|
+
env=cli_env(agy_bin),
|
|
107
|
+
**hidden_console_kwargs(),
|
|
108
|
+
)
|
|
109
|
+
output = f"{result.stdout or ''}\n{result.stderr or ''}".lower()
|
|
110
|
+
if result.returncode != 0 and (
|
|
111
|
+
"authentication required" in output or "authentication failed" in output
|
|
112
|
+
):
|
|
113
|
+
agy_quota_probe.mark_cli_signed_out(before)
|
|
114
|
+
return result
|
|
115
|
+
finally:
|
|
116
|
+
agy_quota_probe.BACKGROUND_AGY_LOCK.release()
|
|
98
117
|
|
|
99
118
|
|
|
100
119
|
def _ping_worker(started_at: float) -> None:
|
|
@@ -102,6 +121,20 @@ def _ping_worker(started_at: float) -> None:
|
|
|
102
121
|
binary = _resolve_agy_bin()
|
|
103
122
|
result = _run_agy_ping(binary) if binary is not None else FileNotFoundError()
|
|
104
123
|
fields = keeper_outcome.classify(result)
|
|
124
|
+
except _PingSkipped as exc:
|
|
125
|
+
_debug_log(f"Antigravity ping skipped: {exc}")
|
|
126
|
+
# A probe may win the shared lock after dispatch. Allow the next refresh
|
|
127
|
+
# to retry, and preserve any previous outcome rather than recording failure.
|
|
128
|
+
try:
|
|
129
|
+
keeper_outcome.update_state(
|
|
130
|
+
AGY_WINDOW_KEEPER_STATE_PATH,
|
|
131
|
+
lambda state: (
|
|
132
|
+
{"last_ping_at": None} if state.get("last_ping_at") == started_at else {}
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
finally:
|
|
136
|
+
_release()
|
|
137
|
+
return
|
|
105
138
|
except Exception as exc:
|
|
106
139
|
fields = keeper_outcome.classify(exc)
|
|
107
140
|
try:
|
|
@@ -142,6 +175,12 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
|
|
|
142
175
|
# leave the state untouched so the next refresh tries again.
|
|
143
176
|
if not google_auth_reachable():
|
|
144
177
|
return
|
|
178
|
+
if agy_quota_probe.BACKGROUND_AGY_LOCK.locked():
|
|
179
|
+
_debug_log("Antigravity ping skipped: background agy is busy")
|
|
180
|
+
return
|
|
181
|
+
if not agy_quota_probe.agy_cli_signed_in(PING_TIMEOUT_SECONDS):
|
|
182
|
+
_debug_log("Antigravity ping skipped: CLI is signed out")
|
|
183
|
+
return
|
|
145
184
|
if not _try_acquire():
|
|
146
185
|
return
|
|
147
186
|
keeper_outcome.update_state(
|
|
@@ -128,13 +128,23 @@ def _status_antigravity(now: int) -> dict[str, Any]:
|
|
|
128
128
|
),
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
def weekly_status(group: agy_quota_probe.AgyQuotaGroup) -> dict[str, Any]:
|
|
132
|
+
window = window_status(group.weekly)
|
|
133
|
+
if group.weekly_fetched_at is not None:
|
|
134
|
+
window["updated_at"] = group.weekly_fetched_at
|
|
135
|
+
window["age_seconds"] = _age_seconds(group.weekly_fetched_at, now)
|
|
136
|
+
resets_at = parse_optional_iso8601_utc(group.weekly_resets_at)
|
|
137
|
+
if resets_at is not None:
|
|
138
|
+
window["resets_in_seconds"] = int(max(0, resets_at.timestamp() - now))
|
|
139
|
+
return window
|
|
140
|
+
|
|
131
141
|
return {
|
|
132
142
|
"available": True,
|
|
133
143
|
"groups": [
|
|
134
144
|
{
|
|
135
145
|
"name": group.name,
|
|
136
146
|
"five_hour": window_status(group.five_hour),
|
|
137
|
-
"seven_day":
|
|
147
|
+
"seven_day": weekly_status(group),
|
|
138
148
|
}
|
|
139
149
|
for group in quota.groups
|
|
140
150
|
],
|