cct-cli 0.7.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calc_terminal/__init__.py +14 -0
- calc_terminal/__main__.py +14 -0
- calc_terminal/activity.py +1334 -0
- calc_terminal/agent.py +3387 -0
- calc_terminal/agent_runtime.py +519 -0
- calc_terminal/ai_context.py +447 -0
- calc_terminal/ai_modes.py +752 -0
- calc_terminal/ai_personalization.py +286 -0
- calc_terminal/ai_preview_feedback.py +213 -0
- calc_terminal/aicore.py +2572 -0
- calc_terminal/anim.py +367 -0
- calc_terminal/app.py +3685 -0
- calc_terminal/art.py +639 -0
- calc_terminal/atomsim.py +368 -0
- calc_terminal/attachments.py +743 -0
- calc_terminal/benchmark_system.py +414 -0
- calc_terminal/browser/__init__.py +36 -0
- calc_terminal/browser/browser_state.py +346 -0
- calc_terminal/browser/devserver.py +176 -0
- calc_terminal/browser/engine.py +494 -0
- calc_terminal/browser/navigation.py +84 -0
- calc_terminal/browser/preview.py +429 -0
- calc_terminal/browser/preview_entry.py +95 -0
- calc_terminal/browser/project_detector.py +144 -0
- calc_terminal/browser/server.py +449 -0
- calc_terminal/browser/state.py +75 -0
- calc_terminal/browser/watcher.py +99 -0
- calc_terminal/browser_gui/__init__.py +1 -0
- calc_terminal/browser_gui/__main__.py +3 -0
- calc_terminal/browser_gui/launcher.py +173 -0
- calc_terminal/browser_gui/playwright_browser.py +117 -0
- calc_terminal/browser_gui/qt_browser.py +1501 -0
- calc_terminal/browser_gui/webview_browser.py +57 -0
- calc_terminal/capabilities/__init__.py +35 -0
- calc_terminal/capabilities/adapters/__init__.py +33 -0
- calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
- calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
- calc_terminal/capabilities/adapters/filesystem.py +206 -0
- calc_terminal/capabilities/adapters/git_adapter.py +202 -0
- calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
- calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
- calc_terminal/capabilities/adapters/platforms.py +200 -0
- calc_terminal/capabilities/adapters/python_exec.py +93 -0
- calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
- calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
- calc_terminal/capabilities/adapters/structural_bio.py +161 -0
- calc_terminal/capabilities/adapters/terminal.py +99 -0
- calc_terminal/capabilities/bus.py +178 -0
- calc_terminal/capabilities/discovery.py +207 -0
- calc_terminal/capabilities/schema.py +221 -0
- calc_terminal/cat.ico +0 -0
- calc_terminal/cat_browser.py +2018 -0
- calc_terminal/chat_store.py +703 -0
- calc_terminal/cli.py +1178 -0
- calc_terminal/code_editor.py +640 -0
- calc_terminal/collaboration.py +723 -0
- calc_terminal/commands_data.py +139 -0
- calc_terminal/compatibility_engine.py +352 -0
- calc_terminal/compute/__init__.py +31 -0
- calc_terminal/compute/fabric.py +350 -0
- calc_terminal/config.py +227 -0
- calc_terminal/core/__init__.py +41 -0
- calc_terminal/core/checkpoint.py +156 -0
- calc_terminal/core/input/__init__.py +45 -0
- calc_terminal/core/mode_registry.py +300 -0
- calc_terminal/core/project_graph.py +172 -0
- calc_terminal/core/recovery.py +129 -0
- calc_terminal/core/security_layer.py +112 -0
- calc_terminal/core/task_graph.py +202 -0
- calc_terminal/core/unified_runtime.py +184 -0
- calc_terminal/core/verification.py +257 -0
- calc_terminal/customization.py +1566 -0
- calc_terminal/derivations.py +153 -0
- calc_terminal/device_control.py +263 -0
- calc_terminal/diagnostics/__init__.py +27 -0
- calc_terminal/diagnostics/doctor_engine.py +382 -0
- calc_terminal/diagnostics/self_test.py +247 -0
- calc_terminal/doctor.py +519 -0
- calc_terminal/easter_eggs.py +274 -0
- calc_terminal/editor/__init__.py +1 -0
- calc_terminal/editor/actions.py +263 -0
- calc_terminal/editor/commands.py +160 -0
- calc_terminal/editor/shortcuts.py +226 -0
- calc_terminal/engine.py +259 -0
- calc_terminal/errors.py +120 -0
- calc_terminal/event_stream.py +146 -0
- calc_terminal/eventbus.py +133 -0
- calc_terminal/extensions.py +733 -0
- calc_terminal/fallback_cli.py +1321 -0
- calc_terminal/first_run.py +265 -0
- calc_terminal/fomoji_auth.py +1043 -0
- calc_terminal/formulas.py +82 -0
- calc_terminal/fs_cache.py +121 -0
- calc_terminal/fs_watcher.py +277 -0
- calc_terminal/game.py +193 -0
- calc_terminal/gen1.py +5 -0
- calc_terminal/generators.py +245 -0
- calc_terminal/gestures/__init__.py +42 -0
- calc_terminal/gestures/bindings.py +175 -0
- calc_terminal/gestures/manager.py +477 -0
- calc_terminal/goodbye.py +363 -0
- calc_terminal/gpu3d.py +290 -0
- calc_terminal/graphs.py +358 -0
- calc_terminal/hardware_analyzer.py +440 -0
- calc_terminal/host/__init__.py +30 -0
- calc_terminal/host/browser_manager.py +187 -0
- calc_terminal/host/desktop.py +1386 -0
- calc_terminal/host/launcher.py +395 -0
- calc_terminal/host/terminal.py +279 -0
- calc_terminal/identity.py +216 -0
- calc_terminal/input/__init__.py +54 -0
- calc_terminal/input/capabilities.py +258 -0
- calc_terminal/input/focus.py +87 -0
- calc_terminal/input/gestures.py +64 -0
- calc_terminal/input/pointer.py +114 -0
- calc_terminal/input/touch.py +345 -0
- calc_terminal/keys.py +84 -0
- calc_terminal/live_automation.py +165 -0
- calc_terminal/mathtext.py +433 -0
- calc_terminal/mcp.py +386 -0
- calc_terminal/memory.py +337 -0
- calc_terminal/memory_v2.py +479 -0
- calc_terminal/metrics.py +333 -0
- calc_terminal/mode_detection.py +146 -0
- calc_terminal/model.py +2431 -0
- calc_terminal/model_router.py +665 -0
- calc_terminal/models/__init__.py +0 -0
- calc_terminal/models/active_state.py +187 -0
- calc_terminal/models/dynamic_registry.py +584 -0
- calc_terminal/models/manager.py +781 -0
- calc_terminal/models/model_metadata.json +3526 -0
- calc_terminal/models/profiles.py +194 -0
- calc_terminal/models/registry.py +265 -0
- calc_terminal/models/schema.py +197 -0
- calc_terminal/models/validator.py +287 -0
- calc_terminal/models/verification_engine.py +368 -0
- calc_terminal/native_picker.py +215 -0
- calc_terminal/ollama_catalog.py +279 -0
- calc_terminal/ollama_download.py +233 -0
- calc_terminal/orchestrator.py +304 -0
- calc_terminal/package_research.py +322 -0
- calc_terminal/packages.py +1024 -0
- calc_terminal/pc_specs.py +116 -0
- calc_terminal/permissions.py +334 -0
- calc_terminal/pet.py +106 -0
- calc_terminal/pipeline.py +505 -0
- calc_terminal/platform/__init__.py +491 -0
- calc_terminal/platform/desktop.py +491 -0
- calc_terminal/platform/web.py +781 -0
- calc_terminal/preview/__init__.py +1 -0
- calc_terminal/preview/dev_server.py +303 -0
- calc_terminal/preview/diagnostics.py +131 -0
- calc_terminal/preview/live_reload.py +66 -0
- calc_terminal/preview/manager.py +129 -0
- calc_terminal/project_stats.py +209 -0
- calc_terminal/projects.py +328 -0
- calc_terminal/providers/__init__.py +0 -0
- calc_terminal/providers/adapters/__init__.py +80 -0
- calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
- calc_terminal/providers/adapters/base.py +106 -0
- calc_terminal/providers/adapters/chinese_adapters.py +420 -0
- calc_terminal/providers/adapters/gemini_adapter.py +101 -0
- calc_terminal/providers/adapters/ollama_adapter.py +83 -0
- calc_terminal/providers/adapters/openai_adapter.py +159 -0
- calc_terminal/providers/adapters/other_adapters.py +246 -0
- calc_terminal/providers/anthropic_provider.py +172 -0
- calc_terminal/providers/auto_update.py +416 -0
- calc_terminal/providers/base_provider.py +105 -0
- calc_terminal/providers/discovery_manager.py +207 -0
- calc_terminal/providers/gemini_provider.py +178 -0
- calc_terminal/providers/lifecycle.py +767 -0
- calc_terminal/providers/ollama_adapter.py +707 -0
- calc_terminal/providers/openai_provider.py +254 -0
- calc_terminal/providers/provider_manager.py +1827 -0
- calc_terminal/providers/providers.json +4075 -0
- calc_terminal/reactionsim.py +279 -0
- calc_terminal/registry.py +337 -0
- calc_terminal/report.py +162 -0
- calc_terminal/research/__init__.py +45 -0
- calc_terminal/research/artifact_intel.py +126 -0
- calc_terminal/research/data_lineage.py +123 -0
- calc_terminal/research/experiment_ledger.py +303 -0
- calc_terminal/research/reproducibility.py +131 -0
- calc_terminal/resilience/__init__.py +47 -0
- calc_terminal/resilience/agent_state.py +121 -0
- calc_terminal/resilience/capability_matcher.py +174 -0
- calc_terminal/resilience/circuit_breaker.py +158 -0
- calc_terminal/resilience/failover_engine.py +230 -0
- calc_terminal/resilience/health_monitor.py +192 -0
- calc_terminal/resilience/ollama_adapter.py +125 -0
- calc_terminal/resilience/orchestrator.py +312 -0
- calc_terminal/resilience/types.py +134 -0
- calc_terminal/sandbox.py +98 -0
- calc_terminal/scires.py +558 -0
- calc_terminal/security_scanner.py +126 -0
- calc_terminal/session.py +294 -0
- calc_terminal/sim3d.py +206 -0
- calc_terminal/solver.py +276 -0
- calc_terminal/sound.py +127 -0
- calc_terminal/task_reports.py +287 -0
- calc_terminal/terminal_host.py +201 -0
- calc_terminal/terminal_identity.py +411 -0
- calc_terminal/test_ai_mode_reliability.py +344 -0
- calc_terminal/test_browser.py +368 -0
- calc_terminal/test_code_editor_upgrade.py +485 -0
- calc_terminal/test_customization.py +1148 -0
- calc_terminal/test_customization_ui.py +612 -0
- calc_terminal/test_dynamic_registry.py +304 -0
- calc_terminal/test_extensions.py +436 -0
- calc_terminal/test_overhaul.py +557 -0
- calc_terminal/test_project_detect.py +255 -0
- calc_terminal/test_root_cause_fix.py +527 -0
- calc_terminal/test_stability.py +532 -0
- calc_terminal/test_terminal_identity.py +132 -0
- calc_terminal/test_v079_speed.py +460 -0
- calc_terminal/theme.py +1107 -0
- calc_terminal/timeline.py +139 -0
- calc_terminal/todos.py +246 -0
- calc_terminal/tool_call_normalizer.py +419 -0
- calc_terminal/tui.py +104 -0
- calc_terminal/ui/__init__.py +8 -0
- calc_terminal/ui/activity_panel.py +231 -0
- calc_terminal/ui/activity_stream_panel.py +238 -0
- calc_terminal/ui/animations.py +122 -0
- calc_terminal/ui/app.py +7271 -0
- calc_terminal/ui/attach_panel.py +597 -0
- calc_terminal/ui/attachments.py +424 -0
- calc_terminal/ui/backup_panel.py +810 -0
- calc_terminal/ui/browser_shell.py +887 -0
- calc_terminal/ui/cat_agent.py +357 -0
- calc_terminal/ui/chats_panel.py +899 -0
- calc_terminal/ui/command_palette.py +125 -0
- calc_terminal/ui/command_palette_modal.py +166 -0
- calc_terminal/ui/composer.py +1141 -0
- calc_terminal/ui/context_menu.py +197 -0
- calc_terminal/ui/conversation.py +1435 -0
- calc_terminal/ui/customization_panel.py +1229 -0
- calc_terminal/ui/dashboard.py +404 -0
- calc_terminal/ui/design_system.py +557 -0
- calc_terminal/ui/diff_panel.py +213 -0
- calc_terminal/ui/editor.py +2102 -0
- calc_terminal/ui/empty_state.py +302 -0
- calc_terminal/ui/events.py +487 -0
- calc_terminal/ui/extensions_panel.py +815 -0
- calc_terminal/ui/footer.py +166 -0
- calc_terminal/ui/gestures_panel.py +383 -0
- calc_terminal/ui/goodbye_screen.py +100 -0
- calc_terminal/ui/header.py +1034 -0
- calc_terminal/ui/help_panel.py +254 -0
- calc_terminal/ui/live_activities.py +914 -0
- calc_terminal/ui/mcp_panel.py +570 -0
- calc_terminal/ui/memory_center.py +524 -0
- calc_terminal/ui/mode_colors_panel.py +525 -0
- calc_terminal/ui/nav_screens.py +747 -0
- calc_terminal/ui/ollama_panel.py +536 -0
- calc_terminal/ui/palette.py +221 -0
- calc_terminal/ui/permission_panel.py +269 -0
- calc_terminal/ui/personalization_panel.py +517 -0
- calc_terminal/ui/personalize_center.py +1568 -0
- calc_terminal/ui/preview_panel.py +441 -0
- calc_terminal/ui/resizers.py +402 -0
- calc_terminal/ui/sidebar.py +1285 -0
- calc_terminal/ui/statusbar.py +168 -0
- calc_terminal/ui/theme_css.py +1396 -0
- calc_terminal/ui/thinking.py +226 -0
- calc_terminal/ui/timeline_panel.py +102 -0
- calc_terminal/ui/todo_panel.py +193 -0
- calc_terminal/ui/viewport.py +136 -0
- calc_terminal/ui/vision_panel.py +489 -0
- calc_terminal/ui/welcome_modal.py +343 -0
- calc_terminal/ui/widgets.py +160 -0
- calc_terminal/ui/workspace.py +831 -0
- calc_terminal/viewers/__init__.py +1 -0
- calc_terminal/viewers/document_viewer.py +252 -0
- calc_terminal/viewers/image_viewer.py +241 -0
- calc_terminal/viewers/pdf_viewer.py +203 -0
- calc_terminal/viewers/presentation_viewer.py +164 -0
- calc_terminal/viewers/registry.py +120 -0
- calc_terminal/viewers/spreadsheet_viewer.py +204 -0
- calc_terminal/vision/__init__.py +89 -0
- calc_terminal/vision/analysis.py +194 -0
- calc_terminal/vision/annotations.py +297 -0
- calc_terminal/vision/capture.py +171 -0
- calc_terminal/vision/context.py +231 -0
- calc_terminal/vision/correlation.py +169 -0
- calc_terminal/vision/cursor.py +258 -0
- calc_terminal/vision/events.py +66 -0
- calc_terminal/vision/frame_pipeline.py +259 -0
- calc_terminal/vision/priority.py +218 -0
- calc_terminal/vision/provider.py +180 -0
- calc_terminal/vision/safety.py +149 -0
- calc_terminal/vision/session.py +281 -0
- calc_terminal/vision/verify.py +162 -0
- calc_terminal/vision.py +514 -0
- calc_terminal/vscode_integration.py +113 -0
- calc_terminal/web/__init__.py +8 -0
- calc_terminal/web/cat_runtime.py +710 -0
- calc_terminal/web/server.py +2891 -0
- calc_terminal/web/static/css/app.css +3152 -0
- calc_terminal/web/static/icons/badge-72.png +0 -0
- calc_terminal/web/static/icons/cat.ico +0 -0
- calc_terminal/web/static/icons/icon-128.png +0 -0
- calc_terminal/web/static/icons/icon-144.png +0 -0
- calc_terminal/web/static/icons/icon-152.png +0 -0
- calc_terminal/web/static/icons/icon-192.png +0 -0
- calc_terminal/web/static/icons/icon-384.png +0 -0
- calc_terminal/web/static/icons/icon-512.png +0 -0
- calc_terminal/web/static/icons/icon-72.png +0 -0
- calc_terminal/web/static/icons/icon-96.png +0 -0
- calc_terminal/web/static/icons/icon.svg +34 -0
- calc_terminal/web/static/icons/new-project.png +0 -0
- calc_terminal/web/static/icons/open-project.png +0 -0
- calc_terminal/web/static/index.html +734 -0
- calc_terminal/web/static/js/app.js +2403 -0
- calc_terminal/web/static/manifest.json +88 -0
- calc_terminal/web/static/sw.js +230 -0
- calc_terminal/workflow_engine.py +769 -0
- calc_terminal/workspace.py +593 -0
- calc_terminal/workspace_index.py +385 -0
- cct_cli-0.7.9.0.dist-info/METADATA +210 -0
- cct_cli-0.7.9.0.dist-info/RECORD +325 -0
- cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
- cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
- cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
- cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
|
@@ -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
|