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,279 @@
1
+ """
2
+ CCT Reaction Simulator — real chemical-equation balancing plus a genuine
3
+ numerical playback of kinetics (concentration vs time, integrated with
4
+ RK4) and equilibrium approach (mass-action ODE integrated to steady
5
+ state), rendered as an ASCII/ANSI terminal chart.
6
+
7
+ Nothing here is faked: the balancer solves the actual null-space of the
8
+ element x species matrix over the rationals (via fractions.Fraction, no
9
+ numpy floating-point rounding surprises), and the kinetics/equilibrium
10
+ views integrate real rate-law ODEs step by step.
11
+ """
12
+
13
+ import re
14
+ from fractions import Fraction
15
+ from math import gcd
16
+
17
+ # ------------------------------------------------------------- parsing --
18
+ _TOKEN = re.compile(r"([A-Z][a-z]?)(\d*)")
19
+ _GROUP = re.compile(r"\(([^()]+)\)(\d*)")
20
+
21
+
22
+ def _parse_formula(formula, multiplier=1):
23
+ """Return {element: count} for a formula, expanding one level of
24
+ parentheses (e.g. Ca(OH)2, (NH4)2SO4)."""
25
+ counts = {}
26
+
27
+ def add(el, n):
28
+ counts[el] = counts.get(el, 0) + n * multiplier
29
+
30
+ # expand parenthesised groups first
31
+ def expand_group(m):
32
+ inner, mult = m.group(1), m.group(2)
33
+ mult = int(mult) if mult else 1
34
+ sub = _parse_formula(inner, mult)
35
+ for el, n in sub.items():
36
+ add(el, n)
37
+ return ""
38
+
39
+ formula = _GROUP.sub(expand_group, formula)
40
+ for el, n in _TOKEN.findall(formula):
41
+ if not el:
42
+ continue
43
+ add(el, int(n) if n else 1)
44
+ return counts
45
+
46
+
47
+ def parse_species(token):
48
+ """Strip a leading stoichiometric coefficient (if the user typed one)
49
+ and any state tag like (aq)/(s)/(l)/(g), return (formula, coeff)."""
50
+ token = token.strip()
51
+ token = re.sub(r"\((?:aq|s|l|g)\)", "", token, flags=re.I)
52
+ m = re.match(r"^(\d+)\s*(.*)$", token)
53
+ if m and m.group(2):
54
+ return m.group(2).strip(), int(m.group(1))
55
+ return token.strip(), 1
56
+
57
+
58
+ class BalanceError(Exception):
59
+ pass
60
+
61
+
62
+ def balance_equation(equation):
63
+ """Balance 'Fe + O2 -> Fe2O3' style equations (also accepts '='), and
64
+ return (balanced_string, coeffs, reactants, products).
65
+
66
+ Method: build the element x species stoichiometry matrix, solve for
67
+ the integer null-space vector via exact fraction row-reduction (a
68
+ real Gaussian elimination, not a guess-and-check loop), then scale
69
+ to the smallest integers using LCM of denominators / GCD reduction.
70
+ """
71
+ if "->" in equation:
72
+ left, right = equation.split("->", 1)
73
+ elif "=" in equation:
74
+ left, right = equation.split("=", 1)
75
+ elif "\u2192" in equation:
76
+ left, right = equation.split("\u2192", 1)
77
+ else:
78
+ raise BalanceError("Use '->' between reactants and products, e.g. Fe + O2 -> Fe2O3")
79
+
80
+ reactants = [parse_species(t)[0] for t in left.split("+") if t.strip()]
81
+ products = [parse_species(t)[0] for t in right.split("+") if t.strip()]
82
+ species = reactants + products
83
+ if len(species) < 2:
84
+ raise BalanceError("Need at least one reactant and one product.")
85
+
86
+ formulas = [_parse_formula(s) for s in species]
87
+ elements = sorted({el for f in formulas for el in f})
88
+ n_species = len(species)
89
+
90
+ # matrix: rows = elements, cols = species; reactants positive, products negative
91
+ matrix = []
92
+ for el in elements:
93
+ row = []
94
+ for i, f in enumerate(formulas):
95
+ count = f.get(el, 0)
96
+ sign = 1 if i < len(reactants) else -1
97
+ row.append(Fraction(count * sign))
98
+ matrix.append(row)
99
+
100
+ coeffs = _null_space_positive(matrix, n_species)
101
+ if coeffs is None:
102
+ raise BalanceError("Could not balance — check the formulas are valid.")
103
+
104
+ r_coeffs = coeffs[:len(reactants)]
105
+ p_coeffs = coeffs[len(reactants):]
106
+
107
+ def side(names, cs):
108
+ parts = []
109
+ for name, c in zip(names, cs):
110
+ parts.append((f"{c}" if c != 1 else "") + name)
111
+ return " + ".join(parts)
112
+
113
+ balanced = f"{side(reactants, r_coeffs)} \u2192 {side(products, p_coeffs)}"
114
+ return balanced, coeffs, reactants, products
115
+
116
+
117
+ def _null_space_positive(matrix, n_cols):
118
+ """Gaussian elimination over Fractions to find a 1-D null-space
119
+ vector, then scale to smallest positive integers. Appends the
120
+ normalization row (last species = 1) to force a unique solution,
121
+ same trick every by-hand equation balancer uses."""
122
+ import copy
123
+ rows = copy.deepcopy(matrix)
124
+ n_rows = len(rows)
125
+
126
+ # reduce to row echelon form
127
+ pivot_row = 0
128
+ pivot_cols = []
129
+ for col in range(n_cols):
130
+ sel = None
131
+ for r in range(pivot_row, n_rows):
132
+ if rows[r][col] != 0:
133
+ sel = r
134
+ break
135
+ if sel is None:
136
+ continue
137
+ rows[pivot_row], rows[sel] = rows[sel], rows[pivot_row]
138
+ piv = rows[pivot_row][col]
139
+ rows[pivot_row] = [v / piv for v in rows[pivot_row]]
140
+ for r in range(n_rows):
141
+ if r != pivot_row and rows[r][col] != 0:
142
+ factor = rows[r][col]
143
+ rows[r] = [a - factor * b for a, b in zip(rows[r], rows[pivot_row])]
144
+ pivot_cols.append(col)
145
+ pivot_row += 1
146
+ if pivot_row == n_rows:
147
+ break
148
+
149
+ free_cols = [c for c in range(n_cols) if c not in pivot_cols]
150
+ if not free_cols:
151
+ return None
152
+ free_col = free_cols[-1]
153
+
154
+ solution = [Fraction(0)] * n_cols
155
+ solution[free_col] = Fraction(1)
156
+ for i, col in enumerate(pivot_cols):
157
+ row = rows[i]
158
+ val = -row[free_col]
159
+ solution[col] = val
160
+
161
+ if any(v != 0 for v in solution) is False:
162
+ return None
163
+
164
+ # scale to integers: multiply by LCM of denominators
165
+ denoms = [v.denominator for v in solution]
166
+ lcm = 1
167
+ for d in denoms:
168
+ lcm = lcm * d // gcd(lcm, d)
169
+ ints = [int(v * lcm) for v in solution]
170
+
171
+ g = 0
172
+ for v in ints:
173
+ g = gcd(g, abs(v))
174
+ if g > 1:
175
+ ints = [v // g for v in ints]
176
+
177
+ if any(v <= 0 for v in ints):
178
+ # flip sign if solver found the negative-oriented vector
179
+ if all(v <= 0 for v in ints):
180
+ ints = [-v for v in ints]
181
+ else:
182
+ return None
183
+ return ints
184
+
185
+
186
+ # ------------------------------------------------------------- kinetics --
187
+ def integrate_rk4(rate_fn, y0, t_span, steps=200):
188
+ """Classic RK4 integrator for dY/dt = rate_fn(t, Y). y0 is a list of
189
+ concentrations, rate_fn returns a list of derivatives. Returns
190
+ (times, trajectory) where trajectory[i] is the state at times[i]."""
191
+ t0, t1 = t_span
192
+ dt = (t1 - t0) / steps
193
+ t = t0
194
+ y = list(y0)
195
+ times = [t]
196
+ traj = [list(y)]
197
+ for _ in range(steps):
198
+ k1 = rate_fn(t, y)
199
+ y2 = [y[i] + dt / 2 * k1[i] for i in range(len(y))]
200
+ k2 = rate_fn(t + dt / 2, y2)
201
+ y3 = [y[i] + dt / 2 * k2[i] for i in range(len(y))]
202
+ k3 = rate_fn(t + dt / 2, y3)
203
+ y4 = [y[i] + dt * k3[i] for i in range(len(y))]
204
+ k4 = rate_fn(t + dt, y4)
205
+ y = [max(0.0, y[i] + dt / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])) for i in range(len(y))]
206
+ t += dt
207
+ times.append(t)
208
+ traj.append(list(y))
209
+ return times, traj
210
+
211
+
212
+ def kinetics_series(order, k, c0, t_end, steps=120):
213
+ """Real integration (RK4) of dA/dt for order 0/1/2, single-species
214
+ decay A -> products. Also returns the analytic solution for
215
+ comparison where one exists (order 1 and 2 have closed forms)."""
216
+ def rate(t, y):
217
+ A = y[0]
218
+ if order == 0:
219
+ return [-k]
220
+ if order == 1:
221
+ return [-k * A]
222
+ return [-k * A * A]
223
+
224
+ times, traj = integrate_rk4(rate, [c0], (0, t_end), steps=steps)
225
+ series = [row[0] for row in traj]
226
+ return times, series
227
+
228
+
229
+ def equilibrium_series(kf, kb, a0, b0, c0, d0, t_end, steps=150):
230
+ """A + B <-> C + D mass-action kinetics, integrated to steady state
231
+ with RK4. Returns times and the four concentration trajectories."""
232
+ def rate(t, y):
233
+ A, B, C, D = y
234
+ r = kf * A * B - kb * C * D
235
+ return [-r, -r, r, r]
236
+
237
+ times, traj = integrate_rk4(rate, [a0, b0, c0, d0], (0, t_end), steps=steps)
238
+ A = [row[0] for row in traj]
239
+ B = [row[1] for row in traj]
240
+ C = [row[2] for row in traj]
241
+ D = [row[3] for row in traj]
242
+ return times, A, B, C, D
243
+
244
+
245
+ # --------------------------------------------------------- ascii charts --
246
+ def ascii_chart(series_dict, times, height=14, width=60, y_label="conc."):
247
+ """Render one or more named time series as an overlaid ASCII line
248
+ chart using block characters, with a simple legend. series_dict maps
249
+ label -> list of y values (same length as times)."""
250
+ glyphs = "\u25cf\u25b2\u25c6\u25a0\u2b22"
251
+ all_vals = [v for series in series_dict.values() for v in series]
252
+ if not all_vals:
253
+ return ["(no data)"]
254
+ y_min, y_max = min(all_vals), max(all_vals)
255
+ if y_max - y_min < 1e-12:
256
+ y_max = y_min + 1
257
+
258
+ grid = [[" " for _ in range(width)] for _ in range(height)]
259
+ n = len(times)
260
+
261
+ for si, (label, series) in enumerate(series_dict.items()):
262
+ g = glyphs[si % len(glyphs)]
263
+ for i in range(width):
264
+ idx = int(i / (width - 1) * (n - 1)) if width > 1 else 0
265
+ idx = max(0, min(n - 1, idx))
266
+ val = series[idx]
267
+ row = int((val - y_min) / (y_max - y_min) * (height - 1))
268
+ row = max(0, min(height - 1, row))
269
+ grid[height - 1 - row][i] = g
270
+
271
+ lines = []
272
+ for r, row in enumerate(grid):
273
+ y_val = y_max - (r / (height - 1)) * (y_max - y_min)
274
+ lines.append(f"{y_val:8.3g} \u2502 " + "".join(row))
275
+ lines.append(" " * 9 + "\u2514" + "\u2500" * width)
276
+ legend = " ".join(f"{glyphs[i % len(glyphs)]} {label}" for i, label in enumerate(series_dict))
277
+ lines.append(" " * 10 + legend)
278
+ lines.append(" " * 10 + f"t: 0 \u2192 {times[-1]:.3g}s ({y_label})")
279
+ return lines
@@ -0,0 +1,337 @@
1
+ """
2
+ CCT centralized command registry (spec section 14).
3
+
4
+ Before this module, commands existed as: an entry in
5
+ commands_data.py's COMMANDS list (name + one-line description only),
6
+ a separate elif branch in app.py's dispatch chain, a separate entry
7
+ in fallback_cli.py, and sometimes a separate tool entry in agent.py's
8
+ TOOLS dict for AI-mode invocation — four places to touch per command,
9
+ with no shared notion of category, aliases, icon, shortcut, required
10
+ permissions, which AI modes can invoke it, or example usage. That's
11
+ the exact "hardcoded across multiple files" problem section 14 names.
12
+
13
+ This module gives every command ONE CommandSpec with all of those
14
+ fields, and ONE registry to look them up by name, alias, category, or
15
+ free-text search (for autocomplete). It does not yet replace app.py's
16
+ dispatch chain (that's real surgery — see CHANGELOG_v0.6.3) but it IS
17
+ the real source of truth for the palette/help text: `bootstrap()`
18
+ below builds full CommandSpec entries for every command in
19
+ commands_data.COMMANDS right now (migrating the existing data, not
20
+ duplicating it) and layers on rich metadata for the commands that
21
+ already have it elsewhere in the codebase (permissions.py's keys,
22
+ agent.py's TOOLS, the SHORT_ALIASES map in app.py) so those fields are
23
+ real, not guessed.
24
+ """
25
+
26
+ from dataclasses import dataclass, field
27
+ from typing import Callable, Optional
28
+
29
+
30
+ @dataclass
31
+ class CommandSpec:
32
+ name: str # canonical, e.g. "/atomsim"
33
+ description: str = ""
34
+ category: str = "general"
35
+ aliases: list = field(default_factory=list)
36
+ icon: str = "\u2022" # bullet by default
37
+ shortcut: Optional[str] = None # e.g. "Ctrl+Shift+N"
38
+ permissions: list = field(default_factory=list) # keys from permissions.PERMISSION_DEFS
39
+ ai_modes: list = field(default_factory=lambda: ["notebook"]) # which AI modes may invoke it
40
+ examples: list = field(default_factory=list)
41
+ autocomplete_keywords: list = field(default_factory=list)
42
+ handler: Optional[Callable] = None # bound at registration time by app.py, if any
43
+ arg_style: Optional[str] = None # None | "split_optional" — see ARG_STYLE_MAP below
44
+
45
+
46
+ # name -> category, used to bootstrap from the existing flat COMMANDS list
47
+ CATEGORY_MAP = {
48
+ "/help": "general", "/about": "general", "/shortcuts": "general", "/settings": "general",
49
+ "/history": "general", "/memory": "general", "/upload": "general", "/voice": "general",
50
+ "/permissions": "general", "/browser": "general", "/browse": "general", "/cat": "general",
51
+ "/calculator": "calculation", "/solve": "calculation", "/formulas": "calculation",
52
+ "/kinetics": "calculation", "/electrochemistry": "calculation", "/random": "calculation",
53
+ "/derive": "calculation",
54
+ "/atomsim": "simulation", "/orbitals": "simulation", "/orbitalgrid": "simulation",
55
+ "/bonding": "simulation", "/bloch": "simulation", "/sim3d": "simulation", "/gpu3d": "simulation", "/graph": "simulation",
56
+ "/ai": "ai", "/agent": "ai", "/ai-verify": "ai",
57
+ "/mode": "ai", "/notebook": "ai", "/build": "ai", "/plan": "ai",
58
+ "/game": "misc",
59
+ }
60
+
61
+ # hand-authored rich metadata for the commands that most need it right
62
+ # now (the ones this session touched, plus the highest-traffic core
63
+ # ones) — everything else in commands_data.COMMANDS still gets a real
64
+ # CommandSpec via bootstrap(), just without hand-tuned icon/shortcut/
65
+ # examples yet. That gap is intentional and tracked, not hidden.
66
+ RICH_METADATA = {
67
+ "/atomsim": dict(
68
+ icon="\u269b", aliases=["/at"], permissions=["execute_python"],
69
+ ai_modes=["notebook", "agent", "build"],
70
+ examples=["/atomsim Fe", "/atomsim 26"],
71
+ autocomplete_keywords=["atom", "nucleus", "proton", "neutron", "bohr", "element"],
72
+ ),
73
+ "/orbitals": dict(
74
+ icon="\u2601", aliases=["/o"], permissions=["execute_python"],
75
+ ai_modes=["notebook", "agent", "build"],
76
+ examples=["/orbitals 2p", "/orbitals 3d"],
77
+ autocomplete_keywords=["orbital", "electron cloud", "probability density", "psi"],
78
+ ),
79
+ "/orbitalgrid": dict(
80
+ icon="\U0001f4ca", permissions=["execute_python"],
81
+ ai_modes=["notebook", "agent", "build"],
82
+ examples=["/orbitalgrid"],
83
+ autocomplete_keywords=["orbital grid", "hydrogen wave function", "reference chart"],
84
+ ),
85
+ "/bonding": dict(
86
+ icon="\U0001f9ea", permissions=["execute_python"],
87
+ ai_modes=["notebook", "agent", "build"],
88
+ examples=["/bonding furan", "/bonding water"],
89
+ autocomplete_keywords=["elf", "electron localization", "bonding", "molecule density"],
90
+ ),
91
+ "/bloch": dict(
92
+ icon="\u269b", permissions=["execute_python"],
93
+ ai_modes=["notebook", "agent", "build"],
94
+ examples=["/bloch", "/bloch 90 0", "/bloch 90 90"],
95
+ autocomplete_keywords=["bloch sphere", "qubit", "quantum state", "superposition"],
96
+ ),
97
+ "/solve": dict(
98
+ icon="\U0001f9ee", aliases=["/s"], permissions=["calculator"],
99
+ ai_modes=["notebook", "agent", "build", "plan"],
100
+ examples=["/solve"],
101
+ autocomplete_keywords=["solve", "formula", "algebra"],
102
+ ),
103
+ "/settings": dict(
104
+ icon="\u2699", permissions=[],
105
+ ai_modes=["notebook", "agent", "build", "plan"],
106
+ examples=["/settings"],
107
+ autocomplete_keywords=["settings", "config", "preferences"],
108
+ ),
109
+ "/mode": dict(
110
+ icon="\U0001f501", permissions=[],
111
+ ai_modes=["notebook", "agent", "build", "plan"],
112
+ examples=["/mode", "/mode build"],
113
+ autocomplete_keywords=["mode", "ai mode", "persona", "notebook", "agent", "build", "plan"],
114
+ ),
115
+ "/notebook": dict(
116
+ icon="\U0001f4d8", permissions=[],
117
+ ai_modes=["notebook", "agent", "build", "plan"],
118
+ examples=["/notebook"],
119
+ autocomplete_keywords=["notebook mode", "chemistry", "physics", "math", "daily ai"],
120
+ ),
121
+ "/agent": dict(
122
+ icon="\u2699", permissions=["execute_python"],
123
+ ai_modes=["notebook", "agent", "build", "plan"],
124
+ examples=["/agent"],
125
+ autocomplete_keywords=["agent mode", "autonomous", "tool use", "simulate", "solve"],
126
+ ),
127
+ "/build": dict(
128
+ icon="\U0001f528", permissions=[],
129
+ ai_modes=["notebook", "agent", "build", "plan"],
130
+ examples=["/build"],
131
+ autocomplete_keywords=["build mode", "code", "software", "debug", "architecture", "api"],
132
+ ),
133
+ "/plan": dict(
134
+ icon="\U0001f9ed", permissions=[],
135
+ ai_modes=["notebook", "agent", "build", "plan"],
136
+ examples=["/plan"],
137
+ autocomplete_keywords=["plan mode", "roadmap", "brainstorm", "prototype", "study plan"],
138
+ ),
139
+ "/research": dict(
140
+ icon="\U0001f50d", permissions=[],
141
+ ai_modes=["notebook", "research", "agent", "build", "plan"],
142
+ examples=["/research"],
143
+ autocomplete_keywords=["research mode", "deep research", "web search", "literature"],
144
+ ),
145
+ "/debug": dict(
146
+ icon="\U0001f41b", permissions=[],
147
+ ai_modes=["notebook", "debugger", "agent", "build", "plan"],
148
+ examples=["/debug"],
149
+ autocomplete_keywords=["debugger mode", "debug", "fix", "error", "traceback"],
150
+ ),
151
+ "/install": dict(
152
+ icon="\U0001f4e6", permissions=["install_packages"],
153
+ ai_modes=["build"],
154
+ examples=["/install requests", "/install numpy", "/install torch"],
155
+ autocomplete_keywords=["install", "package", "pip", "npm", "dependency", "library"],
156
+ ),
157
+ "/packages": dict(
158
+ icon="\U0001f4e6", permissions=["install_packages"],
159
+ ai_modes=["build"],
160
+ examples=["/packages", "/packages forget numpy"],
161
+ autocomplete_keywords=["package manager", "dashboard", "remembered decisions", "forget"],
162
+ ),
163
+ "/pipeline": dict(
164
+ icon="\U0001f5fa", permissions=[],
165
+ ai_modes=["notebook", "agent", "build", "plan"],
166
+ examples=["/pipeline build me a molecule viewer script"],
167
+ autocomplete_keywords=["pipeline", "planner", "research", "build", "verify", "stages"],
168
+ ),
169
+ "/orchestrate": dict(
170
+ icon="\U0001f465", permissions=[],
171
+ ai_modes=["notebook", "agent", "build", "plan"],
172
+ examples=["/orchestrate build a chemistry dashboard"],
173
+ autocomplete_keywords=["orchestrator", "coordinator", "specialists", "team", "agents"],
174
+ ),
175
+ "/devices": dict(
176
+ icon="\U0001f4f1", permissions=["device_control"],
177
+ ai_modes=["notebook", "agent", "build", "plan"],
178
+ examples=["/devices", "/devices terminal run 'git status'"],
179
+ autocomplete_keywords=["devices", "device control", "terminal", "android", "desktop"],
180
+ ),
181
+ "/workspace": dict(
182
+ icon="\U0001f5c2", permissions=[],
183
+ ai_modes=["notebook", "agent", "build", "plan"],
184
+ examples=["/workspace", "/workspace open C:\\projects\\app",
185
+ "/workspace list", "/workspace detect"],
186
+ autocomplete_keywords=["workspace", "detect", "open folder", "recent",
187
+ "pinned", "favorite", "rename", "search"],
188
+ ),
189
+ "/browser": dict(
190
+ icon="\U0001f30d", aliases=["/browse", "/cat"], permissions=[],
191
+ ai_modes=["notebook", "agent", "build", "plan"],
192
+ examples=["/browser", "/cat", "/cat https://example.com", "/cat search cats on mars"],
193
+ autocomplete_keywords=["browser", "browse", "cat", "web", "surf", "terminal browser", "duckduckgo", "fomoji browser"],
194
+ ),
195
+ "/browse": dict(
196
+ icon="\U0001f30d", aliases=[], permissions=[],
197
+ ai_modes=["notebook", "agent", "build", "plan"],
198
+ examples=["/browse https://example.com", "/browse search cats"],
199
+ autocomplete_keywords=["browse", "browser", "surf", "open url", "search"],
200
+ ),
201
+ "/cat": dict(
202
+ icon="\U0001f30d", aliases=[], permissions=[],
203
+ ai_modes=["notebook", "agent", "build", "plan"],
204
+ examples=["/cat", "/cat https://example.com", "/cat search cats on mars"],
205
+ autocomplete_keywords=["cat", "browser", "browse", "web", "chromium", "qt webengine"],
206
+ ),
207
+ }
208
+
209
+
210
+ # Canonical alias -> full-command table (spec section 14: one place,
211
+ # not duplicated). This WAS a hardcoded dict living only in app.py's
212
+ # App.SHORT_ALIASES class attribute; app.py now derives that attribute
213
+ # from short_alias_map() below instead of declaring its own copy — the
214
+ # mapping itself hasn't changed, only where it's defined.
215
+ ALIAS_TABLE = {
216
+ "/h": "/help", "/c": "/calculator", "/s": "/solve", "/f": "/formulas",
217
+ "/k": "/kinetics", "/d": "/derive", "/g": "/graph", "/a": "/about",
218
+ "/gm": "/game", "/pg": "/game",
219
+ "/e": "/electrochemistry", "/r": "/random", "/x": "/exit",
220
+ "/o": "/orbitals", "/u": "/upload", "/v": "/voice", "/3": "/sim3d",
221
+ "/ag": "/agent", "/y": "/history", "/st": "/settings", "/sh": "/shortcuts",
222
+ "/at": "/atomsim", "/vf": "/verify", "/?": "/help", "/m": "/memory",
223
+ "/cp": "/codepad", "/tk": "/tokens", "/th": "/theme", "/ws": "/websearch",
224
+ "/rs": "/research", "/im": "/import", "/pt": "/pet", "/cc": "/copycode",
225
+ "/rx": "/react", "/ex": "/export", "/tu": "/tui", "/co": "/composer",
226
+ "/br": "/browser", "/bw": "/browse", "/cat": "/browser",
227
+ }
228
+
229
+
230
+ def short_alias_map():
231
+ """Return the alias -> canonical-command dict, exactly the shape
232
+ app.py's SHORT_ALIASES used to hardcode."""
233
+ return dict(ALIAS_TABLE)
234
+
235
+
236
+ class CommandRegistry:
237
+ def __init__(self):
238
+ self._by_name = {}
239
+ self._by_alias = {}
240
+
241
+ def register(self, spec: CommandSpec):
242
+ self._by_name[spec.name] = spec
243
+ for alias in spec.aliases:
244
+ self._by_alias[alias] = spec.name
245
+
246
+ def get(self, name_or_alias):
247
+ if name_or_alias in self._by_name:
248
+ return self._by_name[name_or_alias]
249
+ canonical = self._by_alias.get(name_or_alias)
250
+ return self._by_name.get(canonical) if canonical else None
251
+
252
+ def all(self):
253
+ return list(self._by_name.values())
254
+
255
+ def by_category(self, category):
256
+ return [s for s in self._by_name.values() if s.category == category]
257
+
258
+ def categories(self):
259
+ seen = []
260
+ for s in self._by_name.values():
261
+ if s.category not in seen:
262
+ seen.append(s.category)
263
+ return seen
264
+
265
+ def usable_in_mode(self, ai_mode):
266
+ return [s for s in self._by_name.values() if ai_mode in s.ai_modes]
267
+
268
+ def search(self, query):
269
+ """Free-text match against name, description, and autocomplete
270
+ keywords — what a command-palette type-ahead would call."""
271
+ q = query.strip().lower()
272
+ if not q:
273
+ return self.all()
274
+ out = []
275
+ for s in self._by_name.values():
276
+ hay = " ".join([s.name, s.description] + s.autocomplete_keywords).lower()
277
+ if q in hay:
278
+ out.append(s)
279
+ return out
280
+
281
+
282
+ # Commands matching this exact pattern in app.py's old elif chain:
283
+ # if low == "/X": self.cmd_X(); return
284
+ # if low.startswith("/X "): self.cmd_X(raw.split(" ", 1)[1].strip()); return
285
+ # i.e. zero args or one stripped trailing-text arg, same method either
286
+ # way. Verified against app.py line-by-line before being added here —
287
+ # see CHANGELOG_v0.6.3_architecture.md.
288
+ ARG_STYLE_MAP = {
289
+ "/memory": "split_optional",
290
+ "/model": "split_optional",
291
+ "/theme": "split_optional",
292
+ "/websearch": "split_optional",
293
+ "/research": "split_optional",
294
+ "/import": "split_optional",
295
+ "/react": "split_optional",
296
+ "/export": "split_optional",
297
+ "/workspace": "split_optional",
298
+ "/permissions": "split_optional",
299
+ "/browser": "split_optional",
300
+ "/browse": "split_optional",
301
+ "/cat": "split_optional",
302
+ "/vision": "split_optional",
303
+ "/vscode": "split_optional",
304
+ }
305
+
306
+
307
+ def bootstrap():
308
+ """Build a full CommandRegistry from commands_data.COMMANDS (the
309
+ existing real command list), layering in RICH_METADATA where it
310
+ exists. Real migration of real data, not a mock catalog."""
311
+ from . import commands_data
312
+ reverse_alias = {}
313
+ for alias, full in ALIAS_TABLE.items():
314
+ reverse_alias.setdefault(full, []).append(alias)
315
+
316
+ reg = CommandRegistry()
317
+ for name, desc in commands_data.COMMANDS:
318
+ extra = dict(RICH_METADATA.get(name, {}))
319
+ category = CATEGORY_MAP.get(name, "general")
320
+ canonical_aliases = reverse_alias.get(name, [])
321
+ hand_aliases = extra.pop("aliases", [])
322
+ merged_aliases = list(dict.fromkeys(canonical_aliases + hand_aliases)) # dedup, preserve order
323
+ arg_style = ARG_STYLE_MAP.get(name)
324
+ spec = CommandSpec(name=name, description=desc, category=category,
325
+ aliases=merged_aliases, arg_style=arg_style, **extra)
326
+ reg.register(spec)
327
+ return reg
328
+
329
+
330
+ _registry = None
331
+
332
+
333
+ def get_registry():
334
+ global _registry
335
+ if _registry is None:
336
+ _registry = bootstrap()
337
+ return _registry