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,419 @@
1
+ import json
2
+ import re
3
+ import sys
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def _log(msg):
10
+ try:
11
+ print(f"[Normalizer] {msg}", file=sys.stderr)
12
+ except Exception:
13
+ pass
14
+
15
+
16
+ TOOL_ALIASES = {
17
+ "runcommand": "run_terminal",
18
+ "run_command": "run_terminal",
19
+ "execute_command": "run_terminal",
20
+ "execute_terminal": "run_terminal",
21
+ "run_cmd": "run_terminal",
22
+ "terminal": "run_terminal",
23
+ "shell": "run_terminal",
24
+ "bash": "run_terminal",
25
+ "listdirectory": "list_directory",
26
+ "list_dir": "list_directory",
27
+ "list_files": "list_directory",
28
+ "ls": "list_directory",
29
+ "dir": "list_directory",
30
+ "create_file": "write_file",
31
+ "new_file": "write_file",
32
+ "rewrite_file": "write_file",
33
+ "writefile": "write_file",
34
+ "write": "write_file",
35
+ "edit_file": "edit_file",
36
+ "editfile": "edit_file",
37
+ "modify_file": "edit_file",
38
+ "patch_file": "edit_file",
39
+ "replace_in_file": "edit_file",
40
+ "str_replace_editor": "edit_file",
41
+ "delete_file": "delete_file",
42
+ "deletefile": "delete_file",
43
+ "remove_file": "delete_file",
44
+ "rm": "delete_file",
45
+ "move_file": "rename_file",
46
+ "movefile": "rename_file",
47
+ "rename": "rename_file",
48
+ "mv": "rename_file",
49
+ "create_directory": "create_folder",
50
+ "create_dir": "create_folder",
51
+ "mkdir": "create_folder",
52
+ "read_file": "read_file",
53
+ "readfile": "read_file",
54
+ "cat": "read_file",
55
+ "search_workspace": "search_workspace",
56
+ "search": "search_workspace",
57
+ "find": "search_workspace",
58
+ "grep": "search_workspace",
59
+ "run_build": "run_build",
60
+ "build_project": "run_build",
61
+ "build": "run_build",
62
+ "run_tests": "run_tests",
63
+ "run_test": "run_tests",
64
+ "runtests": "run_tests",
65
+ "test_suite": "run_tests",
66
+ "inspect_project": "inspect_project",
67
+ "project_info": "inspect_project",
68
+ "inspect": "inspect_project",
69
+ "install_package": "install_packages",
70
+ "install": "install_packages",
71
+ "archive_list": "archive_list",
72
+ "list_archive": "archive_list",
73
+ "zip_list": "archive_list",
74
+ "archive_extract": "archive_extract",
75
+ "extract_archive": "archive_extract",
76
+ "unzip": "archive_extract",
77
+ "extract": "archive_extract",
78
+ "archive_delete_entries": "archive_delete_entries",
79
+ "archive_remove_entries": "archive_delete_entries",
80
+ "archive_add_entries": "archive_add_entries",
81
+ "add_to_archive": "archive_add_entries",
82
+ "archive_repack": "archive_repack",
83
+ "repack_archive": "archive_repack",
84
+ "archive_validate": "archive_validate",
85
+ "validate_archive": "archive_validate",
86
+ }
87
+
88
+
89
+ def resolve_tool_name(name):
90
+ if not name:
91
+ return name
92
+ # normalize separators/case first so "Create File", "write-file",
93
+ # "RUN COMMAND" style names all resolve (requirement #2)
94
+ low = re.sub(r"[\s\-]+", "_", str(name).lower().strip())
95
+ if low in TOOL_ALIASES:
96
+ resolved = TOOL_ALIASES[low]
97
+ if resolved != low:
98
+ _log(f"Alias resolved: {name} -> {resolved}")
99
+ return resolved
100
+ return low
101
+
102
+
103
+ @dataclass
104
+ class ToolCall:
105
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
106
+ name: str = ""
107
+ arguments: Dict[str, Any] = field(default_factory=dict)
108
+ thought: str = ""
109
+ source_format: str = ""
110
+ confidence: float = 1.0
111
+
112
+
113
+ def _find_balanced_json(text: str, start: int = 0) -> Optional[str]:
114
+ if start >= len(text) or text[start] != "{":
115
+ return None
116
+ depth = 0
117
+ in_string = False
118
+ escape = False
119
+ for i in range(start, len(text)):
120
+ c = text[i]
121
+ if escape:
122
+ escape = False
123
+ continue
124
+ if c == "\\":
125
+ escape = True
126
+ continue
127
+ if c == '"':
128
+ in_string = not in_string
129
+ continue
130
+ if in_string:
131
+ continue
132
+ if c == "{":
133
+ depth += 1
134
+ elif c == "}":
135
+ depth -= 1
136
+ if depth == 0:
137
+ return text[start : i + 1]
138
+ return None
139
+
140
+
141
+ def _parse_json_protocol(text: str, known_tools: set) -> List[ToolCall]:
142
+ calls = []
143
+ patterns = [
144
+ r'\{"action"\s*:\s*"tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}',
145
+ r'\{"action"\s*:\s*"tool"\s*,\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*(\{[^}]*\})\s*\}',
146
+ r'\{"tool"\s*:\s*"([^"]+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}',
147
+ r'\{"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*(\{[^}]*\})\s*\}',
148
+ r'\{"name"\s*:\s*"([^"]+)"\s*,\s*"arguments"\s*:\s*(\{[^}]*\})\s*\}',
149
+ ]
150
+ for pattern in patterns:
151
+ for match in re.finditer(pattern, text):
152
+ tool_name = match.group(1)
153
+ args_str = match.group(2)
154
+ if tool_name in known_tools:
155
+ try:
156
+ args = json.loads(args_str)
157
+ except json.JSONDecodeError:
158
+ args = {}
159
+ calls.append(ToolCall(
160
+ name=tool_name,
161
+ arguments=args,
162
+ source_format="json_protocol",
163
+ confidence=0.95,
164
+ ))
165
+ idx = 0
166
+ while idx < len(text):
167
+ brace_pos = text.find("{", idx)
168
+ if brace_pos == -1:
169
+ break
170
+ obj_str = _find_balanced_json(text, brace_pos)
171
+ if not obj_str:
172
+ idx = brace_pos + 1
173
+ continue
174
+ try:
175
+ obj = json.loads(obj_str)
176
+ except json.JSONDecodeError:
177
+ idx = brace_pos + 1
178
+ continue
179
+ if isinstance(obj, dict):
180
+ action = obj.get("action", "")
181
+ tool = obj.get("tool", obj.get("name", ""))
182
+ args = obj.get("args", obj.get("parameters", obj.get("arguments", {})))
183
+ if action == "tool" and tool in known_tools:
184
+ calls.append(ToolCall(
185
+ name=tool,
186
+ arguments=args if isinstance(args, dict) else {},
187
+ source_format="json_protocol_action",
188
+ confidence=0.9,
189
+ ))
190
+ elif action in known_tools:
191
+ calls.append(ToolCall(
192
+ name=action,
193
+ arguments={k: v for k, v in obj.items() if k != "action"},
194
+ source_format="json_protocol_action",
195
+ confidence=0.85,
196
+ ))
197
+ elif tool in known_tools and tool:
198
+ calls.append(ToolCall(
199
+ name=tool,
200
+ arguments=args if isinstance(args, dict) else {},
201
+ source_format="json_protocol",
202
+ confidence=0.8,
203
+ ))
204
+ idx = brace_pos + len(obj_str)
205
+ return calls
206
+
207
+
208
+ def _extract_xml_params(block):
209
+ params = {}
210
+ for m in re.finditer(r'<parameter\s+name="([^"]+)">(.*?)</parameter>', block, re.DOTALL):
211
+ params[m.group(1).strip()] = m.group(2).strip()
212
+ return params
213
+
214
+ def _parse_xml_tool_calls(text, known_tools):
215
+ calls = []; seen = set()
216
+
217
+ ire = re.compile(r'<invoke\s+name="([^"]+)">(.*?)</invoke>', re.DOTALL)
218
+ for m in ire.finditer(text):
219
+ rn = m.group(1).strip(); bl = m.group(2).strip()
220
+ tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
221
+ if not p:
222
+ try: p = json.loads(bl)
223
+ except: p = {'raw': bl}
224
+ k = (tn, json.dumps(p, sort_keys=True))
225
+ if k in seen: continue; seen.add(k)
226
+ if tn in known_tools:
227
+ calls.append(ToolCall(name=tn, arguments=p, source_format='xml_invoke', confidence=0.9))
228
+ _log('TOOL_DETECTED: ' + tn + ' (xml_invoke)')
229
+
230
+ mmre = re.compile(r'<minimax:toolcall>(.*?)</minimax:toolcall>', re.DOTALL)
231
+ for m in mmre.finditer(text):
232
+ for im in ire.finditer(m.group(1)):
233
+ rn = im.group(1).strip(); bl = im.group(2).strip()
234
+ tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
235
+ if not p:
236
+ try: p = json.loads(bl)
237
+ except: p = {'raw': bl}
238
+ k = (tn, json.dumps(p, sort_keys=True))
239
+ if k in seen: continue; seen.add(k)
240
+ if tn in known_tools:
241
+ calls.append(ToolCall(name=tn, arguments=p, source_format='xml_minimax', confidence=0.9))
242
+
243
+ tnre = re.compile(r'<tool_name>([^<]+)</tool_name>\s*(?:<parameters>|<arguments>)(.*?)(?:</parameters>|</arguments>)', re.DOTALL)
244
+ for m in tnre.finditer(text):
245
+ rn = m.group(1).strip(); ct = m.group(2).strip()
246
+ tn = resolve_tool_name(rn); k = (tn, ct)
247
+ if k in seen: continue; seen.add(k)
248
+ if tn in known_tools:
249
+ try: a = json.loads(ct)
250
+ except: a = _extract_xml_params(ct)
251
+ calls.append(ToolCall(name=tn, arguments=a if a else {'raw': ct}, source_format='xml_tool_name', confidence=0.85))
252
+
253
+ tare = re.compile(r'<tool>([^<]+)</tool>\s*<args>(.*?)</args>', re.DOTALL)
254
+ for m in tare.finditer(text):
255
+ rn = m.group(1).strip(); ct = m.group(2).strip()
256
+ tn = resolve_tool_name(rn); k = (tn, ct)
257
+ if k in seen: continue; seen.add(k)
258
+ if tn in known_tools:
259
+ try: a = json.loads(ct)
260
+ except: a = {'raw': ct}
261
+ calls.append(ToolCall(name=tn, arguments=a, source_format='xml_tool_args', confidence=0.85))
262
+
263
+ bre = re.compile(r'<invoke\s+name="([^"]+)">(.*?)$', re.DOTALL)
264
+ for m in bre.finditer(text):
265
+ rn = m.group(1).strip(); bl = m.group(2).strip()
266
+ tn = resolve_tool_name(rn); p = _extract_xml_params(bl)
267
+ if p:
268
+ k = (tn, json.dumps(p, sort_keys=True))
269
+ if k not in seen:
270
+ seen.add(k)
271
+ if tn in known_tools:
272
+ calls.append(ToolCall(name=tn, arguments=p, source_format='xml_bare_invoke', confidence=0.75))
273
+
274
+ # <tool_call><name>...</name><arguments>...</arguments></tool_call>
275
+ tc_tag_re = re.compile(r'<tool_call>\s*<name>([^<]+)</name>\s*<arguments>(.*?)</arguments>\s*</tool_call>', re.DOTALL)
276
+ for m in tc_tag_re.finditer(text):
277
+ rn = m.group(1).strip(); ct = m.group(2).strip()
278
+ tn = resolve_tool_name(rn); k = (tn, ct)
279
+ if k in seen: continue; seen.add(k)
280
+ if tn in known_tools:
281
+ try: a = json.loads(ct)
282
+ except: a = _extract_xml_params(ct)
283
+ calls.append(ToolCall(name=tn, arguments=a if a else {'raw': ct}, source_format='xml_tool_call_tag', confidence=0.9))
284
+
285
+ return calls
286
+ def _parse_function_call_syntax(text: str, known_tools: set) -> List[ToolCall]:
287
+ calls = []
288
+ pattern = r'(\w+)\s*\((.*?)\)'
289
+ for match in re.finditer(pattern, text, re.DOTALL):
290
+ func_name = match.group(1)
291
+ args_str = match.group(2).strip()
292
+ if func_name not in known_tools:
293
+ continue
294
+ args = {}
295
+ if args_str:
296
+ try:
297
+ args = json.loads("{" + args_str + "}")
298
+ except json.JSONDecodeError:
299
+ key_val_pattern = r'(\w+)\s*=\s*("(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\'|\d+(?:\.\d+)?|True|False|None|\[.*?\]|\{.*?\})'
300
+ for kv in re.finditer(key_val_pattern, args_str):
301
+ key = kv.group(1)
302
+ val_str = kv.group(2)
303
+ try:
304
+ val = json.loads(val_str)
305
+ except json.JSONDecodeError:
306
+ val = val_str.strip('"').strip("'")
307
+ args[key] = val
308
+ calls.append(ToolCall(
309
+ name=func_name,
310
+ arguments=args,
311
+ source_format="function_call",
312
+ confidence=0.75,
313
+ ))
314
+ return calls
315
+
316
+
317
+ def _heuristic_extract(text: str, known_tools: set) -> List[ToolCall]:
318
+ calls = []
319
+ for tool in known_tools:
320
+ patterns = [
321
+ rf'(?:use|call|invoke|run|execute)\s+{re.escape(tool)}\s+(?:with|using|on)\s+(.+?)(?:\.|$|\n)',
322
+ rf'{re.escape(tool)}\s*\(([^)]*)\)',
323
+ rf'["\']?{re.escape(tool)}["\']?\s*:\s*(\{{[^}}]+\}})',
324
+ ]
325
+ for pattern in patterns:
326
+ for match in re.finditer(pattern, text, re.IGNORECASE):
327
+ args_str = match.group(1).strip()
328
+ args = {}
329
+ try:
330
+ args = json.loads(args_str)
331
+ except (json.JSONDecodeError, ValueError):
332
+ if "=" in args_str:
333
+ for part in args_str.split(","):
334
+ if "=" in part:
335
+ k, v = part.split("=", 1)
336
+ args[k.strip()] = v.strip().strip("\"'")
337
+ calls.append(ToolCall(
338
+ name=tool,
339
+ arguments=args,
340
+ source_format="heuristic",
341
+ confidence=0.5,
342
+ ))
343
+ return calls
344
+
345
+
346
+ def normalize_tool_calls(response_text: str, known_tools: Optional[List[str]] = None) -> List[ToolCall]:
347
+ if not response_text or not response_text.strip():
348
+ return []
349
+ # Strip <think>...</think> and reasoning blocks before tool parsing so thoughts don't trigger false tools
350
+ response_text = re.sub(r"<think>.*?</think>", "", response_text, flags=re.DOTALL)
351
+ response_text = re.sub(r">\s*\*Thinking:\*.*?(?=\n\n|\Z)", "", response_text, flags=re.DOTALL)
352
+ tools_set = set(known_tools) if known_tools else set()
353
+ all_calls: List[ToolCall] = []
354
+ seen = set()
355
+ json_calls = _parse_json_protocol(response_text, tools_set)
356
+ for c in json_calls:
357
+ key = (c.name, json.dumps(c.arguments, sort_keys=True))
358
+ if key not in seen:
359
+ seen.add(key)
360
+ all_calls.append(c)
361
+ xml_calls = _parse_xml_tool_calls(response_text, tools_set)
362
+ for c in xml_calls:
363
+ key = (c.name, json.dumps(c.arguments, sort_keys=True))
364
+ if key not in seen:
365
+ seen.add(key)
366
+ all_calls.append(c)
367
+ func_calls = _parse_function_call_syntax(response_text, tools_set)
368
+ for c in func_calls:
369
+ key = (c.name, json.dumps(c.arguments, sort_keys=True))
370
+ if key not in seen:
371
+ seen.add(key)
372
+ all_calls.append(c)
373
+ if not all_calls and tools_set:
374
+ heuristic_calls = _heuristic_extract(response_text, tools_set)
375
+ for c in heuristic_calls:
376
+ key = (c.name, json.dumps(c.arguments, sort_keys=True))
377
+ if key not in seen:
378
+ seen.add(key)
379
+ all_calls.append(c)
380
+ all_calls.sort(key=lambda c: c.confidence, reverse=True)
381
+ return all_calls
382
+
383
+
384
+ def extract_final_answer(response_text: str) -> Optional[str]:
385
+ if not response_text:
386
+ return None
387
+ patterns = [
388
+ r'(?:final|answer|result|output)\s*(?:is|:)\s*["\']?(.+?)["\']?\s*(?:\.|$|\n)',
389
+ r'(?:the)\s+(?:answer|result|final)\s+(?:is|:)\s*(.+?)(?:\.|$|\n)',
390
+ r'(?:ANSWER|RESULT|FINAL)\s*[:=]\s*(.+?)(?:\.|$|\n)',
391
+ ]
392
+ for pattern in patterns:
393
+ match = re.search(pattern, response_text, re.IGNORECASE)
394
+ if match:
395
+ return match.group(1).strip()
396
+ return response_text.strip() if len(response_text.strip()) < 500 else None
397
+
398
+
399
+ def is_tool_call_response(response_text: str, known_tools: Optional[List[str]] = None) -> bool:
400
+ if not response_text:
401
+ return False
402
+ tool_indicators = [
403
+ r'"action"\s*:\s*"tool"',
404
+ r'"tool"\s*:\s*"[^"]+"',
405
+ r'"name"\s*:\s*"[^"]+"\s*,\s*"arguments"',
406
+ r'<minimax:toolcall>',
407
+ r'<tool_call>',
408
+ r'<invoke\s+name=',
409
+ ]
410
+ for indicator in tool_indicators:
411
+ if re.search(indicator, response_text):
412
+ return True
413
+ if known_tools:
414
+ tools_set = set(known_tools)
415
+ func_pattern = r'(\w+)\s*\('
416
+ for match in re.finditer(func_pattern, response_text):
417
+ if match.group(1) in tools_set:
418
+ return True
419
+ return False
calc_terminal/tui.py ADDED
@@ -0,0 +1,104 @@
1
+ """
2
+ CCT TUI Dashboard — a real Textual application (actual widgets, actual
3
+ panes, actual layout engine — not raw print() dressed up to look like
4
+ one) offering a genuine split-pane view: a command sidebar, a scrolling
5
+ notebook-history feed, and a live status footer. This is optional and
6
+ additive: the primary interface stays the existing print()-based
7
+ terminal UI everywhere else, and /tui is one explicit command that
8
+ drops into this richer view and returns cleanly to the normal prompt
9
+ on quit.
10
+
11
+ Textual (like any full-screen TUI) needs a real attached terminal —
12
+ launching it against a pipe or non-tty stdin/stdout does not hang (it
13
+ fails fast), matching the same honesty precedent as keys.py's
14
+ stdin_is_interactive() guard for the raw-keypress live views.
15
+ """
16
+
17
+ from . import keys
18
+
19
+ TEXTUAL_AVAILABLE = True
20
+ try:
21
+ from textual.app import App, ComposeResult
22
+ from textual.containers import Horizontal, Vertical, VerticalScroll
23
+ from textual.widgets import Header, Footer, Static, Label
24
+ except Exception:
25
+ TEXTUAL_AVAILABLE = False
26
+
27
+
28
+ if TEXTUAL_AVAILABLE:
29
+
30
+ class NotebookCard(Static):
31
+ def __init__(self, index, nb):
32
+ body = (
33
+ f"[b]{index}. {nb.get('topic', 'Notebook')}[/b]\n"
34
+ f"[dim]{nb.get('question', '')[:90]}[/dim]\n"
35
+ f"[green]{nb.get('final_answer', '')}[/green]"
36
+ )
37
+ super().__init__(body, classes="notebook-card")
38
+
39
+ class CCTDashboard(App):
40
+ """Sidebar of quick actions + scrolling notebook history +
41
+ live status footer, all real Textual widgets/layout."""
42
+
43
+ CSS = """
44
+ Screen { background: $surface; }
45
+ #sidebar { width: 26; border-right: solid $accent; padding: 1; }
46
+ #main { padding: 1 2; }
47
+ .notebook-card { border: round $accent; padding: 1; margin-bottom: 1; }
48
+ #statusline { height: 1; color: $text-muted; }
49
+ """
50
+ BINDINGS = [("q", "quit_dashboard", "Back to terminal"), ("r", "refresh", "Refresh")]
51
+
52
+ def __init__(self, history, stats):
53
+ super().__init__()
54
+ self._history = history
55
+ self._stats = stats
56
+
57
+ def compose(self) -> ComposeResult:
58
+ yield Header(show_clock=True)
59
+ with Horizontal():
60
+ with Vertical(id="sidebar"):
61
+ yield Label("[b]CAT[/b]")
62
+ yield Label("Chemistry Calc Terminal")
63
+ yield Label("")
64
+ yield Label("[b]Session[/b]")
65
+ yield Label(f"Solved: {self._stats.get('solved', 0)}")
66
+ yield Label(f"Provider: {self._stats.get('provider', 'none')}")
67
+ yield Label(f"Model: {self._stats.get('model', 'none')}")
68
+ yield Label("")
69
+ yield Label("[dim]q back to terminal[/dim]")
70
+ yield Label("[dim]r refresh[/dim]")
71
+ with VerticalScroll(id="main"):
72
+ if self._history:
73
+ for i, nb in enumerate(reversed(self._history), 1):
74
+ yield NotebookCard(len(self._history) - i + 1, nb)
75
+ else:
76
+ yield Static("No notebooks solved yet this session.")
77
+ yield Static(id="statusline")
78
+ yield Footer()
79
+
80
+ def on_mount(self):
81
+ self.query_one("#statusline", Static).update(
82
+ f" {len(self._history)} notebook(s) in session history")
83
+
84
+ def action_quit_dashboard(self):
85
+ self.exit()
86
+
87
+ def action_refresh(self):
88
+ self.refresh()
89
+
90
+
91
+ def launch_dashboard(history, stats):
92
+ """Runs the Textual dashboard synchronously (blocks until the user
93
+ presses q or closes it), then returns control to the normal CLI
94
+ loop. Returns (ok, message)."""
95
+ if not TEXTUAL_AVAILABLE:
96
+ return False, "textual isn't installed — run: pip install textual"
97
+ if not keys.stdin_is_interactive():
98
+ return False, "The TUI dashboard needs a real attached terminal (not piped input)."
99
+ try:
100
+ app = CCTDashboard(history, stats)
101
+ app.run()
102
+ return True, ""
103
+ except Exception as e:
104
+ return False, f"Could not launch TUI dashboard: {e}"
@@ -0,0 +1,8 @@
1
+ """
2
+ calc_terminal.ui — the single owner of every interactive interface.
3
+
4
+ Contains only rendering + event dispatch: no chemistry logic, no AI
5
+ logic, no calculation logic. Business logic stays in the modules one
6
+ level up (aicore.py, permissions.py, session.py, engine.py, ...); this
7
+ package renders their state and turns user interaction into events.
8
+ """