usage-cli 0.30.22__tar.gz → 0.30.23__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.30.22/usage_cli.egg-info → usage_cli-0.30.23}/PKG-INFO +5 -3
- {usage_cli-0.30.22 → usage_cli-0.30.23}/README.md +4 -2
- usage_cli-0.30.23/adapters/muse.py +43 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/registry.py +2 -2
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/reporter.py +8 -2
- {usage_cli-0.30.22 → usage_cli-0.30.23}/i18n.json +25 -5
- usage_cli-0.30.23/loaders/muse_loader.py +169 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/agy.py +39 -3
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/prefs.py +21 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/state.py +8 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/web_panel.py +17 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/pricing.py +15 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/pyproject.toml +1 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_html_report_snapshot.py +15 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_menubar.py +18 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_menubar_agy.py +74 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_menubar_prefs.py +15 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_menubar_state.py +15 -1
- usage_cli-0.30.23/tests/test_muse_loader.py +179 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_web_panel_payload.py +44 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_wintray.py +28 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/html_report.py +1 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/report_daily_chart.py +2 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/report_filter.py +1 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/tables.py +8 -1
- {usage_cli-0.30.22 → usage_cli-0.30.23/usage_cli.egg-info}/PKG-INFO +5 -3
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.egg-info/SOURCES.txt +3 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.py +8 -2
- {usage_cli-0.30.22 → usage_cli-0.30.23}/wintray/app.py +12 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/LICENSE +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/agy.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/claude.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/codex.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/grok.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/rate_limits.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/adapters/types.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/aggregator.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/blocks.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/diagnoser.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/insights.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/persona_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/subscription.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/analyzer/usage_snapshot.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/doctor.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/fsevents_watch.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/i18n.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/installer/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/installer/login_item.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/installer/session_hooks.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/installer/setup_hook.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/installer/statusline_settings.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/agy_disk_cache.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/agy_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/agy_quota_probe.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/cache_quarantine.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/claude_paths.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/codex_disk_cache.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/codex_events.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/codex_fork_replay.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/codex_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/codex_paths.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/disk_cache_common.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/disk_cache_lifecycle.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/grok_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/grok_quota_probe.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/history_disk_cache.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/history_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/jsonl_limits.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/loaders/jsonl_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/main.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/actions.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/app.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/chrome.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/grok.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/menu.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/notify.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/popover.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/refresh.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/title.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/menubar/update.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/base.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/dynamic_height.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/panel_scale.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/panel_window.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/panel_window_state.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/payload.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/panels/window_drag.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/prefs.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/project_resolver.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/agy_window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/burn_rate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/codex_window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/usage_rate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/quota/window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/service_status.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/setup.cfg +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/setup_app.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_adapters_claude.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_adapters_codex.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_adapters_rate_limits.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_aggregator.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_agy_adapter.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_agy_disk_cache.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_agy_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_agy_quota_probe.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_agy_window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_analyzer_blocks.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_analyzer_pipeline.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_burn_rate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_cache_quarantine.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_claude_paths.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_codex_consistency.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_codex_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_codex_window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_diagnoser.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_disk_cache_shards.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_doctor.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_dynamic_height.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_fsevents_watch.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_grok_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_grok_quota_probe.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_history_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_hook_scripts_are_standalone.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_i18n_format_fallback.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_i18n_key_parity.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_i18n_packaged_path.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_insights.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_jsonl_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_login_item.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_main.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_menubar_grok.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_packaged_resources.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_packaging.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_panel_scale.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_panel_window_state.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_panels.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_panels_lazy_import.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_persona_loader.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_pricing.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_project_resolver.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_report_daily_chart.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_report_filter_js.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_reporter_build_data.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_service_status.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_session_resume.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_setup_app.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_setup_hook.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_setup_hook_coexistence.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_setup_hook_resume.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_setup_hook_terse.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_subprocess_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_subscription.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_time_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_tui.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_tui_sprite.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_ui_tables.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_update_checker.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_update_gate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_update_release_notes.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_cli.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_client.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_lang.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_logging.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_notifications.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_rate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_snapshot.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_snapshot_replay.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_statusline.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_statusline_agy.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_statusline_grok.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_terse_mode.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_usage_terse_reminder.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_wheel_packaging.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_win_login_item.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_window_keeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tests/test_windows_manifest.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tui/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tui/app.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/tui/sprite.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/report_charts.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/report_scripts.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/ui/report_styles.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/updates/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/updates/checker.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/updates/gate.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/updates/release_notes.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.egg-info/dependency_links.txt +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.egg-info/entry_points.txt +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.egg-info/requires.txt +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_cli.egg-info/top_level.txt +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_client.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/subprocess_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/time_utils.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/usage_dir_sweeper.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/usage_lang.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_common/usage_logging.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_diagnosis_snapshot.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_notifications.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_session_resume.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_statusline.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_statusline_agy.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_statusline_forwarder.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_statusline_grok.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_terse_mode.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/usage_terse_reminder.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/wintray/__init__.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/wintray/login_item.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/wintray/menu.py +0 -0
- {usage_cli-0.30.22 → usage_cli-0.30.23}/wintray/watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: usage-cli
|
|
3
|
-
Version: 0.30.
|
|
3
|
+
Version: 0.30.23
|
|
4
4
|
Summary: usage — 在 macOS menu bar 與 Windows 系統匣顯示 Claude Code、Codex、Antigravity 用量的小工具(也提供終端機 TUI)
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
Requires-Python: >=3.13
|
|
@@ -62,8 +62,9 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
|
|
|
62
62
|
### Live Visibility
|
|
63
63
|
|
|
64
64
|
- **Always-on Monitor:** Your quota lives in the menu bar, color-coded from green to red. Click when you want the full session, weekly, and per-project breakdown.
|
|
65
|
-
- **Antigravity Support:** Antigravity (Gemini) session and weekly quota show up as a third card in every theme except World Cup 2026, which stays a two-team HUD. Numbers come straight from the official quota API, using the sign-in the Antigravity CLI already keeps on your machine — refreshed every few minutes, with live reset countdowns.
|
|
65
|
+
- **Antigravity Support:** Antigravity (Gemini) session and weekly quota show up as a third card in every theme except World Cup 2026, which stays a two-team HUD. Numbers come straight from the official quota API, using the sign-in the Antigravity CLI already keeps on your machine — refreshed every few minutes, with live reset countdowns. Antigravity keeps two separate quota pools: the card shows Gemini by default, and tapping the `Gemini ⇄` tag next to the title switches it to Claude / GPT — the choice is remembered.
|
|
66
66
|
- **Grok CLI Support:** A fourth card reads Grok CLI's weekly credit percentage straight from its own local debug log — no network call. Grok CLI doesn't expose session or burn-rate data, so the card shows a single weekly bar; its per-request token usage still counts toward today's cost and project totals like Claude Code and Codex.
|
|
67
|
+
- **Muse Code Spending:** Muse Code's per-request tokens and cost count toward today's cost, project totals, the HTML report, and the `usage` CLI, read from its own local session logs. Muse keeps no local quota data, so there is no Muse quota card.
|
|
67
68
|
- **Service Status Alerts:** An orange-red banner appears when Claude Code, Claude API, or Codex API has an outage or degraded performance, read from their public Statuspage.io pages — never an LLM usage API. Antigravity isn't covered; it has no public status page.
|
|
68
69
|
- **Context Nudges & Notifications:** When your context window hits 70% — or earlier when it is filling fast — the status line nudges you to `/clear` or `/compact` to prevent token waste. You can also opt-in to system notifications for quota limits and recoveries.
|
|
69
70
|
- **Prompt Cache Health:** The status line shows Claude Code's prompt cache hit rate with a countdown to expiry, so you can tell at a glance whether finishing now still reuses the cached context or lets it go cold and re-sends everything. Needs Claude Code 2.1.251 or newer; on older versions the segment simply doesn't appear.
|
|
@@ -204,8 +205,9 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
204
205
|
| Always on screen | ✅ | — | ✅ |
|
|
205
206
|
| macOS menu bar & Windows system tray | ✅ | — | macOS only |
|
|
206
207
|
| Claude Code & Codex usage | ✅ | Claude only | ✅ |
|
|
207
|
-
| Antigravity (Gemini)
|
|
208
|
+
| Antigravity usage (Gemini and Claude / GPT) | ✅ | — | — |
|
|
208
209
|
| Grok CLI usage | ✅ | — | — |
|
|
210
|
+
| Muse Code token spend | ✅ | — | — |
|
|
209
211
|
| Claude Code & Codex service-status alerts | ✅ | — | — |
|
|
210
212
|
| HTML deep reports & UI | ✅ | ✅ | — |
|
|
211
213
|
| AI Update Daily | ✅ | — | — |
|
|
@@ -42,8 +42,9 @@ It lands in your Applications folder automatically. Open it once; if macOS 15 or
|
|
|
42
42
|
### Live Visibility
|
|
43
43
|
|
|
44
44
|
- **Always-on Monitor:** Your quota lives in the menu bar, color-coded from green to red. Click when you want the full session, weekly, and per-project breakdown.
|
|
45
|
-
- **Antigravity Support:** Antigravity (Gemini) session and weekly quota show up as a third card in every theme except World Cup 2026, which stays a two-team HUD. Numbers come straight from the official quota API, using the sign-in the Antigravity CLI already keeps on your machine — refreshed every few minutes, with live reset countdowns.
|
|
45
|
+
- **Antigravity Support:** Antigravity (Gemini) session and weekly quota show up as a third card in every theme except World Cup 2026, which stays a two-team HUD. Numbers come straight from the official quota API, using the sign-in the Antigravity CLI already keeps on your machine — refreshed every few minutes, with live reset countdowns. Antigravity keeps two separate quota pools: the card shows Gemini by default, and tapping the `Gemini ⇄` tag next to the title switches it to Claude / GPT — the choice is remembered.
|
|
46
46
|
- **Grok CLI Support:** A fourth card reads Grok CLI's weekly credit percentage straight from its own local debug log — no network call. Grok CLI doesn't expose session or burn-rate data, so the card shows a single weekly bar; its per-request token usage still counts toward today's cost and project totals like Claude Code and Codex.
|
|
47
|
+
- **Muse Code Spending:** Muse Code's per-request tokens and cost count toward today's cost, project totals, the HTML report, and the `usage` CLI, read from its own local session logs. Muse keeps no local quota data, so there is no Muse quota card.
|
|
47
48
|
- **Service Status Alerts:** An orange-red banner appears when Claude Code, Claude API, or Codex API has an outage or degraded performance, read from their public Statuspage.io pages — never an LLM usage API. Antigravity isn't covered; it has no public status page.
|
|
48
49
|
- **Context Nudges & Notifications:** When your context window hits 70% — or earlier when it is filling fast — the status line nudges you to `/clear` or `/compact` to prevent token waste. You can also opt-in to system notifications for quota limits and recoveries.
|
|
49
50
|
- **Prompt Cache Health:** The status line shows Claude Code's prompt cache hit rate with a countdown to expiry, so you can tell at a glance whether finishing now still reuses the cached context or lets it go cold and re-sends everything. Needs Claude Code 2.1.251 or newer; on older versions the segment simply doesn't appear.
|
|
@@ -184,8 +185,9 @@ If the menu bar shows `--`, it's usually not broken — there's just no local da
|
|
|
184
185
|
| Always on screen | ✅ | — | ✅ |
|
|
185
186
|
| macOS menu bar & Windows system tray | ✅ | — | macOS only |
|
|
186
187
|
| Claude Code & Codex usage | ✅ | Claude only | ✅ |
|
|
187
|
-
| Antigravity (Gemini)
|
|
188
|
+
| Antigravity usage (Gemini and Claude / GPT) | ✅ | — | — |
|
|
188
189
|
| Grok CLI usage | ✅ | — | — |
|
|
190
|
+
| Muse Code token spend | ✅ | — | — |
|
|
189
191
|
| Claude Code & Codex service-status alerts | ✅ | — | — |
|
|
190
192
|
| HTML deep reports & UI | ✅ | ✅ | — |
|
|
191
193
|
| AI Update Daily | ✅ | — | — |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from loaders import muse_loader
|
|
7
|
+
from loaders.history_loader import UsageEntry as MuseUsageEntry
|
|
8
|
+
|
|
9
|
+
from i18n import t
|
|
10
|
+
|
|
11
|
+
from .types import AgentInfo, UsageEntry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detect() -> AgentInfo | None:
|
|
15
|
+
if muse_loader.session_paths():
|
|
16
|
+
return AgentInfo(
|
|
17
|
+
id="muse",
|
|
18
|
+
name=t("muse_name"),
|
|
19
|
+
data_dir=str(muse_loader.MUSE_SESSIONS_DIR),
|
|
20
|
+
installed=True,
|
|
21
|
+
)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_entries(hours_back: int = 0) -> list[UsageEntry]:
|
|
26
|
+
return [_to_usage_entry(entry) for entry in muse_loader.load_entries(hours_back)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _to_usage_entry(entry: MuseUsageEntry) -> UsageEntry:
|
|
30
|
+
return UsageEntry(
|
|
31
|
+
timestamp=entry.timestamp,
|
|
32
|
+
session_id=entry.session_id,
|
|
33
|
+
message_id=entry.message_id,
|
|
34
|
+
request_id=entry.request_id,
|
|
35
|
+
model=entry.model,
|
|
36
|
+
input_tokens=entry.input_tokens,
|
|
37
|
+
output_tokens=entry.output_tokens,
|
|
38
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
39
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
40
|
+
cost_usd=entry.cost_usd,
|
|
41
|
+
project=entry.project,
|
|
42
|
+
agent_id="muse",
|
|
43
|
+
)
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
6
|
|
|
7
7
|
from .types import AgentInfo
|
|
8
|
-
from . import agy, claude, codex, grok
|
|
8
|
+
from . import agy, claude, codex, grok, muse
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def detect_agents() -> list[AgentInfo]:
|
|
12
12
|
agents: list[AgentInfo] = []
|
|
13
|
-
for detector in [claude.detect, codex.detect, agy.detect, grok.detect]:
|
|
13
|
+
for detector in [claude.detect, codex.detect, agy.detect, grok.detect, muse.detect]:
|
|
14
14
|
info = detector()
|
|
15
15
|
if info:
|
|
16
16
|
agents.append(info)
|
|
@@ -22,7 +22,7 @@ from typing import Any, NotRequired, TypedDict, cast
|
|
|
22
22
|
from loaders import cache_quarantine, codex_loader
|
|
23
23
|
from analyzer import persona_loader
|
|
24
24
|
from analyzer import subscription
|
|
25
|
-
from adapters import agy, claude, codex, grok
|
|
25
|
+
from adapters import agy, claude, codex, grok, muse
|
|
26
26
|
from adapters.types import AgentInfo, UsageEntry
|
|
27
27
|
from pricing import calculate_cost, is_model_priced
|
|
28
28
|
|
|
@@ -31,7 +31,13 @@ from . import diagnoser, usage_snapshot
|
|
|
31
31
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
|
-
AGENT_LOADERS = {
|
|
34
|
+
AGENT_LOADERS = {
|
|
35
|
+
"claude-code": claude,
|
|
36
|
+
"codex": codex,
|
|
37
|
+
"antigravity": agy,
|
|
38
|
+
"grok": grok,
|
|
39
|
+
"muse": muse,
|
|
40
|
+
}
|
|
35
41
|
AGENT_NAMES = {"claude-code": "Claude Code", "codex": "Codex"}
|
|
36
42
|
_YEAR_WEEKS = 53
|
|
37
43
|
YEAR_CACHE_PATH = Path(os.path.expanduser("~/.usage/year_cache.json"))
|
|
@@ -88,11 +88,15 @@
|
|
|
88
88
|
"codex_credits": "重置點數 · 餘額 {balance}",
|
|
89
89
|
"codex_credits_unlimited": "重置點數 · 無限",
|
|
90
90
|
"agy_name": "Antigravity",
|
|
91
|
+
"agy_group_gemini": "Gemini ⇄",
|
|
92
|
+
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
93
|
+
"agy_group_switch_tooltip": "切換 Antigravity 額度組",
|
|
91
94
|
"agy_quota_full": "額度全滿",
|
|
92
95
|
"agy_stale_minutes": "約 {minutes} 分鐘前",
|
|
93
96
|
"agy_stale_hours": "約 {hours} 小時前",
|
|
94
97
|
"agy_stale_tooltip": "顯示的是快取舊值。額度直接向官方 API 取得,通常每幾分鐘自動更新;出現舊值代表最近取用失敗(多半是 agy 尚未登入,或網路問題),成功後會自動恢復。",
|
|
95
98
|
"grok_name": "Grok",
|
|
99
|
+
"muse_name": "Muse",
|
|
96
100
|
"grok_stale_minutes": "約 {minutes} 分鐘前",
|
|
97
101
|
"grok_stale_hours": "約 {hours} 小時前",
|
|
98
102
|
"grok_stale_tooltip": "資料來自 Grok CLI 本機 log,不是即時 API;請最近執行過 grok 指令後再查看,才會更新。",
|
|
@@ -334,7 +338,7 @@
|
|
|
334
338
|
"report_title": "你的 AI 用量回顧",
|
|
335
339
|
"report_insights_section": "洞察",
|
|
336
340
|
"report_insights_quiet": "這期跑得很平穩,沒什麼值得點名的異常。",
|
|
337
|
-
"report_empty_state_hint": "還沒有可以回顧的用量。先用 Claude Code、Codex、Antigravity 或
|
|
341
|
+
"report_empty_state_hint": "還沒有可以回顧的用量。先用 Claude Code、Codex、Antigravity、Grok 或 Muse 跑幾次,再回來看這份報告。",
|
|
338
342
|
"report_insights_change_up": "本期燒了 {tokens} tokens、{cost_usd},比上期多 {pct}%。",
|
|
339
343
|
"report_insights_spike_v2": "最猛的一天是 {date},{tokens} tokens,是平日的 {mean_multiplier:.2f} 倍。",
|
|
340
344
|
"report_insights_shift_new_project": "{project} 是這期新冒出來的專案,已佔 {pct:.1f}% 用量。",
|
|
@@ -497,11 +501,15 @@
|
|
|
497
501
|
"codex_credits": "Reset credits · balance {balance}",
|
|
498
502
|
"codex_credits_unlimited": "Reset credits · unlimited",
|
|
499
503
|
"agy_name": "Antigravity",
|
|
504
|
+
"agy_group_gemini": "Gemini ⇄",
|
|
505
|
+
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
506
|
+
"agy_group_switch_tooltip": "Switch Antigravity quota group",
|
|
500
507
|
"agy_quota_full": "Quota full",
|
|
501
508
|
"agy_stale_minutes": "about {minutes} minutes ago",
|
|
502
509
|
"agy_stale_hours": "about {hours} hours ago",
|
|
503
510
|
"agy_stale_tooltip": "Showing a cached value. Quota is fetched straight from the official API, usually updating every few minutes; an old value means the last fetch failed (often agy isn't signed in, or there's a network issue), and it recovers automatically once a fetch succeeds.",
|
|
504
511
|
"grok_name": "Grok",
|
|
512
|
+
"muse_name": "Muse",
|
|
505
513
|
"grok_stale_minutes": "about {minutes} minutes ago",
|
|
506
514
|
"grok_stale_hours": "about {hours} hours ago",
|
|
507
515
|
"grok_stale_tooltip": "Data comes from Grok CLI's local log, not a live API. Run a grok command recently for it to update.",
|
|
@@ -743,7 +751,7 @@
|
|
|
743
751
|
"report_title": "Your AI Usage Recap",
|
|
744
752
|
"report_insights_section": "Insights",
|
|
745
753
|
"report_insights_quiet": "This period ran steady — nothing unusual worth calling out.",
|
|
746
|
-
"report_empty_state_hint": "No usage to recap yet. Run Claude Code, Codex, Antigravity, or
|
|
754
|
+
"report_empty_state_hint": "No usage to recap yet. Run Claude Code, Codex, Antigravity, Grok, or Muse a few times, then come back to this report.",
|
|
747
755
|
"report_insights_change_up": "This period burned {tokens} tokens and {cost_usd} — up {pct}% vs last period.",
|
|
748
756
|
"report_insights_spike_v2": "Heaviest single day was {date} at {tokens} tokens — {mean_multiplier:.2f}× a normal day.",
|
|
749
757
|
"report_insights_shift_new_project": "{project} is new this period and already {pct:.1f}% of usage.",
|
|
@@ -906,11 +914,15 @@
|
|
|
906
914
|
"codex_credits": "重置点数 · 余额 {balance}",
|
|
907
915
|
"codex_credits_unlimited": "重置点数 · 无限",
|
|
908
916
|
"agy_name": "Antigravity",
|
|
917
|
+
"agy_group_gemini": "Gemini ⇄",
|
|
918
|
+
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
919
|
+
"agy_group_switch_tooltip": "切换 Antigravity 额度组",
|
|
909
920
|
"agy_quota_full": "额度已满",
|
|
910
921
|
"agy_stale_minutes": "约 {minutes} 分钟前",
|
|
911
922
|
"agy_stale_hours": "约 {hours} 小时前",
|
|
912
923
|
"agy_stale_tooltip": "显示的是缓存旧值。额度直接从官方 API 获取,通常每隔几分钟自动更新;出现旧值说明最近获取失败(多半是 agy 尚未登录,或网络问题),成功后会自动恢复。",
|
|
913
924
|
"grok_name": "Grok",
|
|
925
|
+
"muse_name": "Muse",
|
|
914
926
|
"grok_stale_minutes": "约 {minutes} 分钟前",
|
|
915
927
|
"grok_stale_hours": "约 {hours} 小时前",
|
|
916
928
|
"grok_stale_tooltip": "资料来自 Grok CLI 本地日志,不是即时 API;请最近运行过 grok 指令后再查看,才会更新。",
|
|
@@ -1152,7 +1164,7 @@
|
|
|
1152
1164
|
"report_title": "你的 AI 用量回顾",
|
|
1153
1165
|
"report_insights_section": "洞察",
|
|
1154
1166
|
"report_insights_quiet": "这期跑得很平稳,没什么值得点名的异常。",
|
|
1155
|
-
"report_empty_state_hint": "还没有可以回顾的用量。先用 Claude Code、Codex、Antigravity 或
|
|
1167
|
+
"report_empty_state_hint": "还没有可以回顾的用量。先用 Claude Code、Codex、Antigravity、Grok 或 Muse 跑几次,再回来看这份报告。",
|
|
1156
1168
|
"report_insights_change_up": "本期烧了 {tokens} tokens、{cost_usd},比上期多 {pct}%。",
|
|
1157
1169
|
"report_insights_spike_v2": "最重的一天是 {date},{tokens} tokens,是平日的 {mean_multiplier:.2f} 倍。",
|
|
1158
1170
|
"report_insights_shift_new_project": "{project} 是这期新冒出来的项目,已占 {pct:.1f}% 用量。",
|
|
@@ -1315,11 +1327,15 @@
|
|
|
1315
1327
|
"codex_credits": "リセットクレジット · 残高 {balance}",
|
|
1316
1328
|
"codex_credits_unlimited": "リセットクレジット · 無制限",
|
|
1317
1329
|
"agy_name": "Antigravity",
|
|
1330
|
+
"agy_group_gemini": "Gemini ⇄",
|
|
1331
|
+
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
1332
|
+
"agy_group_switch_tooltip": "Antigravity の割り当てグループを切り替え",
|
|
1318
1333
|
"agy_quota_full": "クォータは満タンです",
|
|
1319
1334
|
"agy_stale_minutes": "約 {minutes} 分前",
|
|
1320
1335
|
"agy_stale_hours": "約 {hours} 時間前",
|
|
1321
1336
|
"agy_stale_tooltip": "キャッシュされた値を表示しています。利用枠は公式 API から直接取得し、通常数分ごとに自動更新されます。古い値が表示されるのは直近の取得に失敗したためです(多くの場合 agy が未サインイン、またはネットワークの問題)。取得に成功すると自動的に復旧します。",
|
|
1322
1337
|
"grok_name": "Grok",
|
|
1338
|
+
"muse_name": "Muse",
|
|
1323
1339
|
"grok_stale_minutes": "約 {minutes} 分前",
|
|
1324
1340
|
"grok_stale_hours": "約 {hours} 時間前",
|
|
1325
1341
|
"grok_stale_tooltip": "データは Grok CLI のローカルログから読み取るもので、リアルタイム API ではありません。更新するには最近 grok コマンドを実行してください。",
|
|
@@ -1561,7 +1577,7 @@
|
|
|
1561
1577
|
"report_title": "AI 使用量の振り返り",
|
|
1562
1578
|
"report_insights_section": "インサイト",
|
|
1563
1579
|
"report_insights_quiet": "今期は安定したペースで、特筆すべき異常はありません。",
|
|
1564
|
-
"report_empty_state_hint": "まだ振り返る使用量がありません。Claude Code・Codex・Antigravity・Grok のどれかを何回か使ってから、このレポートを開き直してください。",
|
|
1580
|
+
"report_empty_state_hint": "まだ振り返る使用量がありません。Claude Code・Codex・Antigravity・Grok・Muse のどれかを何回か使ってから、このレポートを開き直してください。",
|
|
1565
1581
|
"report_insights_change_up": "今期は {tokens} tokens、{cost_usd} を使い、前期より {pct}% 増えています。",
|
|
1566
1582
|
"report_insights_spike_v2": "最も重い 1 日は {date} で、{tokens} tokens、通常日の {mean_multiplier:.2f} 倍です。",
|
|
1567
1583
|
"report_insights_shift_new_project": "{project} は今期新しく出てきたプロジェクトで、すでに使用量の {pct:.1f}% です。",
|
|
@@ -1724,11 +1740,15 @@
|
|
|
1724
1740
|
"codex_credits": "리셋 크레딧 · 잔액 {balance}",
|
|
1725
1741
|
"codex_credits_unlimited": "리셋 크레딧 · 무제한",
|
|
1726
1742
|
"agy_name": "Antigravity",
|
|
1743
|
+
"agy_group_gemini": "Gemini ⇄",
|
|
1744
|
+
"agy_group_claude_gpt": "Claude / GPT ⇄",
|
|
1745
|
+
"agy_group_switch_tooltip": "Antigravity 할당량 그룹 전환",
|
|
1727
1746
|
"agy_quota_full": "할당량이 가득 찼습니다",
|
|
1728
1747
|
"agy_stale_minutes": "약 {minutes}분 전",
|
|
1729
1748
|
"agy_stale_hours": "약 {hours}시간 전",
|
|
1730
1749
|
"agy_stale_tooltip": "캐시된 값을 표시하고 있습니다. 사용량은 공식 API에서 직접 가져오며 보통 몇 분마다 자동 업데이트됩니다. 예전 값이 보이는 건 최근 가져오기에 실패했기 때문입니다(대개 agy 미로그인 또는 네트워크 문제). 가져오기에 성공하면 자동으로 복구됩니다.",
|
|
1731
1750
|
"grok_name": "Grok",
|
|
1751
|
+
"muse_name": "Muse",
|
|
1732
1752
|
"grok_stale_minutes": "약 {minutes}분 전",
|
|
1733
1753
|
"grok_stale_hours": "약 {hours}시간 전",
|
|
1734
1754
|
"grok_stale_tooltip": "데이터는 Grok CLI의 로컬 로그에서 읽으며 실시간 API가 아닙니다. 업데이트하려면 최근에 grok 명령을 실행하세요.",
|
|
@@ -1970,7 +1990,7 @@
|
|
|
1970
1990
|
"report_title": "AI 사용량 리캡",
|
|
1971
1991
|
"report_insights_section": "인사이트",
|
|
1972
1992
|
"report_insights_quiet": "이번 기간은 안정적인 페이스로, 특별히 짚을 이상은 없습니다.",
|
|
1973
|
-
"report_empty_state_hint": "아직 정리할 사용량이 없습니다. Claude Code, Codex, Antigravity, Grok 중 하나를 몇 번 사용한 뒤 이 리포트를 다시 열어 보세요.",
|
|
1993
|
+
"report_empty_state_hint": "아직 정리할 사용량이 없습니다. Claude Code, Codex, Antigravity, Grok, Muse 중 하나를 몇 번 사용한 뒤 이 리포트를 다시 열어 보세요.",
|
|
1974
1994
|
"report_insights_change_up": "이번 기간은 {tokens} tokens, {cost_usd}를 썼고 지난 기간보다 {pct}% 늘었습니다.",
|
|
1975
1995
|
"report_insights_spike_v2": "가장 무거운 하루는 {date}로, {tokens} tokens이며 보통 날의 {mean_multiplier:.2f}배입니다.",
|
|
1976
1996
|
"report_insights_shift_new_project": "{project}는 이번 기간 새로 떠오른 프로젝트이고 이미 사용량의 {pct:.1f}%입니다.",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
|
|
4
|
+
"""Read Muse Code per-call usage from local session journals."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from datetime import UTC, datetime, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from loaders.history_loader import UsageEntry
|
|
16
|
+
from project_resolver import resolve_project_name
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
MUSE_SESSIONS_DIR = Path(os.path.expanduser("~/.local/share/muse/sessions"))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def session_paths() -> tuple[Path, ...]:
|
|
23
|
+
try:
|
|
24
|
+
return tuple(MUSE_SESSIONS_DIR.glob("*/*/*/*/session.jsonl"))
|
|
25
|
+
except OSError:
|
|
26
|
+
return ()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_entries(hours_back: int = 0) -> list[UsageEntry]:
|
|
30
|
+
"""Return each completed run and independent automated review once."""
|
|
31
|
+
cutoff = datetime.now(UTC) - timedelta(hours=hours_back) if hours_back > 0 else None
|
|
32
|
+
entries: list[UsageEntry] = []
|
|
33
|
+
seen: set[str] = set()
|
|
34
|
+
for path in session_paths():
|
|
35
|
+
try:
|
|
36
|
+
entries.extend(_read_session(path, cutoff, seen))
|
|
37
|
+
except OSError:
|
|
38
|
+
logger.warning("failed to read Muse session %s", path, exc_info=True)
|
|
39
|
+
entries.sort(key=lambda entry: entry.timestamp)
|
|
40
|
+
return entries
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read_session(path: Path, cutoff: datetime | None, seen: set[str]) -> list[UsageEntry]:
|
|
44
|
+
session_id = path.parent.name
|
|
45
|
+
workspace = ""
|
|
46
|
+
route_cwd = ""
|
|
47
|
+
entries: list[UsageEntry] = []
|
|
48
|
+
with path.open("rb") as journal:
|
|
49
|
+
for line in journal:
|
|
50
|
+
# Most lines include large prompts. Decode only records relevant to usage
|
|
51
|
+
# or project attribution; wrapped children are decoded below if present.
|
|
52
|
+
if not any(
|
|
53
|
+
marker in line
|
|
54
|
+
for marker in (
|
|
55
|
+
b"model_completed",
|
|
56
|
+
b"automated_review_completed",
|
|
57
|
+
b"runtime.session.metadata",
|
|
58
|
+
b"runtime.session.route_facts",
|
|
59
|
+
)
|
|
60
|
+
):
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
record = json.loads(line)
|
|
64
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError):
|
|
65
|
+
continue
|
|
66
|
+
if not isinstance(record, dict):
|
|
67
|
+
continue
|
|
68
|
+
records = [record]
|
|
69
|
+
children = record.get("children")
|
|
70
|
+
if isinstance(children, list):
|
|
71
|
+
for child in children:
|
|
72
|
+
if not isinstance(child, dict) or not isinstance(child.get("record_json"), str):
|
|
73
|
+
continue
|
|
74
|
+
try:
|
|
75
|
+
nested = json.loads(child["record_json"])
|
|
76
|
+
except (json.JSONDecodeError, RecursionError):
|
|
77
|
+
continue
|
|
78
|
+
if isinstance(nested, dict):
|
|
79
|
+
records.append(nested)
|
|
80
|
+
for item in records:
|
|
81
|
+
payload = item.get("payload")
|
|
82
|
+
if not isinstance(payload, dict):
|
|
83
|
+
continue
|
|
84
|
+
record_data = payload.get("record")
|
|
85
|
+
if isinstance(record_data, dict):
|
|
86
|
+
if item.get("payload_type") == "runtime.session.metadata":
|
|
87
|
+
value = record_data.get("workspace_root")
|
|
88
|
+
if isinstance(value, str) and value:
|
|
89
|
+
workspace = value
|
|
90
|
+
elif item.get("payload_type") == "runtime.session.route_facts":
|
|
91
|
+
value = record_data.get("cwd")
|
|
92
|
+
if isinstance(value, str) and value:
|
|
93
|
+
route_cwd = value
|
|
94
|
+
entry = _usage_entry(item, payload, session_id)
|
|
95
|
+
if entry is None or entry.message_id in seen:
|
|
96
|
+
continue
|
|
97
|
+
seen.add(entry.message_id)
|
|
98
|
+
if cutoff is None or entry.timestamp >= cutoff:
|
|
99
|
+
entries.append(entry)
|
|
100
|
+
project_path = workspace or route_cwd
|
|
101
|
+
project = resolve_project_name(project_path) if project_path else ""
|
|
102
|
+
for entry in entries:
|
|
103
|
+
entry.project = project
|
|
104
|
+
return entries
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _usage_entry(
|
|
108
|
+
item: dict[str, Any], payload: dict[str, Any], session_id: str
|
|
109
|
+
) -> UsageEntry | None:
|
|
110
|
+
if item.get("payload_type") != "runtime.session":
|
|
111
|
+
return None
|
|
112
|
+
event = payload.get("event")
|
|
113
|
+
if not isinstance(event, dict):
|
|
114
|
+
return None
|
|
115
|
+
kind = event.get("kind")
|
|
116
|
+
is_run = payload.get("kind") == "run" and kind == "model_completed"
|
|
117
|
+
is_review = payload.get("kind") == "approval" and kind == "automated_review_completed"
|
|
118
|
+
if not (is_run or is_review):
|
|
119
|
+
return None
|
|
120
|
+
record_id = item.get("id")
|
|
121
|
+
micros = item.get("recorded_at")
|
|
122
|
+
usage = event.get("usage")
|
|
123
|
+
model = event.get("model")
|
|
124
|
+
if is_review and isinstance(model, dict):
|
|
125
|
+
model = model.get("model_id")
|
|
126
|
+
if (
|
|
127
|
+
not isinstance(record_id, str)
|
|
128
|
+
or not record_id
|
|
129
|
+
or isinstance(micros, bool)
|
|
130
|
+
or not isinstance(micros, int)
|
|
131
|
+
or not isinstance(usage, dict)
|
|
132
|
+
or not isinstance(model, str)
|
|
133
|
+
or not model
|
|
134
|
+
):
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
seconds, microseconds = divmod(micros, 1_000_000)
|
|
138
|
+
timestamp = datetime.fromtimestamp(seconds, UTC).replace(microsecond=microseconds)
|
|
139
|
+
except (OverflowError, OSError, ValueError):
|
|
140
|
+
return None
|
|
141
|
+
input_tokens = _token_count(usage.get("input_tokens"))
|
|
142
|
+
output_tokens = _token_count(usage.get("output_tokens"))
|
|
143
|
+
cache_read = _token_count(
|
|
144
|
+
usage.get(
|
|
145
|
+
"cache_read_tokens", usage.get("cached_input_tokens", usage.get("cached_tokens", 0))
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
cache_write = _token_count(usage.get("cache_write_tokens", 0))
|
|
149
|
+
if input_tokens is None or output_tokens is None or cache_read is None or cache_write is None:
|
|
150
|
+
return None
|
|
151
|
+
if cache_read + cache_write > input_tokens:
|
|
152
|
+
return None
|
|
153
|
+
return UsageEntry(
|
|
154
|
+
timestamp=timestamp,
|
|
155
|
+
session_id=session_id,
|
|
156
|
+
message_id=record_id,
|
|
157
|
+
request_id=record_id,
|
|
158
|
+
model=model,
|
|
159
|
+
input_tokens=input_tokens - cache_read - cache_write,
|
|
160
|
+
output_tokens=output_tokens,
|
|
161
|
+
cache_creation_tokens=cache_write,
|
|
162
|
+
cache_read_tokens=cache_read,
|
|
163
|
+
cost_usd=None,
|
|
164
|
+
project="",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _token_count(value: object) -> int | None:
|
|
169
|
+
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
|
@@ -23,6 +23,7 @@ from loaders.agy_quota_probe import (
|
|
|
23
23
|
from loaders.agy_quota_probe import (
|
|
24
24
|
find_agy as find_agy,
|
|
25
25
|
)
|
|
26
|
+
from menubar.prefs import _agy_quota_group
|
|
26
27
|
from menubar.state import (
|
|
27
28
|
AGY_COLOR,
|
|
28
29
|
AgyStaleState,
|
|
@@ -43,6 +44,7 @@ AGY_STALE_SECONDS = 20 * 60
|
|
|
43
44
|
AGY_SESSION_FORECAST_MIN_SPAN_SECONDS = 15 * 60
|
|
44
45
|
AGY_WEEKLY_FORECAST_MIN_SPAN_SECONDS = 30 * 60
|
|
45
46
|
AGY_WEEKLY_WARNING_MAX_SECONDS = 24 * 3600
|
|
47
|
+
_last_quota: AgyQuotaResult | None = None
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
@dataclass(frozen=True, slots=True)
|
|
@@ -69,11 +71,15 @@ def project_quota(
|
|
|
69
71
|
language: str,
|
|
70
72
|
now: float | None = None,
|
|
71
73
|
burn_rate_trackers: dict[str, BurnRateTracker] | None = None,
|
|
74
|
+
group_preference: str = "gemini",
|
|
72
75
|
) -> AgyQuotaProjection | None:
|
|
73
|
-
"""Select and convert the
|
|
76
|
+
"""Select and convert the preferred quota group without I/O."""
|
|
74
77
|
if quota is None or not quota.groups:
|
|
75
78
|
return None
|
|
76
|
-
|
|
79
|
+
preferred = next(
|
|
80
|
+
(group for group in quota.groups if _matches_group(group.name, group_preference)), None
|
|
81
|
+
)
|
|
82
|
+
selected = preferred or next(
|
|
77
83
|
(group for group in quota.groups if "gemini" in group.name.lower()),
|
|
78
84
|
min(quota.groups, key=_group_remaining_percent),
|
|
79
85
|
)
|
|
@@ -133,14 +139,44 @@ def load_refresh_result(
|
|
|
133
139
|
) -> AgyRefreshResult:
|
|
134
140
|
"""Load/probe quota for a worker thread; never call this on the main thread."""
|
|
135
141
|
if find_agy() is None:
|
|
142
|
+
global _last_quota
|
|
143
|
+
_last_quota = None
|
|
136
144
|
return AgyRefreshResult(projection=None, hide_agy=True)
|
|
137
145
|
try:
|
|
138
|
-
|
|
146
|
+
_last_quota = load_quota()
|
|
147
|
+
projection = project_quota(
|
|
148
|
+
_last_quota,
|
|
149
|
+
language,
|
|
150
|
+
burn_rate_trackers=burn_rate_trackers,
|
|
151
|
+
group_preference=_agy_quota_group(),
|
|
152
|
+
)
|
|
139
153
|
except Exception:
|
|
154
|
+
_last_quota = None
|
|
140
155
|
projection = None
|
|
141
156
|
return AgyRefreshResult(projection=projection, hide_agy=projection is None)
|
|
142
157
|
|
|
143
158
|
|
|
159
|
+
def reproject_cached_quota(
|
|
160
|
+
language: str, burn_rate_trackers: dict[str, BurnRateTracker]
|
|
161
|
+
) -> AgyQuotaProjection | None:
|
|
162
|
+
"""Change groups from the last loaded snapshot without probing the network."""
|
|
163
|
+
burn_rate_trackers["agy_session"] = BurnRateTracker()
|
|
164
|
+
burn_rate_trackers["agy_weekly"] = BurnRateTracker()
|
|
165
|
+
return project_quota(
|
|
166
|
+
_last_quota,
|
|
167
|
+
language,
|
|
168
|
+
burn_rate_trackers=burn_rate_trackers,
|
|
169
|
+
group_preference=_agy_quota_group(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _matches_group(name: str, preference: str) -> bool:
|
|
174
|
+
normalized = name.lower()
|
|
175
|
+
if preference == "claude_gpt":
|
|
176
|
+
return "claude" in normalized and "gpt" in normalized
|
|
177
|
+
return "gemini" in normalized
|
|
178
|
+
|
|
179
|
+
|
|
144
180
|
def fallback_projection(language: str) -> AgyQuotaProjection:
|
|
145
181
|
"""Return inert rows while the card is hidden after an unavailable probe."""
|
|
146
182
|
return AgyQuotaProjection(
|
|
@@ -13,6 +13,7 @@ from prefs import _load_preferences, _save_preferences
|
|
|
13
13
|
DEFAULT_QUOTA_CARD_ORDER = ("claude", "codex", "agy", "grok")
|
|
14
14
|
DEFAULT_PANEL_FLAVOR = "mocha"
|
|
15
15
|
PANEL_FLAVORS = ("latte", "frappe", "macchiato", "mocha")
|
|
16
|
+
AGY_QUOTA_GROUPS = ("gemini", "claude_gpt")
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _resolved_preferences(prefs: Mapping[str, object] | None = None) -> Mapping[str, object]:
|
|
@@ -66,6 +67,26 @@ def _valid_panel_flavor(value: object) -> str | None:
|
|
|
66
67
|
return value
|
|
67
68
|
|
|
68
69
|
|
|
70
|
+
def _agy_quota_group(prefs: Mapping[str, object] | None = None) -> str:
|
|
71
|
+
data = _resolved_preferences(prefs)
|
|
72
|
+
group = _valid_agy_quota_group(data.get("agy_quota_group"))
|
|
73
|
+
return "gemini" if group is None else group
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _save_agy_quota_group(group: object) -> bool:
|
|
77
|
+
valid_group = _valid_agy_quota_group(group)
|
|
78
|
+
if valid_group is None:
|
|
79
|
+
return False
|
|
80
|
+
prefs = _load_preferences()
|
|
81
|
+
prefs["agy_quota_group"] = valid_group
|
|
82
|
+
_save_preferences(prefs)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _valid_agy_quota_group(value: object) -> str | None:
|
|
87
|
+
return value if isinstance(value, str) and value in AGY_QUOTA_GROUPS else None
|
|
88
|
+
|
|
89
|
+
|
|
69
90
|
def _quota_card_order(prefs: Mapping[str, object] | None = None) -> tuple[str, ...]:
|
|
70
91
|
data = _resolved_preferences(prefs)
|
|
71
92
|
order = _valid_quota_card_order(data.get("quota_card_order"))
|
|
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any, Protocol, TypedDict, cast
|
|
|
18
18
|
|
|
19
19
|
from i18n import _t
|
|
20
20
|
from installer.statusline_settings import _statusline_enabled
|
|
21
|
-
from loaders import codex_loader, grok_loader
|
|
21
|
+
from loaders import codex_loader, grok_loader, muse_loader
|
|
22
22
|
from loaders.claude_paths import claude_config_dirs
|
|
23
23
|
from loaders.codex_paths import codex_home
|
|
24
24
|
from loaders.history_loader import UsageEntry, load_entries
|
|
@@ -262,6 +262,7 @@ def _history_file_sources() -> tuple[Path, ...]:
|
|
|
262
262
|
codex_loader.STATE_DB,
|
|
263
263
|
codex_dir / "state_5.sqlite-wal",
|
|
264
264
|
grok_loader.GROK_LOG_PATH,
|
|
265
|
+
*muse_loader.session_paths(),
|
|
265
266
|
)
|
|
266
267
|
|
|
267
268
|
|
|
@@ -432,6 +433,12 @@ def app_load_history_entries(
|
|
|
432
433
|
if os.environ.get("USAGE_DEBUG") == "1":
|
|
433
434
|
logger.warning("Grok project usage load failed", exc_info=True)
|
|
434
435
|
error_key = _classify_history_load_error(exc)
|
|
436
|
+
try:
|
|
437
|
+
entries.extend(muse_loader.load_entries(hours_back=0))
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
if os.environ.get("USAGE_DEBUG") == "1":
|
|
440
|
+
logger.warning("Muse project usage load failed", exc_info=True)
|
|
441
|
+
error_key = _classify_history_load_error(exc)
|
|
435
442
|
app._history_load_error_key = error_key
|
|
436
443
|
app._history_entries_cache = list(entries)
|
|
437
444
|
app._history_entries_cache_fingerprint = fingerprint
|
|
@@ -19,6 +19,7 @@ from Quartz import CGColorCreateGenericRGB
|
|
|
19
19
|
|
|
20
20
|
from menubar.prefs import (
|
|
21
21
|
_panel_flavor,
|
|
22
|
+
_save_agy_quota_group,
|
|
22
23
|
_save_panel_flavor,
|
|
23
24
|
_save_quota_card_order,
|
|
24
25
|
_valid_quota_card_order,
|
|
@@ -188,6 +189,22 @@ class UsageScriptBridge(NSObject):
|
|
|
188
189
|
if parsed["action"] == "set_panel_flavor":
|
|
189
190
|
_save_panel_flavor(parsed.get("flavor"))
|
|
190
191
|
return
|
|
192
|
+
if parsed["action"] == "set_agy_quota_group":
|
|
193
|
+
if _save_agy_quota_group(parsed.get("group")):
|
|
194
|
+
from menubar.agy import reproject_cached_quota
|
|
195
|
+
|
|
196
|
+
state = getattr(self.delegate, "latest_state", None)
|
|
197
|
+
if state is not None:
|
|
198
|
+
projection = reproject_cached_quota(
|
|
199
|
+
state.language, self.delegate.burn_rate_trackers
|
|
200
|
+
)
|
|
201
|
+
if projection is not None:
|
|
202
|
+
state.agy_session = projection.session
|
|
203
|
+
state.agy_weekly = projection.weekly
|
|
204
|
+
state.agy_group_name = projection.group_name
|
|
205
|
+
state.agy_stale = projection.stale
|
|
206
|
+
self.delegate.popover_controller.setState_(state)
|
|
207
|
+
return
|
|
191
208
|
return
|
|
192
209
|
if action == "refresh":
|
|
193
210
|
self.delegate.refreshNow_(None)
|
|
@@ -370,6 +370,11 @@ def _resolve_model_key(model: str, pricing: PricingTable) -> str | None:
|
|
|
370
370
|
if normalized in pricing:
|
|
371
371
|
return normalized
|
|
372
372
|
|
|
373
|
+
if normalized.startswith("muse-"):
|
|
374
|
+
meta_key = f"meta/{normalized}"
|
|
375
|
+
if meta_key in pricing:
|
|
376
|
+
return meta_key
|
|
377
|
+
|
|
373
378
|
dated_matches = [key for key in pricing if DATE_SUFFIX_RE.sub("", key) == normalized]
|
|
374
379
|
if dated_matches:
|
|
375
380
|
return sorted(dated_matches, key=lambda key: (len(key), key))[0]
|
|
@@ -540,6 +545,16 @@ def _fallback_pricing() -> PricingTable:
|
|
|
540
545
|
"cache_creation_input_token_cost": 0.25e-6,
|
|
541
546
|
"cache_read_input_token_cost": 0.02e-6,
|
|
542
547
|
},
|
|
548
|
+
"meta/muse-spark-1.3": {
|
|
549
|
+
"input_cost_per_token": 1.25e-6,
|
|
550
|
+
"output_cost_per_token": 4.25e-6,
|
|
551
|
+
"cache_read_input_token_cost": 0.15e-6,
|
|
552
|
+
},
|
|
553
|
+
"meta/muse-spark-1.3-contributor": {
|
|
554
|
+
"input_cost_per_token": 0.1e-6,
|
|
555
|
+
"output_cost_per_token": 0.2e-6,
|
|
556
|
+
"cache_read_input_token_cost": 0.002e-6,
|
|
557
|
+
},
|
|
543
558
|
"gpt-5.5": {
|
|
544
559
|
"input_cost_per_token": 5e-6,
|
|
545
560
|
"output_cost_per_token": 30e-6,
|