usage-cli 0.34.10__tar.gz → 0.34.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {usage_cli-0.34.10/usage_cli.egg-info → usage_cli-0.34.12}/PKG-INFO +1 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/claude.py +3 -4
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/rate_limits.py +5 -4
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/blocks.py +4 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/persona_loader.py +2 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/reporter.py +4 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/i18n.json +75 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/session_hooks.py +5 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/setup_hook.py +48 -31
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_disk_cache.py +3 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_loader.py +25 -6
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/agy_quota_probe.py +3 -9
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_usage.py +7 -3
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_loader.py +11 -4
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/muse_quota_probe.py +5 -9
- {usage_cli-0.34.10 → usage_cli-0.34.12}/main.py +9 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/actions.py +3 -3
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/agy.py +9 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/fsevents_watch.py +7 -5
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/state.py +5 -6
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/title.py +2 -2
- {usage_cli-0.34.10 → usage_cli-0.34.12}/pyproject.toml +1 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/agy_window_keeper.py +3 -9
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/codex_window_keeper.py +3 -9
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/window_keeper.py +38 -13
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_claude.py +22 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_rate_limits.py +58 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_loader.py +22 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_quota_probe.py +19 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_window_keeper.py +44 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_analyzer_blocks.py +20 -0
- usage_cli-0.34.12/tests/test_check_release_version.py +47 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_check_upstream.py +8 -1
- usage_cli-0.34.12/tests/test_claude_expired_windows.py +86 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_pane.py +80 -1
- usage_cli-0.34.12/tests/test_cli_lookup.py +123 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_loader.py +44 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_window_keeper.py +20 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_fsevents_watch.py +27 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_html_report_snapshot.py +37 -24
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar.py +184 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_state.py +38 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_quota_probe.py +20 -0
- usage_cli-0.34.12/tests/test_report_chart_redesign.py +218 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_cold_read.py +1 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_filter_js.py +94 -22
- usage_cli-0.34.12/tests/test_report_headings.py +373 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_reporter_build_data.py +55 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook.py +172 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_ui_tables.py +99 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_cli.py +52 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline.py +14 -11
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_window_keeper.py +96 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/html_report.py +277 -121
- usage_cli-0.34.12/ui/report_charts.py +62 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_filter.py +217 -133
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_styles.py +100 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/tables.py +17 -9
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/cli.py +12 -3
- {usage_cli-0.34.10 → usage_cli-0.34.12/usage_cli.egg-info}/PKG-INFO +1 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/SOURCES.txt +6 -0
- usage_cli-0.34.12/usage_common/cli_lookup.py +62 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/pricing.py +6 -1
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline.py +11 -8
- usage_cli-0.34.10/ui/report_charts.py +0 -26
- {usage_cli-0.34.10 → usage_cli-0.34.12}/LICENSE +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/README.md +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/THIRD_PARTY_NOTICES.md +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/agy.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/codex.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/grok.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/muse.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/registry.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/adapters/types.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/aggregator.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/dispatch_ledger.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/history_persist.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/insights.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/ledger_backfill.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/subscription.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/claude_pane.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/codex_hook_trust.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/login_item.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/installer/statusline_settings.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/_shared_cache_file.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/chromium_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_desktop.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/claude_paths.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_events.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/codex_paths.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/grok_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/history_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/loaders/muse_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/app.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/chrome.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/grok.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/manual_refresh.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/menu.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/muse.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/new_badge.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/notify.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/popover.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/prefs.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/refresh.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/update.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/menubar/whats_new.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/base.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/catalog.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/dynamic_height.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_scale.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_window.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/panel_window_state.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/payload.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/web_panel.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/panels/window_drag.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/burn_rate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/keeper_outcome.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/notifications.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/quota_snapshot.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/status_payload.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/quota/usage_rate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/setup.cfg +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/setup_app.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_aggregator.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_app_version.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_chromium_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_desktop.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_codex_hook_trust.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_compact_cards.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_fresh_cache.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_quota_display.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_desktop_quota_review.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_dispatch_ledger.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_doctor.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_history_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_history_persist.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_insights.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_keeper_menu.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_keeper_outcome.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_ledger_backfill.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_login_item.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_main.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_manual_refresh.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_agy.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_muse.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_notify.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_prefs.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_menubar_update.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_coverage.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_muse_review_regressions.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_new_badge.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_packaging.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_footer_layout.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panels.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_pricing.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_quota_aware.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_quota_label.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_report_share_mask.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_service_status.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_session_resume.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_app.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_subscription.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_taskbar_menu.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_taskbar_obstacles.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_time_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_tui.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_checker.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_gate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_client.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_web_panel_payload.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_whats_new.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tests/test_wintray.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/app.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/tui/sprite.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_daily_chart.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_ledger.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/ui/report_scripts.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/checker.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/gate.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/release_notes.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/updates/whats_new.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_app/doctor.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/app_version.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/i18n.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/prefs.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/project_resolver.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/service_status.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/time_utils.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_quota_aware.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_session_resume.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_agy.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_statusline_grok.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_terse_mode.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/usage_hooks/usage_terse_reminder.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/__init__.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/app.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/login_item.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/menu.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/prefs.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/quota_label.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_menu.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_obstacles.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/taskbar_overlay.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/watch.py +0 -0
- {usage_cli-0.34.10 → usage_cli-0.34.12}/wintray/whats_new.py +0 -0
|
@@ -16,6 +16,7 @@ from typing import Any
|
|
|
16
16
|
from loaders.claude_paths import claude_config_dirs
|
|
17
17
|
from loaders.jsonl_utils import iter_jsonl_dicts
|
|
18
18
|
from usage_common.project_resolver import resolve_project_name
|
|
19
|
+
from usage_common.time_utils import parse_optional_iso8601_utc
|
|
19
20
|
|
|
20
21
|
from .types import AgentInfo
|
|
21
22
|
|
|
@@ -190,10 +191,8 @@ def _parse_assistant_entry(data: dict[str, Any], project: str) -> list[UsageEntr
|
|
|
190
191
|
else 0
|
|
191
192
|
)
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ts = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
196
|
-
except (ValueError, AttributeError):
|
|
194
|
+
ts = parse_optional_iso8601_utc(data.get("timestamp", ""))
|
|
195
|
+
if ts is None:
|
|
197
196
|
return []
|
|
198
197
|
|
|
199
198
|
message_id = message.get("id", "")
|
|
@@ -36,7 +36,8 @@ def _read_status() -> dict[str, Any] | None:
|
|
|
36
36
|
try:
|
|
37
37
|
with open(path, "r", encoding="utf-8") as f:
|
|
38
38
|
data: dict[str, Any] = json.load(f)
|
|
39
|
-
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
return data
|
|
40
41
|
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
41
42
|
continue
|
|
42
43
|
return None
|
|
@@ -51,9 +52,9 @@ def load_rate_limits() -> RateLimits | None:
|
|
|
51
52
|
if data is None:
|
|
52
53
|
return None
|
|
53
54
|
|
|
54
|
-
rl = data.get("rate_limits")
|
|
55
|
-
five = rl.get("five_hour")
|
|
56
|
-
seven = rl.get("seven_day")
|
|
55
|
+
rl = usage_client._as_dict(data.get("rate_limits"))
|
|
56
|
+
five = usage_client._as_dict(rl.get("five_hour"))
|
|
57
|
+
seven = usage_client._as_dict(rl.get("seven_day"))
|
|
57
58
|
|
|
58
59
|
now_ts = datetime.now(timezone.utc).timestamp()
|
|
59
60
|
five_pct = _as_finite_float(five.get("used_percentage"))
|
|
@@ -56,7 +56,10 @@ def analyze_blocks(entries: list[UsageEntry]) -> list[SessionBlock]:
|
|
|
56
56
|
block.is_active = True
|
|
57
57
|
elapsed = (now - block.start_time).total_seconds() / 60
|
|
58
58
|
if elapsed > 0:
|
|
59
|
-
|
|
59
|
+
active_tokens = (
|
|
60
|
+
block.input_tokens + block.output_tokens + block.cache_creation_tokens
|
|
61
|
+
)
|
|
62
|
+
block.burn_rate = active_tokens / elapsed
|
|
60
63
|
|
|
61
64
|
return blocks
|
|
62
65
|
|
|
@@ -22,7 +22,8 @@ from usage_common.time_utils import parse_optional_iso8601_utc
|
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# Habits move slowly, while a rescan reads every session file (~20s on heavy users).
|
|
26
|
+
_CACHE_TTL_SECONDS = 3600.0
|
|
26
27
|
_cache: dict[int, tuple[float, PersonaProfile]] = {}
|
|
27
28
|
|
|
28
29
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import contextlib
|
|
10
|
+
from bisect import bisect_right
|
|
10
11
|
from collections import defaultdict
|
|
11
12
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
13
|
from dataclasses import dataclass
|
|
@@ -146,6 +147,7 @@ class CubeModelData(TypedDict):
|
|
|
146
147
|
|
|
147
148
|
|
|
148
149
|
class CubeReportData(TypedDict):
|
|
150
|
+
today: NotRequired[str]
|
|
149
151
|
dates: list[str]
|
|
150
152
|
agents: list[CubeAgentData]
|
|
151
153
|
models: list[CubeModelData]
|
|
@@ -1236,6 +1238,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1236
1238
|
)
|
|
1237
1239
|
cube_rows.sort(key=lambda row: (row[0], row[1], row[2], row[3]))
|
|
1238
1240
|
cube: CubeReportData = {
|
|
1241
|
+
"today": today.isoformat(),
|
|
1239
1242
|
"dates": cube_dates,
|
|
1240
1243
|
"agents": cube_agents,
|
|
1241
1244
|
"models": cube_models,
|
|
@@ -1293,7 +1296,7 @@ def build_report_data(agents: list[AgentInfo], period: str = "month") -> ReportD
|
|
|
1293
1296
|
start_time = _session_start_text(session.start_time)
|
|
1294
1297
|
session_rows.append(
|
|
1295
1298
|
{
|
|
1296
|
-
"date_idx":
|
|
1299
|
+
"date_idx": max(0, bisect_right(cube_dates, start_time[:10]) - 1),
|
|
1297
1300
|
"project_idx": project_indices[session.project or "unknown"],
|
|
1298
1301
|
"model_idx": model_indices[session.model or "unknown"],
|
|
1299
1302
|
"start_time": start_time,
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"zh-TW": {
|
|
3
|
+
"report_title_tools_top": "{tool} 用得最多,佔 {pct}%",
|
|
4
|
+
"report_title_tools_single": "全部用量來自 {tool}",
|
|
5
|
+
"report_sub_tools": "各工具 token 佔比 · 共 {total} tokens",
|
|
6
|
+
"report_title_projects_top3": "前 3 個專案佔了 {pct}% 的用量",
|
|
7
|
+
"report_sub_projects": "各專案 token 佔比",
|
|
8
|
+
"report_title_trend_peak_week": "{date} 那週用量最高",
|
|
9
|
+
"report_sub_trend": "每日與每週 token 用量",
|
|
10
|
+
"report_title_composition_top": "{pct}% 的 token 是{kind}",
|
|
11
|
+
"report_sub_composition": "各類 token 佔比",
|
|
12
|
+
"report_title_sessions_top": "最大的一段會話用了 {tokens} tokens",
|
|
13
|
+
"report_title_streak": "已經連續 {days} 天每天都有用",
|
|
14
|
+
"report_source_note": "資料來源:本機 {tools} 使用紀錄 · {from} – {to}",
|
|
15
|
+
"report_source_pricing": "API 等值依公開 API 價目換算",
|
|
16
|
+
"report_weekly_chart_title": "每週用量",
|
|
3
17
|
"panel_default_name": "預設",
|
|
4
18
|
"panel_load_error": "面板載入失敗,請回報這個問題:",
|
|
5
19
|
"panel_matrix": "駭客任務",
|
|
@@ -545,6 +559,7 @@
|
|
|
545
559
|
"report_project_others": "其他專案",
|
|
546
560
|
"report_ledger_lead": "Claude 派給 Codex、Antigravity 的工作,各吃掉多少週額度。",
|
|
547
561
|
"report_trend_partial_week": "僅 {days} 天",
|
|
562
|
+
"report_trend_incomplete_week": "不完整週",
|
|
548
563
|
"report_history_scope": "分工具紀錄從 {date} 起;更早的每日總量只在下方熱力圖",
|
|
549
564
|
"report_composition_missing": "另有 {tokens} tokens 沒有輸入/輸出拆解",
|
|
550
565
|
"report_wrapped_cost_scope": "只含有價格紀錄的日子({date} 起)",
|
|
@@ -558,6 +573,20 @@
|
|
|
558
573
|
"report_ledger_all_reset": "最近 7 天的派工都在額度重置前,算在上週。"
|
|
559
574
|
},
|
|
560
575
|
"en": {
|
|
576
|
+
"report_title_tools_top": "{tool} leads with {pct}% of tokens",
|
|
577
|
+
"report_title_tools_single": "All usage came from {tool}",
|
|
578
|
+
"report_sub_tools": "Token share by tool · {total} tokens in total",
|
|
579
|
+
"report_title_projects_top3": "The top 3 projects account for {pct}% of tokens",
|
|
580
|
+
"report_sub_projects": "Token share by project",
|
|
581
|
+
"report_title_trend_peak_week": "The week of {date} was the busiest",
|
|
582
|
+
"report_sub_trend": "Daily and weekly token usage",
|
|
583
|
+
"report_title_composition_top": "{kind} makes up {pct}% of tokens",
|
|
584
|
+
"report_sub_composition": "Token share by type",
|
|
585
|
+
"report_title_sessions_top": "The largest session used {tokens} tokens",
|
|
586
|
+
"report_title_streak": "{days} days in a row with usage",
|
|
587
|
+
"report_source_note": "Source: local {tools} usage logs · {from} – {to}",
|
|
588
|
+
"report_source_pricing": "API-equivalent cost is based on public API prices",
|
|
589
|
+
"report_weekly_chart_title": "Weekly usage",
|
|
561
590
|
"panel_default_name": "Default",
|
|
562
591
|
"panel_load_error": "Panel failed to load. Please report this:",
|
|
563
592
|
"panel_matrix": "Matrix",
|
|
@@ -1103,6 +1132,7 @@
|
|
|
1103
1132
|
"report_project_others": "Other projects",
|
|
1104
1133
|
"report_ledger_lead": "How much of each weekly quota went to work Claude handed to Codex and Antigravity.",
|
|
1105
1134
|
"report_trend_partial_week": "only {days} days",
|
|
1135
|
+
"report_trend_incomplete_week": "Incomplete week",
|
|
1106
1136
|
"report_history_scope": "Per-tool records start on {date}; earlier daily totals appear only in the heatmap below.",
|
|
1107
1137
|
"report_composition_missing": "Another {tokens} tokens have no input/output breakdown.",
|
|
1108
1138
|
"report_wrapped_cost_scope": "Only days with price records (from {date})",
|
|
@@ -1116,6 +1146,20 @@
|
|
|
1116
1146
|
"report_ledger_all_reset": "All dispatched work in the last 7 days came before the quota reset, so it counts toward last week."
|
|
1117
1147
|
},
|
|
1118
1148
|
"zh-CN": {
|
|
1149
|
+
"report_title_tools_top": "{tool} 用得最多,占 {pct}%",
|
|
1150
|
+
"report_title_tools_single": "全部用量来自 {tool}",
|
|
1151
|
+
"report_sub_tools": "各工具 token 占比 · 共 {total} tokens",
|
|
1152
|
+
"report_title_projects_top3": "前 3 个项目占了 {pct}% 的用量",
|
|
1153
|
+
"report_sub_projects": "各项目 token 占比",
|
|
1154
|
+
"report_title_trend_peak_week": "{date} 那周用量最高",
|
|
1155
|
+
"report_sub_trend": "每日与每周 token 用量",
|
|
1156
|
+
"report_title_composition_top": "{pct}% 的 token 是{kind}",
|
|
1157
|
+
"report_sub_composition": "各类 token 占比",
|
|
1158
|
+
"report_title_sessions_top": "最大的一段会话用了 {tokens} tokens",
|
|
1159
|
+
"report_title_streak": "已经连续 {days} 天每天都有使用",
|
|
1160
|
+
"report_source_note": "数据来源:本机 {tools} 使用记录 · {from} – {to}",
|
|
1161
|
+
"report_source_pricing": "API 等值按公开 API 价格换算",
|
|
1162
|
+
"report_weekly_chart_title": "每周用量",
|
|
1119
1163
|
"panel_default_name": "默认",
|
|
1120
1164
|
"panel_load_error": "面板加载失败,请反馈这个问题:",
|
|
1121
1165
|
"panel_matrix": "黑客任务",
|
|
@@ -1661,6 +1705,7 @@
|
|
|
1661
1705
|
"report_project_others": "其他项目",
|
|
1662
1706
|
"report_ledger_lead": "Claude 派给 Codex、Antigravity 的工作,各吃掉多少周额度。",
|
|
1663
1707
|
"report_trend_partial_week": "仅 {days} 天",
|
|
1708
|
+
"report_trend_incomplete_week": "不完整周",
|
|
1664
1709
|
"report_history_scope": "分工具记录从 {date} 起;更早的每日总量只在下方热力图",
|
|
1665
1710
|
"report_composition_missing": "另有 {tokens} tokens 没有输入/输出拆解",
|
|
1666
1711
|
"report_wrapped_cost_scope": "只含有价格记录的日子({date} 起)",
|
|
@@ -1674,6 +1719,20 @@
|
|
|
1674
1719
|
"report_ledger_all_reset": "最近 7 天的派工都在额度重置前,算在上周。"
|
|
1675
1720
|
},
|
|
1676
1721
|
"ja": {
|
|
1722
|
+
"report_title_tools_top": "{tool} が最多で {pct}%",
|
|
1723
|
+
"report_title_tools_single": "すべての使用量は {tool} から",
|
|
1724
|
+
"report_sub_tools": "ツール別 token 比率 · 合計 {total} tokens",
|
|
1725
|
+
"report_title_projects_top3": "上位 3 プロジェクトが使用量の {pct}%",
|
|
1726
|
+
"report_sub_projects": "プロジェクト別 token 比率",
|
|
1727
|
+
"report_title_trend_peak_week": "{date} の週が使用量最多",
|
|
1728
|
+
"report_sub_trend": "日別・週別 token 使用量",
|
|
1729
|
+
"report_title_composition_top": "token の {pct}% は{kind}",
|
|
1730
|
+
"report_sub_composition": "種類別 token 比率",
|
|
1731
|
+
"report_title_sessions_top": "最大のセッションは {tokens} tokens",
|
|
1732
|
+
"report_title_streak": "{days} 日連続で毎日使用中",
|
|
1733
|
+
"report_source_note": "データ元:ローカルの {tools} 使用記録 · {from} – {to}",
|
|
1734
|
+
"report_source_pricing": "API 相当額は公開 API 料金で換算",
|
|
1735
|
+
"report_weekly_chart_title": "週別使用量",
|
|
1677
1736
|
"panel_default_name": "デフォルト",
|
|
1678
1737
|
"panel_load_error": "パネルの読み込みに失敗しました。報告してください:",
|
|
1679
1738
|
"panel_matrix": "マトリックス",
|
|
@@ -2219,6 +2278,7 @@
|
|
|
2219
2278
|
"report_project_others": "その他のプロジェクト",
|
|
2220
2279
|
"report_ledger_lead": "Claude が Codex・Antigravity に任せた作業が、それぞれの週間クォータをどれだけ使ったか。",
|
|
2221
2280
|
"report_trend_partial_week": "{days} 日分のみ",
|
|
2281
|
+
"report_trend_incomplete_week": "不完全な週",
|
|
2222
2282
|
"report_history_scope": "ツール別記録は {date} から。それ以前の日別合計は下のヒートマップにのみ表示します。",
|
|
2223
2283
|
"report_composition_missing": "ほかに {tokens} tokens は入力/出力の内訳がありません。",
|
|
2224
2284
|
"report_wrapped_cost_scope": "価格記録のある日のみ({date} 以降)",
|
|
@@ -2232,6 +2292,20 @@
|
|
|
2232
2292
|
"report_ledger_all_reset": "直近 7 日間に委任した作業はすべてクォータのリセット前のため、先週分に含まれます。"
|
|
2233
2293
|
},
|
|
2234
2294
|
"ko": {
|
|
2295
|
+
"report_title_tools_top": "{tool} 사용량이 {pct}%로 가장 많음",
|
|
2296
|
+
"report_title_tools_single": "모든 사용량은 {tool}에서 발생",
|
|
2297
|
+
"report_sub_tools": "도구별 token 비율 · 총 {total} tokens",
|
|
2298
|
+
"report_title_projects_top3": "상위 3개 프로젝트가 사용량의 {pct}%",
|
|
2299
|
+
"report_sub_projects": "프로젝트별 token 비율",
|
|
2300
|
+
"report_title_trend_peak_week": "{date} 주의 사용량이 가장 많음",
|
|
2301
|
+
"report_sub_trend": "일별 및 주별 token 사용량",
|
|
2302
|
+
"report_title_composition_top": "token의 {pct}%는 {kind}",
|
|
2303
|
+
"report_sub_composition": "유형별 token 비율",
|
|
2304
|
+
"report_title_sessions_top": "가장 큰 세션에서 {tokens} tokens 사용",
|
|
2305
|
+
"report_title_streak": "{days}일 연속 매일 사용 중",
|
|
2306
|
+
"report_source_note": "데이터 출처: 로컬 {tools} 사용 기록 · {from} – {to}",
|
|
2307
|
+
"report_source_pricing": "API 환산 비용은 공개 API 요금 기준",
|
|
2308
|
+
"report_weekly_chart_title": "주별 사용량",
|
|
2235
2309
|
"panel_default_name": "기본",
|
|
2236
2310
|
"panel_load_error": "패널을 불러오지 못했습니다. 제보해 주세요:",
|
|
2237
2311
|
"panel_matrix": "매트릭스",
|
|
@@ -2777,6 +2851,7 @@
|
|
|
2777
2851
|
"report_project_others": "기타 프로젝트",
|
|
2778
2852
|
"report_ledger_lead": "Claude가 Codex와 Antigravity에 맡긴 작업이 각 주간 한도를 얼마나 썼는지 보여 줍니다.",
|
|
2779
2853
|
"report_trend_partial_week": "{days}일만 포함",
|
|
2854
|
+
"report_trend_incomplete_week": "불완전한 주",
|
|
2780
2855
|
"report_history_scope": "도구별 기록은 {date}부터입니다. 이전 일별 합계는 아래 히트맵에만 표시됩니다.",
|
|
2781
2856
|
"report_composition_missing": "추가 {tokens} tokens에는 입력/출력 내역이 없습니다.",
|
|
2782
2857
|
"report_wrapped_cost_scope": "가격 기록이 있는 날만 포함 ({date}부터)",
|
|
@@ -1158,7 +1158,11 @@ def self_heal() -> None:
|
|
|
1158
1158
|
return
|
|
1159
1159
|
_migrate_bundled_python_commands_if_needed(settings)
|
|
1160
1160
|
setup_hook._migrate_windows_statusline_command_if_needed(settings)
|
|
1161
|
-
|
|
1161
|
+
usage_settings = settings.get(BACKUP_KEY)
|
|
1162
|
+
statusline_disabled = (
|
|
1163
|
+
isinstance(usage_settings, dict) and setup_hook.DISABLED_SL_KEY in usage_settings
|
|
1164
|
+
)
|
|
1165
|
+
if not is_setup() and "statusLine" not in settings and not statusline_disabled:
|
|
1162
1166
|
exit_code = _run_quietly(setup)
|
|
1163
1167
|
if exit_code == 0:
|
|
1164
1168
|
_append_self_heal_log("install_hook", "initial setup")
|
|
@@ -86,7 +86,7 @@ PREV_SL_KEY = "previousStatusLine"
|
|
|
86
86
|
# Where the status-line switch parks the live statusLine while it is off, so it
|
|
87
87
|
# never overwrites the pre-install backup under PREV_SL_KEY.
|
|
88
88
|
DISABLED_SL_KEY = "disabledStatusLine"
|
|
89
|
-
HOOK_VERSION = "1.
|
|
89
|
+
HOOK_VERSION = "1.17"
|
|
90
90
|
FORWARDER_VERSION = "1.1"
|
|
91
91
|
# Antigravity's Go runner hands its status-line command to cmd.exe unquoted, so
|
|
92
92
|
# every character cmd.exe treats specially has to be kept out of the path.
|
|
@@ -811,9 +811,7 @@ def _replace_grok_status_line(content: str, replacement: str) -> str | None:
|
|
|
811
811
|
else:
|
|
812
812
|
start, end = section
|
|
813
813
|
candidate = content[:start] + replacement + content[end:]
|
|
814
|
-
|
|
815
|
-
tomllib.loads(candidate)
|
|
816
|
-
except tomllib.TOMLDecodeError:
|
|
814
|
+
if _validated_table_edit(content, candidate, "ui", "status_line") != candidate:
|
|
817
815
|
return None
|
|
818
816
|
return candidate
|
|
819
817
|
|
|
@@ -887,8 +885,9 @@ def _unsetup_grok() -> bool:
|
|
|
887
885
|
restored = content[:start] + content[end:]
|
|
888
886
|
if start > 0 and restored[:start].endswith("\n"):
|
|
889
887
|
restored = restored[: start - 1] + restored[start:]
|
|
888
|
+
if _validated_table_edit(content, restored, "ui", "status_line") != restored:
|
|
889
|
+
return False
|
|
890
890
|
try:
|
|
891
|
-
tomllib.loads(restored)
|
|
892
891
|
_atomic_write_text(GROK_SETTINGS, restored)
|
|
893
892
|
GROK_PREVIOUS_STATUSLINE.unlink(missing_ok=True)
|
|
894
893
|
except (OSError, tomllib.TOMLDecodeError):
|
|
@@ -969,13 +968,31 @@ def _find_top_level_dotted_line(
|
|
|
969
968
|
return dotted_line.search(content, 0, top_level_end)
|
|
970
969
|
|
|
971
970
|
|
|
971
|
+
def _validated_table_edit(content: str, candidate: str, name: str, key: str) -> str:
|
|
972
|
+
"""Reject edits that change anything except the requested TOML key."""
|
|
973
|
+
try:
|
|
974
|
+
before, after = tomllib.loads(content), tomllib.loads(candidate)
|
|
975
|
+
except tomllib.TOMLDecodeError:
|
|
976
|
+
return content
|
|
977
|
+
|
|
978
|
+
def without_target(table: dict[str, Any], path: list[str]) -> None:
|
|
979
|
+
part = path[0]
|
|
980
|
+
if len(path) == 1:
|
|
981
|
+
table.pop(part, None)
|
|
982
|
+
elif isinstance(child := table.get(part), dict):
|
|
983
|
+
without_target(child, path[1:])
|
|
984
|
+
if not child:
|
|
985
|
+
table.pop(part)
|
|
986
|
+
|
|
987
|
+
path = [*name.split("."), key]
|
|
988
|
+
without_target(before, path)
|
|
989
|
+
without_target(after, path)
|
|
990
|
+
return candidate if before == after else content
|
|
991
|
+
|
|
992
|
+
|
|
972
993
|
def _insert_table_line(content: str, name: str, line: str) -> str:
|
|
973
994
|
def validated(candidate: str) -> str:
|
|
974
|
-
|
|
975
|
-
tomllib.loads(candidate)
|
|
976
|
-
except tomllib.TOMLDecodeError:
|
|
977
|
-
return content
|
|
978
|
-
return candidate
|
|
995
|
+
return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
|
|
979
996
|
|
|
980
997
|
table = _find_table(content, name)
|
|
981
998
|
if table is None:
|
|
@@ -1017,11 +1034,9 @@ def _replace_table_line(
|
|
|
1017
1034
|
content: str, name: str, line_regex: re.Pattern[str], replacement: str
|
|
1018
1035
|
) -> str:
|
|
1019
1036
|
def validated(candidate: str) -> str:
|
|
1020
|
-
|
|
1021
|
-
tomllib.loads(
|
|
1022
|
-
|
|
1023
|
-
return content
|
|
1024
|
-
return candidate
|
|
1037
|
+
return _validated_table_edit(
|
|
1038
|
+
content, candidate, name, next(iter(tomllib.loads(replacement)))
|
|
1039
|
+
)
|
|
1025
1040
|
|
|
1026
1041
|
table = _find_table(content, name)
|
|
1027
1042
|
if table is None:
|
|
@@ -1042,13 +1057,9 @@ def _replace_table_line(
|
|
|
1042
1057
|
return validated(candidate)
|
|
1043
1058
|
|
|
1044
1059
|
|
|
1045
|
-
def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str]) -> str:
|
|
1060
|
+
def _remove_table_line(content: str, name: str, line_regex: re.Pattern[str], key: str) -> str:
|
|
1046
1061
|
def validated(candidate: str) -> str:
|
|
1047
|
-
|
|
1048
|
-
tomllib.loads(candidate)
|
|
1049
|
-
except tomllib.TOMLDecodeError:
|
|
1050
|
-
return content
|
|
1051
|
-
return candidate
|
|
1062
|
+
return _validated_table_edit(content, candidate, name, key)
|
|
1052
1063
|
|
|
1053
1064
|
table = _find_table(content, name)
|
|
1054
1065
|
if table is None:
|
|
@@ -1072,11 +1083,7 @@ def _ensure_table_line(content: str, name: str, line_regex: re.Pattern[str], lin
|
|
|
1072
1083
|
itself is absent."""
|
|
1073
1084
|
|
|
1074
1085
|
def validated(candidate: str) -> str:
|
|
1075
|
-
|
|
1076
|
-
tomllib.loads(candidate)
|
|
1077
|
-
except tomllib.TOMLDecodeError:
|
|
1078
|
-
return content
|
|
1079
|
-
return candidate
|
|
1086
|
+
return _validated_table_edit(content, candidate, name, next(iter(tomllib.loads(line))))
|
|
1080
1087
|
|
|
1081
1088
|
table = _find_table(content, name)
|
|
1082
1089
|
if table is None:
|
|
@@ -1097,7 +1104,7 @@ def _replace_tui_status_line(content: str, replacement: str) -> str:
|
|
|
1097
1104
|
|
|
1098
1105
|
|
|
1099
1106
|
def _remove_tui_status_line(content: str) -> str:
|
|
1100
|
-
return _remove_table_line(content, "tui", _SL_REGEX)
|
|
1107
|
+
return _remove_table_line(content, "tui", _SL_REGEX, "status_line")
|
|
1101
1108
|
|
|
1102
1109
|
|
|
1103
1110
|
def _read_codex_config() -> tuple[str, dict[str, Any]] | None:
|
|
@@ -1150,6 +1157,10 @@ def _setup_codex() -> None:
|
|
|
1150
1157
|
print(_t("setup_codex_config_unmodifiable"))
|
|
1151
1158
|
return
|
|
1152
1159
|
_atomic_write_text(CODEX_CONFIG, content)
|
|
1160
|
+
written = _read_codex_config()
|
|
1161
|
+
if written is None or _codex_status_line(written[1]) != CODEX_STATUS_LINE:
|
|
1162
|
+
print(_t("setup_codex_config_unmodifiable"))
|
|
1163
|
+
return
|
|
1153
1164
|
print(_t("setup_codex_configured"))
|
|
1154
1165
|
if old is not None and old not in LEGACY_CODEX_STATUS_LINES:
|
|
1155
1166
|
print(_t("setup_codex_backup_written", path=CODEX_BACKUP))
|
|
@@ -1335,15 +1346,20 @@ def unsetup() -> int:
|
|
|
1335
1346
|
settings = _load_settings()
|
|
1336
1347
|
sl = settings.get("statusLine")
|
|
1337
1348
|
backup = settings.get(BACKUP_KEY)
|
|
1338
|
-
parked = backup.
|
|
1339
|
-
if sl is None:
|
|
1340
|
-
sl = parked
|
|
1349
|
+
parked = backup.get(DISABLED_SL_KEY) if isinstance(backup, dict) else None
|
|
1341
1350
|
|
|
1342
|
-
if _is_usage_hook(sl) or _is_legacy_tt_hook(sl):
|
|
1351
|
+
if _is_usage_hook(sl) or _is_legacy_tt_hook(sl) or (sl is None and parked is not None):
|
|
1343
1352
|
legacy_backup = settings.get(LEGACY_TT_BACKUP_KEY)
|
|
1344
1353
|
prev = backup.get(PREV_SL_KEY) if isinstance(backup, dict) else None
|
|
1345
1354
|
if not isinstance(prev, dict) and isinstance(legacy_backup, dict):
|
|
1346
1355
|
prev = legacy_backup.get(PREV_SL_KEY)
|
|
1356
|
+
if (
|
|
1357
|
+
not isinstance(prev, dict)
|
|
1358
|
+
and isinstance(parked, dict)
|
|
1359
|
+
and not _is_usage_hook(parked)
|
|
1360
|
+
and not _is_legacy_tt_hook(parked)
|
|
1361
|
+
):
|
|
1362
|
+
prev = parked
|
|
1347
1363
|
|
|
1348
1364
|
if isinstance(prev, dict):
|
|
1349
1365
|
settings["statusLine"] = prev
|
|
@@ -1354,6 +1370,7 @@ def unsetup() -> int:
|
|
|
1354
1370
|
|
|
1355
1371
|
if isinstance(backup, dict):
|
|
1356
1372
|
backup.pop(PREV_SL_KEY, None)
|
|
1373
|
+
backup.pop(DISABLED_SL_KEY, None)
|
|
1357
1374
|
if not backup:
|
|
1358
1375
|
del settings[BACKUP_KEY]
|
|
1359
1376
|
if isinstance(legacy_backup, dict):
|
|
@@ -91,11 +91,13 @@ def seed_caches(
|
|
|
91
91
|
continue
|
|
92
92
|
if len(file_cache) >= maxsize:
|
|
93
93
|
file_cache.popitem(last=False)
|
|
94
|
+
wal = file_data.get("wal")
|
|
94
95
|
file_cache[Path(path_str)] = _FileCacheEntry(
|
|
95
96
|
mtime=float(file_data["mtime"]),
|
|
96
97
|
size=int(file_data["size"]),
|
|
97
98
|
entries=[_deserialize_entry(entry) for entry in entries_data],
|
|
98
99
|
skipped_missing_dedup_key=int(file_data["skipped_missing_dedup_key"]),
|
|
100
|
+
wal=None if wal is None else (int(wal[0]), int(wal[1])),
|
|
99
101
|
)
|
|
100
102
|
except (KeyError, TypeError, ValueError):
|
|
101
103
|
continue
|
|
@@ -118,6 +120,7 @@ def flush_caches(
|
|
|
118
120
|
str(path): {
|
|
119
121
|
"mtime": entry.mtime,
|
|
120
122
|
"size": entry.size,
|
|
123
|
+
"wal": entry.wal,
|
|
121
124
|
"entries": [_serialize_entry(item) for item in entry.entries],
|
|
122
125
|
"skipped_missing_dedup_key": entry.skipped_missing_dedup_key,
|
|
123
126
|
}
|
|
@@ -38,7 +38,7 @@ AGY_SESSIONS_DIR = Path(os.path.expanduser("~/.gemini/antigravity-cli/conversati
|
|
|
38
38
|
# below that count evicts databases parsed during the prior refresh, causing a
|
|
39
39
|
# permanent full SQLite/protobuf reparse on every load (LRU thrashing).
|
|
40
40
|
_FILE_CACHE_MAXSIZE = 4096
|
|
41
|
-
_AGY_DB_CACHE_SCHEMA =
|
|
41
|
+
_AGY_DB_CACHE_SCHEMA = 4
|
|
42
42
|
AGY_CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_db_cache.json"))
|
|
43
43
|
_disk_cache_seeded = False
|
|
44
44
|
_DISK_CACHE_FLUSH_INTERVAL_S = 300.0
|
|
@@ -92,6 +92,7 @@ class _FileCacheEntry:
|
|
|
92
92
|
size: int
|
|
93
93
|
entries: list[AgyUsageEntry]
|
|
94
94
|
skipped_missing_dedup_key: int
|
|
95
|
+
wal: tuple[int, int] | None = None
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
_file_cache: OrderedDict[Path, _FileCacheEntry] = OrderedDict()
|
|
@@ -120,22 +121,24 @@ def load_entries_with_stats(hours_back: int = 0) -> AgyLoadResult:
|
|
|
120
121
|
skipped_missing_dedup_key = 0
|
|
121
122
|
cutoff_ts = cutoff.timestamp() if cutoff else None
|
|
122
123
|
file_cache_snapshot = {
|
|
123
|
-
str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
|
|
124
|
+
str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
|
|
124
125
|
}
|
|
125
126
|
for path in AGY_SESSIONS_DIR.glob("*.db"):
|
|
126
127
|
if cutoff_ts is not None:
|
|
127
128
|
try:
|
|
128
|
-
|
|
129
|
-
continue
|
|
129
|
+
modified = path.stat().st_mtime
|
|
130
130
|
except OSError as exc:
|
|
131
131
|
logger.warning("failed to stat Antigravity database %s: %s", path, exc)
|
|
132
132
|
continue
|
|
133
|
+
wal = _wal_fingerprint(path)
|
|
134
|
+
if max(modified, wal[0] / 1e9 if wal else 0.0) < cutoff_ts:
|
|
135
|
+
continue
|
|
133
136
|
file_entries, skipped = _load_database(path, cutoff, seen_dedup_keys)
|
|
134
137
|
entries.extend(file_entries)
|
|
135
138
|
skipped_missing_dedup_key += skipped
|
|
136
139
|
|
|
137
140
|
if {
|
|
138
|
-
str(path): (entry.mtime, entry.size) for path, entry in _file_cache.items()
|
|
141
|
+
str(path): (entry.mtime, entry.size, entry.wal) for path, entry in _file_cache.items()
|
|
139
142
|
} != file_cache_snapshot:
|
|
140
143
|
_disk_cache_dirty = True
|
|
141
144
|
_flush_caches_to_disk()
|
|
@@ -176,6 +179,15 @@ def flush_caches_on_terminate() -> None:
|
|
|
176
179
|
_flush_caches_on_terminate(lambda: _flush_caches_to_disk(force=True))
|
|
177
180
|
|
|
178
181
|
|
|
182
|
+
def _wal_fingerprint(path: Path) -> tuple[int, int] | None:
|
|
183
|
+
"""Return the ``-wal`` file's (mtime_ns, size); WAL commits leave the main file untouched."""
|
|
184
|
+
try:
|
|
185
|
+
stat = Path(f"{path}-wal").stat()
|
|
186
|
+
except OSError:
|
|
187
|
+
return None
|
|
188
|
+
return (stat.st_mtime_ns, stat.st_size)
|
|
189
|
+
|
|
190
|
+
|
|
179
191
|
def _load_database(
|
|
180
192
|
path: Path,
|
|
181
193
|
cutoff: datetime | None,
|
|
@@ -187,8 +199,14 @@ def _load_database(
|
|
|
187
199
|
logger.warning("failed to stat Antigravity database %s: %s", path, exc)
|
|
188
200
|
return [], 0
|
|
189
201
|
|
|
202
|
+
wal = _wal_fingerprint(path)
|
|
190
203
|
cached = _file_cache.get(path)
|
|
191
|
-
if
|
|
204
|
+
if (
|
|
205
|
+
cached is not None
|
|
206
|
+
and cached.mtime == st.st_mtime
|
|
207
|
+
and cached.size == st.st_size
|
|
208
|
+
and cached.wal == wal
|
|
209
|
+
):
|
|
192
210
|
_file_cache.move_to_end(path)
|
|
193
211
|
return _filter_entries(
|
|
194
212
|
cached.entries,
|
|
@@ -208,6 +226,7 @@ def _load_database(
|
|
|
208
226
|
size=st.st_size,
|
|
209
227
|
entries=parsed_entries,
|
|
210
228
|
skipped_missing_dedup_key=skipped_missing_dedup_key,
|
|
229
|
+
wal=wal,
|
|
211
230
|
)
|
|
212
231
|
return _filter_entries(parsed_entries, skipped_missing_dedup_key, cutoff, seen_dedup_keys)
|
|
213
232
|
|
|
@@ -18,7 +18,6 @@ from __future__ import annotations
|
|
|
18
18
|
import json
|
|
19
19
|
import os
|
|
20
20
|
import platform
|
|
21
|
-
import shutil
|
|
22
21
|
import subprocess
|
|
23
22
|
import sys
|
|
24
23
|
import tempfile
|
|
@@ -35,6 +34,7 @@ from typing import Any
|
|
|
35
34
|
from urllib.error import HTTPError, URLError
|
|
36
35
|
from urllib.request import Request, urlopen
|
|
37
36
|
|
|
37
|
+
from usage_common.cli_lookup import cli_env, resolve_cli
|
|
38
38
|
from usage_common.subprocess_utils import hidden_console_kwargs
|
|
39
39
|
|
|
40
40
|
CACHE_PATH = Path(os.path.expanduser("~/.usage/agy_quota_cache.json"))
|
|
@@ -168,6 +168,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
168
168
|
capture_output=True,
|
|
169
169
|
timeout=timeout,
|
|
170
170
|
stdin=subprocess.DEVNULL,
|
|
171
|
+
env=cli_env(agy),
|
|
171
172
|
**hidden_console_kwargs(),
|
|
172
173
|
)
|
|
173
174
|
except (OSError, subprocess.SubprocessError):
|
|
@@ -189,14 +190,7 @@ def _probe_via_cli(timeout: float) -> AgyQuotaResult | None:
|
|
|
189
190
|
|
|
190
191
|
def find_agy() -> str | None:
|
|
191
192
|
"""Find the Antigravity CLI in PATH or common user installation paths."""
|
|
192
|
-
|
|
193
|
-
if path is not None:
|
|
194
|
-
return path
|
|
195
|
-
for candidate in ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"):
|
|
196
|
-
expanded = os.path.expanduser(candidate)
|
|
197
|
-
if os.access(expanded, os.X_OK):
|
|
198
|
-
return expanded
|
|
199
|
-
return None
|
|
193
|
+
return resolve_cli("agy", ("~/.local/bin/agy", "/opt/homebrew/bin/agy", "/usr/local/bin/agy"))
|
|
200
194
|
|
|
201
195
|
|
|
202
196
|
def load_quota(max_age_minutes: float = 5) -> AgyQuotaResult | None:
|
|
@@ -303,9 +303,10 @@ def _has_rate_limits(data: dict[str, Any]) -> bool:
|
|
|
303
303
|
rl = data.get("rate_limits")
|
|
304
304
|
if not isinstance(rl, dict):
|
|
305
305
|
return False
|
|
306
|
-
windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if
|
|
306
|
+
windows = [w for w in (rl.get("five_hour"), rl.get("seven_day")) if w is not None]
|
|
307
307
|
return bool(windows) and all(
|
|
308
|
-
_as_finite_float(w.get("used_percentage")) is not None
|
|
308
|
+
isinstance(w, dict) and _as_finite_float(w.get("used_percentage")) is not None
|
|
309
|
+
for w in windows
|
|
309
310
|
)
|
|
310
311
|
|
|
311
312
|
|
|
@@ -347,7 +348,10 @@ def _build_snapshot(data: dict[str, Any], *, data_source: str = "hook") -> Usage
|
|
|
347
348
|
|
|
348
349
|
return UsageSnapshot(
|
|
349
350
|
current_percent=five_pct,
|
|
350
|
-
|
|
351
|
+
# Keep an unknown hook reset unknown so the keeper cannot remember a
|
|
352
|
+
# parse-time default as a real boundary. Display expiry logic above
|
|
353
|
+
# still uses the default to preserve the reported percentage.
|
|
354
|
+
current_reset_at=_as_finite_float(five.get("resets_at")),
|
|
351
355
|
weekly_percent=seven_pct,
|
|
352
356
|
weekly_reset_at=seven_reset,
|
|
353
357
|
current_status=status,
|
|
@@ -652,16 +652,23 @@ def _rate_limits_from_websocket_error(
|
|
|
652
652
|
headers = _as_dict(event.get("headers"))
|
|
653
653
|
primary_reset = _as_optional_float(headers.get("X-Codex-Primary-Reset-At"))
|
|
654
654
|
secondary_reset = _as_optional_float(headers.get("X-Codex-Secondary-Reset-At"))
|
|
655
|
-
|
|
655
|
+
recorded_at = _timestamp_from_log_ts(ts)
|
|
656
|
+
recorded_ts = recorded_at.timestamp() if recorded_at is not None else None
|
|
656
657
|
if primary_reset is None:
|
|
657
658
|
primary_reset_after = _as_optional_float(headers.get("X-Codex-Primary-Reset-After-Seconds"))
|
|
658
|
-
primary_reset =
|
|
659
|
+
primary_reset = (
|
|
660
|
+
recorded_ts + primary_reset_after
|
|
661
|
+
if recorded_ts is not None and primary_reset_after is not None
|
|
662
|
+
else None
|
|
663
|
+
)
|
|
659
664
|
if secondary_reset is None:
|
|
660
665
|
secondary_reset_after = _as_optional_float(
|
|
661
666
|
headers.get("X-Codex-Secondary-Reset-After-Seconds")
|
|
662
667
|
)
|
|
663
668
|
secondary_reset = (
|
|
664
|
-
|
|
669
|
+
recorded_ts + secondary_reset_after
|
|
670
|
+
if recorded_ts is not None and secondary_reset_after is not None
|
|
671
|
+
else None
|
|
665
672
|
)
|
|
666
673
|
return _build_rate_limits(
|
|
667
674
|
primary_pct=_as_optional_float(headers.get("X-Codex-Primary-Used-Percent")),
|
|
@@ -669,7 +676,7 @@ def _rate_limits_from_websocket_error(
|
|
|
669
676
|
secondary_pct=_as_optional_float(headers.get("X-Codex-Secondary-Used-Percent")),
|
|
670
677
|
secondary_reset=secondary_reset,
|
|
671
678
|
model=_event_value(body, "model") or "unknown",
|
|
672
|
-
updated_at=
|
|
679
|
+
updated_at=recorded_at,
|
|
673
680
|
)
|
|
674
681
|
|
|
675
682
|
|