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,116 @@
1
+ """PC capacity detection for Ollama model suggestions."""
2
+ import os
3
+ import platform
4
+ import shutil
5
+
6
+ def get_specs():
7
+ """Return dict with ram_gb, cpu_count, gpu, os, arch."""
8
+ ram_gb = 8
9
+ try:
10
+ import psutil # type: ignore
11
+ ram_gb = round(psutil.virtual_memory().total / (1024**3), 1)
12
+ except Exception:
13
+ # fallback via ctypes on Windows or via /proc on linux
14
+ try:
15
+ if platform.system() == "Windows":
16
+ import ctypes
17
+ class MEMORYSTATUSEX(ctypes.Structure):
18
+ _fields_ = [("dwLength", ctypes.c_ulong),("dwMemoryLoad", ctypes.c_ulong),("ullTotalPhys", ctypes.c_ulonglong),("ullAvailPhys", ctypes.c_ulonglong),("ullTotalPageFile", ctypes.c_ulonglong),("ullAvailPageFile", ctypes.c_ulonglong),("ullTotalVirtual", ctypes.c_ulonglong),("ullAvailVirtual", ctypes.c_ulonglong),("ullAvailExtendedVirtual", ctypes.c_ulonglong)]
19
+ stat = MEMORYSTATUSEX(dwLength=ctypes.sizeof(MEMORYSTATUSEX))
20
+ ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat))
21
+ ram_gb = round(int(stat.ullTotalPhys) / (1024**3), 1)
22
+ except Exception:
23
+ pass
24
+ cpu = os.cpu_count() or 4
25
+ gpu = "Unknown"
26
+ gpu_vram = 0
27
+ # Try nvidia-smi, torch, or wmic
28
+ try:
29
+ import subprocess as sp
30
+ if shutil.which("nvidia-smi"):
31
+ out = sp.check_output(["nvidia-smi","--query-gpu=name,memory.total","--format=csv,noheader"], timeout=3, text=True)
32
+ if out.strip():
33
+ part = out.splitlines()[0]
34
+ gpu = part.split(",")[0].strip()
35
+ try:
36
+ vram_str = part.split(",")[1].strip().replace(" MiB","")
37
+ gpu_vram = round(int(vram_str)/1024,1)
38
+ except Exception:
39
+ pass
40
+ elif platform.system() == "Windows":
41
+ try:
42
+ out = sp.check_output("wmic path win32_VideoController get name", shell=True, timeout=3, text=True)
43
+ lines = [l.strip() for l in out.splitlines() if l.strip() and "Name" not in l]
44
+ if lines:
45
+ gpu = lines[0][:40]
46
+ except Exception:
47
+ pass
48
+ # torch fallback
49
+ if gpu == "Unknown":
50
+ try:
51
+ import torch # type: ignore
52
+ if torch.cuda.is_available():
53
+ gpu = torch.cuda.get_device_name(0)
54
+ try:
55
+ props = torch.cuda.get_device_properties(0)
56
+ gpu_vram = round(props.total_memory/(1024**3),1)
57
+ except Exception:
58
+ pass
59
+ except Exception:
60
+ pass
61
+ except Exception:
62
+ pass
63
+ disk_free = 0
64
+ try:
65
+ du = shutil.disk_usage(os.path.expanduser("~"))
66
+ disk_free = round(du.free/(1024**3),1)
67
+ except Exception:
68
+ pass
69
+ return {
70
+ "ram_gb": ram_gb,
71
+ "cpu_count": cpu,
72
+ "gpu": gpu,
73
+ "gpu_vram_gb": gpu_vram,
74
+ "disk_free_gb": disk_free,
75
+ "os": platform.system(),
76
+ "arch": platform.machine(),
77
+ "platform": platform.platform(),
78
+ }
79
+
80
+ def suggested_max_params(specs):
81
+ """Return recommended max model size label and list of suitable models.
82
+ Heuristic: RAM is primary, VRAM bonus if GPU present."""
83
+ ram = specs.get("ram_gb", 8)
84
+ vram = specs.get("gpu_vram_gb", 0)
85
+ effective = max(ram, vram*1.2 if vram else 0) # GPU gives headroom
86
+ # Map to max params
87
+ if effective >= 48:
88
+ tier = "70B+ (you can run 70B-405B with quantization)"
89
+ max_gb = 80
90
+ elif effective >= 24:
91
+ tier = "32B-34B comfortably, 70B quantized"
92
+ max_gb = 35
93
+ elif effective >= 16:
94
+ tier = "13B-14B comfortably, 20B+ quantized"
95
+ max_gb = 16
96
+ elif effective >= 8:
97
+ tier = "7B-8B comfortably"
98
+ max_gb = 9
99
+ else:
100
+ tier = "1B-3B recommended"
101
+ max_gb = 4
102
+ return tier, max_gb
103
+
104
+ def is_suitable(model, max_gb):
105
+ try:
106
+ return float(model.get("size_gb", 99)) <= max_gb * 1.1
107
+ except Exception:
108
+ return True
109
+
110
+ def recommendation_reason(model, specs, max_gb):
111
+ sz = model.get("size_gb", 0)
112
+ ram = specs.get("ram_gb", 8)
113
+ if sz <= max_gb:
114
+ return "✓ Fits your PC"
115
+ else:
116
+ return f"⚠ Needs ~{sz}GB, you have {ram}GB RAM — may be slow / OOM"
@@ -0,0 +1,334 @@
1
+ """
2
+ CCT Permission Manager — a small, honest permission-state store for the
3
+ primary Textual UI (see calc_terminal/ui/).
4
+
5
+ This does not retroactively sandbox every existing code path in the
6
+ app (sandbox.py / security_scanner.py already do real enforcement for
7
+ /codepad execution). What this module adds is a *single source of
8
+ truth* for the toggles shown in the composer's Permissions panel, plus
9
+ a `request()` helper that produces the data an inline permission card
10
+ needs (action, reason, allow-once vs always-allow vs deny) — mirroring
11
+ the shape code_editor.py's `_scan_and_confirm` already prints, just
12
+ structured so a real widget (not print()) can render it.
13
+
14
+ ## Three-mode layer (spec v0.7 section 2)
15
+
16
+ Added on TOP of the existing per-key toggles below, not as a separate
17
+ system — MODES change how `needs_prompt()`/`allowed()` interpret those
18
+ same toggles, they don't duplicate the toggle state:
19
+
20
+ - "ask" (default): every mutating action always prompts,
21
+ regardless of a key's on/off toggle, until the user
22
+ picks "Always Allow This Session" on that specific
23
+ action's card (matches the spec's Mode 1 exactly).
24
+ - "restricted": mutating actions are never silently applied; the
25
+ caller is expected to show a Suggested-Patch-style
26
+ Accept/Reject prompt instead of Allow-Once/Always-
27
+ Allow/Deny (see `restricted_review(key)` below).
28
+ - "full": nothing prompts. Matches the spec's Mode 3 — callers
29
+ should still surface the "visible warning" the spec
30
+ asks for; this module only tracks the state.
31
+
32
+ Honest scope note: today, `execute_python` is the only mutating AI
33
+ action actually wired to a permission gate anywhere in the codebase
34
+ (see ui/app.py's on_message_run_code). The spec's Section 1 ("AI file
35
+ create/edit/rename/delete") describes a whole AI-driven file-editing
36
+ tool CCT doesn't have yet — so "restricted" mode's file-patch-review
37
+ behavior has nowhere to apply until that tool exists. This module
38
+ implements the mode semantics correctly and generally so that gate
39
+ works the moment such a tool is added, rather than only for the one
40
+ action that exists today.
41
+ """
42
+
43
+ if __name__ == "__main__":
44
+ print("This is a library file and is not meant to be run directly.")
45
+ import sys
46
+ sys.exit(1)
47
+
48
+ import itertools
49
+ import time
50
+
51
+ _request_ids = itertools.count(1)
52
+
53
+
54
+ # (key, label, default_on) — order matches the composer's checklist.
55
+ PERMISSION_DEFS = [
56
+ ("read_files", "Read Files", True),
57
+ ("write_files", "Write Files", True),
58
+ ("delete_files", "Delete Files", False),
59
+ ("execute_python", "Execute Python", True),
60
+ ("shell_commands", "Shell Commands", True),
61
+ ("install_packages", "Install Packages", True),
62
+ ("device_control", "Device Control", False),
63
+ ("internet", "Internet", False),
64
+ ("browser_automation", "Browser Automation", False),
65
+ ("formula_library", "Formula Library", True),
66
+ ("notebook", "Notebook", True),
67
+ ("calculator", "Calculator", True),
68
+ ]
69
+
70
+ # Which permission keys represent a MUTATING action (create/write/
71
+ # execute/delete/install-class capability) vs. a read-only one. Modes
72
+ # only change prompting behavior for mutating keys — read_files,
73
+ # formula_library, notebook, and calculator stay governed by their
74
+ # plain toggle in every mode, since "ask every time before reading a
75
+ # file" would make the app unusable and isn't what the spec asks for
76
+ # either (its examples are all write/edit/execute/delete/install/commit).
77
+ MUTATING_KEYS = {
78
+ "write_files",
79
+ "delete_files",
80
+ "execute_python",
81
+ "shell_commands",
82
+ "install_packages",
83
+ "device_control",
84
+ "browser_automation",
85
+ }
86
+
87
+ MODES = ("ask", "restricted", "full")
88
+ MODE_LABELS = {
89
+ "ask": "\U0001f512 Ask Every Time",
90
+ "restricted": "\U0001f7e1 Restricted",
91
+ "full": "\U0001f7e2 Full Access",
92
+ }
93
+
94
+
95
+ class PermissionManager:
96
+ """Holds on/off state for each permission and a log of decisions.
97
+
98
+ `always_allow` entries bypass future inline prompts for that key
99
+ until the session ends or the user flips it off again in the panel.
100
+ `always_deny` (v0.7.7) entries work the opposite way — once a user
101
+ picks "Always Deny" on a permission card, that key stops prompting
102
+ and is refused outright for the rest of the session.
103
+ """
104
+
105
+ def __init__(self):
106
+ self._state = {key: default for key, _label, default in PERMISSION_DEFS}
107
+ self._always_allow = set()
108
+ self._always_deny = set() # v0.7.7: keys the user permanently (this session) denied
109
+ self.log = [] # list of (timestamp, key, action_text, decision)
110
+ self.mode = "ask" # spec v0.7 default: Ask Every Time
111
+
112
+ def allowed(self, key):
113
+ return bool(self._state.get(key, False))
114
+
115
+ def new_request_id(self):
116
+ """One id per inline PermissionCard, so PermissionGranted/Denied
117
+ (posted later, after a real UI round trip) can be matched back
118
+ to the specific pending call that's waiting on it."""
119
+ return f"perm-req-{next(_request_ids)}"
120
+
121
+ def toggle(self, key):
122
+ if key in self._state:
123
+ self._state[key] = not self._state[key]
124
+ if not self._state[key]:
125
+ self._always_allow.discard(key)
126
+ else:
127
+ # re-enabling a permission in the panel means the user
128
+ # wants it usable again — clear any always-deny memory
129
+ # for it, same trust-reset semantics as set_mode.
130
+ self._always_deny.discard(key)
131
+ return self._state.get(key, False)
132
+
133
+ def set(self, key, value):
134
+ if key in self._state:
135
+ self._state[key] = bool(value)
136
+ if value:
137
+ self._always_deny.discard(key)
138
+
139
+ def snapshot(self):
140
+ """Ordered list of (key, label, is_on) for rendering the panel."""
141
+ return [(key, label, self._state.get(key, False)) for key, label, _d in PERMISSION_DEFS]
142
+
143
+ # ------------------------------------------------------- modes --
144
+ def set_mode(self, mode, persist=True):
145
+ """Switch the global permission mode. Invalid names are
146
+ rejected (returns False) rather than silently defaulting —
147
+ a typo here should never silently grant broader access than
148
+ intended."""
149
+ if mode not in MODES:
150
+ return False
151
+ self.mode = mode
152
+ if mode == "ask":
153
+ self._always_allow.clear()
154
+ if persist:
155
+ try:
156
+ from . import config as _cfg
157
+ cfg = _cfg.load_config()
158
+ cfg.default_permission_mode = mode
159
+ _cfg.save_config(cfg)
160
+ except Exception:
161
+ pass
162
+ return True
163
+
164
+ def load_persisted_mode(self):
165
+ try:
166
+ from . import config as _cfg
167
+ cfg = _cfg.load_config()
168
+ m = getattr(cfg, "default_permission_mode", "ask")
169
+ if m in MODES:
170
+ self.mode = m
171
+ except Exception:
172
+ pass
173
+
174
+ def mode_label(self):
175
+ return MODE_LABELS[self.mode]
176
+
177
+ def needs_prompt(self, key):
178
+ """True if this action should show an inline permission card
179
+ before proceeding.
180
+
181
+ - "full" mode: never prompts (matches spec Mode 3 exactly).
182
+ - "restricted" mode: mutating keys always need review (there's
183
+ no silent path — see restricted_review() for the specific
184
+ Accept/Reject framing instead of Allow/Deny); non-mutating
185
+ keys fall back to plain toggle behavior.
186
+ - "ask" mode: mutating keys always need a prompt UNLESS this
187
+ exact key was already granted "Always Allow This Session"
188
+ earlier in the current mode; non-mutating keys fall back to
189
+ plain toggle behavior.
190
+
191
+ v0.7.7: a key in `always_deny` never prompts — it's refused
192
+ outright (denied without a card) by the decision check.
193
+ """
194
+ if key in self._always_deny:
195
+ return False
196
+ if self.mode == "full":
197
+ return False
198
+ if key in MUTATING_KEYS:
199
+ if self.mode == "restricted":
200
+ return True
201
+ # mode == "ask"
202
+ return key not in self._always_allow
203
+ return (not self.allowed(key)) and key not in self._always_allow
204
+
205
+ def is_blocked(self, key):
206
+ """True if action is hard-blocked and must NOT execute.
207
+ - always_deny blocks in any mode.
208
+ - restricted mode blocks all mutating actions (no silent allow).
209
+ - install_flow_state already reports allow=False for restricted, but
210
+ this is the single general check for all mutating keys."""
211
+ if key in self._always_deny:
212
+ return True
213
+ if self.mode == "restricted" and key in MUTATING_KEYS:
214
+ # restricted still prompts, but caller must not auto-allow;
215
+ # hard-block if the prompt is bypassed
216
+ return False # prompt path handles it; not auto-blocked here
217
+ return False
218
+
219
+ def check(self, key):
220
+ """Centralized permission evaluation. Returns (allowed, needs_prompt, blocked).
221
+ Callers should use this instead of scattering needs_prompt/allowed checks."""
222
+ if self.refused_by_always_deny(key):
223
+ return (False, False, True)
224
+ if self.mode == "full":
225
+ return (True, False, False)
226
+ if self.mode == "restricted" and key in MUTATING_KEYS:
227
+ return (False, True, False) # must prompt, not auto-allowed
228
+ if key in MUTATING_KEYS:
229
+ if key in self._always_allow:
230
+ return (True, False, False)
231
+ return (False, True, False)
232
+ # non-mutating: governed by toggle
233
+ if self.allowed(key):
234
+ return (True, False, False)
235
+ return (False, False, True) if key in self._always_deny else (False, True, False)
236
+
237
+ def refused_by_always_deny(self, key):
238
+ """True when this key was permanently (this session) denied and
239
+ should be refused without showing a card at all."""
240
+ return key in self._always_deny
241
+
242
+ def restricted_review(self, key):
243
+ """True if this key, under the CURRENT mode, should be shown
244
+ as a Suggested-Patch-style Accept/Reject prompt (spec Mode 2)
245
+ rather than the Allow-Once/Always-Allow/Deny card (spec Mode 1).
246
+ Only meaningful when needs_prompt(key) is already True."""
247
+ return self.mode == "restricted" and key in MUTATING_KEYS
248
+
249
+ def decide(self, key, decision, action_text="", reason=""):
250
+ """Record a decision from an inline permission card.
251
+
252
+ decision is one of 'allow_once', 'always_allow', 'deny',
253
+ 'always_deny', 'cancel' (v0.7.7 adds the last two — the spec's
254
+ install dialog offers Allow Once / Always Allow / Deny / Always
255
+ Deny / Cancel).
256
+ Returns True if the action should proceed. In "full" mode this
257
+ is never called by a correctly-behaving caller (needs_prompt
258
+ is already False), but if it IS called, "full" still honors
259
+ the explicit decision rather than silently overriding it.
260
+ """
261
+ self.log.append((time.time(), key, action_text, decision))
262
+ if decision == "always_allow":
263
+ self._state[key] = True
264
+ self._always_allow.add(key)
265
+ return True
266
+ if decision == "always_deny":
267
+ self._state[key] = False
268
+ self._always_deny.add(key)
269
+ self._always_allow.discard(key)
270
+ return False
271
+ if decision == "allow_once" or decision == "allow":
272
+ # "allow" is the CLI callback's spelling of a one-shot grant
273
+ # (see app.py _cli_perm_callback) — treating it as refusal
274
+ # made "[Enter] Allow" silently DENY the action.
275
+ return True
276
+ return False # deny and cancel both refuse
277
+
278
+
279
+ # ------------------------------------------------------- installs --
280
+ # Package installation is governed by the same three-mode execution
281
+ # policy as every other mutating action:
282
+ #
283
+ # - full access -> executes automatically, no prompt (the install is
284
+ # still logged in manager.log and the package
285
+ # history).
286
+ # - ask every time -> prompt=True; the caller shows its approval card.
287
+ # - restricted -> allow=False; installation is disabled outright
288
+ # and the caller must report a real permission
289
+ # error, never a fake success.
290
+ #
291
+ # The UI flows (calc_terminal/app.py and calc_terminal/ui/app.py) call
292
+ # `install_flow_state()` before starting an install. This module only
293
+ # tracks state; it never shows UI.
294
+
295
+ _SAVED_MODE = None
296
+
297
+
298
+ def install_flow_state():
299
+ """Returns a dict describing how the install permission flow should
300
+ behave RIGHT NOW under the active mode:
301
+ {"allow": bool, "prompt": bool, "restore_to": None}
302
+ - restricted mode -> allow=False (installation disabled entirely).
303
+ - ask mode -> allow=True, prompt=True.
304
+ - full mode -> allow=True, prompt=False (automatic execution;
305
+ still recorded via decide()/history by callers).
306
+ """
307
+ global _SAVED_MODE
308
+ if manager.mode == "restricted":
309
+ return {"allow": False, "prompt": False, "restore_to": None}
310
+ if manager.mode == "ask":
311
+ return {"allow": True, "prompt": True, "restore_to": None}
312
+ # full — automatic execution, no narrowing, nothing to restore
313
+ _SAVED_MODE = None
314
+ return {"allow": True, "prompt": False, "restore_to": None}
315
+
316
+
317
+ def restore_mode():
318
+ """Restores the mode saved by install_flow_state() (spec section 3:
319
+ "After installation, restore the previous permission mode
320
+ automatically"). Safe to call even when nothing was saved."""
321
+ global _SAVED_MODE
322
+ if _SAVED_MODE is not None:
323
+ manager.set_mode(_SAVED_MODE)
324
+ _SAVED_MODE = None
325
+
326
+
327
+ # Module-level shared instance — the composer, the permission panel, and
328
+ # any inline request card all read/write through this one object so the
329
+ # checklist state and prompt behaviour stay consistent across a session.
330
+ manager = PermissionManager()
331
+ try:
332
+ manager.load_persisted_mode()
333
+ except Exception:
334
+ pass
calc_terminal/pet.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ Live mascot pet — Feature request v0.5.6+: "a mascot animated pet
3
+ character who can walk, run, and sleep in real time in the terminal."
4
+
5
+ The app is a blocking-input CLI (it sits on `input()` between turns),
6
+ so there's no free-running redraw thread here — but the pet still
7
+ *feels* alive: every time the prompt box is about to be drawn, it
8
+ looks at how long it's been since the user last did anything and
9
+ picks a state accordingly:
10
+
11
+ * fired off a question quickly (< RUN_THRESHOLD s) -> "run"
12
+ * normal pace -> "walk"
13
+ * left the terminal alone for a while (idle) -> "sleep"
14
+
15
+ Each redraw also advances an internal tick, so across a few turns the
16
+ walk/run cycles actually animate across the screen instead of freezing
17
+ on one frame, and /pet plays a short live loop on demand.
18
+ """
19
+
20
+ import sys
21
+ import time
22
+
23
+ from . import theme
24
+
25
+ WALK_FRAMES = ["\\o/", " o<", "/o\\", ">o "]
26
+ RUN_FRAMES = ["\\o_/", "_o/-", "-\\o_", "/-o\\"]
27
+ SLEEP_FRAMES = ["(-.-) zzz", "(-.-) zz", "(-.-) z", "(-.-) "]
28
+
29
+ RUN_THRESHOLD = 4 # seconds since last activity -> "run"
30
+ SLEEP_THRESHOLD = 45 # seconds idle -> "sleep"
31
+
32
+ _last_activity = time.time()
33
+ _tick = 0
34
+
35
+
36
+ def mark_activity():
37
+ """Call this once per submitted command/question — resets the idle
38
+ clock so the *next* time the pet is drawn, it reflects how long the
39
+ user took to respond this round."""
40
+ global _last_activity
41
+ _last_activity = time.time()
42
+
43
+
44
+ def current_state():
45
+ elapsed = time.time() - _last_activity
46
+ if elapsed >= SLEEP_THRESHOLD:
47
+ return "sleep"
48
+ if elapsed <= RUN_THRESHOLD:
49
+ return "run"
50
+ return "walk"
51
+
52
+
53
+ def _frames_for(state):
54
+ return {"walk": WALK_FRAMES, "run": RUN_FRAMES, "sleep": SLEEP_FRAMES}[state]
55
+
56
+
57
+ def _color_for(state):
58
+ return {"walk": theme.CYAN, "run": theme.ORANGE, "sleep": theme.PURPLE}.get(
59
+ state, theme.CYAN)
60
+
61
+
62
+ def frame_line(width=78, state=None):
63
+ """One line showing the pet in its current (or forced) state at its
64
+ current tick position. Calling this repeatedly advances the walk."""
65
+ global _tick
66
+ _tick += 1
67
+ state = state or current_state()
68
+ frames = _frames_for(state)
69
+ frame = frames[_tick % len(frames)]
70
+ color = _color_for(state)
71
+
72
+ if state == "sleep":
73
+ pos = 2
74
+ else:
75
+ span = max(4, width - len(frame) - 6)
76
+ step = 2 if state == "run" else 1
77
+ pos = (_tick * step) % span
78
+
79
+ return " " * pos + theme.gradient(frame, color, theme.PURPLE, bold=True)
80
+
81
+
82
+ def print_frame(width=78, state=None):
83
+ if not sys.stdout.isatty():
84
+ return
85
+ print(frame_line(width, state=state))
86
+
87
+
88
+ def demo(width=78, seconds_per_state=1.2):
89
+ """A short live loop cycling through all three states in place —
90
+ used by /pet so you can actually watch it walk, run, then fall
91
+ asleep without waiting around for real idle time to pass."""
92
+ if not sys.stdout.isatty():
93
+ print(theme.faint(" (mascot needs a real terminal to animate — "
94
+ "showing static frames instead)"))
95
+ for state in ("walk", "run", "sleep"):
96
+ print(theme.dim(f" {state}: ") + frame_line(width, state=state))
97
+ return
98
+ for state, delay in (("walk", 0.05), ("run", 0.03), ("sleep", 0.4)):
99
+ steps = max(6, int(seconds_per_state / delay))
100
+ for _ in range(steps):
101
+ line = frame_line(width, state=state)
102
+ sys.stdout.write("\r" + line + " " * 10)
103
+ sys.stdout.flush()
104
+ time.sleep(delay)
105
+ sys.stdout.write("\r" + " " * (width + 10) + "\r")
106
+ sys.stdout.flush()