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,156 @@
1
+ """
2
+ CAT Checkpoint & Rollback System per §14:
3
+ - Automated snapshot of targeted files before agent modifications
4
+ - Precise git-style unified diff computation
5
+ - One-click safe rollback restoring previous workspace states
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import difflib
11
+ import json
12
+ import os
13
+ import shutil
14
+ import time
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+
19
+
20
+ @dataclass
21
+ class FileSnapshot:
22
+ path: str
23
+ content: str
24
+ existed: bool
25
+
26
+ def to_dict(self) -> Dict[str, Any]:
27
+ return {
28
+ "path": self.path,
29
+ "content": self.content,
30
+ "existed": self.existed,
31
+ }
32
+
33
+
34
+ @dataclass
35
+ class Checkpoint:
36
+ id: str
37
+ description: str
38
+ timestamp: float = field(default_factory=time.time)
39
+ snapshots: Dict[str, FileSnapshot] = field(default_factory=dict)
40
+ applied: bool = True
41
+
42
+ def to_dict(self) -> Dict[str, Any]:
43
+ return {
44
+ "id": self.id,
45
+ "description": self.description,
46
+ "timestamp": self.timestamp,
47
+ "files": list(self.snapshots.keys()),
48
+ "applied": self.applied,
49
+ }
50
+
51
+
52
+ class CheckpointManager:
53
+ """Manages workspace snapshots, diffs, and rollbacks."""
54
+
55
+ def __init__(self, storage_dir: Optional[str] = None):
56
+ self.storage_dir = storage_dir or os.path.join(os.path.expanduser("~"), ".cct_checkpoints")
57
+ self.checkpoints: Dict[str, Checkpoint] = {}
58
+ os.makedirs(self.storage_dir, exist_ok=True)
59
+
60
+ def create_checkpoint(self, description: str, files: List[str]) -> Checkpoint:
61
+ """Capture pre-modification state of specified files."""
62
+ cid = f"chk_{int(time.time())}_{uuid.uuid4().hex[:6]}"
63
+ snapshots = {}
64
+ for f in files:
65
+ norm = os.path.abspath(os.path.expanduser(f))
66
+ if os.path.exists(norm) and os.path.isfile(norm):
67
+ try:
68
+ with open(norm, "r", encoding="utf-8", errors="ignore") as fp:
69
+ content = fp.read()
70
+ snapshots[norm] = FileSnapshot(path=norm, content=content, existed=True)
71
+ except Exception:
72
+ pass
73
+ else:
74
+ snapshots[norm] = FileSnapshot(path=norm, content="", existed=False)
75
+
76
+ chk = Checkpoint(id=cid, description=description, snapshots=snapshots)
77
+ self.checkpoints[cid] = chk
78
+ self._persist_checkpoint(chk)
79
+ return chk
80
+
81
+ def get_diff(self, checkpoint_id: str) -> Dict[str, str]:
82
+ """Compute unified diff for each file in checkpoint against current workspace."""
83
+ chk = self.checkpoints.get(checkpoint_id)
84
+ if not chk:
85
+ return {}
86
+
87
+ diffs = {}
88
+ for path, snap in chk.snapshots.items():
89
+ orig_lines = snap.content.splitlines(keepends=True) if snap.existed else []
90
+ curr_lines = []
91
+ if os.path.exists(path) and os.path.isfile(path):
92
+ try:
93
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
94
+ curr_lines = f.readlines()
95
+ except Exception:
96
+ pass
97
+
98
+ diff = list(
99
+ difflib.unified_diff(
100
+ orig_lines,
101
+ curr_lines,
102
+ fromfile=f"a/{os.path.basename(path)} (checkpoint)",
103
+ tofile=f"b/{os.path.basename(path)} (current)",
104
+ )
105
+ )
106
+ if diff:
107
+ diffs[path] = "".join(diff)
108
+
109
+ return diffs
110
+
111
+ def rollback(self, checkpoint_id: str) -> Tuple[bool, List[str]]:
112
+ """Restore workspace files to their exact checkpoint state."""
113
+ chk = self.checkpoints.get(checkpoint_id)
114
+ if not chk:
115
+ return False, ["Checkpoint not found"]
116
+
117
+ restored_files = []
118
+ errors = []
119
+ for path, snap in chk.snapshots.items():
120
+ try:
121
+ if snap.existed:
122
+ os.makedirs(os.path.dirname(path), exist_ok=True)
123
+ with open(path, "w", encoding="utf-8") as f:
124
+ f.write(snap.content)
125
+ restored_files.append(path)
126
+ else:
127
+ # File was newly created after checkpoint, so remove it
128
+ if os.path.exists(path):
129
+ os.remove(path)
130
+ restored_files.append(f"Deleted {path}")
131
+ except Exception as ex:
132
+ errors.append(f"Failed to restore {path}: {ex}")
133
+
134
+ chk.applied = False
135
+ return len(errors) == 0, restored_files
136
+
137
+ def list_checkpoints(self) -> List[Checkpoint]:
138
+ return sorted(self.checkpoints.values(), key=lambda c: c.timestamp, reverse=True)
139
+
140
+ def _persist_checkpoint(self, chk: Checkpoint):
141
+ try:
142
+ path = os.path.join(self.storage_dir, f"{chk.id}.json")
143
+ data = {
144
+ "id": chk.id,
145
+ "description": chk.description,
146
+ "timestamp": chk.timestamp,
147
+ "applied": chk.applied,
148
+ "snapshots": {p: s.to_dict() for p, s in chk.snapshots.items()},
149
+ }
150
+ with open(path, "w", encoding="utf-8") as f:
151
+ json.dump(data, f, indent=2)
152
+ except Exception:
153
+ pass
154
+
155
+
156
+ checkpoint_manager = CheckpointManager()
@@ -0,0 +1,45 @@
1
+ """
2
+ CAT Core Input Package — proxy re-export of calc_terminal.input.
3
+ """
4
+
5
+ from ...input import (
6
+ DeviceType,
7
+ InputCapabilities,
8
+ detect_capabilities,
9
+ get_input_capabilities,
10
+ set_touch_mode,
11
+ toggle_touch_mode,
12
+ register_listener,
13
+ unregister_listener,
14
+ TouchTapRecognizer,
15
+ TouchScrollHandler,
16
+ TAP_MAX_DISTANCE,
17
+ TAP_MAX_DURATION,
18
+ DOUBLE_TAP_MAX_DELAY,
19
+ LONG_PRESS_DURATION,
20
+ TouchHitZone,
21
+ RESIZER_TOUCH_PADDING,
22
+ TouchFocusManager,
23
+ GestureBridge,
24
+ )
25
+
26
+ __all__ = [
27
+ "DeviceType",
28
+ "InputCapabilities",
29
+ "detect_capabilities",
30
+ "get_input_capabilities",
31
+ "set_touch_mode",
32
+ "toggle_touch_mode",
33
+ "register_listener",
34
+ "unregister_listener",
35
+ "TouchTapRecognizer",
36
+ "TouchScrollHandler",
37
+ "TAP_MAX_DISTANCE",
38
+ "TAP_MAX_DURATION",
39
+ "DOUBLE_TAP_MAX_DELAY",
40
+ "LONG_PRESS_DURATION",
41
+ "TouchHitZone",
42
+ "RESIZER_TOUCH_PADDING",
43
+ "TouchFocusManager",
44
+ "GestureBridge",
45
+ ]
@@ -0,0 +1,300 @@
1
+ """
2
+ CAT Custom Mode Engine & Registry per §2, §3, §55:
3
+ - Preserves the default AI modes (Notebook, Research, Plan, Build, Debugger, Agent)
4
+ - Supports user-created custom modes (e.g. BioLab, QuantumResearch, MLTrainer)
5
+ - Modes request capabilities from Capability Bus, never duplicating tool logic.
6
+ - Modes are installable, removable, editable, exportable, importable, validated, and permission-controlled.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ from dataclasses import dataclass, field, asdict
15
+ from typing import Any, Dict, List, Optional, Tuple, Union
16
+
17
+
18
+ CUSTOM_MODES_PATH = os.path.join(os.path.expanduser("~"), ".cct_custom_modes.json")
19
+
20
+ # Default immutable modes per §2
21
+ DEFAULT_MODE_KEYS = ("notebook", "research", "plan", "build", "debugger", "agent")
22
+
23
+
24
+ @dataclass
25
+ class ModeBehavior:
26
+ planning: str = "standard" # standard, high, adaptive
27
+ verification: str = "standard" # standard, strict, relaxed
28
+ citations: str = "optional" # required, optional, none
29
+ experiment_tracking: bool = False
30
+ coding: str = "standard"
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return asdict(self)
34
+
35
+
36
+ @dataclass
37
+ class ModeSpec:
38
+ name: str
39
+ description: str = ""
40
+ inherits: List[str] = field(default_factory=lambda: ["notebook"])
41
+ capabilities: List[str] = field(default_factory=list)
42
+ behavior: ModeBehavior = field(default_factory=ModeBehavior)
43
+ permissions: List[str] = field(default_factory=list)
44
+ preferred_models: List[str] = field(default_factory=list)
45
+ backup_models: List[str] = field(default_factory=list)
46
+ accent_hex: str = "#38bdf8"
47
+ icon: str = "●"
48
+ system_prompt: str = ""
49
+ enabled: bool = True
50
+ custom: bool = True
51
+
52
+ def to_dict(self) -> Dict[str, Any]:
53
+ return {
54
+ "name": self.name,
55
+ "description": self.description,
56
+ "inherits": self.inherits,
57
+ "capabilities": self.capabilities,
58
+ "behavior": self.behavior.to_dict() if isinstance(self.behavior, ModeBehavior) else self.behavior,
59
+ "permissions": self.permissions,
60
+ "preferred_models": self.preferred_models,
61
+ "backup_models": self.backup_models,
62
+ "accent_hex": self.accent_hex,
63
+ "icon": self.icon,
64
+ "system_prompt": self.system_prompt,
65
+ "enabled": self.enabled,
66
+ "custom": self.custom,
67
+ }
68
+
69
+ @classmethod
70
+ def from_dict(cls, data: Dict[str, Any]) -> ModeSpec:
71
+ beh_data = data.get("behavior", {})
72
+ if isinstance(beh_data, dict):
73
+ behavior = ModeBehavior(
74
+ planning=beh_data.get("planning", "standard"),
75
+ verification=beh_data.get("verification", "standard"),
76
+ citations=beh_data.get("citations", "optional"),
77
+ experiment_tracking=bool(beh_data.get("experiment_tracking", False)),
78
+ coding=beh_data.get("coding", "standard"),
79
+ )
80
+ else:
81
+ behavior = ModeBehavior()
82
+
83
+ return cls(
84
+ name=data["name"],
85
+ description=data.get("description", ""),
86
+ inherits=data.get("inherits", ["notebook"]),
87
+ capabilities=data.get("capabilities", []),
88
+ behavior=behavior,
89
+ permissions=data.get("permissions", []),
90
+ preferred_models=data.get("preferred_models", []),
91
+ backup_models=data.get("backup_models", []),
92
+ accent_hex=data.get("accent_hex", "#38bdf8"),
93
+ icon=data.get("icon", "●"),
94
+ system_prompt=data.get("system_prompt", ""),
95
+ enabled=bool(data.get("enabled", True)),
96
+ custom=bool(data.get("custom", True)),
97
+ )
98
+
99
+
100
+ class ModeRegistry:
101
+ """Registry managing custom user modes alongside project default modes."""
102
+
103
+ def __init__(self, storage_path: str = CUSTOM_MODES_PATH):
104
+ self.storage_path = storage_path
105
+ self._custom_modes: Dict[str, ModeSpec] = {}
106
+ self.load()
107
+
108
+ def is_default_mode(self, name: str) -> bool:
109
+ return name.strip().lower() in DEFAULT_MODE_KEYS
110
+
111
+ def validate_mode(self, data: Dict[str, Any]) -> Tuple[bool, List[str]]:
112
+ errors = []
113
+ name = data.get("name", "").strip()
114
+ if not name:
115
+ errors.append("Mode name cannot be empty")
116
+ elif not re.match(r"^[A-Za-z0-9_\-]+$", name):
117
+ errors.append("Mode name must be alphanumeric with dashes or underscores only")
118
+ elif self.is_default_mode(name):
119
+ errors.append(f"Cannot overwrite default mode '{name}'")
120
+ return (len(errors) == 0), errors
121
+
122
+ def register_mode(
123
+ self,
124
+ spec: Optional[ModeSpec] = None,
125
+ persist: bool = True,
126
+ name: str = "",
127
+ description: str = "",
128
+ system_prompt: str = "",
129
+ icon: str = "●",
130
+ capabilities: Optional[List[str]] = None,
131
+ inherits: Optional[str] = "agent",
132
+ ) -> Tuple[bool, str]:
133
+ if spec is None:
134
+ spec = ModeSpec(
135
+ name=name,
136
+ description=description,
137
+ system_prompt=system_prompt,
138
+ icon=icon,
139
+ capabilities=capabilities or [],
140
+ inherits=inherits,
141
+ )
142
+
143
+ canonical = spec.name.strip().lower()
144
+ if self.is_default_mode(canonical):
145
+ return False, f"Cannot overwrite default mode '{spec.name}'"
146
+
147
+ ok, errs = self.validate_mode(spec.to_dict())
148
+ if not ok:
149
+ return False, "; ".join(errs)
150
+
151
+ self._custom_modes[canonical] = spec
152
+ if persist:
153
+ self.save()
154
+ self._sync_with_ai_modes()
155
+ return True, f"Mode '{spec.name}' registered successfully"
156
+
157
+ def unregister_mode(self, name: str) -> Tuple[bool, str]:
158
+ canonical = name.strip().lower()
159
+ if self.is_default_mode(canonical):
160
+ return False, f"Cannot delete default mode '{name}'"
161
+ if canonical not in self._custom_modes:
162
+ return False, f"Custom mode '{name}' not found"
163
+
164
+ del self._custom_modes[canonical]
165
+ self.save()
166
+ self._sync_with_ai_modes()
167
+ return True, f"Mode '{name}' removed successfully"
168
+
169
+ def get_mode(self, name: str) -> Optional[ModeSpec]:
170
+ """Lookup mode by exact or case-insensitive name."""
171
+ canonical = (name or "").strip().lower()
172
+ return self._custom_modes.get(canonical)
173
+
174
+ def resolve_mode_name(self, query: str) -> Optional[str]:
175
+ """Resolve a mode name from typed command or natural language (e.g., 'Use BioLab mode')."""
176
+ if not query:
177
+ return None
178
+ clean = query.strip()
179
+ # Direct check
180
+ low = clean.lower().lstrip("/")
181
+ if low in DEFAULT_MODE_KEYS:
182
+ return low
183
+ if low in self._custom_modes:
184
+ return self._custom_modes[low].name
185
+
186
+ # Natural language pattern check: e.g. "Use BioLab mode", "switch to Quantum mode"
187
+ m = re.search(r"(?:use|switch to|enter|mode)\s+([A-Za-z0-9_\-]+)", clean, re.IGNORECASE)
188
+ if m:
189
+ cand = m.group(1).lower()
190
+ if cand in DEFAULT_MODE_KEYS:
191
+ return cand
192
+ if cand in self._custom_modes:
193
+ return self._custom_modes[cand].name
194
+
195
+ return None
196
+
197
+ def list_modes(self, include_defaults: bool = True) -> List[Dict[str, Any]]:
198
+ modes = []
199
+ if include_defaults:
200
+ try:
201
+ from .. import ai_modes
202
+ for k in ai_modes.MODE_ORDER:
203
+ m = ai_modes.meta(k)
204
+ modes.append({
205
+ "name": m.get("label", k.title()),
206
+ "key": k,
207
+ "description": m.get("purpose", ""),
208
+ "icon": m.get("icon", "●"),
209
+ "default": True,
210
+ "enabled": True,
211
+ })
212
+ except Exception:
213
+ for k in DEFAULT_MODE_KEYS:
214
+ modes.append({"name": k.title(), "key": k, "default": True, "enabled": True})
215
+
216
+ for spec in self._custom_modes.values():
217
+ modes.append({
218
+ "name": spec.name,
219
+ "key": spec.name.lower(),
220
+ "description": spec.description,
221
+ "icon": spec.icon,
222
+ "default": False,
223
+ "enabled": spec.enabled,
224
+ "capabilities": spec.capabilities,
225
+ "inherits": spec.inherits,
226
+ })
227
+ return modes
228
+
229
+ def export_mode(self, name: str, output_path: str) -> Tuple[bool, str]:
230
+ spec = self.get_mode(name)
231
+ if not spec:
232
+ return False, f"Mode '{name}' not found"
233
+ try:
234
+ with open(output_path, "w", encoding="utf-8") as f:
235
+ json.dump(spec.to_dict(), f, indent=2)
236
+ return True, f"Exported mode '{name}' to {output_path}"
237
+ except Exception as e:
238
+ return False, f"Export failed: {e}"
239
+
240
+ def import_mode(self, source: Union[str, Dict[str, Any]]) -> Tuple[bool, str]:
241
+ try:
242
+ if isinstance(source, str):
243
+ if os.path.exists(source):
244
+ with open(source, "r", encoding="utf-8") as f:
245
+ data = json.load(f)
246
+ else:
247
+ data = json.loads(source)
248
+ else:
249
+ data = source
250
+ spec = ModeSpec.from_dict(data)
251
+ return self.register_mode(spec)
252
+ except Exception as e:
253
+ return False, f"Import failed: {e}"
254
+
255
+ def save(self) -> None:
256
+ try:
257
+ data = {k: spec.to_dict() for k, spec in self._custom_modes.items()}
258
+ with open(self.storage_path, "w", encoding="utf-8") as f:
259
+ json.dump(data, f, indent=2)
260
+ except Exception:
261
+ pass
262
+
263
+ def load(self) -> None:
264
+ if not os.path.exists(self.storage_path):
265
+ return
266
+ try:
267
+ with open(self.storage_path, "r", encoding="utf-8") as f:
268
+ data = json.load(f)
269
+ if isinstance(data, dict):
270
+ for k, v in data.items():
271
+ if not self.is_default_mode(k):
272
+ self._custom_modes[k.lower()] = ModeSpec.from_dict(v)
273
+ self._sync_with_ai_modes()
274
+ except Exception:
275
+ pass
276
+
277
+ def _sync_with_ai_modes(self) -> None:
278
+ """Dynamically synchronize registered custom modes into ai_modes.MODE_META."""
279
+ try:
280
+ from .. import ai_modes
281
+ for spec in self._custom_modes.values():
282
+ if not spec.enabled:
283
+ continue
284
+ k = spec.name.lower()
285
+ rgb = ai_modes._hex_to_rgb(spec.accent_hex) or (140, 140, 240)
286
+ ai_modes.MODE_META[k] = {
287
+ "label": spec.name,
288
+ "icon": spec.icon,
289
+ "accent": rgb,
290
+ "purpose": spec.description or f"Custom mode {spec.name}",
291
+ "gradient": ai_modes._auto_gradient(rgb),
292
+ "custom_spec": spec,
293
+ }
294
+ if k not in ai_modes.MODE_ORDER:
295
+ ai_modes.MODE_ORDER.append(k)
296
+ except Exception:
297
+ pass
298
+
299
+
300
+ mode_registry = ModeRegistry()
@@ -0,0 +1,172 @@
1
+ """
2
+ CAT Project Graph & Workspace Intelligence per §13:
3
+ - Module dependency graph, symbol definitions, call hierarchies, external imports, test mappings
4
+ - AST-based parsing for Python source files
5
+ - Incrementally updated symbol and dependency index
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import os
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional, Set
14
+
15
+
16
+ @dataclass
17
+ class SymbolInfo:
18
+ name: str
19
+ kind: str # function, class, method, variable, import
20
+ file_path: str
21
+ line_number: int
22
+ docstring: Optional[str] = None
23
+ parameters: List[str] = field(default_factory=list)
24
+
25
+ def to_dict(self) -> Dict[str, Any]:
26
+ return {
27
+ "name": self.name,
28
+ "kind": self.kind,
29
+ "file_path": self.file_path,
30
+ "line_number": self.line_number,
31
+ "docstring": self.docstring,
32
+ "parameters": self.parameters,
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class FileNode:
38
+ file_path: str
39
+ symbols: List[SymbolInfo] = field(default_factory=list)
40
+ imports: List[str] = field(default_factory=list)
41
+ imported_by: List[str] = field(default_factory=list)
42
+ is_test_file: bool = False
43
+ associated_sources: List[str] = field(default_factory=list)
44
+
45
+
46
+ class ProjectGraph:
47
+ """Incrementally updated semantic graph of project codebase."""
48
+
49
+ def __init__(self, workspace_root: Optional[str] = None):
50
+ self.workspace_root = os.path.abspath(workspace_root or os.getcwd())
51
+ self.files: Dict[str, FileNode] = {}
52
+ self.symbol_index: Dict[str, List[SymbolInfo]] = {}
53
+
54
+ def scan_directory(self, root_dir: Optional[str] = None, max_files: int = 500):
55
+ """Scan workspace and construct symbol and dependency graph."""
56
+ scan_root = os.path.abspath(root_dir or self.workspace_root)
57
+ count = 0
58
+ for dirpath, dirnames, filenames in os.walk(scan_root):
59
+ # Skip hidden and cache dirs
60
+ dirnames[:] = [d for d in dirnames if not d.startswith((".", "__pycache__", "node_modules", "venv", ".venv"))]
61
+ for fname in filenames:
62
+ if fname.endswith(".py"):
63
+ full_path = os.path.join(dirpath, fname)
64
+ self.index_file(full_path)
65
+ count += 1
66
+ if count >= max_files:
67
+ break
68
+ if count >= max_files:
69
+ break
70
+ self._resolve_dependencies()
71
+
72
+ def index_file(self, file_path: str):
73
+ """Parse file via AST and extract symbols and imports."""
74
+ norm_path = os.path.normpath(file_path)
75
+ rel_path = os.path.relpath(norm_path, self.workspace_root) if norm_path.startswith(self.workspace_root) else norm_path
76
+ is_test = "test" in os.path.basename(norm_path).lower()
77
+
78
+ node = FileNode(file_path=norm_path, is_test_file=is_test)
79
+ try:
80
+ with open(norm_path, "r", encoding="utf-8", errors="ignore") as f:
81
+ content = f.read()
82
+ tree = ast.parse(content, filename=norm_path)
83
+ except Exception:
84
+ self.files[norm_path] = node
85
+ return
86
+
87
+ for stmt in ast.walk(tree):
88
+ if isinstance(stmt, ast.FunctionDef) or isinstance(stmt, ast.AsyncFunctionDef):
89
+ params = [a.arg for a in stmt.args.args]
90
+ doc = ast.get_docstring(stmt)
91
+ sym = SymbolInfo(
92
+ name=stmt.name,
93
+ kind="function",
94
+ file_path=norm_path,
95
+ line_number=stmt.lineno,
96
+ docstring=doc,
97
+ parameters=params,
98
+ )
99
+ node.symbols.append(sym)
100
+ self.symbol_index.setdefault(sym.name, []).append(sym)
101
+ elif isinstance(stmt, ast.ClassDef):
102
+ doc = ast.get_docstring(stmt)
103
+ sym = SymbolInfo(
104
+ name=stmt.name,
105
+ kind="class",
106
+ file_path=norm_path,
107
+ line_number=stmt.lineno,
108
+ docstring=doc,
109
+ )
110
+ node.symbols.append(sym)
111
+ self.symbol_index.setdefault(sym.name, []).append(sym)
112
+ elif isinstance(stmt, ast.Import):
113
+ for alias in stmt.names:
114
+ node.imports.append(alias.name)
115
+ elif isinstance(stmt, ast.ImportFrom):
116
+ if stmt.module:
117
+ node.imports.append(stmt.module)
118
+
119
+ self.files[norm_path] = node
120
+
121
+ def _resolve_dependencies(self):
122
+ """Cross-reference imports to determine internal file dependencies."""
123
+ for path, node in self.files.items():
124
+ for imp in node.imports:
125
+ for target_path in self.files.keys():
126
+ # Check if imported module matches target file path
127
+ mod_name = os.path.splitext(os.path.basename(target_path))[0]
128
+ if imp.endswith(mod_name) and target_path != path:
129
+ if path not in self.files[target_path].imported_by:
130
+ self.files[target_path].imported_by.append(path)
131
+ if node.is_test_file and target_path not in node.associated_sources:
132
+ node.associated_sources.append(target_path)
133
+
134
+ def find_symbols(self, name_query: str) -> List[SymbolInfo]:
135
+ """Find symbols matching name query."""
136
+ results = []
137
+ q = name_query.lower()
138
+ for sym_name, syms in self.symbol_index.items():
139
+ if q in sym_name.lower():
140
+ results.extend(syms)
141
+ return results
142
+
143
+ def get_file_dependencies(self, file_path: str) -> List[str]:
144
+ norm = os.path.normpath(file_path)
145
+ node = self.files.get(norm)
146
+ return node.imports if node else []
147
+
148
+ def get_file_dependents(self, file_path: str) -> List[str]:
149
+ norm = os.path.normpath(file_path)
150
+ node = self.files.get(norm)
151
+ return node.imported_by if node else []
152
+
153
+ def get_test_files_for_source(self, source_path: str) -> List[str]:
154
+ norm = os.path.normpath(source_path)
155
+ base = os.path.splitext(os.path.basename(norm))[0]
156
+ matching_tests = []
157
+ for path, node in self.files.items():
158
+ if node.is_test_file:
159
+ test_base = os.path.basename(path).lower()
160
+ if base.lower() in test_base or norm in node.associated_sources:
161
+ matching_tests.append(path)
162
+ return matching_tests
163
+
164
+ def summarize(self) -> Dict[str, Any]:
165
+ return {
166
+ "total_files_indexed": len(self.files),
167
+ "total_symbols": sum(len(s) for s in self.symbol_index.values()),
168
+ "test_files_count": sum(1 for n in self.files.values() if n.is_test_file),
169
+ }
170
+
171
+
172
+ project_graph = ProjectGraph()