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
@@ -0,0 +1,233 @@
1
+ """Background Ollama download manager with progress streaming.
2
+ Uses Ollama's POST /api/pull streaming JSON lines.
3
+ """
4
+ import json
5
+ import time
6
+ import threading
7
+ import urllib.request
8
+ import urllib.error
9
+ from typing import Optional, Dict, Any, Tuple
10
+
11
+ def ollama_base_url():
12
+ try:
13
+ from . import config as _cfg
14
+ return _cfg.load_config().ollama_endpoint.rstrip("/")
15
+ except Exception:
16
+ return "http://localhost:11434"
17
+
18
+ def is_ollama_installed():
19
+ import shutil
20
+ return bool(shutil.which("ollama"))
21
+
22
+ def is_ollama_running(base_url=None, timeout=3):
23
+ import urllib.request
24
+ base = (base_url or ollama_base_url()).rstrip("/")
25
+ try:
26
+ with urllib.request.urlopen(f"{base}/api/tags", timeout=timeout) as r:
27
+ return r.status in (200, 204)
28
+ except Exception:
29
+ return False
30
+
31
+ def ensure_ollama_running(base_url=None, timeout=12, auto_start=True):
32
+ """Ensure Ollama is reachable; if not and auto_start, try `ollama serve`."""
33
+ if is_ollama_running(base_url, timeout=3):
34
+ return True, "already running"
35
+ if not auto_start:
36
+ return False, "not running"
37
+ if not is_ollama_installed():
38
+ return False, "Ollama not installed — get it from https://ollama.com/download"
39
+ # Try to start ollama serve as background process
40
+ try:
41
+ import subprocess, shutil, sys, os
42
+ ollama_bin = shutil.which("ollama")
43
+ if not ollama_bin:
44
+ return False, "ollama binary not found"
45
+ # Use CREATE_NO_WINDOW on Windows to avoid popup
46
+ kwargs = {}
47
+ if sys.platform == "win32":
48
+ try:
49
+ import subprocess as sp
50
+ kwargs["creationflags"] = getattr(sp, "CREATE_NO_WINDOW", 0)
51
+ si = sp.STARTUPINFO()
52
+ si.dwFlags |= sp.STARTF_USESHOWWINDOW
53
+ kwargs["startupinfo"] = si
54
+ except Exception:
55
+ pass
56
+ else:
57
+ kwargs["start_new_session"] = True
58
+ # Start serve detached
59
+ try:
60
+ subprocess.Popen([ollama_bin, "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs)
61
+ except Exception as e:
62
+ return False, f"failed to start: {e}"
63
+ # Wait for it to be reachable
64
+ import time
65
+ for _ in range(timeout):
66
+ time.sleep(1)
67
+ if is_ollama_running(base_url, timeout=2):
68
+ return True, "started"
69
+ return False, "started but not reachable yet — wait a moment and retry"
70
+ except Exception as e:
71
+ return False, str(e)
72
+
73
+ def installed_models(base_url=None):
74
+ """Return set of installed model names via /api/tags."""
75
+ base = (base_url or ollama_base_url()).rstrip("/")
76
+ try:
77
+ with urllib.request.urlopen(f"{base}/api/tags", timeout=10) as r:
78
+ data = json.loads(r.read().decode("utf-8"))
79
+ return {m.get("name","") for m in data.get("models",[]) if m.get("name")}
80
+ except Exception:
81
+ return set()
82
+
83
+ def pull_model(model_name, on_progress=None, on_done=None, base_url=None):
84
+ """
85
+ Background pull. Calls on_progress dict with keys:
86
+ status, completed, total, percent, speed, eta_seconds
87
+ Streaming is done on a daemon thread.
88
+ Returns thread.
89
+ """
90
+ base = (base_url or ollama_base_url()).rstrip("/")
91
+ url = f"{base}/api/pull"
92
+
93
+ def _worker():
94
+ payload = json.dumps({"name": model_name, "stream": True}).encode("utf-8")
95
+ req = urllib.request.Request(url, data=payload, method="POST")
96
+ req.add_header("Content-Type","application/json")
97
+ start = time.time()
98
+ last_completed = 0
99
+ last_time = start
100
+ try:
101
+ with urllib.request.urlopen(req, timeout=600) as resp:
102
+ # Ollama streams JSON lines: {"status":"pulling manifest", "completed":..., "total":...}
103
+ buf = b""
104
+ for chunk in iter(lambda: resp.read(8192), b""):
105
+ if not chunk:
106
+ break
107
+ buf += chunk
108
+ while b"\n" in buf:
109
+ line, buf = buf.split(b"\n",1)
110
+ line=line.strip()
111
+ if not line:
112
+ continue
113
+ try:
114
+ js = json.loads(line.decode("utf-8"))
115
+ except Exception:
116
+ continue
117
+ status = js.get("status","")
118
+ completed = js.get("completed")
119
+ total = js.get("total")
120
+ percent = None
121
+ if isinstance(completed,int) and isinstance(total,int) and total>0:
122
+ percent = int(completed*100/total)
123
+ # speed / eta
124
+ now = time.time()
125
+ dt = now - last_time
126
+ speed = None
127
+ eta = None
128
+ if isinstance(completed,int) and dt>0.5:
129
+ delta = completed - last_completed
130
+ if delta>0:
131
+ speed = delta/dt # bytes/s
132
+ if isinstance(total,int) and total>completed and speed>0:
133
+ eta = int((total - completed)/speed)
134
+ last_completed = completed if isinstance(completed,int) else last_completed
135
+ last_time = now
136
+ prog = {"status": status, "completed": completed, "total": total, "percent": percent, "speed": speed, "eta": eta, "raw": js}
137
+ if on_progress:
138
+ try:
139
+ on_progress(prog)
140
+ except Exception:
141
+ pass
142
+ if status and "success" in status.lower():
143
+ break
144
+ if on_done:
145
+ try:
146
+ on_done(True, None)
147
+ except Exception:
148
+ pass
149
+ except urllib.error.HTTPError as e:
150
+ msg = f"HTTP {e.code}"
151
+ try:
152
+ body = e.read().decode("utf-8")
153
+ j = json.loads(body)
154
+ msg = j.get("error", msg)
155
+ except Exception:
156
+ pass
157
+ if on_done:
158
+ try:
159
+ on_done(False, msg)
160
+ except Exception:
161
+ pass
162
+ except Exception as e:
163
+ if on_done:
164
+ try:
165
+ on_done(False, str(e))
166
+ except Exception:
167
+ pass
168
+
169
+ th = threading.Thread(target=_worker, daemon=True, name=f"ollama-pull-{model_name}")
170
+ th.start()
171
+ return th
172
+
173
+ def estimate_minutes(size_gb, speed_mbps=50):
174
+ """Very rough estimate: size_gb at speed_mbps megabits."""
175
+ try:
176
+ # speed in MB/s
177
+ mbps = speed_mbps # megabits
178
+ mbytes = float(size_gb)*1024
179
+ # assume 50 Mbps ~ 6.25 MB/s
180
+ secs = mbytes / (mbps/8) if mbps>0 else 0
181
+ return max(1, int(secs/60))
182
+ except Exception:
183
+ return 5
184
+
185
+ def pull_model_sync(model_name, base_url=None):
186
+ """Blocking pull for CLI, returns (ok, msg)."""
187
+ done = {}
188
+ def _done(ok, msg):
189
+ done["ok"]=ok
190
+ done["msg"]=msg
191
+ th = pull_model(model_name, base_url=base_url, on_done=lambda ok,msg: done.update(ok=ok,msg=msg))
192
+ th.join(timeout=600)
193
+ return done.get("ok", False), done.get("msg")
194
+
195
+
196
+ def delete_model(model_name: str, base_url=None) -> tuple[bool, Optional[str]]:
197
+ """Delete a model locally via Ollama DELETE /api/delete."""
198
+ base = (base_url or ollama_base_url()).rstrip("/")
199
+ url = f"{base}/api/delete"
200
+ payload = json.dumps({"name": model_name}).encode("utf-8")
201
+ req = urllib.request.Request(url, data=payload, method="DELETE")
202
+ req.add_header("Content-Type", "application/json")
203
+ try:
204
+ with urllib.request.urlopen(req, timeout=15) as resp:
205
+ if resp.status in (200, 204):
206
+ return True, None
207
+ return False, f"HTTP {resp.status}"
208
+ except urllib.error.HTTPError as e:
209
+ msg = f"HTTP {e.code}"
210
+ try:
211
+ body = e.read().decode("utf-8")
212
+ j = json.loads(body)
213
+ msg = j.get("error", msg)
214
+ except Exception:
215
+ pass
216
+ return False, msg
217
+ except Exception as e:
218
+ return False, str(e)
219
+
220
+
221
+ def inspect_model(model_name: str, base_url=None) -> Optional[dict]:
222
+ """Retrieve detailed model metadata via Ollama POST /api/show."""
223
+ base = (base_url or ollama_base_url()).rstrip("/")
224
+ url = f"{base}/api/show"
225
+ payload = json.dumps({"name": model_name}).encode("utf-8")
226
+ req = urllib.request.Request(url, data=payload, method="POST")
227
+ req.add_header("Content-Type", "application/json")
228
+ try:
229
+ with urllib.request.urlopen(req, timeout=10) as resp:
230
+ return json.loads(resp.read().decode("utf-8"))
231
+ except Exception:
232
+ return None
233
+
@@ -0,0 +1,304 @@
1
+ """
2
+ CCT — orchestrator.py: Agent Orchestrator (v0.7.7 spec section 9).
3
+
4
+ A Coordinator assigns work to specialist agents, and the Coordinator
5
+ continuously monitors them. This layers on top of agent.py's existing
6
+ multi-agent team (researcher/planner/specialist/programmer/debugger/
7
+ tester — see the "Multi-agent collaboration" section of agent.py) the
8
+ spec's remaining specialist roles:
9
+
10
+ Coordinator — decides the roster, watches every stage, reports
11
+ Planner — breaks the request into ordered sub-tasks
12
+ Research — gathers outside context (live web search)
13
+ Coding — executes real tools / writes code
14
+ Reviewer — cross-checks the coding agent's work for gaps
15
+ Testing — spot-checks numbers + sandbox-runs generated code
16
+ Security — real static security scan of generated code
17
+ Documentation — writes the summary/report pass
18
+ Performance — flags obvious performance problems in generated code
19
+ UI — reviews UI-facing suggestions (only for UI requests)
20
+ Architecture — reviews structure/design of the proposed solution
21
+
22
+ Same honest architecture note as agent.py: CCT talks to one configured
23
+ provider, so "agents" are specialized prompt passes over that provider,
24
+ sequenced by the Coordinator, with the Coding agent running the real
25
+ tool loop. The Coordinator is the only object that decides which
26
+ specialists join and in what order; it reports every agent start/finish
27
+ through `on_agent(role_key, role_label, status)` so the UI can show a
28
+ live roster.
29
+
30
+ The orchestrated run returns a full record:
31
+ final_text, steps, meta, agents where `agents` is the ordered list
32
+ of {key, label, status, summary} the coordinator monitored.
33
+ """
34
+
35
+ if __name__ == "__main__":
36
+ print("This is a library file and is not meant to be run directly.")
37
+ import sys
38
+ sys.exit(1)
39
+
40
+ import json
41
+ import re
42
+
43
+ from . import aicore
44
+ from . import agent
45
+ from . import security_scanner
46
+ from . import sound
47
+ from . import event_stream
48
+
49
+ # All spec-listed specialist roles. `matches` is a content gate — the
50
+ # role only joins the roster when the request actually needs it.
51
+ SPECIALIST_ROLES = {
52
+ "coordinator": ("Coordinator", "assigns work and monitors every other agent"),
53
+ "planner": ("Planner", "breaks the request into an ordered task list"),
54
+ "research": ("Research", "gathers outside context via live web search"),
55
+ "coding": ("Coding", "executes real tools to do the work"),
56
+ "reviewer": ("Reviewer", "cross-checks the result against the request"),
57
+ "testing": ("Testing", "spot-checks numbers and sandbox-runs generated code"),
58
+ "security": ("Security", "static security scan of any generated code"),
59
+ "documentation": ("Documentation", "writes the final report"),
60
+ "performance": ("Performance", "flags performance problems in generated code"),
61
+ "ui": ("UI", "reviews UI-facing suggestions"),
62
+ "architecture": ("Architecture", "reviews the design structure"),
63
+ }
64
+
65
+ _CODE_HINTS = re.compile(
66
+ r"\b(code|python|script|program|function|write a .*(class|module)|"
67
+ r"generate.*(code|script))\b", re.I)
68
+ _UI_HINTS = re.compile(
69
+ r"\b(ui|interface|screen|layout|widget|button|menu|dashboard|"
70
+ r"frontend|web page|landing page|design)\b", re.I)
71
+ _ARCH_HINTS = re.compile(
72
+ r"\b(architecture|design|structure|module|layers?|service|"
73
+ r"microservice|api design|database schema|data model)\b", re.I)
74
+ _PERF_HINTS = re.compile(
75
+ r"\b(performance|fast|speed up|optimize|slow|efficient|benchmark)\b", re.I)
76
+
77
+ _REVIEWER_PROMPT = """You are the REVIEWER on an AI team working inside CCT
78
+ (Chemistry Calc Terminal). You are handed the ORIGINAL request and the
79
+ CODING agent's final answer. Look for: parts of the request left
80
+ unanswered, implied assumptions that were never stated, and results
81
+ that don't obviously follow from the given data. Reply with ONLY a JSON
82
+ object:
83
+ {"ok": true, "note": ""} if the answer covers the request, or
84
+ {"ok": false, "note": "<what's missing or wrong, one sentence>"}."""
85
+
86
+ _PERFORMANCE_PROMPT = """You are the PERFORMANCE agent on an AI team working
87
+ inside CCT. You are handed the generated code (or solution). Identify at
88
+ most 3 concrete performance problems (algorithmic complexity, repeated
89
+ work, I/O patterns) — no fluff. Reply with ONLY a JSON object:
90
+ {"ok": true, "note": "no significant issues"} or
91
+ {"ok": false, "note": "<problem 1; problem 2; ...>"}."""
92
+
93
+ _ARCHITECTURE_PROMPT = """You are the ARCHITECTURE agent on an AI team working
94
+ inside CCT. You are handed the original request and the proposed
95
+ solution. Assess whether the structure (files/modules/components/API
96
+ shape) is sound, and name at most 2 concrete structural improvements.
97
+ Reply with ONLY a JSON object:
98
+ {"ok": true, "note": "structure is sound"} or
99
+ {"ok": false, "note": "<concrete improvements>"}."""
100
+
101
+ _UI_PROMPT = """You are the UI agent on an AI team working inside CCT. You
102
+ are handed the request and the proposed solution. Assess usability and
103
+ visual clarity at a high level; suggest at most 2 concrete improvements
104
+ (layout, hierarchy, affordances — not pixel details). Reply with ONLY a
105
+ JSON object:
106
+ {"ok": true, "note": "UI approach is sound"} or
107
+ {"ok": false, "note": "<concrete improvements>"}."""
108
+
109
+
110
+ def _extract_json(text):
111
+ if not text:
112
+ return {}
113
+ try:
114
+ text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip(), flags=re.MULTILINE)
115
+ m = re.search(r"\{.*\}", text, re.S)
116
+ return json.loads(m.group(0)) if m else {}
117
+ except Exception:
118
+ return {}
119
+
120
+
121
+ def _quick_judge(prompt_text, system_prompt):
122
+ """One focused model pass that returns (ok, note) — the generic
123
+ reviewer-style check used by reviewer/performance/architecture/ui.
124
+ Never raises; a garbled response is treated as 'ok' (no fabricated
125
+ criticism) — same honesty rule agent.py's _verify_answer uses."""
126
+ try:
127
+ raw = aicore.query_ai(prompt_text, system_prompt=system_prompt)
128
+ data = _extract_json(raw)
129
+ if data.get("ok") is False and data.get("note"):
130
+ return False, str(data["note"])
131
+ return True, ""
132
+ except Exception:
133
+ return True, ""
134
+
135
+
136
+ def run_orchestrated(user_text, max_steps=agent.MAX_STEPS, on_agent=None,
137
+ mode="agent", permission_callback=None, turn_id=""):
138
+ """Runs the Coordinator-led specialist pipeline. Returns
139
+ (final_text, steps, meta, agents) where agents is the monitored
140
+ roster (list of {key, label, status, summary})."""
141
+
142
+ agents = []
143
+ _act_ids = {} # key -> activity id for live activity tracking
144
+
145
+ def _report(key, status, summary=""):
146
+ label = SPECIALIST_ROLES[key][0]
147
+ agents.append({"key": key, "label": label, "status": status, "summary": summary})
148
+ if on_agent:
149
+ on_agent(key, label, status, summary)
150
+ # v0.8.0: Emit to event stream
151
+ try:
152
+ if status == "start":
153
+ event_stream.stream.emit("agent_started", source="orchestrator",
154
+ role=key, label=label)
155
+ elif status == "done":
156
+ event_stream.stream.emit("agent_completed", source="orchestrator",
157
+ role=key, label=label, summary=summary)
158
+ except Exception:
159
+ pass
160
+
161
+ def _run(key):
162
+ _report(key, "start")
163
+ sound.play("tool")
164
+ # v0.8.0: Create live Activity for this specialist agent
165
+ try:
166
+ from . import activity as _act
167
+ label = SPECIALIST_ROLES[key][0]
168
+ desc = SPECIALIST_ROLES[key][1]
169
+ act = _act.manager.create(
170
+ type=_act.TYPE_STATUS, action="agent",
171
+ title=f"Agent: {label}",
172
+ status=_act.RUNNING, turn_id=turn_id,
173
+ details=desc, description=f"{label} — {desc}"
174
+ )
175
+ _act_ids[key] = act.id
176
+ except Exception:
177
+ pass
178
+
179
+ def _done(key, summary=""):
180
+ _report(key, "done", summary)
181
+ sound.play("success")
182
+ # v0.8.0: Complete the live Activity for this specialist
183
+ try:
184
+ from . import activity as _act
185
+ aid = _act_ids.get(key)
186
+ if aid:
187
+ _act.manager.update(aid, status=_act.COMPLETED, result=summary[:100] if summary else "Done")
188
+ except Exception:
189
+ pass
190
+
191
+ system_prompt = agent.AGENT_SYSTEM_PROMPT if mode == "agent" else agent.AI_SYSTEM_PROMPT
192
+
193
+ # ---- Coordinator: build the roster -----------------------------------
194
+ wants_code = bool(_CODE_HINTS.search(user_text))
195
+ wants_ui = bool(_UI_HINTS.search(user_text))
196
+ wants_arch = bool(_ARCH_HINTS.search(user_text))
197
+ wants_perf = bool(_PERF_HINTS.search(user_text))
198
+ wants_research = bool(agent._RESEARCH_HINTS.search(user_text))
199
+ roster = ["planner"]
200
+ if wants_research:
201
+ roster.insert(0, "research")
202
+ roster.append("coding")
203
+ roster.append("reviewer")
204
+ if wants_ui:
205
+ roster.append("ui")
206
+ if wants_perf:
207
+ roster.append("performance")
208
+ if wants_arch:
209
+ roster.append("architecture")
210
+ if wants_code:
211
+ roster.extend(["security", "testing"])
212
+ roster.append("documentation")
213
+ _report("coordinator", "start",
214
+ f"roster: {', '.join(SPECIALIST_ROLES[r][0] for r in roster)}")
215
+
216
+ # ---- Planner -----------------------------------------------------------
217
+ _run("planner")
218
+ plan = agent._plan_steps(user_text, system_prompt)
219
+ _done("planner", f"{len(plan)} milestone(s)")
220
+
221
+ # ---- Research ----------------------------------------------------------
222
+ research_brief = ""
223
+ if wants_research:
224
+ _run("research")
225
+ research_brief = agent._research_pass(user_text, system_prompt)
226
+ _done("research", "brief folded in" if research_brief else "nothing relevant found")
227
+
228
+ # ---- Coding (the real tool loop) --------------------------------------
229
+ _run("coding")
230
+ plan_note = ("\n\nTEAM PLAN (from the Planner \u2014 follow it, adapt if needed):\n" +
231
+ "\n".join(f"{i+1}. {p}" for i, p in enumerate(plan)))
232
+ if research_brief:
233
+ plan_note += "\n\n" + research_brief
234
+ final_text, steps, meta = agent.run_agent(
235
+ user_text + plan_note, max_steps=max_steps, verbose=False, mode=mode,
236
+ permission_callback=permission_callback, turn_id=turn_id)
237
+ _done("coding", f"{len(steps)} tool step(s)")
238
+
239
+ # ---- Reviewer -----------------------------------------------------------
240
+ _run("reviewer")
241
+ ok, note = _quick_judge(
242
+ "ORIGINAL REQUEST:\n" + user_text +
243
+ "\n\nCODING AGENT'S ANSWER:\n" + final_text,
244
+ _REVIEWER_PROMPT)
245
+ if not ok and note:
246
+ final_text = final_text + f"\n\n\u2696 Reviewer flag: {note}"
247
+ _done("reviewer", "ok" if ok else f"flag: {note}")
248
+
249
+ # ---- UI / Performance / Architecture (content-gated) -------------------
250
+ for key, prompt, prompt_text in (
251
+ ("ui", _UI_PROMPT, "REQUEST:\n" + user_text + "\n\nSOLUTION:\n" + final_text),
252
+ ("performance", _PERFORMANCE_PROMPT, "GENERATED CODE/SOLUTION:\n" + final_text),
253
+ ("architecture", _ARCHITECTURE_PROMPT,
254
+ "ORIGINAL REQUEST:\n" + user_text + "\n\nPROPOSED SOLUTION:\n" + final_text),
255
+ ):
256
+ if key not in roster:
257
+ continue
258
+ _run(key)
259
+ p_ok, p_note = _quick_judge(prompt_text, prompt)
260
+ if not p_ok and p_note:
261
+ final_text = final_text + f"\n\n\u26a0 {SPECIALIST_ROLES[key][0]} note: {p_note}"
262
+ _done(key, "ok" if p_ok else f"note: {p_note}")
263
+
264
+ # ---- Security + Testing (only for coding requests) ---------------------
265
+ if wants_code:
266
+ _run("security")
267
+ code = agent._extract_code_block(final_text)
268
+ findings = security_scanner.scan_code(code) if code else []
269
+ if findings:
270
+ final_text += agent._DEBUGGER_NOTE_TEMPLATE.format(
271
+ summary=security_scanner.summarize(findings),
272
+ details="\n".join(f" {repr(f)}" for f in findings[:5]))
273
+ _done("security",
274
+ f"{len(findings)} finding(s)" if findings else "no risky patterns")
275
+
276
+ _run("testing")
277
+ final_text, debug_note = agent._debug_pass(final_text)
278
+ if debug_note:
279
+ sb = debug_note.get("sandbox_test")
280
+ _done("testing", f"risk {debug_note.get('risk')}" +
281
+ (f" \u00b7 sandbox exit {sb.get('returncode')}" if isinstance(sb, dict) else ""))
282
+ else:
283
+ _done("testing", "no code block to test")
284
+ else:
285
+ _run("testing")
286
+ ok_t, note_t = agent._verify_answer(user_text, plan, final_text, system_prompt)
287
+ if not ok_t and note_t:
288
+ final_text = final_text + f"\n\n\u26a0 Tester flagged this for review: {note_t}"
289
+ _done("testing", "ok" if ok_t else f"flag: {note_t}")
290
+
291
+ # ---- Documentation ------------------------------------------------------
292
+ _run("documentation")
293
+ doc = ("\u2705 Orchestrated run complete\n\n"
294
+ "Agents that worked on this:\n" +
295
+ "\n".join(f"- {SPECIALIST_ROLES[a['key']][0]} \u2014 {a['status']}"
296
+ + (f" ({a['summary']})" if a.get("summary") else "")
297
+ for a in agents if a["key"] != "coordinator"))
298
+ final_text = final_text.rstrip() + "\n\n" + doc
299
+ _done("documentation", "final report appended")
300
+
301
+ _report("coordinator", "done", f"{len(agents)} agent events monitored")
302
+ meta = dict(meta or {})
303
+ meta["roster"] = [a["key"] for a in agents]
304
+ return final_text, steps, meta, agents