usage-cli 0.33.6__tar.gz → 0.33.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usage_cli-0.33.6/usage_cli.egg-info → usage_cli-0.33.8}/PKG-INFO +1 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/dispatch_ledger.py +4 -3
- {usage_cli-0.33.6 → usage_cli-0.33.8}/i18n.json +15 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_quota_probe.py +56 -8
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/agy.py +53 -22
- {usage_cli-0.33.6 → usage_cli-0.33.8}/pyproject.toml +2 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/status_payload.py +3 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_quota_probe.py +93 -4
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_check_upstream.py +14 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_dispatch_ledger.py +12 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_agy.py +45 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_footer_layout.py +3 -5
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_agy.py +58 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_agy.py +44 -1
- {usage_cli-0.33.6 → usage_cli-0.33.8}/LICENSE +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/README.md +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/agy.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/claude.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/codex.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/grok.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/muse.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/rate_limits.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/registry.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/adapters/types.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/aggregator.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/blocks.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/history_persist.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/insights.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/reporter.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/subscription.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/claude_pane.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/login_item.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/session_hooks.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/setup_hook.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/installer/statusline_settings.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/agy_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_paths.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/claude_usage.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_events.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/codex_paths.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/grok_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/history_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/loaders/muse_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/main.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/actions.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/app.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/chrome.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/grok.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/menu.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/new_badge.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/notify.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/popover.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/prefs.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/refresh.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/state.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/title.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/update.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/menubar/whats_new.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/base.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/catalog.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/dynamic_height.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_scale.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_window.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/panel_window_state.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/payload.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/web_panel.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/panels/window_drag.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/burn_rate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/notifications.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/usage_rate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/quota/window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/setup.cfg +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/setup_app.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_aggregator.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_app_version.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_doctor.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_history_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_history_persist.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_html_report_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_insights.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_login_item.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_main.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_new_badge.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_packaging.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panels.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_pricing.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_quota_label.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_service_status.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_session_resume.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_app.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_subscription.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_time_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_tui.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_checker.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_gate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_client.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_whats_new.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tests/test_wintray.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/app.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/tui/sprite.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/html_report.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_charts.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_filter.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_ledger.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_scripts.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/report_styles.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/ui/tables.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/checker.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/gate.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/release_notes.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/updates/whats_new.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/cli.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_app/doctor.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/SOURCES.txt +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/app_version.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/i18n.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/prefs.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/pricing.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/service_status.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/time_utils.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/__init__.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/app.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/login_item.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/menu.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/prefs.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/quota_label.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/watch.py +0 -0
- {usage_cli-0.33.6 → usage_cli-0.33.8}/wintray/whats_new.py +0 -0
|
@@ -290,11 +290,12 @@ def load_windows() -> dict[str, QuotaWindow]:
|
|
|
290
290
|
fetched = _timestamp(agy.fetched_at)
|
|
291
291
|
if fetched:
|
|
292
292
|
for group in agy.groups:
|
|
293
|
-
|
|
294
|
-
if
|
|
293
|
+
weekly = group.weekly
|
|
294
|
+
if weekly is None or weekly.resets_in_minutes is None:
|
|
295
295
|
continue
|
|
296
|
+
minutes = weekly.resets_in_minutes
|
|
296
297
|
window = QuotaWindow(
|
|
297
|
-
100 -
|
|
298
|
+
100 - weekly.remaining_percent,
|
|
298
299
|
fetched + timedelta(minutes=minutes),
|
|
299
300
|
timedelta(days=7),
|
|
300
301
|
)
|
|
@@ -99,6 +99,9 @@
|
|
|
99
99
|
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
100
100
|
"agy_group_switch_tooltip": "切換 Antigravity 額度組",
|
|
101
101
|
"agy_quota_full": "額度全滿",
|
|
102
|
+
"agy_window_not_reported": "Google 沒有回報這項額度",
|
|
103
|
+
"agy_session_paused": "暫停",
|
|
104
|
+
"agy_session_paused_detail": "本週額度用完,等週重置",
|
|
102
105
|
"agy_stale_minutes": "約 {minutes} 分鐘前",
|
|
103
106
|
"agy_stale_hours": "約 {hours} 小時前",
|
|
104
107
|
"agy_stale_tooltip": "顯示的是快取舊值。額度直接向官方 API 取得,通常每幾分鐘自動更新;出現舊值代表最近取用失敗(多半是 agy 尚未登入,或網路問題),成功後會自動恢復。",
|
|
@@ -643,6 +646,9 @@
|
|
|
643
646
|
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
644
647
|
"agy_group_switch_tooltip": "Switch Antigravity quota group",
|
|
645
648
|
"agy_quota_full": "Quota full",
|
|
649
|
+
"agy_window_not_reported": "Not reported by Google",
|
|
650
|
+
"agy_session_paused": "Paused",
|
|
651
|
+
"agy_session_paused_detail": "Weekly quota used up; waits for weekly reset",
|
|
646
652
|
"agy_stale_minutes": "about {minutes} minutes ago",
|
|
647
653
|
"agy_stale_hours": "about {hours} hours ago",
|
|
648
654
|
"agy_stale_tooltip": "Showing a cached value. Quota is fetched straight from the official API, usually updating every few minutes; an old value means the last fetch failed (often agy isn't signed in, or there's a network issue), and it recovers automatically once a fetch succeeds.",
|
|
@@ -1187,6 +1193,9 @@
|
|
|
1187
1193
|
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
1188
1194
|
"agy_group_switch_tooltip": "切换 Antigravity 额度组",
|
|
1189
1195
|
"agy_quota_full": "额度已满",
|
|
1196
|
+
"agy_window_not_reported": "Google 没有回报这项额度",
|
|
1197
|
+
"agy_session_paused": "暂停",
|
|
1198
|
+
"agy_session_paused_detail": "本周额度用完,等周重置",
|
|
1190
1199
|
"agy_stale_minutes": "约 {minutes} 分钟前",
|
|
1191
1200
|
"agy_stale_hours": "约 {hours} 小时前",
|
|
1192
1201
|
"agy_stale_tooltip": "显示的是缓存旧值。额度直接从官方 API 获取,通常每隔几分钟自动更新;出现旧值说明最近获取失败(多半是 agy 尚未登录,或网络问题),成功后会自动恢复。",
|
|
@@ -1731,6 +1740,9 @@
|
|
|
1731
1740
|
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
1732
1741
|
"agy_group_switch_tooltip": "Antigravity の割り当てグループを切り替え",
|
|
1733
1742
|
"agy_quota_full": "クォータは満タンです",
|
|
1743
|
+
"agy_window_not_reported": "Google から報告されていません",
|
|
1744
|
+
"agy_session_paused": "一時停止",
|
|
1745
|
+
"agy_session_paused_detail": "今週の上限に達したため、週のリセット待ち",
|
|
1734
1746
|
"agy_stale_minutes": "約 {minutes} 分前",
|
|
1735
1747
|
"agy_stale_hours": "約 {hours} 時間前",
|
|
1736
1748
|
"agy_stale_tooltip": "キャッシュされた値を表示しています。利用枠は公式 API から直接取得し、通常数分ごとに自動更新されます。古い値が表示されるのは直近の取得に失敗したためです(多くの場合 agy が未サインイン、またはネットワークの問題)。取得に成功すると自動的に復旧します。",
|
|
@@ -2275,6 +2287,9 @@
|
|
|
2275
2287
|
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
2276
2288
|
"agy_group_switch_tooltip": "Antigravity 할당량 그룹 전환",
|
|
2277
2289
|
"agy_quota_full": "할당량이 가득 찼습니다",
|
|
2290
|
+
"agy_window_not_reported": "Google에서 보고하지 않음",
|
|
2291
|
+
"agy_session_paused": "일시 중지",
|
|
2292
|
+
"agy_session_paused_detail": "이번 주 할당량 소진, 주간 초기화 대기",
|
|
2278
2293
|
"agy_stale_minutes": "약 {minutes}분 전",
|
|
2279
2294
|
"agy_stale_hours": "약 {hours}시간 전",
|
|
2280
2295
|
"agy_stale_tooltip": "캐시된 값을 표시하고 있습니다. 사용량은 공식 API에서 직접 가져오며 보통 몇 분마다 자동 업데이트됩니다. 예전 값이 보이는 건 최근 가져오기에 실패했기 때문입니다(대개 agy 미로그인 또는 네트워크 문제). 가져오기에 성공하면 자동으로 복구됩니다.",
|
|
@@ -59,7 +59,8 @@ _KEYRING_BASE64_PREFIX = "go-keyring-base64:"
|
|
|
59
59
|
|
|
60
60
|
_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
61
61
|
_QUOTA_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"
|
|
62
|
-
|
|
62
|
+
_MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"
|
|
63
|
+
# All endpoints return small JSON objects; cap the read so a broken endpoint
|
|
63
64
|
# cannot make us buffer an unbounded response.
|
|
64
65
|
_MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
|
65
66
|
# Antigravity's installed-app public client constants (same values quotio ships).
|
|
@@ -108,8 +109,8 @@ class AgyQuotaGroup:
|
|
|
108
109
|
|
|
109
110
|
name: str
|
|
110
111
|
models: list[str]
|
|
111
|
-
weekly: AgyQuotaWindow
|
|
112
|
-
five_hour: AgyQuotaWindow
|
|
112
|
+
weekly: AgyQuotaWindow | None
|
|
113
|
+
five_hour: AgyQuotaWindow | None
|
|
113
114
|
|
|
114
115
|
|
|
115
116
|
@dataclass(frozen=True, slots=True)
|
|
@@ -133,7 +134,10 @@ def probe_quota(timeout_seconds: float = 15) -> AgyQuotaResult | None:
|
|
|
133
134
|
groups_raw = _extract_groups(raw)
|
|
134
135
|
if not groups_raw:
|
|
135
136
|
return None
|
|
136
|
-
|
|
137
|
+
models_raw = None
|
|
138
|
+
if not any(_group_id(group) == "gemini" for group in groups_raw):
|
|
139
|
+
models_raw = _post_json(_MODELS_URL, access_token, {}, timeout_seconds)
|
|
140
|
+
result = _build_result(groups_raw, models_raw)
|
|
137
141
|
if result is not None:
|
|
138
142
|
with suppress(OSError):
|
|
139
143
|
AUTH_EXPIRED_PATH.unlink(missing_ok=True)
|
|
@@ -505,7 +509,9 @@ def _extract_groups(raw: object) -> list[dict[str, object]] | None:
|
|
|
505
509
|
return None
|
|
506
510
|
|
|
507
511
|
|
|
508
|
-
def _build_result(
|
|
512
|
+
def _build_result(
|
|
513
|
+
groups_raw: list[dict[str, object]], models_raw: object = None
|
|
514
|
+
) -> AgyQuotaResult | None:
|
|
509
515
|
"""Classify buckets into the Gemini / Claude-GPT groups UI expects."""
|
|
510
516
|
windows: dict[str, dict[str, AgyQuotaWindow]] = {}
|
|
511
517
|
for group in groups_raw:
|
|
@@ -527,11 +533,47 @@ def _build_result(groups_raw: list[dict[str, object]]) -> AgyQuotaResult | None:
|
|
|
527
533
|
continue
|
|
528
534
|
slot[period] = window
|
|
529
535
|
result_groups = _assemble_groups(windows)
|
|
536
|
+
if not any(_group_id(group) == "gemini" for group in groups_raw):
|
|
537
|
+
gemini = _gemini_group_from_models(models_raw)
|
|
538
|
+
if gemini is not None:
|
|
539
|
+
result_groups.insert(0, gemini)
|
|
530
540
|
if not result_groups:
|
|
531
541
|
return None
|
|
532
542
|
return AgyQuotaResult(groups=result_groups, fetched_at=datetime.now(UTC).isoformat())
|
|
533
543
|
|
|
534
544
|
|
|
545
|
+
def _gemini_group_from_models(raw: object) -> AgyQuotaGroup | None:
|
|
546
|
+
"""Use the lowest reported Gemini fraction and classify its reset window."""
|
|
547
|
+
models = raw.get("models") if isinstance(raw, dict) else None
|
|
548
|
+
if not isinstance(models, dict):
|
|
549
|
+
return None
|
|
550
|
+
candidates: list[tuple[float, dict[str, object]]] = []
|
|
551
|
+
for model_id, model in models.items():
|
|
552
|
+
if not isinstance(model_id, str) or not model_id.startswith("gemini-"):
|
|
553
|
+
continue
|
|
554
|
+
info = model.get("quotaInfo") if isinstance(model, dict) else None
|
|
555
|
+
if not isinstance(info, dict):
|
|
556
|
+
continue
|
|
557
|
+
fraction = _coerce_float(info.get("remainingFraction"))
|
|
558
|
+
reset = _bucket_reset_time(info)
|
|
559
|
+
if fraction is not None and reset is not None and _parse_timestamp(reset) is not None:
|
|
560
|
+
candidates.append((fraction, info))
|
|
561
|
+
if not candidates:
|
|
562
|
+
return None
|
|
563
|
+
_, bucket = min(candidates, key=lambda candidate: candidate[0])
|
|
564
|
+
window = _bucket_to_window(bucket)
|
|
565
|
+
reset = _bucket_reset_time(bucket)
|
|
566
|
+
assert reset is not None
|
|
567
|
+
session = _seconds_until(reset) <= 5 * 3600 + 5 * 60
|
|
568
|
+
template = _assemble_groups({"gemini": {}})[0]
|
|
569
|
+
return AgyQuotaGroup(
|
|
570
|
+
name=template.name,
|
|
571
|
+
models=template.models,
|
|
572
|
+
weekly=None if session else window,
|
|
573
|
+
five_hour=window if session else None,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
|
|
535
577
|
def _assemble_groups(
|
|
536
578
|
windows: dict[str, dict[str, AgyQuotaWindow]],
|
|
537
579
|
) -> list[AgyQuotaGroup]:
|
|
@@ -744,7 +786,11 @@ def _result_to_payload(result: AgyQuotaResult) -> dict[str, object]:
|
|
|
744
786
|
}
|
|
745
787
|
|
|
746
788
|
|
|
747
|
-
def _window_to_payload(
|
|
789
|
+
def _window_to_payload(
|
|
790
|
+
window: AgyQuotaWindow | None,
|
|
791
|
+
) -> dict[str, float | int | str | None] | None:
|
|
792
|
+
if window is None:
|
|
793
|
+
return None
|
|
748
794
|
return {
|
|
749
795
|
"remaining_percent": window.remaining_percent,
|
|
750
796
|
"resets_in": window.resets_in,
|
|
@@ -782,8 +828,10 @@ def _group_from_payload(payload: object) -> AgyQuotaGroup | None:
|
|
|
782
828
|
or not name
|
|
783
829
|
or not isinstance(models, list)
|
|
784
830
|
or not all(isinstance(model, str) and model for model in models)
|
|
785
|
-
or weekly
|
|
786
|
-
or five_hour
|
|
831
|
+
or "weekly" not in payload
|
|
832
|
+
or "five_hour" not in payload
|
|
833
|
+
or (payload["weekly"] is not None and weekly is None)
|
|
834
|
+
or (payload["five_hour"] is not None and five_hour is None)
|
|
787
835
|
):
|
|
788
836
|
return None
|
|
789
837
|
return AgyQuotaGroup(name=name, models=models, weekly=weekly, five_hour=five_hour)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import time
|
|
12
|
-
from dataclasses import dataclass
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
13
|
from typing import cast
|
|
14
14
|
|
|
15
15
|
import quota.burn_rate as burn_rate
|
|
@@ -98,9 +98,13 @@ def project_quota(
|
|
|
98
98
|
else:
|
|
99
99
|
session_tracker = burn_rate_trackers["agy_session"]
|
|
100
100
|
weekly_tracker = burn_rate_trackers["agy_weekly"]
|
|
101
|
-
if
|
|
101
|
+
if selected.five_hour is not None and (
|
|
102
|
+
session_tracker.last_timestamp is None or sample_ts > session_tracker.last_timestamp
|
|
103
|
+
):
|
|
102
104
|
session_tracker.record(sample_ts, 100.0 - _remaining_percent(selected.five_hour))
|
|
103
|
-
if
|
|
105
|
+
if selected.weekly is not None and (
|
|
106
|
+
weekly_tracker.last_timestamp is None or sample_ts > weekly_tracker.last_timestamp
|
|
107
|
+
):
|
|
104
108
|
weekly_tracker.record(sample_ts, 100.0 - _remaining_percent(selected.weekly))
|
|
105
109
|
# agy samples arrive every 5 minutes, so the default 10-minute window
|
|
106
110
|
# cannot collect MIN_FORECAST_SAMPLES; this wider window reacts more slowly.
|
|
@@ -112,25 +116,39 @@ def project_quota(
|
|
|
112
116
|
window_seconds=burn_rate.ROLLING_WINDOW_SECONDS,
|
|
113
117
|
min_span_seconds=AGY_WEEKLY_FORECAST_MIN_SPAN_SECONDS,
|
|
114
118
|
)
|
|
119
|
+
session = _window_row(
|
|
120
|
+
_t(language, "session_label"),
|
|
121
|
+
selected.five_hour,
|
|
122
|
+
language,
|
|
123
|
+
age_minutes,
|
|
124
|
+
forecast_seconds=session_forecast,
|
|
125
|
+
)
|
|
126
|
+
weekly = _window_row(
|
|
127
|
+
_t(language, "weekly_label"),
|
|
128
|
+
selected.weekly,
|
|
129
|
+
language,
|
|
130
|
+
age_minutes,
|
|
131
|
+
forecast_seconds=weekly_forecast,
|
|
132
|
+
warning_max_seconds=AGY_WEEKLY_WARNING_MAX_SECONDS,
|
|
133
|
+
window_seconds=WEEKLY_WINDOW_SECONDS,
|
|
134
|
+
)
|
|
135
|
+
# Google pauses the 5-hour bucket once the weekly one is spent; showing it
|
|
136
|
+
# as "full" reads as usable, so say it is paused instead.
|
|
137
|
+
if (
|
|
138
|
+
selected.weekly is not None
|
|
139
|
+
and _remaining_percent(selected.weekly) == 0.0
|
|
140
|
+
and not weekly.reset_done
|
|
141
|
+
):
|
|
142
|
+
session = replace(
|
|
143
|
+
session,
|
|
144
|
+
percent_text=_t(language, "agy_session_paused"),
|
|
145
|
+
reset_text=_t(language, "agy_session_paused_detail"),
|
|
146
|
+
)
|
|
115
147
|
return AgyQuotaProjection(
|
|
116
148
|
group_name=selected.name,
|
|
117
149
|
group_switchable=len(quota.groups) >= 2,
|
|
118
|
-
session=
|
|
119
|
-
|
|
120
|
-
selected.five_hour,
|
|
121
|
-
language,
|
|
122
|
-
age_minutes,
|
|
123
|
-
forecast_seconds=session_forecast,
|
|
124
|
-
),
|
|
125
|
-
weekly=_window_row(
|
|
126
|
-
_t(language, "weekly_label"),
|
|
127
|
-
selected.weekly,
|
|
128
|
-
language,
|
|
129
|
-
age_minutes,
|
|
130
|
-
forecast_seconds=weekly_forecast,
|
|
131
|
-
warning_max_seconds=AGY_WEEKLY_WARNING_MAX_SECONDS,
|
|
132
|
-
window_seconds=WEEKLY_WINDOW_SECONDS,
|
|
133
|
-
),
|
|
150
|
+
session=session,
|
|
151
|
+
weekly=weekly,
|
|
134
152
|
stale=stale,
|
|
135
153
|
five_hour=selected.five_hour,
|
|
136
154
|
)
|
|
@@ -208,8 +226,12 @@ def fallback_projection(language: str) -> AgyQuotaProjection:
|
|
|
208
226
|
|
|
209
227
|
def _group_remaining_percent(group: AgyQuotaGroup) -> float:
|
|
210
228
|
return min(
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
(
|
|
230
|
+
_remaining_percent(window)
|
|
231
|
+
for window in (group.five_hour, group.weekly)
|
|
232
|
+
if window is not None
|
|
233
|
+
),
|
|
234
|
+
default=100.0,
|
|
213
235
|
)
|
|
214
236
|
|
|
215
237
|
|
|
@@ -228,7 +250,7 @@ def _cache_age_minutes(fetched_at: str, now: float) -> float:
|
|
|
228
250
|
|
|
229
251
|
def _window_row(
|
|
230
252
|
title: str,
|
|
231
|
-
window: AgyQuotaWindow,
|
|
253
|
+
window: AgyQuotaWindow | None,
|
|
232
254
|
language: str,
|
|
233
255
|
age_minutes: float = 0,
|
|
234
256
|
forecast_seconds: float | None = None,
|
|
@@ -236,6 +258,15 @@ def _window_row(
|
|
|
236
258
|
*,
|
|
237
259
|
window_seconds: float | None = None,
|
|
238
260
|
) -> QuotaRowState:
|
|
261
|
+
if window is None:
|
|
262
|
+
return QuotaRowState(
|
|
263
|
+
title=title,
|
|
264
|
+
available=False,
|
|
265
|
+
percent=None,
|
|
266
|
+
percent_text="--",
|
|
267
|
+
reset_text=_t(language, "agy_window_not_reported"),
|
|
268
|
+
color=AGY_COLOR,
|
|
269
|
+
)
|
|
239
270
|
remaining = _remaining_percent(window)
|
|
240
271
|
used = 100.0 - remaining
|
|
241
272
|
warning = False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "usage-cli"
|
|
3
|
-
version = "0.33.
|
|
3
|
+
version = "0.33.8"
|
|
4
4
|
description = "usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -81,6 +81,7 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
|
81
81
|
# usage_hooks/usage_session_resume.py 同樣裝進 ~/.claude/ 給系統 python3 (3.9) 跑,同一條限制。
|
|
82
82
|
"usage_hooks/usage_session_resume.py" = ["UP017"]
|
|
83
83
|
"usage_hooks/usage_quota_aware.py" = ["UP017", "UP045"]
|
|
84
|
+
"usage_hooks/usage_statusline_agy.py" = ["UP017"]
|
|
84
85
|
# Imported analytics source is kept with minimal path fixes; keep formatting
|
|
85
86
|
# stable for this integration commit.
|
|
86
87
|
"adapters/**/*.py" = ["E501", "I001", "SIM108", "UP015", "UP017"]
|
|
@@ -97,7 +97,9 @@ def _status_antigravity(now: int) -> dict[str, Any]:
|
|
|
97
97
|
|
|
98
98
|
fetched_timestamp = fetched_at.timestamp()
|
|
99
99
|
|
|
100
|
-
def window_status(window: agy_quota_probe.AgyQuotaWindow) -> dict[str, Any]:
|
|
100
|
+
def window_status(window: agy_quota_probe.AgyQuotaWindow | None) -> dict[str, Any]:
|
|
101
|
+
if window is None:
|
|
102
|
+
return _status_window(None, None, now)
|
|
101
103
|
return {
|
|
102
104
|
"used_percent": round(100 - window.remaining_percent, 1),
|
|
103
105
|
"resets_in_seconds": (
|
|
@@ -160,7 +160,7 @@ def test_find_agy_returns_none_when_all_paths_miss(
|
|
|
160
160
|
# --- token freshness + refresh --------------------------------------------
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def
|
|
163
|
+
def test_quota_url_matches_cli_backend() -> None:
|
|
164
164
|
assert _QUOTA_URL == (
|
|
165
165
|
"https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary"
|
|
166
166
|
)
|
|
@@ -497,9 +497,11 @@ def test_build_result_parses_group_and_windows() -> None:
|
|
|
497
497
|
assert [group.name for group in result.groups] == ["GEMINI MODELS"]
|
|
498
498
|
gemini = result.groups[0]
|
|
499
499
|
assert gemini.models == ["Gemini Flash", "Gemini Pro"]
|
|
500
|
+
assert gemini.weekly is not None
|
|
500
501
|
assert gemini.weekly.remaining_percent == pytest.approx(83.28)
|
|
501
502
|
assert gemini.weekly.resets_in_minutes is not None and gemini.weekly.resets_in_minutes > 0
|
|
502
503
|
assert gemini.weekly.resets_in is not None and "h" in gemini.weekly.resets_in
|
|
504
|
+
assert gemini.five_hour is not None
|
|
503
505
|
assert gemini.five_hour.remaining_percent == pytest.approx(95.27)
|
|
504
506
|
assert gemini.five_hour.resets_in is not None
|
|
505
507
|
|
|
@@ -513,6 +515,7 @@ def test_build_result_handles_remaining_fraction_three_shapes() -> None:
|
|
|
513
515
|
groups_raw = [_gemini_group([_bucket(remaining=remaining, bucket_id="weekly")])]
|
|
514
516
|
result = agy_quota_probe._build_result(groups_raw)
|
|
515
517
|
assert result is not None
|
|
518
|
+
assert result.groups[0].weekly is not None
|
|
516
519
|
assert result.groups[0].weekly.remaining_percent == pytest.approx(50.0)
|
|
517
520
|
|
|
518
521
|
|
|
@@ -532,6 +535,7 @@ def test_build_result_non_full_bucket_without_reset_time_has_no_countdown() -> N
|
|
|
532
535
|
|
|
533
536
|
assert result is not None
|
|
534
537
|
window = result.groups[0].weekly
|
|
538
|
+
assert window is not None
|
|
535
539
|
assert window.remaining_percent == pytest.approx(40.0)
|
|
536
540
|
assert window.resets_in is None
|
|
537
541
|
assert window.resets_in_minutes is None
|
|
@@ -557,6 +561,7 @@ def test_build_result_skips_disabled_bucket_and_fills_missing_window_as_full() -
|
|
|
557
561
|
gemini = result.groups[0]
|
|
558
562
|
# Disabled weekly dropped; only the session bucket survived.
|
|
559
563
|
assert gemini.weekly == AgyQuotaWindow(100.0, None, None)
|
|
564
|
+
assert gemini.five_hour is not None
|
|
560
565
|
assert gemini.five_hour.remaining_percent == pytest.approx(70.0)
|
|
561
566
|
|
|
562
567
|
|
|
@@ -623,6 +628,7 @@ def test_probe_quota_refreshes_then_fetches_quota(
|
|
|
623
628
|
|
|
624
629
|
assert result is not None
|
|
625
630
|
assert result.groups[0].name == "GEMINI MODELS"
|
|
631
|
+
assert result.groups[0].weekly is not None
|
|
626
632
|
assert result.groups[0].weekly.remaining_percent == pytest.approx(83.28)
|
|
627
633
|
# Two calls: refresh then quota; the quota call carried the refreshed token.
|
|
628
634
|
assert [url for url, _auth in visited] == [_TOKEN_URL, _QUOTA_URL]
|
|
@@ -745,7 +751,7 @@ def test_successful_probe_clears_expiration_marker(monkeypatch: pytest.MonkeyPat
|
|
|
745
751
|
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
|
|
746
752
|
monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
|
|
747
753
|
monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
|
|
748
|
-
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: result)
|
|
754
|
+
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: result)
|
|
749
755
|
assert agy_quota_probe.probe_quota() == result
|
|
750
756
|
assert not agy_quota_probe.auth_expired()
|
|
751
757
|
|
|
@@ -755,7 +761,7 @@ def test_unusable_probe_keeps_expiration_marker(monkeypatch: pytest.MonkeyPatch)
|
|
|
755
761
|
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
|
|
756
762
|
monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
|
|
757
763
|
monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
|
|
758
|
-
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: None)
|
|
764
|
+
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: None)
|
|
759
765
|
assert agy_quota_probe.probe_quota() is None
|
|
760
766
|
assert agy_quota_probe.auth_expired()
|
|
761
767
|
|
|
@@ -795,7 +801,7 @@ def test_marker_write_and_delete_errors_are_ignored(monkeypatch: pytest.MonkeyPa
|
|
|
795
801
|
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
|
|
796
802
|
monkeypatch.setattr(agy_quota_probe, "_post_json", lambda *args: {})
|
|
797
803
|
monkeypatch.setattr(agy_quota_probe, "_extract_groups", lambda raw: [{}])
|
|
798
|
-
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw: result)
|
|
804
|
+
monkeypatch.setattr(agy_quota_probe, "_build_result", lambda raw, models=None: result)
|
|
799
805
|
assert agy_quota_probe.probe_quota() == result
|
|
800
806
|
assert agy_quota_probe.auth_expired()
|
|
801
807
|
|
|
@@ -822,6 +828,7 @@ def test_carry_keeps_group_the_api_skipped_once() -> None:
|
|
|
822
828
|
|
|
823
829
|
assert [group.name for group in merged.groups] == ["GEMINI MODELS", "CLAUDE AND GPT MODELS"]
|
|
824
830
|
assert merged.groups[0] == full.groups[0]
|
|
831
|
+
assert merged.groups[1].weekly is not None
|
|
825
832
|
assert merged.groups[1].weekly.remaining_percent == 0.0
|
|
826
833
|
|
|
827
834
|
|
|
@@ -861,3 +868,85 @@ def test_load_quota_writes_carried_group_to_cache(
|
|
|
861
868
|
cached = agy_quota_probe._read_cache()
|
|
862
869
|
assert cached is not None
|
|
863
870
|
assert [group.name for group in cached.groups] == ["GEMINI MODELS", "CLAUDE AND GPT MODELS"]
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
@pytest.mark.parametrize("minutes, weekly", [(300, False), (305, False), (306, True)])
|
|
874
|
+
def test_probe_models_fallback_selects_lowest_gemini(
|
|
875
|
+
monkeypatch: pytest.MonkeyPatch, minutes: int, weekly: bool
|
|
876
|
+
) -> None:
|
|
877
|
+
reset = (datetime.now(UTC) + timedelta(minutes=minutes)).isoformat()
|
|
878
|
+
models = {
|
|
879
|
+
"gemini-pro": {"quotaInfo": {"remainingFraction": 0.8, "resetTime": _FUTURE_RESET}},
|
|
880
|
+
"gemini-flash": {"quotaInfo": {"remainingFraction": "0.5", "resetTime": reset}},
|
|
881
|
+
"claude-opus": {"quotaInfo": {"resetTime": reset}},
|
|
882
|
+
"claude-sonnet": {"quotaInfo": {"remainingFraction": 0.01, "resetTime": reset}},
|
|
883
|
+
"chat_test": {"quotaInfo": {"remainingFraction": 1}},
|
|
884
|
+
"tab_test": {"quotaInfo": {"remainingFraction": 1}},
|
|
885
|
+
}
|
|
886
|
+
summary = _groups_payload(
|
|
887
|
+
[
|
|
888
|
+
{
|
|
889
|
+
"displayName": "Claude and GPT models",
|
|
890
|
+
"buckets": [_bucket(remaining=0.7, bucket_id="weekly")],
|
|
891
|
+
}
|
|
892
|
+
]
|
|
893
|
+
)
|
|
894
|
+
calls = []
|
|
895
|
+
|
|
896
|
+
def post(url: str, token: str, body: dict[str, object], timeout: float) -> object:
|
|
897
|
+
calls.append((url, token, body, timeout))
|
|
898
|
+
return summary if url == _QUOTA_URL else {"models": models}
|
|
899
|
+
|
|
900
|
+
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "same-token")
|
|
901
|
+
monkeypatch.setattr(agy_quota_probe, "_post_json", post)
|
|
902
|
+
result = agy_quota_probe.probe_quota(7)
|
|
903
|
+
assert result is not None and len(result.groups) == 2
|
|
904
|
+
gemini = result.groups[0]
|
|
905
|
+
assert gemini.name == "GEMINI MODELS"
|
|
906
|
+
assert gemini.models == ["Gemini Flash", "Gemini Pro"]
|
|
907
|
+
window = gemini.weekly if weekly else gemini.five_hour
|
|
908
|
+
assert window is not None and window.remaining_percent == 50
|
|
909
|
+
assert (gemini.five_hour if weekly else gemini.weekly) is None
|
|
910
|
+
assert calls == [
|
|
911
|
+
(_QUOTA_URL, "same-token", {}, 7),
|
|
912
|
+
(agy_quota_probe._MODELS_URL, "same-token", {}, 7),
|
|
913
|
+
]
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@pytest.mark.parametrize(
|
|
917
|
+
"models",
|
|
918
|
+
[None, {"models": {}}, {"models": {"claude": {"quotaInfo": {"resetTime": _FUTURE_RESET}}}}],
|
|
919
|
+
)
|
|
920
|
+
def test_probe_models_unavailable_keeps_summary(
|
|
921
|
+
monkeypatch: pytest.MonkeyPatch, models: object
|
|
922
|
+
) -> None:
|
|
923
|
+
summary = _groups_payload([{"displayName": "Claude and GPT models", "buckets": []}])
|
|
924
|
+
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
|
|
925
|
+
monkeypatch.setattr(
|
|
926
|
+
agy_quota_probe, "_post_json", lambda url, *args: summary if url == _QUOTA_URL else models
|
|
927
|
+
)
|
|
928
|
+
result = agy_quota_probe.probe_quota()
|
|
929
|
+
assert result is not None
|
|
930
|
+
assert [group.name for group in result.groups] == ["CLAUDE AND GPT MODELS"]
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def test_probe_summary_gemini_does_not_fetch_models(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
934
|
+
summary = _groups_payload([_gemini_group([])])
|
|
935
|
+
urlopen, visited = _build_urlopen({_QUOTA_URL: summary})
|
|
936
|
+
monkeypatch.setattr(agy_quota_probe, "_resolve_access_token", lambda timeout: "token")
|
|
937
|
+
monkeypatch.setattr(agy_quota_probe, "urlopen", urlopen)
|
|
938
|
+
assert agy_quota_probe.probe_quota() is not None
|
|
939
|
+
assert visited == [(_QUOTA_URL, "Bearer token")]
|
|
940
|
+
|
|
941
|
+
|
|
942
|
+
@pytest.mark.parametrize("weekly", [True, False])
|
|
943
|
+
def test_cache_roundtrips_unreported_window(weekly: bool) -> None:
|
|
944
|
+
window = AgyQuotaWindow(50, "1h 0m", 60)
|
|
945
|
+
group = AgyQuotaGroup(
|
|
946
|
+
"GEMINI MODELS", ["Gemini Flash"], window if weekly else None, None if weekly else window
|
|
947
|
+
)
|
|
948
|
+
result = AgyQuotaResult([group], datetime.now(UTC).isoformat())
|
|
949
|
+
agy_quota_probe._write_cache(result)
|
|
950
|
+
payload = json.loads(agy_quota_probe.CACHE_PATH.read_text())
|
|
951
|
+
assert payload["groups"][0]["five_hour" if weekly else "weekly"] is None
|
|
952
|
+
assert agy_quota_probe._read_cache() == result
|
|
@@ -335,7 +335,14 @@ def test_sqlite_opens_readonly(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
335
335
|
def quota() -> dict[str, Any]:
|
|
336
336
|
return {
|
|
337
337
|
"fetched_at": now(),
|
|
338
|
-
"groups": [
|
|
338
|
+
"groups": [
|
|
339
|
+
{
|
|
340
|
+
"name": "GEMINI MODELS",
|
|
341
|
+
"models": ["Gemini Flash"],
|
|
342
|
+
"weekly": {"remaining_percent": 40},
|
|
343
|
+
"five_hour": {"remaining_percent": 80},
|
|
344
|
+
}
|
|
345
|
+
],
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
|
|
@@ -344,6 +351,12 @@ def test_agy_quota_ok(quota: dict[str, Any]) -> None:
|
|
|
344
351
|
assert check.check_agy_quota_cache()[0] == "OK"
|
|
345
352
|
|
|
346
353
|
|
|
354
|
+
def test_agy_quota_accepts_unreported_window(quota: dict[str, Any]) -> None:
|
|
355
|
+
quota["groups"][0]["weekly"] = None
|
|
356
|
+
write_json(check.agy_quota_probe.CACHE_PATH, quota)
|
|
357
|
+
assert check.check_agy_quota_cache()[0] == "OK"
|
|
358
|
+
|
|
359
|
+
|
|
347
360
|
@pytest.mark.parametrize("missing", ["groups", "weekly", "five_hour"])
|
|
348
361
|
def test_agy_quota_missing(quota: dict[str, Any], missing: str) -> None:
|
|
349
362
|
if missing == "groups":
|
|
@@ -222,6 +222,18 @@ def test_agy_quota_uses_cache_only(
|
|
|
222
222
|
assert result["antigravity:GEMINI MODELS"].resets_at == NOW + timedelta(hours=2)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def test_agy_quota_skips_group_without_reported_weekly(
|
|
226
|
+
paths: tuple[Path, Path, Path], monkeypatch: pytest.MonkeyPatch
|
|
227
|
+
) -> None:
|
|
228
|
+
monkeypatch.setattr(rate_limits, "load_rate_limits", lambda: None)
|
|
229
|
+
monkeypatch.setattr(codex_loader, "load_rate_limits", lambda: None)
|
|
230
|
+
five_hour = agy_quota_probe.AgyQuotaWindow(99.5, None, 100)
|
|
231
|
+
group = agy_quota_probe.AgyQuotaGroup("GEMINI MODELS", ["Gemini Flash"], None, five_hour)
|
|
232
|
+
cached = agy_quota_probe.AgyQuotaResult([group], NOW.isoformat())
|
|
233
|
+
monkeypatch.setattr(agy_quota_probe, "_read_cache", lambda: cached)
|
|
234
|
+
assert "antigravity:GEMINI MODELS" not in ledger.load_windows()
|
|
235
|
+
|
|
236
|
+
|
|
225
237
|
def test_render_ledger_formats_values_and_escapes_title() -> None:
|
|
226
238
|
row: ledger.LedgerRow = {
|
|
227
239
|
"date": "2026-09-26",
|
|
@@ -227,6 +227,27 @@ def test_project_quota_marks_full_session_with_sliding_countdown_as_full() -> No
|
|
|
227
227
|
assert projection.session.reset_text != "Resets in 4h 59m"
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
def test_project_quota_shows_session_paused_when_weekly_is_spent() -> None:
|
|
231
|
+
quota = AgyQuotaResult(
|
|
232
|
+
groups=[
|
|
233
|
+
AgyQuotaGroup(
|
|
234
|
+
name="CLAUDE AND GPT MODELS",
|
|
235
|
+
models=["model"],
|
|
236
|
+
five_hour=AgyQuotaWindow(100.0, None, None),
|
|
237
|
+
weekly=AgyQuotaWindow(0.0, "57h", 3420),
|
|
238
|
+
)
|
|
239
|
+
],
|
|
240
|
+
fetched_at="2026-01-01T00:00:00+00:00",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
projection = menubar_agy.project_quota(quota, "en", now=1_767_225_600.0)
|
|
244
|
+
|
|
245
|
+
assert projection is not None
|
|
246
|
+
assert projection.session.percent_text == "Paused"
|
|
247
|
+
assert projection.session.reset_text == "Weekly quota used up; waits for weekly reset"
|
|
248
|
+
assert projection.weekly.percent == 100.0
|
|
249
|
+
|
|
250
|
+
|
|
230
251
|
def test_project_quota_marks_cached_result_stale_after_twenty_minutes() -> None:
|
|
231
252
|
now = datetime(2026, 1, 1, 1, 0, tzinfo=UTC)
|
|
232
253
|
quota = AgyQuotaResult(
|
|
@@ -513,3 +534,27 @@ def test_fresh_cache_with_expired_login_is_stale() -> None:
|
|
|
513
534
|
assert state is not None
|
|
514
535
|
assert state["ageText"] == "登入過期"
|
|
515
536
|
assert "agy" in state["tooltip"]
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@pytest.mark.parametrize("weekly", [True, False])
|
|
540
|
+
def test_unreported_window_is_unavailable_and_not_recorded(weekly: bool) -> None:
|
|
541
|
+
now = datetime.now(UTC)
|
|
542
|
+
window = AgyQuotaWindow(50, "1h 0m", 60)
|
|
543
|
+
group = AgyQuotaGroup(
|
|
544
|
+
"GEMINI MODELS", [], window if weekly else None, None if weekly else window
|
|
545
|
+
)
|
|
546
|
+
trackers = _burn_rate_trackers()
|
|
547
|
+
projection = menubar_agy.project_quota(
|
|
548
|
+
AgyQuotaResult([group], now.isoformat()),
|
|
549
|
+
"en",
|
|
550
|
+
now=now.timestamp(),
|
|
551
|
+
burn_rate_trackers=trackers,
|
|
552
|
+
)
|
|
553
|
+
assert projection is not None
|
|
554
|
+
row = projection.session if weekly else projection.weekly
|
|
555
|
+
assert row.available is False
|
|
556
|
+
assert row.percent is None
|
|
557
|
+
assert row.percent_text == "--"
|
|
558
|
+
assert row.reset_text == "Not reported by Google"
|
|
559
|
+
assert trackers["agy_session" if weekly else "agy_weekly"].last_timestamp is None
|
|
560
|
+
assert (projection.weekly if weekly else projection.session).percent_text == "50% used"
|
|
@@ -44,11 +44,9 @@ def test_footer_layout(tmp_path: Path, script: str) -> None:
|
|
|
44
44
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
source =
|
|
50
|
-
if 'data-action="set_agy_quota_group"' not in source:
|
|
51
|
-
pytest.skip("panel has no Antigravity group button")
|
|
47
|
+
def test_agy_group_button_visibility() -> None:
|
|
48
|
+
# The app loads assets/panels/panel_core.js; docs/panels are generated demo copies.
|
|
49
|
+
source = (ROOT / "assets" / "panels" / "panel_core.js").read_text(encoding="utf-8")
|
|
52
50
|
node = shutil.which("node")
|
|
53
51
|
if node is None:
|
|
54
52
|
pytest.skip("Node is required to execute panel JavaScript")
|