cct-cli 0.7.9.0__py3-none-any.whl
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.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
calc_terminal/cli.py
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CAT — global command-line entry point (`cat`).
|
|
3
|
+
|
|
4
|
+
This module is what the installed console script calls:
|
|
5
|
+
|
|
6
|
+
cat = calc_terminal.cli:main
|
|
7
|
+
|
|
8
|
+
It makes CAT launchable from any working directory (no `python main.py`,
|
|
9
|
+
no need to be inside the source repository), supports normal CLI
|
|
10
|
+
behavior (`--help`, `--version`, an optional project/workspace path),
|
|
11
|
+
and then hands off to the exact same launch flow the classic
|
|
12
|
+
`python main.py` used: fallback REPL when Textual/attached-terminal
|
|
13
|
+
aren't available, primary chat UI otherwise.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
print("This is a library file and is not meant to be run directly.")
|
|
18
|
+
import sys
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _usage() -> str:
|
|
27
|
+
return (
|
|
28
|
+
"usage: cat [path] [options]\n"
|
|
29
|
+
"\n"
|
|
30
|
+
"CAT CLI v0.7.9.0 \u2014 Coding Agent Terminal / terminal AI coding agent.\n"
|
|
31
|
+
"Protected by Fomoji authentication — no valid Fomoji identity = no access.\n"
|
|
32
|
+
"\n"
|
|
33
|
+
"positional arguments:\n"
|
|
34
|
+
" path project/workspace directory to open (defaults to\n"
|
|
35
|
+
" the current working directory)\n"
|
|
36
|
+
"\n"
|
|
37
|
+
"options:\n"
|
|
38
|
+
" -h, --help show this help message and exit\n"
|
|
39
|
+
" -v, --version print the CAT version and exit\n"
|
|
40
|
+
" --doctor diagnose the CLI install (PATH, launcher,\n"
|
|
41
|
+
" PowerShell alias) and offer safe fixes\n"
|
|
42
|
+
" --auth <cmd> Fomoji auth: status | login | logout | whoami\n"
|
|
43
|
+
" browse [url] open CAT Browser (real HTML/CSS/JS via QWebEngineView)\n"
|
|
44
|
+
" --browser [url] same as `browse`\n"
|
|
45
|
+
" fatty, web launch Fatty CAT in CAT Browser (starts server on :8765)\n"
|
|
46
|
+
" server start Fatty CAT server in foreground (:8765)\n"
|
|
47
|
+
" vision CAT Vision commands: start | stop | pause | analyze | screenshot | clear\n"
|
|
48
|
+
" models dynamic AI models: [--provider <id>] [--new] [--updated] [--deprecated] [refresh]\n"
|
|
49
|
+
" providers manage AI providers: list | audit | refresh | status | test | backup\n"
|
|
50
|
+
" offline activate Emergency Local Mode (zero cloud, Ollama local only)\n"
|
|
51
|
+
"\n"
|
|
52
|
+
"examples:\n"
|
|
53
|
+
" cat open CAT in the current directory\n"
|
|
54
|
+
" cat . same as above\n"
|
|
55
|
+
" cat my-project open CAT with my-project as the workspace\n"
|
|
56
|
+
" cat /path/to/proj open CAT with an absolute workspace path\n"
|
|
57
|
+
" cat --auth login pair this device with your Fomoji identity\n"
|
|
58
|
+
" cat --auth status check Fomoji session\n"
|
|
59
|
+
" cat models list dynamically registered AI models\n"
|
|
60
|
+
" cat models --new show newly discovered models\n"
|
|
61
|
+
" cat providers status check real-time provider health and backup failover pool\n"
|
|
62
|
+
" cat providers backup list backup provider pool priority chain\n"
|
|
63
|
+
" cat providers test groq test connectivity and models for a provider\n"
|
|
64
|
+
" cat offline switch to local-only emergency fallback mode\n"
|
|
65
|
+
" cat fatty launch Fatty CAT (opens browser + starts server)\n"
|
|
66
|
+
" cat browse https://example.com open CAT Browser at a URL\n"
|
|
67
|
+
" cat browse open CAT Browser new-tab page\n"
|
|
68
|
+
" cat server start Fatty CAT web server on port 8765\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _version():
|
|
73
|
+
"""Best-effort version: installed-distribution metadata first (works
|
|
74
|
+
even when heavy deps like requests aren't importable — `cat --version`
|
|
75
|
+
must never fail just because a module import chain breaks), then the
|
|
76
|
+
source-tree constant, then a static fallback."""
|
|
77
|
+
for dist in ("cct-cli", "cat-cli", "cct-ai-ide", "cct"):
|
|
78
|
+
try:
|
|
79
|
+
from importlib.metadata import version as _dist_version
|
|
80
|
+
return _dist_version(dist)
|
|
81
|
+
except Exception:
|
|
82
|
+
continue
|
|
83
|
+
try:
|
|
84
|
+
from .app import VERSION
|
|
85
|
+
return VERSION
|
|
86
|
+
except Exception:
|
|
87
|
+
return "0.7.9.0"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_context(ctx: int) -> str:
|
|
91
|
+
if not ctx or ctx <= 0:
|
|
92
|
+
return "-"
|
|
93
|
+
if ctx >= 1_000_000:
|
|
94
|
+
val = f"{ctx / 1_000_000:.2f}".rstrip("0").rstrip(".")
|
|
95
|
+
return f"{val}M"
|
|
96
|
+
if ctx >= 1000:
|
|
97
|
+
return f"{ctx // 1000}K"
|
|
98
|
+
return str(ctx)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _models_cli(argv: list[str]) -> int:
|
|
102
|
+
"""Full-featured CAT Model Center CLI: list, search, info, install, remove, benchmark, recommend."""
|
|
103
|
+
args = argv[1:]
|
|
104
|
+
sub = args[0].lower() if args else "list"
|
|
105
|
+
|
|
106
|
+
if sub in ("--help", "-h", "help"):
|
|
107
|
+
print("Usage: cat models [command] [options]")
|
|
108
|
+
print("\nCommands:")
|
|
109
|
+
print(" list (default) List available models with device compatibility")
|
|
110
|
+
print(" search <query> Search models by name, family, capability, or category")
|
|
111
|
+
print(" info <name> Detailed model specs, hardware analysis & benchmarks")
|
|
112
|
+
print(" install <name> Download / pull model into local Ollama engine")
|
|
113
|
+
print(" remove <name> Delete local model from Ollama engine")
|
|
114
|
+
print(" benchmark [name] Run benchmark suite on model, or view leaderboard")
|
|
115
|
+
print(" recommend Device-specific model recommendations based on hardware")
|
|
116
|
+
print(" refresh Discover live models from active cloud/local providers")
|
|
117
|
+
print("\nOptions:")
|
|
118
|
+
print(" --category <cat> Filter by category (coding, reasoning, vision, small, large)")
|
|
119
|
+
print(" --provider <id> Filter models by provider (ollama, openai, anthropic, etc.)")
|
|
120
|
+
print(" --new, -n Show newly discovered models")
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
from .hardware_analyzer import HardwareAnalyzer
|
|
124
|
+
from .compatibility_engine import ModelCompatibilityEngine
|
|
125
|
+
from .benchmark_system import CATBenchmarkSystem
|
|
126
|
+
import calc_terminal.ollama_catalog as catalog
|
|
127
|
+
import calc_terminal.ollama_download as ollama
|
|
128
|
+
|
|
129
|
+
# Dynamic sync with local Ollama if running
|
|
130
|
+
installed_local = []
|
|
131
|
+
if ollama.is_ollama_running():
|
|
132
|
+
installed_local = ollama.installed_models()
|
|
133
|
+
catalog.register_installed_models(installed_local)
|
|
134
|
+
|
|
135
|
+
# 1. RECOMMEND COMMAND
|
|
136
|
+
if sub in ("recommend", "rec", "hardware"):
|
|
137
|
+
print("=" * 72)
|
|
138
|
+
print(" CAT HARDWARE ANALYZER & MODEL RECOMMENDATION ENGINE")
|
|
139
|
+
print("=" * 72)
|
|
140
|
+
engine = ModelCompatibilityEngine()
|
|
141
|
+
recs = engine.recommend_models(catalog.all_models(include_dynamic=True))
|
|
142
|
+
prof = engine.profile
|
|
143
|
+
|
|
144
|
+
print(f" System CPU : {prof.cpu_model} ({prof.cpu_threads} threads)")
|
|
145
|
+
print(f" System RAM : {prof.ram_total_gb:.1f} GB ({prof.ram_available_gb:.1f} GB available)")
|
|
146
|
+
if prof.has_gpu:
|
|
147
|
+
print(f" GPU Compute : {prof.gpu_name} ({prof.vram_gb:.1f} GB VRAM via {prof.gpu_backend})")
|
|
148
|
+
else:
|
|
149
|
+
print(" GPU Compute : None detected (CPU / System RAM execution)")
|
|
150
|
+
print(f" AI Capability: {prof.local_ai_tier.value}")
|
|
151
|
+
print("-" * 72)
|
|
152
|
+
|
|
153
|
+
for cat_name, title in [
|
|
154
|
+
("best_coding", "Recommended for Coding & Software Engineering:"),
|
|
155
|
+
("best_reasoning", "Recommended for Complex Reasoning & Math:"),
|
|
156
|
+
("best_fast", "Recommended for Fast On-Device Chat & Autocomplete:"),
|
|
157
|
+
]:
|
|
158
|
+
print(f"\n {title}")
|
|
159
|
+
m_list = recs.get(cat_name, [])
|
|
160
|
+
if not m_list:
|
|
161
|
+
print(" (No specific model found in catalog)")
|
|
162
|
+
for item in m_list[:3]:
|
|
163
|
+
m = item["model"]
|
|
164
|
+
c = item["compat"]
|
|
165
|
+
inst = " [INSTALLED]" if m["name"] in installed_local else ""
|
|
166
|
+
print(f" * {m['name']:<24} {c.tier_label_ascii:<16} | {m['desc']}{inst}")
|
|
167
|
+
|
|
168
|
+
print("\n" + "=" * 72)
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
# 2. SEARCH COMMAND
|
|
172
|
+
if sub in ("search", "find"):
|
|
173
|
+
query = args[1] if len(args) > 1 else ""
|
|
174
|
+
if not query:
|
|
175
|
+
print("Usage: cat models search <query>")
|
|
176
|
+
return 1
|
|
177
|
+
matches = catalog.search_models(query=query)
|
|
178
|
+
print(f"\nCAT Model Search for '{query}' ({len(matches)} results):\n")
|
|
179
|
+
engine = ModelCompatibilityEngine()
|
|
180
|
+
header = f"{'Model Name':<28} | {'Params':<8} | {'Size':<8} | {'Compatibility':<16} | {'Installed'}"
|
|
181
|
+
print(header)
|
|
182
|
+
print("-" * len(header))
|
|
183
|
+
for m in matches[:25]:
|
|
184
|
+
compat = engine.evaluate(m)
|
|
185
|
+
inst = "YES" if m["name"] in installed_local else "-"
|
|
186
|
+
print(f"{m['name']:<28} | {m.get('params','?'):<8} | {m.get('size_gb',0):.1f} GB | {compat.tier_label_ascii:<16} | {inst}")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
# 3. INFO COMMAND
|
|
190
|
+
if sub in ("info", "inspect", "show"):
|
|
191
|
+
name = args[1] if len(args) > 1 else ""
|
|
192
|
+
if not name:
|
|
193
|
+
print("Usage: cat models info <model_name>")
|
|
194
|
+
return 1
|
|
195
|
+
m = catalog.get_model(name)
|
|
196
|
+
if not m:
|
|
197
|
+
# Fallback to local inspect
|
|
198
|
+
m = {"name": name, "params": "Unknown", "size_gb": 4.0, "family": "unknown", "desc": "Custom model", "context": 8192, "caps": ["chat"]}
|
|
199
|
+
|
|
200
|
+
engine = ModelCompatibilityEngine()
|
|
201
|
+
compat = engine.evaluate(m)
|
|
202
|
+
bench = CATBenchmarkSystem()
|
|
203
|
+
bench_score = bench.get_score_for_model(name)
|
|
204
|
+
|
|
205
|
+
print("=" * 72)
|
|
206
|
+
print(f" MODEL INFO: {m['name']}")
|
|
207
|
+
print("=" * 72)
|
|
208
|
+
print(f" Family : {m.get('family', 'Unknown')}")
|
|
209
|
+
print(f" Parameters : {m.get('params', 'Unknown')}")
|
|
210
|
+
print(f" Disk Size : {m.get('size_gb', 0):.1f} GB")
|
|
211
|
+
print(f" Context Window : {m.get('context', 8192)} tokens")
|
|
212
|
+
print(f" Capabilities : {', '.join(m.get('caps', []))}")
|
|
213
|
+
print(f" Description : {m.get('desc', '')}")
|
|
214
|
+
print("-" * 72)
|
|
215
|
+
print(" DEVICE COMPATIBILITY ANALYSIS:")
|
|
216
|
+
print(f" Rating : {compat.tier_label_ascii}")
|
|
217
|
+
print(f" Total Memory : {compat.total_memory_gb:.1f} GB (Weights: {compat.weights_gb:.1f} GB, KV Cache: {compat.kv_cache_gb:.1f} GB)")
|
|
218
|
+
print(f" VRAM Usage : {compat.vram_used_gb:.1f} GB")
|
|
219
|
+
print(f" RAM Offload : {compat.ram_offload_gb:.1f} GB")
|
|
220
|
+
print(f" Est. Speed : {compat.estimated_tps:.1f} tokens/sec")
|
|
221
|
+
print(f" Summary : {compat.summary}")
|
|
222
|
+
if compat.details:
|
|
223
|
+
for d in compat.details:
|
|
224
|
+
print(f" * {d}")
|
|
225
|
+
print("-" * 72)
|
|
226
|
+
print(" BENCHMARK SCORES & METRICS:")
|
|
227
|
+
if bench_score:
|
|
228
|
+
src = bench_score.source.value if hasattr(bench_score.source, "value") else str(bench_score.source)
|
|
229
|
+
print(f" Source Attribution : [{src}]")
|
|
230
|
+
print(f" Overall Score : {bench_score.overall:5.1f} / 100")
|
|
231
|
+
print(f" Coding Capability : {bench_score.coding:5.1f} / 100")
|
|
232
|
+
print(f" Reasoning & Math : {bench_score.reasoning:5.1f} / 100")
|
|
233
|
+
print(f" Tool Use Execution : {bench_score.tool_use:5.1f} / 100")
|
|
234
|
+
print(f" Agentic Tasks : {bench_score.agent_tasks:5.1f} / 100")
|
|
235
|
+
print(f" Inference Speed : {bench_score.speed:5.1f} / 100")
|
|
236
|
+
print(f" Stability & Context: {bench_score.stability:5.1f} / 100")
|
|
237
|
+
else:
|
|
238
|
+
print(" No CAT benchmarks recorded for this model yet.")
|
|
239
|
+
print(f" Run `cat models benchmark {m['name']}` to benchmark on this machine.")
|
|
240
|
+
print("=" * 72)
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
# 4. INSTALL / PULL COMMAND
|
|
244
|
+
if sub in ("install", "pull", "get", "download"):
|
|
245
|
+
name = args[1] if len(args) > 1 else ""
|
|
246
|
+
if not name:
|
|
247
|
+
print("Usage: cat models install <model_name>")
|
|
248
|
+
return 1
|
|
249
|
+
if not ollama.is_ollama_running():
|
|
250
|
+
print("Error: Local Ollama service is not running. Please start Ollama first.", file=sys.stderr)
|
|
251
|
+
return 1
|
|
252
|
+
print(f"Pulling model '{name}' from Ollama library...")
|
|
253
|
+
ok, msg = ollama.pull_model(name)
|
|
254
|
+
if ok:
|
|
255
|
+
print(f"Successfully installed '{name}'.")
|
|
256
|
+
return 0
|
|
257
|
+
else:
|
|
258
|
+
print(f"Failed to install '{name}': {msg}", file=sys.stderr)
|
|
259
|
+
return 1
|
|
260
|
+
|
|
261
|
+
# 5. REMOVE / DELETE COMMAND
|
|
262
|
+
if sub in ("remove", "delete", "rm"):
|
|
263
|
+
name = args[1] if len(args) > 1 else ""
|
|
264
|
+
if not name:
|
|
265
|
+
print("Usage: cat models remove <model_name>")
|
|
266
|
+
return 1
|
|
267
|
+
if not ollama.is_ollama_running():
|
|
268
|
+
print("Error: Local Ollama service is not running.", file=sys.stderr)
|
|
269
|
+
return 1
|
|
270
|
+
print(f"Removing model '{name}' from local Ollama...")
|
|
271
|
+
ok, msg = ollama.delete_model(name)
|
|
272
|
+
if ok:
|
|
273
|
+
print(f"Successfully removed '{name}'.")
|
|
274
|
+
return 0
|
|
275
|
+
else:
|
|
276
|
+
print(f"Failed to remove '{name}': {msg}", file=sys.stderr)
|
|
277
|
+
return 1
|
|
278
|
+
|
|
279
|
+
# 6. BENCHMARK COMMAND
|
|
280
|
+
if sub in ("benchmark", "bench"):
|
|
281
|
+
bench = CATBenchmarkSystem()
|
|
282
|
+
if len(args) > 1 and not args[1].startswith("-"):
|
|
283
|
+
name = args[1]
|
|
284
|
+
print(f"Running CAT Benchmark Suite on model '{name}'...")
|
|
285
|
+
res = bench.run_full_benchmark(name)
|
|
286
|
+
print(f"\nBenchmark Complete for '{name}':")
|
|
287
|
+
for cat_k, score in res.items():
|
|
288
|
+
print(f" - {cat_k:<16} : {score.score:.1f}/100 [{score.source}]")
|
|
289
|
+
return 0
|
|
290
|
+
else:
|
|
291
|
+
print(bench.format_leaderboard())
|
|
292
|
+
print("\nTo benchmark a specific model: cat models benchmark <name>")
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
# 7. LIST COMMAND (DEFAULT)
|
|
296
|
+
# Parse options: --category, --provider, refresh
|
|
297
|
+
cat_filter = None
|
|
298
|
+
provider_filter = None
|
|
299
|
+
state_filter = None
|
|
300
|
+
refresh_requested = False
|
|
301
|
+
|
|
302
|
+
i = 0
|
|
303
|
+
while i < len(args):
|
|
304
|
+
a = args[i]
|
|
305
|
+
if a in ("--category", "-c") and i + 1 < len(args):
|
|
306
|
+
cat_filter = args[i + 1]
|
|
307
|
+
i += 2
|
|
308
|
+
elif a in ("--provider", "-p") and i + 1 < len(args):
|
|
309
|
+
provider_filter = args[i + 1]
|
|
310
|
+
i += 2
|
|
311
|
+
elif a in ("--new", "-n"):
|
|
312
|
+
state_filter = "new"
|
|
313
|
+
i += 1
|
|
314
|
+
elif a in ("refresh", "--refresh", "-r"):
|
|
315
|
+
refresh_requested = True
|
|
316
|
+
i += 1
|
|
317
|
+
elif a in ("list",):
|
|
318
|
+
i += 1
|
|
319
|
+
else:
|
|
320
|
+
if not cat_filter and a in catalog.categories():
|
|
321
|
+
cat_filter = a
|
|
322
|
+
i += 1
|
|
323
|
+
|
|
324
|
+
if refresh_requested:
|
|
325
|
+
print("Refreshing models from AI providers and local engine...")
|
|
326
|
+
try:
|
|
327
|
+
from .providers.discovery_manager import get_discovery_manager
|
|
328
|
+
dm = get_discovery_manager()
|
|
329
|
+
res = dm.discover_all(timeout=6.0)
|
|
330
|
+
total_discovered = sum(len(m) for m in res.values())
|
|
331
|
+
print(f"Discovery complete: {total_discovered} models found across {len(res)} providers.\n")
|
|
332
|
+
except Exception as e:
|
|
333
|
+
print(f"cat models: refresh note: {e}", file=sys.stderr)
|
|
334
|
+
|
|
335
|
+
# If provider specified, query dynamic registry
|
|
336
|
+
if provider_filter and provider_filter != "ollama":
|
|
337
|
+
try:
|
|
338
|
+
from .models.dynamic_registry import get_dynamic_registry
|
|
339
|
+
reg = get_dynamic_registry()
|
|
340
|
+
models = reg.list_models(provider=provider_filter, state=state_filter)
|
|
341
|
+
print(f"CAT Dynamic Model Registry ({len(models)} models for provider '{provider_filter}'):\n")
|
|
342
|
+
header = f"{'Provider':<14} | {'Model ID':<34} | {'Context':<8} | {'State':<10} | {'Capabilities'}"
|
|
343
|
+
print(header)
|
|
344
|
+
print("-" * len(header))
|
|
345
|
+
for m in models:
|
|
346
|
+
caps = ", ".join(m.capabilities[:4])
|
|
347
|
+
ctx = _format_context(m.context_window)
|
|
348
|
+
print(f"{m.provider:<14} | {m.model_id:<34} | {ctx:<8} | {m.availability_state:<10} | {caps}")
|
|
349
|
+
return 0
|
|
350
|
+
except Exception as e:
|
|
351
|
+
print(f"cat models: failed to query registry: {e}", file=sys.stderr)
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
engine = ModelCompatibilityEngine()
|
|
355
|
+
models_to_display = catalog.search_models(category=cat_filter)
|
|
356
|
+
title_suffix = f" [Category: {cat_filter}]" if cat_filter else ""
|
|
357
|
+
print(f"CAT Local & Curated Models ({len(models_to_display)} models){title_suffix}:\n")
|
|
358
|
+
|
|
359
|
+
header = f"{'Model Name':<28} | {'Params':<8} | {'Size':<8} | {'Compatibility':<16} | {'Status'}"
|
|
360
|
+
print(header)
|
|
361
|
+
print("-" * len(header))
|
|
362
|
+
|
|
363
|
+
for m in models_to_display:
|
|
364
|
+
compat = engine.evaluate(m)
|
|
365
|
+
is_inst = m["name"] in installed_local
|
|
366
|
+
status_str = "INSTALLED" if is_inst else "Available"
|
|
367
|
+
print(f"{m['name']:<28} | {m.get('params','?'):<8} | {m.get('size_gb',0):.1f} GB | {compat.tier_label_ascii:<16} | {status_str}")
|
|
368
|
+
|
|
369
|
+
print("\nCommands:")
|
|
370
|
+
print(" cat models recommend Get hardware-tailored recommendations")
|
|
371
|
+
print(" cat models info <name> Inspect model memory footprint & details")
|
|
372
|
+
print(" cat models install <name> Download model into local Ollama engine")
|
|
373
|
+
print(" cat models benchmark View standard benchmark leaderboard")
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _providers_cli(argv: list[str]) -> int:
|
|
378
|
+
args = argv[1:]
|
|
379
|
+
sub = args[0] if args else "list"
|
|
380
|
+
|
|
381
|
+
if sub in ("--help", "-h", "help"):
|
|
382
|
+
print("Usage: cat providers [command]")
|
|
383
|
+
print("\nCommands:")
|
|
384
|
+
print(" status Real-time provider health, latencies & backup failover pool")
|
|
385
|
+
print(" backup Manage dynamic backup provider pool (list|add|remove|reorder|toggle|test)")
|
|
386
|
+
print(" test [id] Test connectivity, models, and response latency for a provider")
|
|
387
|
+
print(" list (default) List all registered providers and model counts")
|
|
388
|
+
print(" audit Comprehensive audit of providers, model coverage & status")
|
|
389
|
+
print(" refresh Trigger parallel model discovery across all providers")
|
|
390
|
+
return 0
|
|
391
|
+
|
|
392
|
+
if sub in ("status", "health"):
|
|
393
|
+
try:
|
|
394
|
+
from .resilience.health_monitor import get_health_monitor
|
|
395
|
+
from .providers import provider_manager as pm
|
|
396
|
+
from . import aicore
|
|
397
|
+
mon = get_health_monitor()
|
|
398
|
+
prim = aicore.load_config() or {}
|
|
399
|
+
p_prov = prim.get("provider", "Not configured")
|
|
400
|
+
p_model = prim.get("model", "")
|
|
401
|
+
p_badge, p_label = mon.get_status_badge(p_prov, p_model)
|
|
402
|
+
p_health = mon.get_health(p_prov, p_model)
|
|
403
|
+
p_lat = f"{p_health.latency_ms}ms" if p_health.latency_ms else "—"
|
|
404
|
+
|
|
405
|
+
print("=" * 74)
|
|
406
|
+
print(" CAT AI PROVIDER RESILIENCE & HEALTH REPORT")
|
|
407
|
+
print("=" * 74)
|
|
408
|
+
print(" PRIMARY PROVIDER:")
|
|
409
|
+
print(f" {p_badge} {p_prov:<16} model: {p_model:<22} [{p_label}] latency: {p_lat}")
|
|
410
|
+
print("-" * 74)
|
|
411
|
+
print(" BACKUP PROVIDERS (Automatic Failover Pool):")
|
|
412
|
+
backups = pm.load_backup_providers()
|
|
413
|
+
if not backups:
|
|
414
|
+
print(" No backup providers configured. Use `cat providers backup add <provider>`")
|
|
415
|
+
else:
|
|
416
|
+
print(f" {'#':<3} | {'Provider':<14} | {'Model':<22} | {'Health / Status':<16} | {'State'}")
|
|
417
|
+
print(" " + "-" * 68)
|
|
418
|
+
for i, b in enumerate(backups):
|
|
419
|
+
bp = b.get("provider", "?")
|
|
420
|
+
bm = b.get("model", "?")
|
|
421
|
+
glyph, lbl = mon.get_status_badge(bp, bm)
|
|
422
|
+
en = "✓ Enabled" if b.get("enabled", True) else "✗ Disabled"
|
|
423
|
+
b_health = mon.get_health(bp, bm)
|
|
424
|
+
lat_str = f" ({b_health.latency_ms}ms)" if b_health.latency_ms else ""
|
|
425
|
+
stat_str = f"{glyph} {lbl}{lat_str}"
|
|
426
|
+
print(f" {i+1:<3} | {bp:<14} | {bm:<22} | {stat_str:<16} | {en}")
|
|
427
|
+
print("=" * 74)
|
|
428
|
+
print("Commands: cat providers test [id] | cat providers backup [list|add|remove|reorder]")
|
|
429
|
+
return 0
|
|
430
|
+
except Exception as e:
|
|
431
|
+
print(f"cat providers status: error: {e}", file=sys.stderr)
|
|
432
|
+
return 1
|
|
433
|
+
|
|
434
|
+
elif sub in ("test", "check"):
|
|
435
|
+
try:
|
|
436
|
+
from .providers import provider_manager as pm
|
|
437
|
+
target = args[1] if len(args) > 1 else ""
|
|
438
|
+
if not target:
|
|
439
|
+
from . import aicore
|
|
440
|
+
cfg = aicore.load_config() or {}
|
|
441
|
+
target = cfg.get("provider", "")
|
|
442
|
+
model = cfg.get("model", "")
|
|
443
|
+
else:
|
|
444
|
+
target = target.strip().lower()
|
|
445
|
+
# Find configured backup or default
|
|
446
|
+
backups = pm.load_backup_providers()
|
|
447
|
+
matched = next((b for b in backups if b.get("provider", "").lower() == target), None)
|
|
448
|
+
if matched:
|
|
449
|
+
cfg = dict(matched)
|
|
450
|
+
model = cfg.get("model", "")
|
|
451
|
+
else:
|
|
452
|
+
cfg = {"provider": target, "model": "default"}
|
|
453
|
+
model = "default"
|
|
454
|
+
|
|
455
|
+
if not target:
|
|
456
|
+
print("cat providers test: specify a provider (e.g. `cat providers test groq` or `cat providers test ollama`)")
|
|
457
|
+
return 1
|
|
458
|
+
|
|
459
|
+
print(f"Testing provider '{target}' ({model})...")
|
|
460
|
+
ok, msg, models = pm.test_provider_connectivity(cfg)
|
|
461
|
+
status = "READY" if ok else "FAILED"
|
|
462
|
+
glyph = "✓" if ok else "✗"
|
|
463
|
+
print(f" {glyph} Status: {status}")
|
|
464
|
+
print(f" Details: {msg}")
|
|
465
|
+
if models:
|
|
466
|
+
disp_models = models[:6]
|
|
467
|
+
print(f" Models detected: {len(models)} ({', '.join(disp_models)}{'...' if len(models) > 6 else ''})")
|
|
468
|
+
return 0 if ok else 1
|
|
469
|
+
except Exception as e:
|
|
470
|
+
print(f"cat providers test: error: {e}", file=sys.stderr)
|
|
471
|
+
return 1
|
|
472
|
+
|
|
473
|
+
elif sub == "backup":
|
|
474
|
+
try:
|
|
475
|
+
from .providers import provider_manager as pm
|
|
476
|
+
b_cmd = args[1] if len(args) > 1 else "list"
|
|
477
|
+
|
|
478
|
+
if b_cmd in ("list", "ls"):
|
|
479
|
+
backups = pm.load_backup_providers()
|
|
480
|
+
print(f"CAT Backup Provider Pool ({len(backups)} configured):")
|
|
481
|
+
print(f" {'#':<3} | {'Provider':<16} | {'Model':<24} | {'Status':<12} | {'State'}")
|
|
482
|
+
print(" " + "-" * 66)
|
|
483
|
+
for i, b in enumerate(backups):
|
|
484
|
+
bp = b.get("provider", "?")
|
|
485
|
+
bm = b.get("model", "?")
|
|
486
|
+
st = b.get("status", "disconnected")
|
|
487
|
+
en = "Enabled" if b.get("enabled", True) else "Disabled"
|
|
488
|
+
print(f" {i+1:<3} | {bp:<16} | {bm:<24} | {st:<12} | {en}")
|
|
489
|
+
print("\nSubcommands:")
|
|
490
|
+
print(" cat providers backup add <provider> [model]")
|
|
491
|
+
print(" cat providers backup remove <index_or_provider>")
|
|
492
|
+
print(" cat providers backup reorder <from_1_based> <to_1_based>")
|
|
493
|
+
print(" cat providers backup enable <index_or_provider>")
|
|
494
|
+
print(" cat providers backup disable <index_or_provider>")
|
|
495
|
+
print(" cat providers backup test [index_or_provider]")
|
|
496
|
+
return 0
|
|
497
|
+
|
|
498
|
+
elif b_cmd == "add":
|
|
499
|
+
if len(args) < 3:
|
|
500
|
+
print("Usage: cat providers backup add <provider> [model]")
|
|
501
|
+
return 1
|
|
502
|
+
prov = args[2].strip().lower()
|
|
503
|
+
model = args[3].strip() if len(args) > 3 else ("llama3.3" if prov == "ollama" else "default")
|
|
504
|
+
entry = {
|
|
505
|
+
"provider": prov,
|
|
506
|
+
"model": model,
|
|
507
|
+
"enabled": True,
|
|
508
|
+
"api_style": "ollama" if prov == "ollama" else "openai",
|
|
509
|
+
"base_url": "http://localhost:11434" if prov == "ollama" else "",
|
|
510
|
+
}
|
|
511
|
+
ok = pm.add_backup_provider(entry)
|
|
512
|
+
if ok:
|
|
513
|
+
print(f"✓ Added '{prov}' ({model}) to backup pool.")
|
|
514
|
+
return 0
|
|
515
|
+
print(f"✗ Failed to add '{prov}' to backup pool.", file=sys.stderr)
|
|
516
|
+
return 1
|
|
517
|
+
|
|
518
|
+
elif b_cmd in ("remove", "rm", "delete"):
|
|
519
|
+
if len(args) < 3:
|
|
520
|
+
print("Usage: cat providers backup remove <index_or_provider>")
|
|
521
|
+
return 1
|
|
522
|
+
target = args[2].strip()
|
|
523
|
+
if target.isdigit():
|
|
524
|
+
target_idx = int(target) - 1
|
|
525
|
+
ok = pm.remove_backup_provider(target_idx)
|
|
526
|
+
else:
|
|
527
|
+
ok = pm.remove_backup_provider(target)
|
|
528
|
+
if ok:
|
|
529
|
+
print(f"✓ Removed backup provider '{args[2]}'.")
|
|
530
|
+
return 0
|
|
531
|
+
print(f"✗ Backup provider '{args[2]}' not found.", file=sys.stderr)
|
|
532
|
+
return 1
|
|
533
|
+
|
|
534
|
+
elif b_cmd in ("reorder", "move"):
|
|
535
|
+
if len(args) < 4:
|
|
536
|
+
print("Usage: cat providers backup reorder <from_1_based> <to_1_based>")
|
|
537
|
+
return 1
|
|
538
|
+
try:
|
|
539
|
+
f_idx = int(args[2]) - 1
|
|
540
|
+
t_idx = int(args[3]) - 1
|
|
541
|
+
ok = pm.reorder_backup_provider(f_idx, t_idx)
|
|
542
|
+
if ok:
|
|
543
|
+
print(f"✓ Reordered backup priority position {args[2]} → {args[3]}.")
|
|
544
|
+
return 0
|
|
545
|
+
print("✗ Invalid index specified.", file=sys.stderr)
|
|
546
|
+
return 1
|
|
547
|
+
except ValueError:
|
|
548
|
+
print("✗ Indices must be integers.", file=sys.stderr)
|
|
549
|
+
return 1
|
|
550
|
+
|
|
551
|
+
elif b_cmd in ("enable", "disable"):
|
|
552
|
+
if len(args) < 3:
|
|
553
|
+
print(f"Usage: cat providers backup {b_cmd} <index_or_provider>")
|
|
554
|
+
return 1
|
|
555
|
+
target = args[2].strip()
|
|
556
|
+
enabled = (b_cmd == "enable")
|
|
557
|
+
if target.isdigit():
|
|
558
|
+
target_idx = int(target) - 1
|
|
559
|
+
ok = pm.set_backup_provider_enabled(target_idx, enabled)
|
|
560
|
+
else:
|
|
561
|
+
ok = pm.set_backup_provider_enabled(target, enabled)
|
|
562
|
+
if ok:
|
|
563
|
+
print(f"✓ {b_cmd.capitalize()}d backup provider '{args[2]}'.")
|
|
564
|
+
return 0
|
|
565
|
+
print(f"✗ Backup provider '{args[2]}' not found.", file=sys.stderr)
|
|
566
|
+
return 1
|
|
567
|
+
|
|
568
|
+
elif b_cmd == "test":
|
|
569
|
+
target = args[2].strip() if len(args) > 2 else "1"
|
|
570
|
+
backups = pm.load_backup_providers()
|
|
571
|
+
if not backups:
|
|
572
|
+
print("No backup providers configured.")
|
|
573
|
+
return 1
|
|
574
|
+
if target.isdigit():
|
|
575
|
+
idx = int(target) - 1
|
|
576
|
+
entry = backups[idx] if 0 <= idx < len(backups) else None
|
|
577
|
+
else:
|
|
578
|
+
entry = next((b for b in backups if b.get("provider", "").lower() == target.lower()), None)
|
|
579
|
+
if not entry:
|
|
580
|
+
print(f"Backup provider '{target}' not found.", file=sys.stderr)
|
|
581
|
+
return 1
|
|
582
|
+
print(f"Testing backup provider '{entry.get('provider')}' ({entry.get('model')})...")
|
|
583
|
+
ok, msg, models = pm.test_provider_connectivity(entry)
|
|
584
|
+
glyph = "✓" if ok else "✗"
|
|
585
|
+
print(f" {glyph} Status: {'READY' if ok else 'FAILED'}")
|
|
586
|
+
print(f" Details: {msg}")
|
|
587
|
+
return 0 if ok else 1
|
|
588
|
+
|
|
589
|
+
else:
|
|
590
|
+
print(f"cat providers backup: unknown subcommand '{b_cmd}'. Use: list | add | remove | reorder | enable | disable | test")
|
|
591
|
+
return 1
|
|
592
|
+
except Exception as e:
|
|
593
|
+
print(f"cat providers backup: error: {e}", file=sys.stderr)
|
|
594
|
+
return 1
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
from .models.dynamic_registry import get_dynamic_registry
|
|
598
|
+
from .providers.discovery_manager import get_discovery_manager
|
|
599
|
+
registry = get_dynamic_registry()
|
|
600
|
+
dm = get_discovery_manager()
|
|
601
|
+
except Exception as e:
|
|
602
|
+
print(f"cat providers: initialization error: {e}", file=sys.stderr)
|
|
603
|
+
return 1
|
|
604
|
+
|
|
605
|
+
if sub == "audit":
|
|
606
|
+
audit = registry.get_audit_summary()
|
|
607
|
+
status_map = dm.get_all_provider_status()
|
|
608
|
+
print("=" * 72)
|
|
609
|
+
print(" CAT AI PROVIDER & MODEL AUDIT REPORT")
|
|
610
|
+
print("=" * 72)
|
|
611
|
+
print(f" Total Models in Dynamic Registry : {audit.get('total_models', 0)}")
|
|
612
|
+
print(f" Registered Providers : {audit.get('total_providers', 0)}")
|
|
613
|
+
print(f" Verified Active Models : {audit.get('models_by_state', {}).get('available', 0)}")
|
|
614
|
+
print(f" Newly Discovered Models : {audit.get('models_by_state', {}).get('new', 0)}")
|
|
615
|
+
print(f" Deprecated Models : {audit.get('models_by_state', {}).get('deprecated', 0)}")
|
|
616
|
+
print("-" * 72)
|
|
617
|
+
print(f" {'Provider':<18} | {'Status':<10} | {'Models':<8} | {'Sample Flagship Models'}")
|
|
618
|
+
print("-" * 72)
|
|
619
|
+
by_prov = audit.get("models_by_provider", {})
|
|
620
|
+
for pid in sorted(by_prov.keys()):
|
|
621
|
+
cnt = by_prov[pid]
|
|
622
|
+
pstatus = status_map.get(pid, {}).get("status", "registered")
|
|
623
|
+
prov_models = registry.list_models(provider=pid)
|
|
624
|
+
sample_ids = [m.model_id for m in prov_models[:2]]
|
|
625
|
+
sample_str = ", ".join(sample_ids)
|
|
626
|
+
print(f" {pid:<18} | {pstatus:<10} | {cnt:<8} | {sample_str}")
|
|
627
|
+
print("=" * 72)
|
|
628
|
+
return 0
|
|
629
|
+
|
|
630
|
+
elif sub in ("refresh", "discover", "update"):
|
|
631
|
+
print("Initiating dynamic discovery across AI providers...")
|
|
632
|
+
res = dm.discover_all(timeout=6.0)
|
|
633
|
+
print("\nDiscovery Results:")
|
|
634
|
+
print(f" {'Provider':<18} | {'Discovered':<14} | {'Status'}")
|
|
635
|
+
print("-" * 52)
|
|
636
|
+
for pid, models in sorted(res.items()):
|
|
637
|
+
status_desc = f"{len(models)} models" if models else "0 models"
|
|
638
|
+
st = "OK" if models else "Degraded / Offline"
|
|
639
|
+
print(f" {pid:<18} | {status_desc:<14} | {st}")
|
|
640
|
+
total = sum(len(m) for m in res.values())
|
|
641
|
+
print("-" * 52)
|
|
642
|
+
print(f"Total discovered models: {total}.")
|
|
643
|
+
return 0
|
|
644
|
+
|
|
645
|
+
elif sub in ("list", "ls"):
|
|
646
|
+
by_prov = registry.get_audit_summary().get("models_by_provider", {})
|
|
647
|
+
print(f"CAT AI Providers ({len(by_prov)} providers registered):\n")
|
|
648
|
+
print(f" {'Provider ID':<22} | {'Registered Models'}")
|
|
649
|
+
print("-" * 48)
|
|
650
|
+
for pid in sorted(by_prov.keys()):
|
|
651
|
+
print(f" {pid:<22} | {by_prov[pid]} models")
|
|
652
|
+
print(f"\nRun `cat models --provider <id>` to inspect models for a provider.")
|
|
653
|
+
print(f"Run `cat providers status` to check real-time health and backup failover.")
|
|
654
|
+
print(f"Run `cat providers audit` for full diagnostic report.")
|
|
655
|
+
return 0
|
|
656
|
+
|
|
657
|
+
else:
|
|
658
|
+
print(f"cat providers: unknown command '{sub}'. Use: status | backup | test | list | audit | refresh")
|
|
659
|
+
return 1
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _offline_cli(argv: list[str]) -> int:
|
|
663
|
+
"""Switch CAT to Emergency Local Mode."""
|
|
664
|
+
try:
|
|
665
|
+
from .resilience.orchestrator import get_orchestrator
|
|
666
|
+
orch = get_orchestrator()
|
|
667
|
+
ok, msg = orch.enable_emergency_offline_mode()
|
|
668
|
+
glyph = "✓" if ok else "⚠"
|
|
669
|
+
print("=" * 72)
|
|
670
|
+
print(" CAT EMERGENCY LOCAL MODE")
|
|
671
|
+
print("=" * 72)
|
|
672
|
+
print(f" {glyph} {msg}")
|
|
673
|
+
print(" - All third-party cloud AI network queries are strictly disabled.")
|
|
674
|
+
print(" - Local inference will be used as the primary and backup engine.")
|
|
675
|
+
print("=" * 72)
|
|
676
|
+
return 0 if ok else 1
|
|
677
|
+
except Exception as e:
|
|
678
|
+
print(f"cat offline: error: {e}", file=sys.stderr)
|
|
679
|
+
return 1
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def main(argv=None):
|
|
683
|
+
"""Console entry point. Returns the process exit code."""
|
|
684
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
685
|
+
|
|
686
|
+
# CAT's art/UI use Unicode (box drawing, arrows, ...). When stdout is
|
|
687
|
+
# piped or redirected on Windows it defaults to the legacy code page
|
|
688
|
+
# (cp1252) and print() would crash with UnicodeEncodeError mid-boot.
|
|
689
|
+
# Force UTF-8 with lossy fallback; interactive consoles are unaffected.
|
|
690
|
+
for stream in (sys.stdout, sys.stderr):
|
|
691
|
+
try:
|
|
692
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
693
|
+
except Exception:
|
|
694
|
+
pass
|
|
695
|
+
|
|
696
|
+
if argv in (["-h"], ["--help"], ["help"]):
|
|
697
|
+
print(_usage())
|
|
698
|
+
return 0
|
|
699
|
+
|
|
700
|
+
if argv in (["-v"], ["--version"], ["version"]):
|
|
701
|
+
print(f"CAT v{_version()}")
|
|
702
|
+
return 0
|
|
703
|
+
|
|
704
|
+
# Assert CAT CLI terminal tab & window identity
|
|
705
|
+
try:
|
|
706
|
+
from .terminal_identity import init_terminal_identity
|
|
707
|
+
init_terminal_identity()
|
|
708
|
+
except Exception:
|
|
709
|
+
pass
|
|
710
|
+
|
|
711
|
+
if argv in (["--doctor"], ["doctor"]):
|
|
712
|
+
from .doctor import main as _doctor_main
|
|
713
|
+
return _doctor_main()
|
|
714
|
+
|
|
715
|
+
# --- Fomoji auth subcommands (never gated — they ARE the gate) ---
|
|
716
|
+
if argv and argv[0] in ("--auth", "auth"):
|
|
717
|
+
try:
|
|
718
|
+
from .fomoji_auth import _auth_cli
|
|
719
|
+
return _auth_cli(argv)
|
|
720
|
+
except Exception as e:
|
|
721
|
+
print(f"cat: auth error: {e}", file=sys.stderr)
|
|
722
|
+
return 1
|
|
723
|
+
if argv and argv[0] in ("--signout", "--sign-out", "--logout", "signout", "sign-out", "logout"):
|
|
724
|
+
try:
|
|
725
|
+
from .fomoji_auth import logout, get_identity
|
|
726
|
+
ident = get_identity()
|
|
727
|
+
who = ident.get("name") if ident else None
|
|
728
|
+
logout()
|
|
729
|
+
try:
|
|
730
|
+
import urllib.request
|
|
731
|
+
from .fomoji_auth import get_fomoji_url
|
|
732
|
+
base = get_fomoji_url()
|
|
733
|
+
req = urllib.request.Request(base + "/api/logout", data=b"{}", method="POST")
|
|
734
|
+
req.add_header("Content-Type", "application/json")
|
|
735
|
+
urllib.request.urlopen(req, timeout=3)
|
|
736
|
+
except Exception:
|
|
737
|
+
pass
|
|
738
|
+
if who:
|
|
739
|
+
print(f"Signed out — {who} disconnected. Run `cat --auth login` to sign in again.")
|
|
740
|
+
else:
|
|
741
|
+
print("Signed out — no active session.")
|
|
742
|
+
return 0
|
|
743
|
+
except Exception as e:
|
|
744
|
+
print(f"cat: signout error: {e}", file=sys.stderr)
|
|
745
|
+
return 1
|
|
746
|
+
# --- CAT Vision (spec 40: cat vision / cat vision start etc) ----
|
|
747
|
+
if argv and argv[0] == "vision":
|
|
748
|
+
sub = argv[1] if len(argv) > 1 else "status"
|
|
749
|
+
if sub in ("status", "start", "stop", "pause", "resume", "analyze", "screenshot", "clear", "help"):
|
|
750
|
+
# Route through the shared VisionSessionManager logic, not a duplicate.
|
|
751
|
+
try:
|
|
752
|
+
from calc_terminal.vision.session import VisionSessionManager
|
|
753
|
+
from calc_terminal.vision.capture import CaptureConfig
|
|
754
|
+
import asyncio
|
|
755
|
+
mgr = VisionSessionManager()
|
|
756
|
+
print(f"CAT Vision: {sub} — launching CAT to handle vision session...")
|
|
757
|
+
print("Open CAT and use the CAT Vision panel (eye icon in main menu) for full screen sharing + annotation.")
|
|
758
|
+
print("Browser/PWA: open FATTY CAT at http://localhost:8765 for live capture.")
|
|
759
|
+
if sub == "help":
|
|
760
|
+
print("\n cat vision start Start capture (browser will prompt)")
|
|
761
|
+
print(" cat vision stop Stop sharing")
|
|
762
|
+
print(" cat vision pause Pause capture")
|
|
763
|
+
print(" cat vision analyze Analyze current screen")
|
|
764
|
+
print(" cat vision clear Clear vision session")
|
|
765
|
+
return 0
|
|
766
|
+
except Exception as e:
|
|
767
|
+
print(f"cat vision {sub} failed: {e}", file=sys.stderr)
|
|
768
|
+
return 1
|
|
769
|
+
print(f"cat: unknown vision subcommand: {sub} (try: cat vision help)")
|
|
770
|
+
return 2
|
|
771
|
+
|
|
772
|
+
# --- Fatty CAT Web / PWA Server & Launcher ---------------------------
|
|
773
|
+
# `cat fatty` / `cat web` / `cat server` / `cat --fatty` / `cat --web`
|
|
774
|
+
if argv and argv[0] in ("fatty", "--fatty", "web", "--web", "pwa", "--pwa", "server", "--server"):
|
|
775
|
+
cmd = argv[0].lstrip("-")
|
|
776
|
+
try:
|
|
777
|
+
from .web.server import ensure_fatty_server, is_server_running, main as _server_main
|
|
778
|
+
except ImportError as e:
|
|
779
|
+
print("cat: the Fatty CAT web server needs the optional `web` extra.", file=sys.stderr)
|
|
780
|
+
print(f" (missing: {e})", file=sys.stderr)
|
|
781
|
+
print(" Install it with: pip install \"cct-cli[web]\"", file=sys.stderr)
|
|
782
|
+
return 1
|
|
783
|
+
if cmd in ("server",):
|
|
784
|
+
print("Starting Fatty CAT server on http://localhost:8765 ... (Press Ctrl+C to stop)")
|
|
785
|
+
try:
|
|
786
|
+
_server_main()
|
|
787
|
+
return 0
|
|
788
|
+
except KeyboardInterrupt:
|
|
789
|
+
print("\nFatty CAT server stopped.")
|
|
790
|
+
return 0
|
|
791
|
+
except Exception as e:
|
|
792
|
+
print(f"cat server error: {e}", file=sys.stderr)
|
|
793
|
+
return 1
|
|
794
|
+
|
|
795
|
+
# `cat fatty` or `cat web`: ensure server is running, then open browser
|
|
796
|
+
print("Launching Fatty CAT at http://localhost:8765 ...")
|
|
797
|
+
if not is_server_running():
|
|
798
|
+
print("Starting background Fatty CAT server...")
|
|
799
|
+
if not ensure_fatty_server(timeout=10.0, auto_start=True):
|
|
800
|
+
print("cat: failed to start Fatty CAT server on port 8765", file=sys.stderr)
|
|
801
|
+
return 1
|
|
802
|
+
print("✓ Fatty CAT server is running at http://localhost:8765/")
|
|
803
|
+
else:
|
|
804
|
+
print("✓ Fatty CAT server is already active at http://localhost:8765/")
|
|
805
|
+
|
|
806
|
+
# Try to launch inside CAT's own browser first
|
|
807
|
+
try:
|
|
808
|
+
from .host.launcher import launch_cat_host, can_launch_host
|
|
809
|
+
if can_launch_host():
|
|
810
|
+
return launch_cat_host(
|
|
811
|
+
start_browser_url="http://localhost:8765/",
|
|
812
|
+
start_mode="browser",
|
|
813
|
+
)
|
|
814
|
+
except SystemExit:
|
|
815
|
+
raise
|
|
816
|
+
except Exception:
|
|
817
|
+
pass
|
|
818
|
+
|
|
819
|
+
# Fallback to system web browser
|
|
820
|
+
import webbrowser
|
|
821
|
+
webbrowser.open("http://localhost:8765/")
|
|
822
|
+
print("Opened http://localhost:8765/ in your default browser.")
|
|
823
|
+
return 0
|
|
824
|
+
|
|
825
|
+
# --- CAT Browser (real Qt WebEngine / Chromium) ----------------------
|
|
826
|
+
# `cat browse [url]` / `cat --browser [url]` — opens CAT's real browser
|
|
827
|
+
# using QWebEngineView (Chromium via Qt WebEngine). Real HTML/CSS/JS.
|
|
828
|
+
if argv and argv[0] in ("browse", "--browse", "--browser", "browser", "--host", "--host-browser", "host", "/cat", "cat-browser"):
|
|
829
|
+
_browse_args = argv[1:]
|
|
830
|
+
url = _browse_args[0] if _browse_args else None
|
|
831
|
+
if url and url.lower() in ("fatty", "--fatty", "web", "--web"):
|
|
832
|
+
url = "http://localhost:8765/"
|
|
833
|
+
if url and url.startswith("-"):
|
|
834
|
+
print(_usage())
|
|
835
|
+
print(f"cat: error: unexpected argument for browse: {url}")
|
|
836
|
+
return 2
|
|
837
|
+
# Try the real browser first (Qt WebEngine / Chromium)
|
|
838
|
+
try:
|
|
839
|
+
from .host.launcher import launch_cat_host, can_launch_host
|
|
840
|
+
if can_launch_host():
|
|
841
|
+
return launch_cat_host(
|
|
842
|
+
start_browser_url=url,
|
|
843
|
+
start_mode="browser" if url else "terminal",
|
|
844
|
+
)
|
|
845
|
+
except SystemExit:
|
|
846
|
+
raise
|
|
847
|
+
except Exception:
|
|
848
|
+
pass
|
|
849
|
+
# Fallback: standalone Qt browser window
|
|
850
|
+
try:
|
|
851
|
+
from .browser_gui.qt_browser import launch_qt_browser
|
|
852
|
+
return launch_qt_browser(start_url=url)
|
|
853
|
+
except Exception:
|
|
854
|
+
pass
|
|
855
|
+
print("[CAT Browser] No browser engine found.", file=sys.stderr)
|
|
856
|
+
print("Install PySide6 for real HTML/CSS/JS rendering in CAT Browser:", file=sys.stderr)
|
|
857
|
+
print(" pip install PySide6", file=sys.stderr)
|
|
858
|
+
return 1
|
|
859
|
+
|
|
860
|
+
# --- Dynamic Model & Provider CLI (v2026.09) -------------------------
|
|
861
|
+
if argv and argv[0] in ("models", "--models", "model", "--model"):
|
|
862
|
+
return _models_cli(argv)
|
|
863
|
+
|
|
864
|
+
if argv and argv[0] in ("providers", "--providers", "provider", "--provider"):
|
|
865
|
+
return _providers_cli(argv)
|
|
866
|
+
|
|
867
|
+
if argv and argv[0] in ("offline", "--offline", "local-only", "--local-only"):
|
|
868
|
+
return _offline_cli(argv)
|
|
869
|
+
|
|
870
|
+
# Optional single positional: the workspace/project directory.
|
|
871
|
+
path = None
|
|
872
|
+
rest = []
|
|
873
|
+
for arg in argv:
|
|
874
|
+
if arg.startswith("-") and arg not in ("-",):
|
|
875
|
+
print(_usage())
|
|
876
|
+
print(f"cat: error: unrecognized argument: {arg}")
|
|
877
|
+
return 2
|
|
878
|
+
if path is None:
|
|
879
|
+
path = arg
|
|
880
|
+
else:
|
|
881
|
+
rest.append(arg)
|
|
882
|
+
if rest:
|
|
883
|
+
print(_usage())
|
|
884
|
+
print(f"cat: error: too many arguments: {' '.join(rest)}")
|
|
885
|
+
return 2
|
|
886
|
+
|
|
887
|
+
if path is not None:
|
|
888
|
+
# `cct .` means "stay in the current directory"; any other path
|
|
889
|
+
# must exist and be a directory before we change into it, so a
|
|
890
|
+
# typo fails with a clear message instead of launching CCT in
|
|
891
|
+
# whatever directory the shell happened to be in.
|
|
892
|
+
expanded = os.path.abspath(os.path.expanduser(path))
|
|
893
|
+
if not os.path.isdir(expanded):
|
|
894
|
+
print(f"cat: error: not a directory: {expanded}", file=sys.stderr)
|
|
895
|
+
return 2
|
|
896
|
+
try:
|
|
897
|
+
os.chdir(expanded)
|
|
898
|
+
except OSError as e:
|
|
899
|
+
print(f"cat: error: cannot open workspace {expanded}: {e}", file=sys.stderr)
|
|
900
|
+
return 2
|
|
901
|
+
|
|
902
|
+
# First-run detection: on the very first launch only, prepare the
|
|
903
|
+
# application-data directory and run model.py's one-time
|
|
904
|
+
# initialization before the main application starts. If that fails
|
|
905
|
+
# nothing is marked as done, the error is reported, and the next
|
|
906
|
+
# `cct` retries. Every later launch skips straight to the app.
|
|
907
|
+
from .first_run import ensure_first_run
|
|
908
|
+
rc = ensure_first_run()
|
|
909
|
+
if rc != 0:
|
|
910
|
+
return rc
|
|
911
|
+
|
|
912
|
+
return bootstrap()
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def _launch_cat_terminal():
|
|
916
|
+
"""Launch the CAT Textual TUI (CCTApp). This is the main CAT experience:
|
|
917
|
+
model.py setup wizard, /ai chat, /agent notebook, etc."""
|
|
918
|
+
try:
|
|
919
|
+
from .terminal_identity import init_terminal_identity
|
|
920
|
+
init_terminal_identity()
|
|
921
|
+
except Exception:
|
|
922
|
+
pass
|
|
923
|
+
from .app import App
|
|
924
|
+
from .ui.app import launch_chat_app
|
|
925
|
+
repl = App()
|
|
926
|
+
config = {}
|
|
927
|
+
try:
|
|
928
|
+
from . import aicore
|
|
929
|
+
config = aicore.load_config()
|
|
930
|
+
except Exception:
|
|
931
|
+
pass
|
|
932
|
+
stats = {
|
|
933
|
+
"solved": len(repl.history),
|
|
934
|
+
"provider": config.get("provider") or "none",
|
|
935
|
+
"model": config.get("model") or "none",
|
|
936
|
+
}
|
|
937
|
+
ok, msg = launch_chat_app(repl, repl.history, stats)
|
|
938
|
+
if not ok:
|
|
939
|
+
try:
|
|
940
|
+
from . import theme
|
|
941
|
+
theme.enable_windows_ansi()
|
|
942
|
+
print(theme.orange(f" {msg}"))
|
|
943
|
+
except Exception:
|
|
944
|
+
print(f" {msg}")
|
|
945
|
+
print(" Falling back to terminal REPL...")
|
|
946
|
+
repl.run()
|
|
947
|
+
return 0
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def bootstrap():
|
|
951
|
+
"""CAT entry point - handles Fomoji auth, then launches the CAT terminal.
|
|
952
|
+
|
|
953
|
+
Flow:
|
|
954
|
+
1. Ensure Fomoji server is running (auto-start if needed)
|
|
955
|
+
2. Check auth status (fast, reads local file)
|
|
956
|
+
3. If already authenticated -> launch CAT terminal directly
|
|
957
|
+
4. If not authenticated:
|
|
958
|
+
a. Start device flow, get code + notification
|
|
959
|
+
b. Open browser to Fomoji connector page (y/n permission)
|
|
960
|
+
c. Poll for approval in BACKGROUND THREAD
|
|
961
|
+
d. After approval -> verify session -> launch CAT terminal
|
|
962
|
+
"""
|
|
963
|
+
from .fomoji_auth import (
|
|
964
|
+
ensure_fomoji_server, status, get_identity,
|
|
965
|
+
check_server_reachable, get_fomoji_url,
|
|
966
|
+
_device_start, _device_poll, _save_local, _print_device_prompt,
|
|
967
|
+
DEFAULT_PERMISSIONS,
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
# Ensure server is running first
|
|
971
|
+
server_ok = ensure_fomoji_server(auto_start=True, timeout=15)
|
|
972
|
+
if not server_ok and not check_server_reachable(timeout=5):
|
|
973
|
+
print("\n Fomoji server could not be started.", file=sys.stderr)
|
|
974
|
+
print(" Start it manually: cd fomoji-updated/fomoji-server && npm start\n",
|
|
975
|
+
file=sys.stderr)
|
|
976
|
+
return 1
|
|
977
|
+
|
|
978
|
+
# Check if user is already authenticated
|
|
979
|
+
auth_status = status()
|
|
980
|
+
|
|
981
|
+
if auth_status == "connected":
|
|
982
|
+
# Already authenticated - launch CAT terminal directly
|
|
983
|
+
try:
|
|
984
|
+
from . import theme
|
|
985
|
+
theme.enable_windows_ansi()
|
|
986
|
+
ident = get_identity()
|
|
987
|
+
name = ident.get("name", "?") if ident else "?"
|
|
988
|
+
fid = ident.get("fomojiId", "?") if ident else "?"
|
|
989
|
+
print(theme.green(f"\n ✓ Already authenticated as {name} ({fid})", bold=True))
|
|
990
|
+
print(theme.dim(" Launching CAT CLI..."))
|
|
991
|
+
print()
|
|
992
|
+
except Exception:
|
|
993
|
+
print("\n Already authenticated. Launching CAT CLI...\n")
|
|
994
|
+
return _launch_cat_terminal()
|
|
995
|
+
|
|
996
|
+
if auth_status == "expired":
|
|
997
|
+
print("\n Your Fomoji session has expired. Re-authenticating...\n")
|
|
998
|
+
|
|
999
|
+
# --- Device flow for non-authenticated users ---
|
|
1000
|
+
import threading
|
|
1001
|
+
import time as _time
|
|
1002
|
+
|
|
1003
|
+
# Start device flow
|
|
1004
|
+
try:
|
|
1005
|
+
perms = DEFAULT_PERMISSIONS
|
|
1006
|
+
start = _device_start(perms)
|
|
1007
|
+
device_code = start["deviceCode"]
|
|
1008
|
+
user_code = start["userCode"]
|
|
1009
|
+
verification_url = f"{get_fomoji_url()}{start.get('verificationUrl', '/connector.html')}"
|
|
1010
|
+
poll_interval = int(start.get("pollIntervalSeconds", 3))
|
|
1011
|
+
except Exception as e:
|
|
1012
|
+
print(f"\n Failed to start device flow: {e}", file=sys.stderr)
|
|
1013
|
+
print(" Make sure the Fomoji server is running.\n", file=sys.stderr)
|
|
1014
|
+
return 1
|
|
1015
|
+
|
|
1016
|
+
# Show code in terminal + Windows system notification + copy to clipboard
|
|
1017
|
+
_print_device_prompt(user_code, verification_url)
|
|
1018
|
+
|
|
1019
|
+
# Shared state for background polling
|
|
1020
|
+
auth_result = {"identity": None, "error": None, "done": False}
|
|
1021
|
+
_close_requested = threading.Event()
|
|
1022
|
+
|
|
1023
|
+
def _poll_for_approval():
|
|
1024
|
+
deadline = _time.monotonic() + 300
|
|
1025
|
+
while _time.monotonic() < deadline:
|
|
1026
|
+
try:
|
|
1027
|
+
result = _device_poll(device_code)
|
|
1028
|
+
st = result.get("status")
|
|
1029
|
+
if st == "approved":
|
|
1030
|
+
identity = {
|
|
1031
|
+
"fomojiId": result.get("fomojiId"),
|
|
1032
|
+
"name": result.get("name"),
|
|
1033
|
+
"identityType": result.get("identityType", "PERSON"),
|
|
1034
|
+
"permissions": result.get("permissions", perms),
|
|
1035
|
+
"applicationId": result.get("applicationId", "cat"),
|
|
1036
|
+
}
|
|
1037
|
+
_save_local({"token": result["connectorToken"], "identity": identity})
|
|
1038
|
+
auth_result["identity"] = identity
|
|
1039
|
+
auth_result["done"] = True
|
|
1040
|
+
_close_requested.set()
|
|
1041
|
+
return
|
|
1042
|
+
elif st in ("denied", "expired"):
|
|
1043
|
+
auth_result["error"] = f"Connection request {st}."
|
|
1044
|
+
auth_result["done"] = True
|
|
1045
|
+
return
|
|
1046
|
+
except Exception:
|
|
1047
|
+
pass
|
|
1048
|
+
_time.sleep(poll_interval)
|
|
1049
|
+
auth_result["error"] = "Timed out waiting for approval."
|
|
1050
|
+
auth_result["done"] = True
|
|
1051
|
+
|
|
1052
|
+
# Ask user if they want to open the browser
|
|
1053
|
+
open_browser = True
|
|
1054
|
+
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
1055
|
+
try:
|
|
1056
|
+
answer = input("\n Open browser now? (y/n): ").strip().lower()
|
|
1057
|
+
except (EOFError, KeyboardInterrupt):
|
|
1058
|
+
answer = "n"
|
|
1059
|
+
if answer != "y":
|
|
1060
|
+
open_browser = False
|
|
1061
|
+
print("\n Skipping browser. You can open it manually later.")
|
|
1062
|
+
print(f" Verification URL: {verification_url}")
|
|
1063
|
+
print(f" Your code: {user_code}\n")
|
|
1064
|
+
|
|
1065
|
+
if not open_browser:
|
|
1066
|
+
# Poll without a browser window
|
|
1067
|
+
poll_thread = threading.Thread(target=_poll_for_approval, daemon=True)
|
|
1068
|
+
poll_thread.start()
|
|
1069
|
+
try:
|
|
1070
|
+
while not auth_result["done"]:
|
|
1071
|
+
_time.sleep(1)
|
|
1072
|
+
except KeyboardInterrupt:
|
|
1073
|
+
print("\n Login cancelled.\n")
|
|
1074
|
+
return 1
|
|
1075
|
+
if auth_result["identity"]:
|
|
1076
|
+
name = auth_result["identity"].get("name", "?")
|
|
1077
|
+
print(f"\n ✓ Authenticated as {name}")
|
|
1078
|
+
return _launch_cat_terminal()
|
|
1079
|
+
elif auth_result["error"]:
|
|
1080
|
+
print(f"\n {auth_result['error']}\n")
|
|
1081
|
+
return 1
|
|
1082
|
+
return 1
|
|
1083
|
+
|
|
1084
|
+
# Open browser for verification
|
|
1085
|
+
from .host.launcher import create_browser_window
|
|
1086
|
+
_time.sleep(1.5)
|
|
1087
|
+
|
|
1088
|
+
app, window, _ = create_browser_window(
|
|
1089
|
+
start_browser_url=verification_url,
|
|
1090
|
+
start_mode="browser",
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
def _close_browser():
|
|
1094
|
+
try:
|
|
1095
|
+
for w in app.topLevelWidgets():
|
|
1096
|
+
if hasattr(w, 'close'):
|
|
1097
|
+
w.close()
|
|
1098
|
+
return
|
|
1099
|
+
except Exception:
|
|
1100
|
+
pass
|
|
1101
|
+
|
|
1102
|
+
# Timer on the main thread that polls the flag and closes when set
|
|
1103
|
+
try:
|
|
1104
|
+
from PySide6.QtCore import QTimer as _QTimer
|
|
1105
|
+
_close_timer = _QTimer()
|
|
1106
|
+
_close_timer.setInterval(200)
|
|
1107
|
+
_close_timer.timeout.connect(lambda: _close_browser() if _close_requested.is_set() else None)
|
|
1108
|
+
_close_timer.start()
|
|
1109
|
+
except Exception:
|
|
1110
|
+
try:
|
|
1111
|
+
from PyQt6.QtCore import QTimer as _QTimer
|
|
1112
|
+
_close_timer = _QTimer()
|
|
1113
|
+
_close_timer.setInterval(200)
|
|
1114
|
+
_close_timer.timeout.connect(lambda: _close_browser() if _close_requested.is_set() else None)
|
|
1115
|
+
_close_timer.start()
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1118
|
+
|
|
1119
|
+
# Poll in background thread
|
|
1120
|
+
poll_thread = threading.Thread(target=_poll_for_approval, daemon=True)
|
|
1121
|
+
poll_thread.start()
|
|
1122
|
+
|
|
1123
|
+
# Start Qt event loop - BLOCKS until browser window closed (after approval)
|
|
1124
|
+
app.exec() # type: ignore
|
|
1125
|
+
|
|
1126
|
+
# After authentication, verify session and launch CAT terminal
|
|
1127
|
+
if auth_result["identity"]:
|
|
1128
|
+
name = auth_result["identity"].get("name", "?")
|
|
1129
|
+
try:
|
|
1130
|
+
from . import theme
|
|
1131
|
+
theme.enable_windows_ansi()
|
|
1132
|
+
print(theme.green(f"\n ✓ Authenticated as {name}", bold=True))
|
|
1133
|
+
print(theme.dim(" Verifying session..."))
|
|
1134
|
+
except Exception:
|
|
1135
|
+
print(f"\n ✓ Authenticated as {name}")
|
|
1136
|
+
|
|
1137
|
+
# Verify the session is valid
|
|
1138
|
+
try:
|
|
1139
|
+
if status() == "connected":
|
|
1140
|
+
try:
|
|
1141
|
+
from . import theme
|
|
1142
|
+
theme.enable_windows_ansi()
|
|
1143
|
+
print(theme.green(" ✓ Session verified!", bold=True))
|
|
1144
|
+
print(theme.green(" Launching CAT CLI...\n", bold=True))
|
|
1145
|
+
except Exception:
|
|
1146
|
+
print(" ✓ Session verified! Launching CAT CLI...\n")
|
|
1147
|
+
return _launch_cat_terminal()
|
|
1148
|
+
else:
|
|
1149
|
+
print(" ✗ Session verification failed. Run `cat` again.\n")
|
|
1150
|
+
except Exception:
|
|
1151
|
+
print(" Launching CAT CLI...\n")
|
|
1152
|
+
return _launch_cat_terminal()
|
|
1153
|
+
|
|
1154
|
+
if auth_result["error"]:
|
|
1155
|
+
print(f"\n {auth_result['error']}\n")
|
|
1156
|
+
return 1
|
|
1157
|
+
|
|
1158
|
+
return 0
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
_LOG_FILE = os.path.join(os.path.expanduser("~"), ".cct_startup.log")
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def _log_startup_failure(what, detail):
|
|
1165
|
+
"""Append one clearly-marked startup-failure record to
|
|
1166
|
+
~/.cct_startup.log for debugging (requirement #8: never silent)."""
|
|
1167
|
+
try:
|
|
1168
|
+
import datetime
|
|
1169
|
+
stamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1170
|
+
with open(_LOG_FILE, "a", encoding="utf-8") as f:
|
|
1171
|
+
f.write(f"[{stamp}] {what}: {detail}\n")
|
|
1172
|
+
except Exception:
|
|
1173
|
+
pass
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
# Back-compat alias — earlier builds called this from main(); both names
|
|
1177
|
+
# now refer to the same centralized bootstrap.
|
|
1178
|
+
_launch = bootstrap
|