usage-cli 0.34.13__tar.gz → 0.34.15__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.13/usage_cli.egg-info → usage_cli-0.34.15}/PKG-INFO +1 -1
- usage_cli-0.34.15/analyzer/ledger_legacy_backfill.py +79 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/reporter.py +11 -3
- {usage_cli-0.34.13 → usage_cli-0.34.15}/i18n.json +20 -15
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_quota_probe.py +38 -2
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/agy.py +5 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/app.py +4 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/notify.py +5 -0
- usage_cli-0.34.15/menubar/quota_countdown.py +34 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/title.py +3 -23
- {usage_cli-0.34.13 → usage_cli-0.34.15}/pyproject.toml +1 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/agy_window_keeper.py +6 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/notifications.py +9 -3
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_quota_probe.py +160 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_window_keeper.py +49 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_check_upstream.py +1 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_html_report_snapshot.py +3 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_keeper_outcome.py +4 -2
- usage_cli-0.34.15/tests/test_ledger_legacy_backfill.py +216 -0
- usage_cli-0.34.15/tests/test_network.py +97 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_threshold_menu.py +2 -2
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_daily_chart.py +21 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_cli.py +2 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_notifications.py +31 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_wintray.py +166 -2
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/html_report.py +10 -4
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_daily_chart.py +6 -4
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_filter.py +1 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/SOURCES.txt +5 -0
- usage_cli-0.34.15/usage_common/network.py +51 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/app.py +31 -5
- {usage_cli-0.34.13 → usage_cli-0.34.15}/LICENSE +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/README.md +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/agy.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/claude.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/codex.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/grok.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/muse.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/rate_limits.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/registry.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/adapters/types.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/blocks.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/insights.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/login_item.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/session_hooks.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/setup_hook.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/agy_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/claude_usage.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/loaders/muse_quota_probe.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/main.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/actions.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/chrome.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/fsevents_watch.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/grok.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/menu.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/muse.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/popover.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/prefs.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/refresh.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/state.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/update.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/base.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/catalog.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_window.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/payload.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/web_panel.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/panels/window_drag.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/status_payload.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/quota/window_keeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/setup.cfg +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/setup_app.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_check_release_version.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_expired_windows.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_pane.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_cli_lookup.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_insights.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_main.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_permission.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_state.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_quota_probe.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panels.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_chart_redesign.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_cold_read.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_headings.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_tui.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/app.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/tui/sprite.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_charts.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/report_styles.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/ui/tables.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/checker.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/gate.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/release_notes.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/updates/whats_new.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/cli.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/cli_lookup.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/pricing.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/__init__.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/login_item.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/menu.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/prefs.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/watch.py +0 -0
- {usage_cli-0.34.13 → usage_cli-0.34.15}/wintray/whats_new.py +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
"""Restore dimensions of total-only ledger days from read-only Codex state."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import sqlite3
|
|
13
|
+
from contextlib import closing
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from loaders import codex_loader
|
|
18
|
+
|
|
19
|
+
from .ledger_backfill import _allocate
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .reporter import _YearDay
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def backfill_days(days: dict[str, _YearDay]) -> bool:
|
|
28
|
+
"""Mutate eligible days only after a successful source read; report changes."""
|
|
29
|
+
eligible = {
|
|
30
|
+
key: day
|
|
31
|
+
for key, day in days.items()
|
|
32
|
+
if day["total_tokens"] > 0 and all(tokens == 0 for tokens in day["agent_tokens"].values())
|
|
33
|
+
}
|
|
34
|
+
if not eligible:
|
|
35
|
+
return False
|
|
36
|
+
try:
|
|
37
|
+
with closing(
|
|
38
|
+
sqlite3.connect(codex_loader._readonly_sqlite_uri(codex_loader.STATE_DB), uri=True)
|
|
39
|
+
) as connection:
|
|
40
|
+
rows = connection.execute(
|
|
41
|
+
"SELECT created_at, tokens_used, model, cwd FROM threads WHERE tokens_used > 0"
|
|
42
|
+
).fetchall()
|
|
43
|
+
except (OSError, sqlite3.Error) as exc:
|
|
44
|
+
logger.warning("Legacy ledger backfill could not read %s: %s", codex_loader.STATE_DB, exc)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
models: dict[str, dict[str, int]] = {key: {} for key in eligible}
|
|
48
|
+
projects: dict[str, dict[str, int]] = {key: {} for key in eligible}
|
|
49
|
+
for created_at, tokens, model, cwd in rows:
|
|
50
|
+
if not isinstance(created_at, int | float) or not isinstance(tokens, int) or tokens <= 0:
|
|
51
|
+
logger.warning("Legacy ledger backfill skipped invalid thread counts or timestamp")
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
day_key = datetime.fromtimestamp(created_at).date().isoformat()
|
|
55
|
+
except (OSError, OverflowError, ValueError):
|
|
56
|
+
logger.warning("Legacy ledger backfill skipped invalid thread timestamp %r", created_at)
|
|
57
|
+
continue
|
|
58
|
+
if day_key not in eligible:
|
|
59
|
+
continue
|
|
60
|
+
model = model if isinstance(model, str) and model else "unknown"
|
|
61
|
+
project = codex_loader._project_from_cwd(cwd) if isinstance(cwd, str) and cwd else "unknown"
|
|
62
|
+
models[day_key][model] = models[day_key].get(model, 0) + tokens
|
|
63
|
+
projects[day_key][project] = projects[day_key].get(project, 0) + tokens
|
|
64
|
+
|
|
65
|
+
for day_key, day in eligible.items():
|
|
66
|
+
source_total = sum(models[day_key].values())
|
|
67
|
+
codex_tokens = min(source_total, day["total_tokens"])
|
|
68
|
+
unknown_tokens = day["total_tokens"] - codex_tokens
|
|
69
|
+
day["agent_tokens"] = {"codex": codex_tokens} if codex_tokens else {}
|
|
70
|
+
day["model_tokens"] = (
|
|
71
|
+
dict(_allocate(models[day_key], {}, codex_tokens)) if codex_tokens else {}
|
|
72
|
+
)
|
|
73
|
+
day["project_tokens"] = (
|
|
74
|
+
dict(_allocate(projects[day_key], {}, codex_tokens)) if codex_tokens else {}
|
|
75
|
+
)
|
|
76
|
+
if unknown_tokens:
|
|
77
|
+
for dimension in (day["agent_tokens"], day["model_tokens"], day["project_tokens"]):
|
|
78
|
+
dimension["unknown"] = dimension.get("unknown", 0) + unknown_tokens
|
|
79
|
+
return True
|
|
@@ -29,7 +29,7 @@ from adapters.types import AgentInfo, UsageEntry
|
|
|
29
29
|
from usage_common.pricing import calculate_cost, is_model_priced
|
|
30
30
|
|
|
31
31
|
from .aggregator import aggregate_sessions
|
|
32
|
-
from . import diagnoser, dispatch_ledger, ledger_backfill, usage_snapshot
|
|
32
|
+
from . import diagnoser, dispatch_ledger, ledger_backfill, ledger_legacy_backfill, usage_snapshot
|
|
33
33
|
|
|
34
34
|
logger = logging.getLogger(__name__)
|
|
35
35
|
|
|
@@ -45,9 +45,9 @@ _YEAR_WEEKS = 53
|
|
|
45
45
|
YEAR_CACHE_PATH = Path(os.path.expanduser("~/.usage/year_cache.json"))
|
|
46
46
|
YEAR_LEDGER_PATH = YEAR_CACHE_PATH.with_name("year_ledger.json")
|
|
47
47
|
YEAR_CACHE_TTL_SECONDS = 6 * 3600
|
|
48
|
-
_YEAR_CACHE_SCHEMA =
|
|
48
|
+
_YEAR_CACHE_SCHEMA = 7
|
|
49
49
|
_YEAR_LEDGER_SCHEMA = 1
|
|
50
|
-
_year_ledger_lock = threading.
|
|
50
|
+
_year_ledger_lock = threading.RLock()
|
|
51
51
|
_YEAR_LEDGER_TRIM_BUFFER_DAYS = 60
|
|
52
52
|
|
|
53
53
|
|
|
@@ -572,6 +572,14 @@ def _year_day_from_json(value: object) -> _YearDay | None:
|
|
|
572
572
|
|
|
573
573
|
|
|
574
574
|
def _read_year_ledger() -> _YearLedger:
|
|
575
|
+
with _year_ledger_lock:
|
|
576
|
+
ledger = _read_year_ledger_unlocked()
|
|
577
|
+
if ledger_legacy_backfill.backfill_days(ledger["days"]):
|
|
578
|
+
_write_year_ledger(ledger)
|
|
579
|
+
return ledger
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _read_year_ledger_unlocked() -> _YearLedger:
|
|
575
583
|
try:
|
|
576
584
|
with YEAR_LEDGER_PATH.open(encoding="utf-8") as file:
|
|
577
585
|
ledger = json.load(file)
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"quota_label_menu": "顯示工作列額度",
|
|
45
45
|
"quota_label_format": "{provider}: {value}%",
|
|
46
46
|
"quota_label_unknown": "{provider}: --",
|
|
47
|
+
"quota_label_countdown": "{provider}: {value}",
|
|
47
48
|
"quota_notifications_menu": "用量提醒通知",
|
|
48
49
|
"quota_notifications_toggle": "開啟通知",
|
|
49
50
|
"quota_notifications_permission_denied": "系統已關閉 usage 的通知 — 打開系統設定…",
|
|
@@ -261,12 +262,12 @@
|
|
|
261
262
|
"report_wrapped_books_hint": "含重複的快取讀取,不等於實際產出的文字量。",
|
|
262
263
|
"report_kpi_active_value": "{active} / {total} 天",
|
|
263
264
|
"report_kpi_cost": "API 等值",
|
|
264
|
-
"report_kpi_cost_unpriced": "其中 {tokens} tokens
|
|
265
|
+
"report_kpi_cost_unpriced": "其中 {tokens} tokens 沒有價格資料,未計入",
|
|
265
266
|
"report_pricing_section": "API 價格覆蓋率(依 token 數)",
|
|
266
267
|
"report_pricing_priced": "可換算 API 等值",
|
|
267
|
-
"report_pricing_unpriced": "
|
|
268
|
+
"report_pricing_unpriced": "沒有價格資料",
|
|
268
269
|
"report_pricing_all_priced": "全部用量都有公開價格。",
|
|
269
|
-
"report_pricing_hint": "API 等值是用公開 API
|
|
270
|
+
"report_pricing_hint": "API 等值是用公開 API 價目表換算的等值金額,不是你的訂閱帳單;沒有價格資料的用量不計入 API 等值,包括沒有公開價格的模型,以及只存了總量的舊紀錄。沒有價格資料不代表免費,只是無法換算。",
|
|
270
271
|
"report_kpi_peak_day": "燒最兇的一天",
|
|
271
272
|
"report_kpi_tokens": "燒掉 Tokens",
|
|
272
273
|
"report_kpi_vs_prev_period": "較前期",
|
|
@@ -623,6 +624,7 @@
|
|
|
623
624
|
"quota_label_menu": "Show Taskbar Quota",
|
|
624
625
|
"quota_label_format": "{provider}: {value}%",
|
|
625
626
|
"quota_label_unknown": "{provider}: --",
|
|
627
|
+
"quota_label_countdown": "{provider}: {value}",
|
|
626
628
|
"quota_notifications_menu": "Usage Alert Notifications",
|
|
627
629
|
"quota_notifications_toggle": "Enable notifications",
|
|
628
630
|
"quota_notifications_permission_denied": "System notifications for usage are off — Open System Settings…",
|
|
@@ -840,12 +842,12 @@
|
|
|
840
842
|
"report_wrapped_books_hint": "Includes repeated cache reads; this is not the amount of text actually generated.",
|
|
841
843
|
"report_kpi_active_value": "{active} / {total} days",
|
|
842
844
|
"report_kpi_cost": "API equivalent",
|
|
843
|
-
"report_kpi_cost_unpriced": "Includes {tokens} tokens without
|
|
845
|
+
"report_kpi_cost_unpriced": "Includes {tokens} tokens without pricing data, excluded from the API equivalent",
|
|
844
846
|
"report_pricing_section": "API pricing coverage (by token count)",
|
|
845
847
|
"report_pricing_priced": "Convertible to API equivalent",
|
|
846
|
-
"report_pricing_unpriced": "No
|
|
848
|
+
"report_pricing_unpriced": "No pricing data",
|
|
847
849
|
"report_pricing_all_priced": "All usage has a public price.",
|
|
848
|
-
"report_pricing_hint": "API equivalent is the API-equivalent value from public price lists, not your subscription bill. Usage without
|
|
850
|
+
"report_pricing_hint": "API equivalent is the API-equivalent value from public price lists, not your subscription bill. Usage without pricing data is left out of API equivalent: models with no public price, and older records that kept only totals. Missing pricing data does not mean free; the API equivalent cannot be calculated.",
|
|
849
851
|
"report_kpi_peak_day": "Peak Day",
|
|
850
852
|
"report_kpi_tokens": "Burned Tokens",
|
|
851
853
|
"report_kpi_vs_prev_period": "vs previous period",
|
|
@@ -1202,6 +1204,7 @@
|
|
|
1202
1204
|
"quota_label_menu": "显示任务栏额度",
|
|
1203
1205
|
"quota_label_format": "{provider}: {value}%",
|
|
1204
1206
|
"quota_label_unknown": "{provider}: --",
|
|
1207
|
+
"quota_label_countdown": "{provider}: {value}",
|
|
1205
1208
|
"quota_notifications_menu": "用量提醒通知",
|
|
1206
1209
|
"quota_notifications_toggle": "开启通知",
|
|
1207
1210
|
"quota_notifications_permission_denied": "系统已关闭 usage 的通知 — 打开系统设置…",
|
|
@@ -1419,12 +1422,12 @@
|
|
|
1419
1422
|
"report_wrapped_books_hint": "含重复的缓存读取,不等于实际生成的文字量。",
|
|
1420
1423
|
"report_kpi_active_value": "{active} / {total} 天",
|
|
1421
1424
|
"report_kpi_cost": "API 等值",
|
|
1422
|
-
"report_kpi_cost_unpriced": "其中 {tokens} tokens
|
|
1425
|
+
"report_kpi_cost_unpriced": "其中 {tokens} tokens 没有价格数据,未计入",
|
|
1423
1426
|
"report_pricing_section": "API 价格覆盖率(按 token 数)",
|
|
1424
1427
|
"report_pricing_priced": "可换算 API 等值",
|
|
1425
|
-
"report_pricing_unpriced": "
|
|
1428
|
+
"report_pricing_unpriced": "没有价格数据",
|
|
1426
1429
|
"report_pricing_all_priced": "全部用量都有公开价格。",
|
|
1427
|
-
"report_pricing_hint": "API 等值是用公开 API
|
|
1430
|
+
"report_pricing_hint": "API 等值是用公开 API 价目表换算的等值金额,不是你的订阅账单;没有价格数据的用量不计入 API 等值,包括没有公开价格的模型,以及只存了总量的旧记录。没有价格数据不代表免费,只是无法换算。",
|
|
1428
1431
|
"report_kpi_peak_day": "烧最猛的一天",
|
|
1429
1432
|
"report_kpi_tokens": "烧掉 Tokens",
|
|
1430
1433
|
"report_kpi_vs_prev_period": "较前期",
|
|
@@ -1781,6 +1784,7 @@
|
|
|
1781
1784
|
"quota_label_menu": "タスクバーにクォータを表示",
|
|
1782
1785
|
"quota_label_format": "{provider}: {value}%",
|
|
1783
1786
|
"quota_label_unknown": "{provider}: --",
|
|
1787
|
+
"quota_label_countdown": "{provider}: {value}",
|
|
1784
1788
|
"quota_notifications_menu": "使用量アラート通知",
|
|
1785
1789
|
"quota_notifications_toggle": "通知を有効にする",
|
|
1786
1790
|
"quota_notifications_permission_denied": "usage の通知はシステムで無効です — システム設定を開く…",
|
|
@@ -1998,12 +2002,12 @@
|
|
|
1998
2002
|
"report_wrapped_books_hint": "繰り返しのキャッシュ読み込みを含むため、実際に生成した文章量とは異なります。",
|
|
1999
2003
|
"report_kpi_active_value": "{active} / {total} 日",
|
|
2000
2004
|
"report_kpi_cost": "API 換算",
|
|
2001
|
-
"report_kpi_cost_unpriced": "うち {tokens} tokens
|
|
2005
|
+
"report_kpi_cost_unpriced": "うち {tokens} tokens は価格データがなく、API 換算に含みません",
|
|
2002
2006
|
"report_pricing_section": "API 価格カバー率(トークン数ベース)",
|
|
2003
2007
|
"report_pricing_priced": "API 相当額に換算可能",
|
|
2004
|
-
"report_pricing_unpriced": "
|
|
2008
|
+
"report_pricing_unpriced": "価格データなし",
|
|
2005
2009
|
"report_pricing_all_priced": "すべての使用量に公開価格があります。",
|
|
2006
|
-
"report_pricing_hint": "API 換算は公開 API
|
|
2010
|
+
"report_pricing_hint": "API 換算は公開 API 価格表で換算した相当額で、サブスクリプションの請求額ではありません。価格データのない使用量(公開価格のないモデルや、合計だけが残っている古い記録)は API 換算に含まれません。価格データがないことは無料を意味しません。API 相当額に換算できないだけです。",
|
|
2007
2011
|
"report_kpi_peak_day": "最も使った日",
|
|
2008
2012
|
"report_kpi_tokens": "使った Tokens",
|
|
2009
2013
|
"report_kpi_vs_prev_period": "前期比",
|
|
@@ -2360,6 +2364,7 @@
|
|
|
2360
2364
|
"quota_label_menu": "작업 표시줄 할당량 표시",
|
|
2361
2365
|
"quota_label_format": "{provider}: {value}%",
|
|
2362
2366
|
"quota_label_unknown": "{provider}: --",
|
|
2367
|
+
"quota_label_countdown": "{provider}: {value}",
|
|
2363
2368
|
"quota_notifications_menu": "사용량 알림",
|
|
2364
2369
|
"quota_notifications_toggle": "알림 켜기",
|
|
2365
2370
|
"quota_notifications_permission_denied": "시스템에서 usage 알림이 꺼져 있습니다 — 시스템 설정 열기…",
|
|
@@ -2577,12 +2582,12 @@
|
|
|
2577
2582
|
"report_wrapped_books_hint": "반복된 캐시 읽기를 포함하므로 실제로 생성한 글의 양과는 다릅니다.",
|
|
2578
2583
|
"report_kpi_active_value": "{active} / {total}일",
|
|
2579
2584
|
"report_kpi_cost": "API 환산",
|
|
2580
|
-
"report_kpi_cost_unpriced": "이 중 {tokens} tokens는
|
|
2585
|
+
"report_kpi_cost_unpriced": "이 중 {tokens} tokens는 가격 데이터가 없어 API 환산에서 제외됨",
|
|
2581
2586
|
"report_pricing_section": "API 가격 적용률 (토큰 수 기준)",
|
|
2582
2587
|
"report_pricing_priced": "API 환산액 계산 가능",
|
|
2583
|
-
"report_pricing_unpriced": "
|
|
2588
|
+
"report_pricing_unpriced": "가격 데이터 없음",
|
|
2584
2589
|
"report_pricing_all_priced": "모든 사용량에 공개 가격이 있습니다.",
|
|
2585
|
-
"report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 공개 가격이 없는
|
|
2590
|
+
"report_pricing_hint": "API 환산은 공개 API 가격표로 환산한 금액이며 구독 청구액이 아닙니다. 가격 데이터가 없는 사용량(공개 가격이 없는 모델, 합계만 남은 이전 기록)은 API 환산에서 제외됩니다. 가격 데이터가 없다고 무료인 것은 아닙니다. API 환산액을 계산할 수 없다는 뜻입니다.",
|
|
2586
2591
|
"report_kpi_peak_day": "가장 많이 쓴 날",
|
|
2587
2592
|
"report_kpi_tokens": "쓴 Tokens",
|
|
2588
2593
|
"report_kpi_vs_prev_period": "이전 기간 대비",
|
|
@@ -34,7 +34,9 @@ from typing import Any
|
|
|
34
34
|
from urllib.error import HTTPError, URLError
|
|
35
35
|
from urllib.request import Request, urlopen
|
|
36
36
|
|
|
37
|
+
from loaders.cache_quarantine import quarantine
|
|
37
38
|
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
39
|
+
from usage_common.network import google_auth_reachable
|
|
38
40
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
39
41
|
|
|
40
42
|
CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
|
|
@@ -79,9 +81,15 @@ _CLIENT_SECRET = "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"
|
|
|
79
81
|
_DEFAULT_RETRY_AFTER_SECONDS = 60.0
|
|
80
82
|
_MAX_RETRY_AFTER_SECONDS = 3600.0
|
|
81
83
|
_rate_limit_until_monotonic = 0.0
|
|
84
|
+
# A failed launch may open a browser sign-in page; don't repeat it every refresh.
|
|
85
|
+
_CLI_FAILURE_COOLDOWN_SECONDS = 1800.0
|
|
86
|
+
_cli_retry_after_monotonic = 0.0
|
|
82
87
|
# When each group last appeared in a real API response; in memory only, reset on restart.
|
|
83
88
|
_group_seen_at: dict[str, datetime] = {}
|
|
84
89
|
_MISSING_GROUP_GRACE_MINUTES = 30
|
|
90
|
+
# Models omit the period; keep known weekly resets weekly near expiry.
|
|
91
|
+
# In memory only: after restart, unseen resets use the time heuristic again.
|
|
92
|
+
_weekly_reset_times: set[str] = set()
|
|
85
93
|
|
|
86
94
|
|
|
87
95
|
def _user_agent() -> str:
|
|
@@ -157,9 +165,23 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
157
165
|
The slash command makes no model call. ``--log-file`` reuses one file instead of
|
|
158
166
|
leaving a new log in the CLI's log directory on every probe.
|
|
159
167
|
"""
|
|
168
|
+
global _cli_retry_after_monotonic
|
|
160
169
|
agy = find_agy()
|
|
161
170
|
if agy is None:
|
|
162
171
|
return None
|
|
172
|
+
if time.monotonic() < _cli_retry_after_monotonic:
|
|
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():
|
|
176
|
+
return None
|
|
177
|
+
result = _run_usage_command(agy, timeout)
|
|
178
|
+
_cli_retry_after_monotonic = (
|
|
179
|
+
0.0 if result is not None else time.monotonic() + _CLI_FAILURE_COOLDOWN_SECONDS
|
|
180
|
+
)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _run_usage_command(agy: str, timeout: float) -> AgyQuotaResult | None:
|
|
163
185
|
command = [agy, "-p", "/usage", "--output-format", "json", "--log-file", str(_CLI_LOG_PATH)]
|
|
164
186
|
try:
|
|
165
187
|
completed = subprocess.run(
|
|
@@ -509,6 +531,7 @@ def _post_json(url: str, access_token: str, body: dict[str, object], timeout: fl
|
|
|
509
531
|
except HTTPError as exc:
|
|
510
532
|
_handle_http_error(exc)
|
|
511
533
|
if exc.code == 401:
|
|
534
|
+
_token_cache.clear()
|
|
512
535
|
_mark_auth_expired()
|
|
513
536
|
return None
|
|
514
537
|
except (URLError, OSError, ValueError):
|
|
@@ -619,7 +642,13 @@ def _gemini_group_from_models(raw: object) -> AgyQuotaGroup | None:
|
|
|
619
642
|
window = _bucket_to_window(bucket)
|
|
620
643
|
reset = _bucket_reset_time(bucket)
|
|
621
644
|
assert reset is not None
|
|
622
|
-
|
|
645
|
+
_weekly_reset_times.difference_update(
|
|
646
|
+
[seen for seen in _weekly_reset_times if _seconds_until(seen) <= 0]
|
|
647
|
+
)
|
|
648
|
+
session = reset not in _weekly_reset_times and _seconds_until(reset) <= 5 * 3600 + 5 * 60
|
|
649
|
+
# A full bucket's reset time slides with every response, so only a spent one is remembered.
|
|
650
|
+
if not session and window is not None and window.remaining_percent < 100.0:
|
|
651
|
+
_weekly_reset_times.add(reset)
|
|
623
652
|
template = _assemble_groups({"gemini": {}})[0]
|
|
624
653
|
return AgyQuotaGroup(
|
|
625
654
|
name=template.name,
|
|
@@ -807,7 +836,14 @@ def _read_cache() -> AgyQuotaResult | None:
|
|
|
807
836
|
try:
|
|
808
837
|
with CACHE_PATH.open(encoding="utf-8") as cache_file:
|
|
809
838
|
payload: object = json.load(cache_file)
|
|
810
|
-
except (
|
|
839
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
840
|
+
quarantine(
|
|
841
|
+
CACHE_PATH, "decode-error" if isinstance(exc, UnicodeDecodeError) else "json-error"
|
|
842
|
+
)
|
|
843
|
+
with suppress(OSError):
|
|
844
|
+
CACHE_PATH.unlink()
|
|
845
|
+
return None
|
|
846
|
+
except OSError:
|
|
811
847
|
return None
|
|
812
848
|
return _result_from_payload(payload)
|
|
813
849
|
|
|
@@ -59,6 +59,7 @@ class AgyQuotaProjection:
|
|
|
59
59
|
five_hour: AgyQuotaWindow | None
|
|
60
60
|
group_switchable: bool = False
|
|
61
61
|
weekly_only: bool = False
|
|
62
|
+
gemini_five_hour: AgyQuotaWindow | None = None
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
@dataclass(frozen=True, slots=True)
|
|
@@ -162,6 +163,10 @@ def project_quota(
|
|
|
162
163
|
weekly=weekly,
|
|
163
164
|
stale=stale,
|
|
164
165
|
five_hour=selected.five_hour,
|
|
166
|
+
gemini_five_hour=next(
|
|
167
|
+
(group.five_hour for group in quota.groups if _matches_group(group.name, "gemini")),
|
|
168
|
+
None,
|
|
169
|
+
),
|
|
165
170
|
)
|
|
166
171
|
|
|
167
172
|
|
|
@@ -843,7 +843,10 @@ class AppDelegate(NSObject):
|
|
|
843
843
|
|
|
844
844
|
def _process_quota_notifications(self, state: PopoverState) -> None:
|
|
845
845
|
try:
|
|
846
|
-
events = self._quota_notifier.update(
|
|
846
|
+
events = self._quota_notifier.update(
|
|
847
|
+
menubar_notify.notification_channels(state),
|
|
848
|
+
menubar_notify.notification_scopes(state),
|
|
849
|
+
)
|
|
847
850
|
for event in events:
|
|
848
851
|
if _quota_notifications_enabled() and not self.mock:
|
|
849
852
|
self._send_quota_notification(event, state)
|
|
@@ -170,6 +170,11 @@ def notification_channels(state: PopoverState) -> dict[str, tuple[float | None,
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def notification_scopes(state: PopoverState) -> dict[str, str]:
|
|
174
|
+
"""Antigravity rows follow the selected model group; track each group apart."""
|
|
175
|
+
return {"agy_session": state.agy_group_name, "agy_weekly": state.agy_group_name}
|
|
176
|
+
|
|
177
|
+
|
|
173
178
|
# Keep the instance alive: UNUserNotificationCenter holds its delegate weakly.
|
|
174
179
|
_notification_delegate: Any = None
|
|
175
180
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
"""Shared quota recovery countdown formatting, without platform UI imports."""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
from menubar.state import QuotaRowState
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _format_countdown(seconds: float) -> str:
|
|
17
|
+
minutes = math.ceil(seconds / 60)
|
|
18
|
+
if minutes >= 1440:
|
|
19
|
+
return f"{minutes // 1440}d{minutes % 1440 // 60}h"
|
|
20
|
+
if minutes >= 60:
|
|
21
|
+
return f"{minutes // 60}h{minutes % 60}m"
|
|
22
|
+
return f"{minutes}m"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _quota_countdown(rows: tuple[QuotaRowState, ...], now: float) -> str | None:
|
|
26
|
+
full = [row for row in rows if row.display_percent is not None and row.display_percent >= 100]
|
|
27
|
+
if not full:
|
|
28
|
+
return None
|
|
29
|
+
resets = []
|
|
30
|
+
for row in full:
|
|
31
|
+
if row.resets_at is None or row.resets_at <= now:
|
|
32
|
+
return None
|
|
33
|
+
resets.append(row.resets_at)
|
|
34
|
+
return _format_countdown(max(resets) - now)
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
import math
|
|
12
11
|
import time
|
|
13
12
|
from collections.abc import Callable
|
|
14
13
|
from typing import Any, Protocol
|
|
@@ -29,7 +28,9 @@ from menubar.chrome import (
|
|
|
29
28
|
_menubar_icon_attachment_string,
|
|
30
29
|
_muse_menubar_icon,
|
|
31
30
|
)
|
|
32
|
-
from menubar.
|
|
31
|
+
from menubar.quota_countdown import _format_countdown as _format_countdown
|
|
32
|
+
from menubar.quota_countdown import _quota_countdown
|
|
33
|
+
from menubar.state import PopoverState, _format_percent
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class _TitleApp(Protocol):
|
|
@@ -75,27 +76,6 @@ _MENUBAR_MAX_ITEMS = 3
|
|
|
75
76
|
SLOW_POLL_INTERVAL_S = 300.0
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
def _format_countdown(seconds: float) -> str:
|
|
79
|
-
minutes = math.ceil(seconds / 60)
|
|
80
|
-
if minutes >= 1440:
|
|
81
|
-
return f"{minutes // 1440}d{minutes % 1440 // 60}h"
|
|
82
|
-
if minutes >= 60:
|
|
83
|
-
return f"{minutes // 60}h{minutes % 60}m"
|
|
84
|
-
return f"{minutes}m"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _quota_countdown(rows: tuple[QuotaRowState, ...], now: float) -> str | None:
|
|
88
|
-
full = [row for row in rows if row.display_percent is not None and row.display_percent >= 100]
|
|
89
|
-
if not full:
|
|
90
|
-
return None
|
|
91
|
-
resets = []
|
|
92
|
-
for row in full:
|
|
93
|
-
if row.resets_at is None or row.resets_at <= now:
|
|
94
|
-
return None
|
|
95
|
-
resets.append(row.resets_at)
|
|
96
|
-
return _format_countdown(max(resets) - now)
|
|
97
|
-
|
|
98
|
-
|
|
99
79
|
def menubar_poll_interval(app: _TitleApp, state: PopoverState) -> float:
|
|
100
80
|
if app._panel_window_is_visible() or any(
|
|
101
81
|
text != "--" and not text.endswith("%") for _icon, text in _menubar_items(app, state)
|
|
@@ -19,6 +19,7 @@ from menubar.agy import AgyRefreshResult
|
|
|
19
19
|
from menubar.prefs import _agy_window_keeper_enabled
|
|
20
20
|
from quota import keeper_outcome
|
|
21
21
|
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
22
|
+
from usage_common.network import google_auth_reachable
|
|
22
23
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -117,7 +118,7 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
|
|
|
117
118
|
if not enabled:
|
|
118
119
|
return
|
|
119
120
|
projection = result.projection
|
|
120
|
-
five_hour = projection.
|
|
121
|
+
five_hour = projection.gemini_five_hour if projection is not None else None
|
|
121
122
|
now = time.time()
|
|
122
123
|
last_ping_at = _load_last_ping()
|
|
123
124
|
state = keeper_outcome.read_state(AGY_WINDOW_KEEPER_STATE_PATH)
|
|
@@ -137,6 +138,10 @@ def maybe_ping(result: AgyRefreshResult, mock: bool) -> None:
|
|
|
137
138
|
retry = not fresh and keeper_outcome.should_retry(state, now, None, gate(None))
|
|
138
139
|
if not (fresh or retry):
|
|
139
140
|
return
|
|
141
|
+
# Offline (e.g. just after a sleep wake) the CLI would open a browser sign-in page;
|
|
142
|
+
# leave the state untouched so the next refresh tries again.
|
|
143
|
+
if not google_auth_reachable():
|
|
144
|
+
return
|
|
140
145
|
if not _try_acquire():
|
|
141
146
|
return
|
|
142
147
|
keeper_outcome.update_state(
|
|
@@ -45,7 +45,10 @@ class QuotaNotifier:
|
|
|
45
45
|
def __init__(self, thresholds: list[float] | None = None) -> None:
|
|
46
46
|
values = [90.0] if thresholds is None else thresholds
|
|
47
47
|
self.set_thresholds(values)
|
|
48
|
-
|
|
48
|
+
# Keyed by (channel, scope): one channel can show different quota pools,
|
|
49
|
+
# e.g. Antigravity's Gemini and Argon groups. Switching the shown pool
|
|
50
|
+
# must not read as a reset of the other one.
|
|
51
|
+
self._channels: dict[tuple[str, str], _ChannelState] = {}
|
|
49
52
|
|
|
50
53
|
def set_thresholds(self, values: list[float]) -> None:
|
|
51
54
|
"""Replace thresholds without resetting channel observations or warning latches."""
|
|
@@ -54,21 +57,24 @@ class QuotaNotifier:
|
|
|
54
57
|
def update(
|
|
55
58
|
self,
|
|
56
59
|
channels: dict[str, tuple[float | None, bool]],
|
|
60
|
+
scopes: dict[str, str] | None = None,
|
|
57
61
|
) -> list[NotificationEvent]:
|
|
58
62
|
events: list[NotificationEvent] = []
|
|
59
63
|
for channel, (percent, available) in channels.items():
|
|
60
64
|
if channel not in VALID_CHANNELS:
|
|
61
65
|
continue
|
|
62
|
-
|
|
66
|
+
scope = (scopes or {}).get(channel, "")
|
|
67
|
+
events.extend(self._update_channel(channel, scope, percent, available))
|
|
63
68
|
return events
|
|
64
69
|
|
|
65
70
|
def _update_channel(
|
|
66
71
|
self,
|
|
67
72
|
channel: str,
|
|
73
|
+
scope: str,
|
|
68
74
|
percent: float | None,
|
|
69
75
|
available: bool,
|
|
70
76
|
) -> list[NotificationEvent]:
|
|
71
|
-
state = self._channels
|
|
77
|
+
state = self._channels.setdefault((channel, scope), _ChannelState())
|
|
72
78
|
events: list[NotificationEvent] = []
|
|
73
79
|
if not available:
|
|
74
80
|
return events
|