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,733 @@
1
+ """
2
+ CCT Extension Manager — VS Code-style (v0.7.9.10).
3
+
4
+ Central registry for Gestures / CAT Vision / Personalize and future
5
+ third-party extensions. Implements the full lifecycle:
6
+
7
+ NOT_INSTALLED -> INSTALL -> INSTALLED_ENABLED
8
+ INSTALLED_ENABLED -> DISABLE -> INSTALLED_DISABLED
9
+ INSTALLED_DISABLED -> ENABLE -> INSTALLED_ENABLED
10
+ INSTALLED_* -> UNINSTALL -> NOT_INSTALLED
11
+
12
+ Main menu queries is_installed() && is_enabled() — never hard-coded.
13
+
14
+ Persistence: ~/.cct_extensions_registry.json (source of truth) +
15
+ ~/.cct_config.json .extensions for backward compat migration.
16
+ Startup: discover() -> validate -> load enabled -> register features.
17
+
18
+ Features: metadata (name, icon, creator, verified, trusted, size,
19
+ version, category), size from filesystem, publisher identity (individual
20
+ vs company), trusted, VS Code-style UX, error isolation.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import os
27
+ import time
28
+ import threading
29
+ from pathlib import Path
30
+ from typing import Dict, List, Optional, Any
31
+
32
+ # --------------------------------------------------------------------------
33
+ # Built-in extension definitions (available to install). These use the same
34
+ # infrastructure as future third-party extensions — no special hard-coded
35
+ # path for these three.
36
+ # --------------------------------------------------------------------------
37
+
38
+ # Base directory for size calculation (calc_terminal package)
39
+ _PKG_ROOT = Path(__file__).parent
40
+
41
+ def _calc_size(paths: List[Path]) -> int:
42
+ """Sum file sizes for given paths (files or dirs)."""
43
+ total = 0
44
+ for p in paths:
45
+ try:
46
+ if p.is_file():
47
+ total += p.stat().st_size
48
+ elif p.is_dir():
49
+ for f in p.rglob("*"):
50
+ if f.is_file():
51
+ try:
52
+ total += f.stat().st_size
53
+ except Exception:
54
+ pass
55
+ except Exception:
56
+ pass
57
+ return total
58
+
59
+ def _format_size(nbytes: int) -> str:
60
+ if nbytes < 1024:
61
+ return f"{nbytes} B"
62
+ if nbytes < 1024 * 1024:
63
+ return f"{nbytes/1024:.1f} KB"
64
+ return f"{nbytes/1024/1024:.1f} MB"
65
+
66
+ # Built-in extensions — these are the only ones shipped with CAT.
67
+ # Future third-party extensions will be discovered from ~/.cct_extensions/
68
+ # or registry and use the same schema.
69
+ BUILTIN_EXTENSIONS: Dict[str, Dict[str, Any]] = {
70
+ "gestures": {
71
+ "id": "gestures",
72
+ "name": "Gestures",
73
+ "display_name": "Gestures",
74
+ "description": "Advanced gesture controls for CAT — double-click, long-press, swipe and custom shortcuts.",
75
+ "icon": "✋",
76
+ "publisher": "Kazi Zillani",
77
+ "publisher_type": "individual", # individual | company | organization
78
+ "publisher_verified": True,
79
+ "verified": True,
80
+ "trusted": True,
81
+ "version": "1.0.0",
82
+ "category": "Productivity",
83
+ "keywords": ["gestures", "shortcuts", "productivity", "touch"],
84
+ "size_bytes": None, # calculated lazily
85
+ "size_display": None,
86
+ "action": "gestures", # header.NAV_ITEMS action id
87
+ "kind": "builtin",
88
+ },
89
+ "personalize": {
90
+ "id": "personalize",
91
+ "name": "Personalize",
92
+ "display_name": "Personalize",
93
+ "description": "AI personality, tone, custom instructions and memory — make CAT yours.",
94
+ "icon": "🎨",
95
+ "publisher": "Kazi Zillani",
96
+ "publisher_type": "individual",
97
+ "publisher_verified": True,
98
+ "verified": True,
99
+ "trusted": True,
100
+ "version": "1.0.0",
101
+ "category": "Personalization",
102
+ "keywords": ["personalize", "personality", "tone", "instructions", "memory"],
103
+ "size_bytes": None,
104
+ "size_display": None,
105
+ "action": "customize_ai",
106
+ "kind": "builtin",
107
+ },
108
+ }
109
+
110
+ # For size, map each builtin to its source files/dirs
111
+ _BUILTIN_PATHS = {
112
+ "gestures": [_PKG_ROOT / "gestures", _PKG_ROOT / "ui" / "gestures_panel.py"],
113
+ "personalize": [_PKG_ROOT / "ai_personalization.py", _PKG_ROOT / "ui" / "personalize_center.py"],
114
+ }
115
+
116
+ # Backward compat alias (old code imported EXTENSIONS)
117
+ EXTENSIONS = BUILTIN_EXTENSIONS
118
+
119
+ # Registry file (source of truth)
120
+ REGISTRY_FILE = Path.home() / ".cct_extensions_registry.json"
121
+ # Directory for custom user extensions
122
+ CUSTOM_EXTENSIONS_DIR = Path.home() / ".cct_extensions"
123
+ # Legacy config key for migration
124
+ _LEGACY_CONFIG_KEY = "extensions"
125
+
126
+ _lock = threading.RLock()
127
+
128
+ # In-memory cache of registry state
129
+ # state: {ext_id: {"installed": bool, "enabled": bool, "version": str, "install_time": float, "size_bytes": int}}
130
+ _cached_state: Optional[Dict[str, Dict[str, Any]]] = None
131
+ _cached_mtime: float = 0
132
+
133
+
134
+ def _load_registry() -> Dict[str, Dict[str, Any]]:
135
+ global _cached_state, _cached_mtime
136
+ with _lock:
137
+ try:
138
+ mtime = REGISTRY_FILE.stat().st_mtime if REGISTRY_FILE.exists() else 0
139
+ if _cached_state is not None and mtime == _cached_mtime:
140
+ return dict(_cached_state)
141
+ except Exception:
142
+ pass
143
+
144
+ # Try registry file
145
+ data: Dict[str, Dict[str, Any]] = {}
146
+ if REGISTRY_FILE.exists():
147
+ try:
148
+ with open(REGISTRY_FILE, "r", encoding="utf-8") as f:
149
+ raw = json.load(f)
150
+ if isinstance(raw, dict):
151
+ # support both {id: {installed, enabled}} and legacy list
152
+ if "extensions" in raw and isinstance(raw["extensions"], dict):
153
+ data = raw["extensions"]
154
+ else:
155
+ # flat dict
156
+ data = raw
157
+ # filter to known structure
158
+ filtered = {}
159
+ for k, v in data.items():
160
+ if isinstance(v, dict):
161
+ filtered[str(k)] = {
162
+ "installed": bool(v.get("installed", False)),
163
+ "enabled": bool(v.get("enabled", False)),
164
+ "version": str(v.get("version", "")),
165
+ "install_time": float(v.get("install_time", 0) or 0),
166
+ "size_bytes": int(v.get("size_bytes", 0) or 0),
167
+ }
168
+ elif isinstance(v, bool):
169
+ # legacy: True => installed+enabled, False => installed disabled? But old code used False = disabled (installed but disabled)
170
+ # For migration, treat True as installed+enabled, False as installed+disabled
171
+ filtered[str(k)] = {
172
+ "installed": True,
173
+ "enabled": bool(v),
174
+ "version": "",
175
+ "install_time": 0,
176
+ "size_bytes": 0,
177
+ }
178
+ data = filtered
179
+ except Exception:
180
+ data = {}
181
+
182
+ # Migration from legacy config.extensions (simple bool dict)
183
+ if not data:
184
+ try:
185
+ from . import config as _cfg
186
+ legacy = getattr(_cfg.load_config(), "extensions", None) or {}
187
+ if isinstance(legacy, dict) and legacy:
188
+ for k, v in legacy.items():
189
+ if k in BUILTIN_EXTENSIONS:
190
+ if isinstance(v, bool):
191
+ data[str(k)] = {
192
+ "installed": True,
193
+ "enabled": bool(v),
194
+ "version": BUILTIN_EXTENSIONS[k].get("version", "1.0.0"),
195
+ "install_time": time.time(),
196
+ "size_bytes": _get_size_for(k),
197
+ }
198
+ elif isinstance(v, dict):
199
+ data[str(k)] = {
200
+ "installed": bool(v.get("installed", True)),
201
+ "enabled": bool(v.get("enabled", True)),
202
+ "version": str(v.get("version", "")),
203
+ "install_time": float(v.get("install_time", 0) or time.time()),
204
+ "size_bytes": int(v.get("size_bytes", 0) or _get_size_for(k)),
205
+ }
206
+ except Exception:
207
+ pass
208
+
209
+ # First run: extensions are NOT installed until user installs them
210
+ # (VS Code-style: Install → Enable → appears in menu).
211
+ # Keep data empty so get_state() returns NOT_INSTALLED for builtins.
212
+ if not data:
213
+ # don't auto-install — user must click Install in Extensions panel
214
+ pass
215
+
216
+ _cached_state = dict(data)
217
+ try:
218
+ _cached_mtime = REGISTRY_FILE.stat().st_mtime if REGISTRY_FILE.exists() else time.time()
219
+ except Exception:
220
+ _cached_mtime = time.time()
221
+ return dict(data)
222
+
223
+
224
+ def _save_registry(data: Dict[str, Dict[str, Any]]):
225
+ global _cached_state, _cached_mtime
226
+ with _lock:
227
+ try:
228
+ REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
229
+ payload = {
230
+ "version": 1,
231
+ "updated": time.time(),
232
+ "extensions": data,
233
+ }
234
+ with open(REGISTRY_FILE, "w", encoding="utf-8") as f:
235
+ json.dump(payload, f, indent=2)
236
+ _cached_state = dict(data)
237
+ try:
238
+ _cached_mtime = REGISTRY_FILE.stat().st_mtime
239
+ except Exception:
240
+ _cached_mtime = time.time()
241
+ # also sync to config.extensions for backward compat (simple bool)
242
+ try:
243
+ from . import config as _cfg
244
+ cfg = _cfg.load_config()
245
+ # keep legacy simple dict for old code that reads config.extensions
246
+ legacy = {}
247
+ for k, v in data.items():
248
+ # only store enabled if installed, else not present? For legacy, store enabled bool if installed, else missing
249
+ if v.get("installed"):
250
+ legacy[k] = bool(v.get("enabled", False))
251
+ cfg.extensions = legacy
252
+ _cfg.save_config(cfg)
253
+ except Exception:
254
+ pass
255
+ except Exception:
256
+ pass
257
+
258
+
259
+ def _get_size_for(ext_id: str) -> int:
260
+ """Calculate size for extension from its source files."""
261
+ # Check cache from builtin
262
+ meta = BUILTIN_EXTENSIONS.get(ext_id)
263
+ if meta and meta.get("size_bytes"):
264
+ return int(meta["size_bytes"])
265
+ paths = _BUILTIN_PATHS.get(ext_id, [])
266
+ nbytes = _calc_size(paths) if paths else 0
267
+ # if no paths, try to find extension dir ~/.cct_extensions/<id>/
268
+ if nbytes == 0:
269
+ ext_dir = Path.home() / ".cct_extensions" / ext_id
270
+ if ext_dir.exists():
271
+ nbytes = _calc_size([ext_dir])
272
+ if nbytes == 0:
273
+ # fallback: estimate 100KB per extension
274
+ nbytes = 1024 * 100
275
+ return nbytes
276
+
277
+
278
+ def _ensure_size(ext_id: str, state: Dict[str, Any]) -> int:
279
+ if state.get("size_bytes"):
280
+ return int(state["size_bytes"])
281
+ nbytes = _get_size_for(ext_id)
282
+ state["size_bytes"] = nbytes
283
+ return nbytes
284
+
285
+ # --------------------------------------------------------------------------
286
+ # Public ExtensionManager API (spec section 15)
287
+ # --------------------------------------------------------------------------
288
+
289
+ def discover() -> List[Dict[str, Any]]:
290
+ """Discover available extensions (built-in + installed third-party)."""
291
+ return list_extensions()
292
+
293
+ def is_installed(extension_id: str) -> bool:
294
+ data = _load_registry()
295
+ rec = data.get(extension_id)
296
+ return bool(rec and rec.get("installed"))
297
+
298
+ def is_enabled(extension_id: str) -> bool:
299
+ data = _load_registry()
300
+ rec = data.get(extension_id)
301
+ return bool(rec and rec.get("installed") and rec.get("enabled"))
302
+
303
+ def is_disabled(extension_id: str) -> bool:
304
+ data = _load_registry()
305
+ rec = data.get(extension_id)
306
+ return bool(rec and rec.get("installed") and not rec.get("enabled"))
307
+
308
+ def get_state(extension_id: str) -> str:
309
+ """Return state string: NOT_INSTALLED | INSTALLED_DISABLED | INSTALLED_ENABLED | INSTALLING etc."""
310
+ data = _load_registry()
311
+ rec = data.get(extension_id)
312
+ if not rec or not rec.get("installed"):
313
+ return "NOT_INSTALLED"
314
+ if rec.get("enabled"):
315
+ return "INSTALLED_ENABLED"
316
+ return "INSTALLED_DISABLED"
317
+
318
+ def get_metadata(extension_id: str) -> Optional[Dict[str, Any]]:
319
+ """Return full metadata for extension, including live size, installed/enabled."""
320
+ base = BUILTIN_EXTENSIONS.get(extension_id)
321
+ if not base:
322
+ # Check custom extension directory ~/.cct_extensions/<id>/
323
+ ext_dir = CUSTOM_EXTENSIONS_DIR / extension_id
324
+ manifest_file = ext_dir / "extension.json"
325
+ data = _load_registry()
326
+ rec = data.get(extension_id, {})
327
+ manifest = {}
328
+ if manifest_file.exists():
329
+ try:
330
+ with open(manifest_file, "r", encoding="utf-8") as f:
331
+ manifest = json.load(f)
332
+ except Exception:
333
+ pass
334
+ if not manifest and rec and isinstance(rec.get("manifest"), dict):
335
+ manifest = rec["manifest"]
336
+
337
+ if manifest or rec:
338
+ nbytes = _calc_size([ext_dir]) if ext_dir.exists() else int(rec.get("size_bytes", 1024 * 100))
339
+ display_name = manifest.get("name") or manifest.get("display_name") or extension_id.title()
340
+ return {
341
+ "id": extension_id,
342
+ "name": display_name,
343
+ "display_name": display_name,
344
+ "description": manifest.get("description", "Custom user extension"),
345
+ "icon": manifest.get("icon", "🧩"),
346
+ "publisher": manifest.get("publisher", rec.get("publisher", "Unknown")),
347
+ "publisher_type": manifest.get("publisher_type", "individual"),
348
+ "publisher_verified": False,
349
+ "verified": False,
350
+ "trusted": bool(manifest.get("trusted", rec.get("trusted", False))),
351
+ "version": manifest.get("version", rec.get("version", "1.0.0")),
352
+ "category": manifest.get("category", "Custom"),
353
+ "keywords": manifest.get("keywords", ["custom", extension_id]),
354
+ "size_bytes": nbytes,
355
+ "size_display": _format_size(nbytes),
356
+ "installed": bool(rec.get("installed", True)),
357
+ "enabled": bool(rec.get("enabled", True)),
358
+ "install_time": rec.get("install_time", 0),
359
+ "state": get_state(extension_id),
360
+ "kind": "custom",
361
+ "action": manifest.get("action", f"ext_{extension_id}"),
362
+ }
363
+ return None
364
+ # built-in
365
+ data = _load_registry()
366
+ rec = data.get(extension_id, {})
367
+ is_inst = bool(rec.get("installed")) if rec else True # default installed for backward compat handled in _load_registry
368
+ # Actually _load_registry ensures installed for builtins if empty, so if rec is None after load, it means not installed? But _load_registry would have created it if empty.
369
+ # For explicit check, if rec is None, treat as not installed only if registry has been initialized with data
370
+ # To know if not installed, check if extension_id in data
371
+ installed = is_installed(extension_id)
372
+ enabled = is_enabled(extension_id)
373
+ # calculate size
374
+ nbytes = 0
375
+ if rec:
376
+ nbytes = rec.get("size_bytes") or _get_size_for(extension_id)
377
+ else:
378
+ nbytes = _get_size_for(extension_id)
379
+ meta = dict(base)
380
+ meta.update({
381
+ "size_bytes": nbytes,
382
+ "size_display": _format_size(nbytes),
383
+ "installed": installed,
384
+ "enabled": enabled,
385
+ "state": get_state(extension_id),
386
+ "install_time": rec.get("install_time", 0) if rec else 0,
387
+ })
388
+ return meta
389
+
390
+ def get_size(extension_id: str) -> int:
391
+ meta = get_metadata(extension_id)
392
+ return int(meta["size_bytes"]) if meta else 0
393
+
394
+ def list_extensions() -> List[Dict[str, Any]]:
395
+ """List all available extensions (built-in + discovered) with live states."""
396
+ out = []
397
+ seen = set()
398
+ # built-ins first
399
+ for kid in BUILTIN_EXTENSIONS:
400
+ meta = get_metadata(kid)
401
+ if meta:
402
+ out.append(meta)
403
+ seen.add(kid)
404
+ # discover custom extensions from ~/.cct_extensions
405
+ if CUSTOM_EXTENSIONS_DIR.exists():
406
+ try:
407
+ for sub in CUSTOM_EXTENSIONS_DIR.iterdir():
408
+ if sub.is_dir() and sub.name not in seen:
409
+ meta = get_metadata(sub.name)
410
+ if meta:
411
+ out.append(meta)
412
+ seen.add(sub.name)
413
+ except Exception:
414
+ pass
415
+ # add any custom installed extensions in registry not in builtins
416
+ data = _load_registry()
417
+ for kid in data:
418
+ if kid not in seen:
419
+ meta = get_metadata(kid)
420
+ if meta:
421
+ out.append(meta)
422
+ seen.add(kid)
423
+ return out
424
+
425
+ def list_installed() -> List[Dict[str, Any]]:
426
+ return [m for m in list_extensions() if m.get("installed")]
427
+
428
+ def list_enabled() -> List[Dict[str, Any]]:
429
+ return [m for m in list_extensions() if m.get("enabled") and m.get("installed")]
430
+
431
+ # Lifecycle: INSTALL -> INSTALLED_ENABLED
432
+ def install(extension_id: str) -> tuple[bool, str]:
433
+ """INSTALL: NOT_INSTALLED -> INSTALLED_ENABLED. Returns (ok, msg)."""
434
+ # validate exists as available
435
+ if extension_id not in BUILTIN_EXTENSIONS:
436
+ # check if it's already installed custom
437
+ data = _load_registry()
438
+ if extension_id in data and data[extension_id].get("installed"):
439
+ return False, f"{extension_id} is already installed"
440
+ # unknown extension — cannot install from marketplace yet (would need download)
441
+ # For built-in, we allow install; for unknown, treat as install of custom placeholder
442
+ if extension_id not in data:
443
+ return False, f"Extension '{extension_id}' not found in marketplace"
444
+ data = _load_registry()
445
+ rec = data.get(extension_id)
446
+ if rec and rec.get("installed"):
447
+ # if already installed but disabled, enable it (spec: INSTALL on installed should probably enable)
448
+ if not rec.get("enabled"):
449
+ rec["enabled"] = True
450
+ _save_registry(data)
451
+ return True, f"{extension_id} enabled"
452
+ return False, f"{extension_id} is already installed"
453
+ # perform install
454
+ nbytes = _get_size_for(extension_id)
455
+ # simulate installing state (could be async, but spec says update immediately)
456
+ data[extension_id] = {
457
+ "installed": True,
458
+ "enabled": True,
459
+ "version": BUILTIN_EXTENSIONS.get(extension_id, {}).get("version", "1.0.0"),
460
+ "install_time": time.time(),
461
+ "size_bytes": nbytes,
462
+ }
463
+ _save_registry(data)
464
+ # CAT Customization lifecycle hook
465
+ if extension_id == "customization":
466
+ try:
467
+ from . import customization as _cust
468
+ _cust.on_install()
469
+ _cust.on_enable()
470
+ except Exception:
471
+ pass
472
+ # feature registration will happen via should_show_in_menu / startup
473
+ return True, f"✓ {BUILTIN_EXTENSIONS.get(extension_id, {}).get('display_name', extension_id)} installed successfully"
474
+
475
+ def uninstall(extension_id: str) -> tuple[bool, str]:
476
+ """UNINSTALL: INSTALLED_* -> NOT_INSTALLED. Returns (ok, msg)."""
477
+ data = _load_registry()
478
+ rec = data.get(extension_id)
479
+ if not rec or not rec.get("installed"):
480
+ return False, f"{extension_id} is not installed"
481
+ # remove
482
+ del data[extension_id]
483
+ _save_registry(data)
484
+ # also clean up extension directory if exists
485
+ try:
486
+ ext_dir = Path.home() / ".cct_extensions" / extension_id
487
+ if ext_dir.exists():
488
+ import shutil
489
+ shutil.rmtree(ext_dir, ignore_errors=True)
490
+ except Exception:
491
+ pass
492
+ if extension_id == "customization":
493
+ try:
494
+ from . import customization as _cust2
495
+ _cust2.on_uninstall()
496
+ except Exception:
497
+ pass
498
+ return True, f"✓ {extension_id} uninstalled"
499
+
500
+ def enable(extension_id: str) -> tuple[bool, str]:
501
+ """ENABLE: INSTALLED_DISABLED -> INSTALLED_ENABLED."""
502
+ data = _load_registry()
503
+ rec = data.get(extension_id)
504
+ if not rec or not rec.get("installed"):
505
+ return False, f"{extension_id} is not installed — install first"
506
+ if rec.get("enabled"):
507
+ return False, f"{extension_id} is already enabled"
508
+ rec["enabled"] = True
509
+ _save_registry(data)
510
+ if extension_id == "customization":
511
+ try:
512
+ from . import customization as _cust3
513
+ _cust3.on_enable()
514
+ except Exception:
515
+ pass
516
+ return True, f"✓ {get_metadata(extension_id)['display_name']} enabled"
517
+
518
+ def disable(extension_id: str) -> tuple[bool, str]:
519
+ """DISABLE: INSTALLED_ENABLED -> INSTALLED_DISABLED."""
520
+ data = _load_registry()
521
+ rec = data.get(extension_id)
522
+ if not rec or not rec.get("installed"):
523
+ return False, f"{extension_id} is not installed"
524
+ if not rec.get("enabled"):
525
+ return False, f"{extension_id} is already disabled"
526
+ rec["enabled"] = False
527
+ _save_registry(data)
528
+ if extension_id == "customization":
529
+ try:
530
+ from . import customization as _cust4
531
+ _cust4.on_disable()
532
+ except Exception:
533
+ pass
534
+ return True, f"✓ {get_metadata(extension_id)['display_name']} disabled"
535
+
536
+ def toggle(extension_id: str) -> tuple[bool, str]:
537
+ if is_enabled(extension_id):
538
+ return disable(extension_id)
539
+ if is_installed(extension_id):
540
+ return enable(extension_id)
541
+ return install(extension_id)
542
+
543
+ # Backward compat for old simple API (bool)
544
+ def set_enabled(key: str, enabled: bool):
545
+ if enabled:
546
+ # if not installed, install
547
+ if not is_installed(key):
548
+ install(key)
549
+ else:
550
+ enable(key)
551
+ else:
552
+ # disable but keep installed
553
+ if is_installed(key):
554
+ disable(key)
555
+ else:
556
+ # if not installed, nothing to disable
557
+ pass
558
+
559
+ def should_show_in_menu(action_id: str) -> bool:
560
+ """For header.NAV_ITEMS filtering: show only if installed+enabled."""
561
+ if action_id in ("vision", "customization"):
562
+ return False
563
+ if action_id.startswith("ext_"):
564
+ key = action_id[4:]
565
+ return is_installed(key) and is_enabled(key)
566
+ # map action -> extension key
567
+ action_to_key = {v["action"]: k for k, v in BUILTIN_EXTENSIONS.items()}
568
+ key = action_to_key.get(action_id)
569
+ if key is None:
570
+ return True
571
+ return is_installed(key) and is_enabled(key)
572
+
573
+
574
+ def create_extension(
575
+ extension_id: str,
576
+ name: str = "",
577
+ description: str = "",
578
+ icon: str = "🧩",
579
+ publisher: str = "User",
580
+ category: str = "Custom",
581
+ version: str = "1.0.0",
582
+ script_code: str = "",
583
+ ) -> tuple[bool, str]:
584
+ """Create a new custom extension in ~/.cct_extensions/<id>/ and register it."""
585
+ try:
586
+ clean_id = (extension_id or "").strip().lower().replace(" ", "_").replace("-", "_")
587
+ if not clean_id:
588
+ return False, "Extension ID cannot be empty."
589
+ if clean_id in BUILTIN_EXTENSIONS:
590
+ return False, f"'{clean_id}' conflicts with a built-in extension."
591
+
592
+ ext_dir = CUSTOM_EXTENSIONS_DIR / clean_id
593
+ ext_dir.mkdir(parents=True, exist_ok=True)
594
+
595
+ display_name = (name or "").strip() or clean_id.title()
596
+ manifest = {
597
+ "id": clean_id,
598
+ "name": display_name,
599
+ "display_name": display_name,
600
+ "description": (description or "").strip() or "Custom user-created extension",
601
+ "icon": (icon or "").strip() or "🧩",
602
+ "publisher": (publisher or "").strip() or "User",
603
+ "publisher_type": "individual",
604
+ "publisher_verified": False,
605
+ "verified": False,
606
+ "trusted": True,
607
+ "version": (version or "").strip() or "1.0.0",
608
+ "category": (category or "").strip() or "Custom",
609
+ "keywords": ["custom", clean_id],
610
+ "kind": "custom",
611
+ "action": f"ext_{clean_id}",
612
+ }
613
+
614
+ manifest_file = ext_dir / "extension.json"
615
+ with open(manifest_file, "w", encoding="utf-8") as f:
616
+ json.dump(manifest, f, indent=2)
617
+
618
+ script_file = ext_dir / f"{clean_id}.py"
619
+ if not script_file.exists() or script_code:
620
+ code = script_code or f'"""\nCAT Extension: {display_name}\n"""\n\ndef activate(app=None):\n pass\n'
621
+ with open(script_file, "w", encoding="utf-8") as f:
622
+ f.write(code)
623
+
624
+ data = _load_registry()
625
+ data[clean_id] = {
626
+ "installed": True,
627
+ "enabled": True,
628
+ "version": manifest["version"],
629
+ "install_time": time.time(),
630
+ "size_bytes": _calc_size([ext_dir]),
631
+ "manifest": manifest,
632
+ }
633
+ _save_registry(data)
634
+ return True, f"✓ Custom extension '{display_name}' created and enabled successfully!"
635
+ except Exception as e:
636
+ return False, f"Failed to create extension: {e}"
637
+
638
+ # --------------------------------------------------------------------------
639
+ # Feature registration (for future extensions that add menu items, panels, etc.)
640
+ # --------------------------------------------------------------------------
641
+
642
+ _registered_features: Dict[str, Dict[str, Any]] = {}
643
+
644
+ def register_feature(extension_id: str, feature_type: str, feature_id: str, meta: Dict[str, Any] = None):
645
+ """Extensions call this to register a feature (menu item, panel, command)."""
646
+ if not is_enabled(extension_id):
647
+ return False
648
+ key = f"{extension_id}:{feature_type}:{feature_id}"
649
+ _registered_features[key] = {"extension": extension_id, "type": feature_type, "id": feature_id, "meta": meta or {}}
650
+ return True
651
+
652
+ def unregister_feature(extension_id: str, feature_type: str, feature_id: str):
653
+ key = f"{extension_id}:{feature_type}:{feature_id}"
654
+ _registered_features.pop(key, None)
655
+
656
+ def get_registered_features(feature_type: str = None) -> List[Dict[str, Any]]:
657
+ if feature_type:
658
+ return [v for v in _registered_features.values() if v["type"] == feature_type]
659
+ return list(_registered_features.values())
660
+
661
+ # --------------------------------------------------------------------------
662
+ # Startup
663
+ # --------------------------------------------------------------------------
664
+
665
+ def validate_all() -> List[str]:
666
+ """Validate installed extensions; return list of errors (empty if ok)."""
667
+ errors = []
668
+ for ext in list_installed():
669
+ try:
670
+ # check that its module files still exist (for built-ins)
671
+ paths = _BUILTIN_PATHS.get(ext["id"], [])
672
+ if paths:
673
+ for p in paths:
674
+ if not p.exists():
675
+ errors.append(f"{ext['id']}: missing {p}")
676
+ except Exception as e:
677
+ errors.append(f"{ext['id']}: {e}")
678
+ return errors
679
+
680
+ def load_enabled():
681
+ """Load enabled extensions at startup — register their features."""
682
+ for ext in list_enabled():
683
+ try:
684
+ # For built-ins, registration is implicit via should_show_in_menu;
685
+ # for future third-party, we would import and call their activate().
686
+ pass
687
+ except Exception as e:
688
+ # error isolation: don't crash CAT
689
+ print(f"[extensions] failed to load {ext['id']}: {e}")
690
+
691
+ def startup():
692
+ """Full startup sequence: discover -> validate -> load enabled -> build menu."""
693
+ try:
694
+ discover()
695
+ errs = validate_all()
696
+ if errs:
697
+ for err in errs:
698
+ print(f"[extensions] validate: {err}")
699
+ load_enabled()
700
+ except Exception as e:
701
+ print(f"[extensions] startup failed: {e} — CAT will continue without extensions")
702
+
703
+ # Backwards compat aliases for old code that used simple enabled check
704
+ def enabled_actions() -> set:
705
+ return {v["action"] for v in BUILTIN_EXTENSIONS.values() if is_enabled(v["id"])}
706
+
707
+ # For tests: reset to defaults (used in testing)
708
+ def _reset_for_tests():
709
+ try:
710
+ if REGISTRY_FILE.exists():
711
+ REGISTRY_FILE.unlink()
712
+ except Exception:
713
+ pass
714
+ # clear legacy config state too
715
+ try:
716
+ from . import config as _cfg
717
+ cfg = _cfg.load_config()
718
+ cfg.extensions = {}
719
+ # also clear registry snapshot in config if any
720
+ if hasattr(cfg, "user_modes"):
721
+ # don't clear user modes here - that's for ai_modes, not extensions
722
+ pass
723
+ _cfg.save_config(cfg)
724
+ except Exception:
725
+ pass
726
+ global _cached_state, _cached_mtime
727
+ _cached_state = None
728
+ _cached_mtime = 0
729
+ # re-create with all installed+enabled
730
+ _load_registry()
731
+
732
+ def _clear_all_for_test():
733
+ _reset_for_tests()