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.
Files changed (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. 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