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,781 @@
1
+ """
2
+ Web/PWA Platform Implementation
3
+
4
+ Runs in browser via Pyodide (WASM) or bridges to a server for native execution.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ import uuid
13
+ from pathlib import Path
14
+ from typing import Any, BinaryIO, Dict, List, Optional, Union
15
+
16
+ from . import (
17
+ PlatformBase,
18
+ PlatformInfo,
19
+ PlatformType,
20
+ FileHandle,
21
+ DirectoryHandle,
22
+ CommandResult,
23
+ ProcessHandle,
24
+ )
25
+
26
+
27
+ class WebFileHandle(FileHandle):
28
+ """File handle for web platform (uses IndexedDB/File System Access API)."""
29
+
30
+ def __init__(self, file_id: str, path: str, writable: bool = False):
31
+ self._file_id = file_id
32
+ self._path = path
33
+ self._writable = writable
34
+ self._position = 0
35
+ self._closed = False
36
+ self._buffer: bytearray = bytearray()
37
+
38
+ async def read(self, size: int = -1) -> bytes:
39
+ if self._closed:
40
+ raise ValueError("File handle closed")
41
+
42
+ # Request file content from browser
43
+ from js import CATBridge # type: ignore
44
+ result = await CATBridge.readFile(self._file_id, self._position, size)
45
+ data = bytes(result.to_py())
46
+ self._position += len(data)
47
+ return data
48
+
49
+ async def write(self, data: Union[bytes, str]) -> int:
50
+ if self._closed:
51
+ raise ValueError("File handle closed")
52
+ if not self._writable:
53
+ raise PermissionError("File not opened for writing")
54
+
55
+ if isinstance(data, str):
56
+ data = data.encode("utf-8")
57
+
58
+ self._buffer.extend(data)
59
+ self._position += len(data)
60
+ return len(data)
61
+
62
+ async def close(self) -> None:
63
+ if self._closed:
64
+ return
65
+ self._closed = True
66
+
67
+ if self._writable and self._buffer:
68
+ from js import CATBridge # type: ignore
69
+ await CATBridge.writeFile(self._file_id, bytes(self._buffer))
70
+ self._buffer.clear()
71
+
72
+ async def seek(self, offset: int, whence: int = 0) -> int:
73
+ if whence == 0: # SEEK_SET
74
+ self._position = offset
75
+ elif whence == 1: # SEEK_CUR
76
+ self._position += offset
77
+ elif whence == 2: # SEEK_END
78
+ # Need to get file size
79
+ from js import CATBridge # type: ignore
80
+ size = await CATBridge.getFileSize(self._file_id)
81
+ self._position = size + offset
82
+ return self._position
83
+
84
+ def tell(self) -> int:
85
+ return self._position
86
+
87
+
88
+ class WebDirectoryHandle(DirectoryHandle):
89
+ """Directory handle for web platform."""
90
+
91
+ def __init__(self, dir_id: str, path: str):
92
+ self._dir_id = dir_id
93
+ self._path = path
94
+
95
+ async def get_file_handle(self, name: str, create: bool = False) -> FileHandle:
96
+ from js import CATBridge # type: ignore
97
+ file_id = await CATBridge.getFileHandle(self._dir_id, name, create)
98
+ return WebFileHandle(file_id, f"{self._path}/{name}", writable=create)
99
+
100
+ async def get_directory_handle(self, name: str, create: bool = False) -> DirectoryHandle:
101
+ from js import CATBridge # type: ignore
102
+ dir_id = await CATBridge.getDirectoryHandle(self._dir_id, name, create)
103
+ return WebDirectoryHandle(dir_id, f"{self._path}/{name}")
104
+
105
+ async def remove_entry(self, name: str, recursive: bool = False) -> None:
106
+ from js import CATBridge # type: ignore
107
+ await CATBridge.removeEntry(self._dir_id, name, recursive)
108
+
109
+ async def list_entries(self) -> List[str]:
110
+ from js import CATBridge # type: ignore
111
+ result = await CATBridge.listDirectory(self._dir_id)
112
+ return list(result.to_py())
113
+
114
+
115
+ class WebProcessHandle(ProcessHandle):
116
+ """Process handle for web platform (runs on server via bridge)."""
117
+
118
+ def __init__(self, process_id: str):
119
+ self._process_id = process_id
120
+ self._returncode: Optional[int] = None
121
+
122
+ async def wait(self, timeout: Optional[float] = None) -> CommandResult:
123
+ from js import CATBridge # type: ignore
124
+ try:
125
+ result = await asyncio.wait_for(
126
+ CATBridge.waitProcess(self._process_id),
127
+ timeout=timeout,
128
+ )
129
+ data = result.to_py()
130
+ self._returncode = data["returncode"]
131
+ return CommandResult(
132
+ returncode=data["returncode"],
133
+ stdout=data["stdout"],
134
+ stderr=data["stderr"],
135
+ success=data["returncode"] == 0,
136
+ )
137
+ except asyncio.TimeoutError:
138
+ await self.kill()
139
+ return CommandResult(
140
+ returncode=-1,
141
+ stdout="",
142
+ stderr="Process timed out",
143
+ success=False,
144
+ )
145
+
146
+ async def terminate(self) -> None:
147
+ from js import CATBridge # type: ignore
148
+ await CATBridge.terminateProcess(self._process_id)
149
+
150
+ async def kill(self) -> None:
151
+ from js import CATBridge # type: ignore
152
+ await CATBridge.killProcess(self._process_id)
153
+
154
+ @property
155
+ def pid(self) -> Optional[int]:
156
+ # Not applicable in web context
157
+ return None
158
+
159
+ @property
160
+ def returncode(self) -> Optional[int]:
161
+ return self._returncode
162
+
163
+
164
+ class WebPlatform(PlatformBase):
165
+ """Web/PWA platform implementation."""
166
+
167
+ def __init__(self):
168
+ self._info = PlatformInfo(
169
+ type=PlatformType.WEB,
170
+ name="FATTY CAT",
171
+ version="0.7.9.0",
172
+ is_mobile=self._detect_mobile(),
173
+ supports_file_system_access=self._check_fs_access(),
174
+ supports_notifications="Notification" in globals(),
175
+ supports_background_sync="serviceWorker" in globals() and "sync" in globals().get("serviceWorker", {}),
176
+ user_agent=globals().get("navigator", {}).userAgent if "navigator" in globals() else "",
177
+ )
178
+ self._root_handle: Optional[WebDirectoryHandle] = None
179
+
180
+ def _detect_mobile(self) -> bool:
181
+ try:
182
+ from js import navigator # type: ignore
183
+ return "Mobi" in navigator.userAgent or "Android" in navigator.userAgent
184
+ except Exception:
185
+ return False
186
+
187
+ def _check_fs_access(self) -> bool:
188
+ try:
189
+ from js import window # type: ignore
190
+ return hasattr(window, "showDirectoryPicker")
191
+ except Exception:
192
+ return False
193
+
194
+ @property
195
+ def info(self) -> PlatformInfo:
196
+ return self._info
197
+
198
+ # --- Filesystem ---
199
+
200
+ async def get_root_directory(self) -> DirectoryHandle:
201
+ if self._root_handle:
202
+ return self._root_handle
203
+
204
+ from js import CATBridge # type: ignore
205
+ # Try to get persisted workspace directory
206
+ dir_id = await CATBridge.getWorkspaceRoot()
207
+ if not dir_id:
208
+ # Request directory permission
209
+ dir_id = await CATBridge.requestWorkspaceAccess()
210
+
211
+ self._root_handle = WebDirectoryHandle(dir_id, "/workspace")
212
+ return self._root_handle
213
+
214
+ async def pick_directory(self, title: str = "Select folder") -> Optional[DirectoryHandle]:
215
+ from js import CATBridge # type: ignore
216
+ dir_id = await CATBridge.pickDirectory(title)
217
+ if dir_id:
218
+ return WebDirectoryHandle(dir_id, "/selected")
219
+ return None
220
+
221
+ async def pick_file(self, title: str = "Select file", accept: Optional[dict] = None) -> Optional[FileHandle]:
222
+ from js import CATBridge # type: ignore
223
+ accept_list = []
224
+ if accept:
225
+ for desc, exts in accept.items():
226
+ accept_list.append({"description": desc, "extensions": exts})
227
+
228
+ file_id = await CATBridge.pickFile(title, accept_list)
229
+ if file_id:
230
+ return WebFileHandle(file_id, "/selected/file", writable=False)
231
+ return None
232
+
233
+ async def resolve_path(self, path: str) -> str:
234
+ # In web, paths are relative to workspace root
235
+ if path.startswith("/"):
236
+ return path
237
+ root = await self.get_root_directory()
238
+ return f"{root._path}/{path}"
239
+
240
+ async def path_exists(self, path: str) -> bool:
241
+ from js import CATBridge # type: ignore
242
+ return await CATBridge.pathExists(path)
243
+
244
+ async def is_directory(self, path: str) -> bool:
245
+ from js import CATBridge # type: ignore
246
+ return await CATBridge.isDirectory(path)
247
+
248
+ async def is_file(self, path: str) -> bool:
249
+ from js import CATBridge # type: ignore
250
+ return await CATBridge.isFile(path)
251
+
252
+ async def list_directory(self, path: str) -> List[str]:
253
+ from js import CATBridge # type: ignore
254
+ result = await CATBridge.listDirectory(path)
255
+ return list(result.to_py())
256
+
257
+ async def read_file(self, path: str, encoding: Optional[str] = None) -> Union[str, bytes]:
258
+ from js import CATBridge # type: ignore
259
+ result = await CATBridge.readFileByPath(path)
260
+ data = bytes(result.to_py())
261
+ if encoding:
262
+ return data.decode(encoding, errors="replace")
263
+ return data
264
+
265
+ async def write_file(self, path: str, data: Union[str, bytes], encoding: Optional[str] = None) -> int:
266
+ from js import CATBridge # type: ignore
267
+ if isinstance(data, str):
268
+ data = data.encode(encoding or "utf-8")
269
+ return await CATBridge.writeFileByPath(path, list(data))
270
+
271
+ async def delete_file(self, path: str) -> None:
272
+ from js import CATBridge # type: ignore
273
+ await CATBridge.deleteFile(path)
274
+
275
+ async def delete_directory(self, path: str, recursive: bool = False) -> None:
276
+ from js import CATBridge # type: ignore
277
+ await CATBridge.deleteDirectory(path, recursive)
278
+
279
+ async def create_directory(self, path: str, parents: bool = True) -> None:
280
+ from js import CATBridge # type: ignore
281
+ await CATBridge.createDirectory(path, parents)
282
+
283
+ async def copy_file(self, src: str, dst: str) -> None:
284
+ from js import CATBridge # type: ignore
285
+ await CATBridge.copyFile(src, dst)
286
+
287
+ async def move_file(self, src: str, dst: str) -> None:
288
+ from js import CATBridge # type: ignore
289
+ await CATBridge.moveFile(src, dst)
290
+
291
+ async def get_file_size(self, path: str) -> int:
292
+ from js import CATBridge # type: ignore
293
+ return await CATBridge.getFileSize(path)
294
+
295
+ async def get_file_mtime(self, path: str) -> float:
296
+ from js import CATBridge # type: ignore
297
+ return await CATBridge.getFileMTime(path)
298
+
299
+ # --- Process Execution ---
300
+
301
+ async def run_command(
302
+ self,
303
+ cmd: Union[str, List[str]],
304
+ cwd: Optional[str] = None,
305
+ env: Optional[dict] = None,
306
+ capture_output: bool = True,
307
+ timeout: Optional[float] = None,
308
+ ) -> CommandResult:
309
+ from js import CATBridge # type: ignore
310
+
311
+ if isinstance(cmd, list):
312
+ cmd_str = " ".join(cmd)
313
+ else:
314
+ cmd_str = cmd
315
+
316
+ result = await CATBridge.runCommand(cmd_str, cwd or "/workspace", env or {}, capture_output, timeout or 30)
317
+ data = result.to_py()
318
+ return CommandResult(
319
+ returncode=data["returncode"],
320
+ stdout=data["stdout"],
321
+ stderr=data["stderr"],
322
+ success=data["returncode"] == 0,
323
+ )
324
+
325
+ async def start_process(
326
+ self,
327
+ cmd: Union[str, List[str]],
328
+ cwd: Optional[str] = None,
329
+ env: Optional[dict] = None,
330
+ ) -> ProcessHandle:
331
+ from js import CATBridge # type: ignore
332
+
333
+ if isinstance(cmd, list):
334
+ cmd_str = " ".join(cmd)
335
+ else:
336
+ cmd_str = cmd
337
+
338
+ process_id = await CATBridge.startProcess(cmd_str, cwd or "/workspace", env or {})
339
+ return WebProcessHandle(process_id)
340
+
341
+ # --- Clipboard ---
342
+
343
+ async def read_clipboard(self) -> str:
344
+ from js import CATBridge # type: ignore
345
+ return await CATBridge.readClipboard()
346
+
347
+ async def write_clipboard(self, text: str) -> None:
348
+ from js import CATBridge # type: ignore
349
+ await CATBridge.writeClipboard(text)
350
+
351
+ # --- Notifications ---
352
+
353
+ async def show_notification(self, title: str, body: str, icon: Optional[str] = None) -> None:
354
+ from js import CATBridge # type: ignore
355
+ await CATBridge.showNotification(title, body, icon or "/icons/icon-192.png")
356
+
357
+ async def request_notification_permission(self) -> bool:
358
+ from js import CATBridge # type: ignore
359
+ return await CATBridge.requestNotificationPermission()
360
+
361
+ # --- Browser/URL ---
362
+
363
+ async def open_url(self, url: str) -> bool:
364
+ from js import window # type: ignore
365
+ try:
366
+ window.open(url, "_blank")
367
+ return True
368
+ except Exception:
369
+ return False
370
+
371
+ # --- Keyboard/Input ---
372
+
373
+ async def show_keyboard(self) -> None:
374
+ # Handled by browser automatically when input is focused
375
+ pass
376
+
377
+ async def hide_keyboard(self) -> None:
378
+ from js import document # type: ignore
379
+ try:
380
+ active = document.activeElement
381
+ if active:
382
+ active.blur()
383
+ except Exception:
384
+ pass
385
+
386
+ # --- Storage ---
387
+
388
+ async def get_storage_estimate(self) -> dict:
389
+ from js import CATBridge # type: ignore
390
+ result = await CATBridge.getStorageEstimate()
391
+ return result.to_py()
392
+
393
+ async def request_persistent_storage(self) -> bool:
394
+ from js import CATBridge # type: ignore
395
+ return await CATBridge.requestPersistentStorage()
396
+
397
+ # --- App Lifecycle ---
398
+
399
+ async def minimize_app(self) -> None:
400
+ # Not applicable in web
401
+ pass
402
+
403
+ async def close_app(self) -> None:
404
+ from js import window # type: ignore
405
+ window.close()
406
+
407
+ # --- Platform-specific ---
408
+
409
+ def get_env(self, key: str, default: Optional[str] = None) -> Optional[str]:
410
+ # In web, env vars come from server config or localStorage
411
+ try:
412
+ from js import localStorage # type: ignore
413
+ return localStorage.getItem(f"cat_env_{key}") or default
414
+ except Exception:
415
+ return default
416
+
417
+ def set_env(self, key: str, value: str) -> None:
418
+ try:
419
+ from js import localStorage # type: ignore
420
+ localStorage.setItem(f"cat_env_{key}", value)
421
+ except Exception:
422
+ pass
423
+
424
+
425
+ # Server-bridge platform for when running via HTTP server (not WASM)
426
+ class ServerBridgePlatform(PlatformBase):
427
+ """Platform that bridges to a backend server via WebSocket/HTTP."""
428
+
429
+ def __init__(self, bridge_url: str = "/api/bridge"):
430
+ self._bridge_url = bridge_url
431
+ self._info = PlatformInfo(
432
+ type=PlatformType.WEB,
433
+ name="FATTY CAT",
434
+ version="0.7.9.0",
435
+ is_mobile=self._detect_mobile(),
436
+ supports_file_system_access=True, # Via server
437
+ supports_notifications=True, # Via server push
438
+ supports_background_sync=True,
439
+ user_agent=globals().get("navigator", {}).userAgent if "navigator" in globals() else "",
440
+ )
441
+ self._ws: Optional[Any] = None
442
+ self._pending: Dict[str, asyncio.Future] = {}
443
+ self._msg_id = 0
444
+
445
+ def _detect_mobile(self) -> bool:
446
+ try:
447
+ from js import navigator # type: ignore
448
+ return "Mobi" in navigator.userAgent or "Android" in navigator.userAgent
449
+ except Exception:
450
+ return False
451
+
452
+ @property
453
+ def info(self) -> PlatformInfo:
454
+ return self._info
455
+
456
+ async def _connect(self):
457
+ if self._ws is None:
458
+ from js import WebSocket # type: ignore
459
+ self._ws = WebSocket(f"ws://{location.host}{self._bridge_url}/ws")
460
+
461
+ def on_message(event):
462
+ data = json.loads(event.data)
463
+ msg_id = data.get("id")
464
+ if msg_id in self._pending:
465
+ fut = self._pending.pop(msg_id)
466
+ if data.get("error"):
467
+ fut.set_exception(Exception(data["error"]))
468
+ else:
469
+ fut.set_result(data.get("result"))
470
+
471
+ self._ws.onmessage = on_message
472
+
473
+ await asyncio.get_event_loop().run_in_executor(None, lambda: None) # Yield
474
+
475
+ async def _call(self, method: str, params: dict) -> Any:
476
+ await self._connect()
477
+ self._msg_id += 1
478
+ msg_id = str(self._msg_id)
479
+ fut = asyncio.get_event_loop().create_future()
480
+ self._pending[msg_id] = fut
481
+
482
+ self._ws.send(json.dumps({"id": msg_id, "method": method, "params": params}))
483
+ return await fut
484
+
485
+ # --- Filesystem ---
486
+
487
+ async def get_root_directory(self) -> DirectoryHandle:
488
+ result = await self._call("fs.get_root", {})
489
+ return ServerBridgeDirectoryHandle(self, result["handle_id"], result["path"])
490
+
491
+ async def pick_directory(self, title: str = "Select folder") -> Optional[DirectoryHandle]:
492
+ result = await self._call("fs.pick_directory", {"title": title})
493
+ if result.get("handle_id"):
494
+ return ServerBridgeDirectoryHandle(self, result["handle_id"], result["path"])
495
+ return None
496
+
497
+ async def pick_file(self, title: str = "Select file", accept: Optional[dict] = None) -> Optional[FileHandle]:
498
+ result = await self._call("fs.pick_file", {"title": title, "accept": accept})
499
+ if result.get("handle_id"):
500
+ return ServerBridgeFileHandle(self, result["handle_id"], result["path"], writable=False)
501
+ return None
502
+
503
+ async def resolve_path(self, path: str) -> str:
504
+ result = await self._call("fs.resolve_path", {"path": path})
505
+ return result["path"]
506
+
507
+ async def path_exists(self, path: str) -> bool:
508
+ result = await self._call("fs.exists", {"path": path})
509
+ return result["exists"]
510
+
511
+ async def is_directory(self, path: str) -> bool:
512
+ result = await self._call("fs.is_dir", {"path": path})
513
+ return result["is_dir"]
514
+
515
+ async def is_file(self, path: str) -> bool:
516
+ result = await self._call("fs.is_file", {"path": path})
517
+ return result["is_file"]
518
+
519
+ async def list_directory(self, path: str) -> List[str]:
520
+ result = await self._call("fs.list_dir", {"path": path})
521
+ return result["entries"]
522
+
523
+ async def read_file(self, path: str, encoding: Optional[str] = None) -> Union[str, bytes]:
524
+ result = await self._call("fs.read_file", {"path": path, "encoding": encoding})
525
+ if encoding:
526
+ return result["content"]
527
+ return bytes(result["content"])
528
+
529
+ async def write_file(self, path: str, data: Union[str, bytes], encoding: Optional[str] = None) -> int:
530
+ if isinstance(data, str):
531
+ content = data
532
+ else:
533
+ content = list(data)
534
+ result = await self._call("fs.write_file", {"path": path, "content": content, "encoding": encoding})
535
+ return result["bytes_written"]
536
+
537
+ async def delete_file(self, path: str) -> None:
538
+ await self._call("fs.delete_file", {"path": path})
539
+
540
+ async def delete_directory(self, path: str, recursive: bool = False) -> None:
541
+ await self._call("fs.delete_dir", {"path": path, "recursive": recursive})
542
+
543
+ async def create_directory(self, path: str, parents: bool = True) -> None:
544
+ await self._call("fs.create_dir", {"path": path, "parents": parents})
545
+
546
+ async def copy_file(self, src: str, dst: str) -> None:
547
+ await self._call("fs.copy_file", {"src": src, "dst": dst})
548
+
549
+ async def move_file(self, src: str, dst: str) -> None:
550
+ await self._call("fs.move_file", {"src": src, "dst": dst})
551
+
552
+ async def get_file_size(self, path: str) -> int:
553
+ result = await self._call("fs.file_size", {"path": path})
554
+ return result["size"]
555
+
556
+ async def get_file_mtime(self, path: str) -> float:
557
+ result = await self._call("fs.file_mtime", {"path": path})
558
+ return result["mtime"]
559
+
560
+ # --- Process Execution ---
561
+
562
+ async def run_command(
563
+ self,
564
+ cmd: Union[str, List[str]],
565
+ cwd: Optional[str] = None,
566
+ env: Optional[dict] = None,
567
+ capture_output: bool = True,
568
+ timeout: Optional[float] = None,
569
+ ) -> CommandResult:
570
+ if isinstance(cmd, list):
571
+ cmd_str = " ".join(cmd)
572
+ else:
573
+ cmd_str = cmd
574
+
575
+ result = await self._call("process.run", {
576
+ "cmd": cmd_str,
577
+ "cwd": cwd,
578
+ "env": env,
579
+ "capture_output": capture_output,
580
+ "timeout": timeout,
581
+ })
582
+ return CommandResult(
583
+ returncode=result["returncode"],
584
+ stdout=result["stdout"],
585
+ stderr=result["stderr"],
586
+ success=result["returncode"] == 0,
587
+ )
588
+
589
+ async def start_process(
590
+ self,
591
+ cmd: Union[str, List[str]],
592
+ cwd: Optional[str] = None,
593
+ env: Optional[dict] = None,
594
+ ) -> ProcessHandle:
595
+ if isinstance(cmd, list):
596
+ cmd_str = " ".join(cmd)
597
+ else:
598
+ cmd_str = cmd
599
+
600
+ result = await self._call("process.start", {"cmd": cmd_str, "cwd": cwd, "env": env})
601
+ return ServerBridgeProcessHandle(self, result["process_id"])
602
+
603
+ # --- Clipboard ---
604
+
605
+ async def read_clipboard(self) -> str:
606
+ result = await self._call("clipboard.read", {})
607
+ return result["text"]
608
+
609
+ async def write_clipboard(self, text: str) -> None:
610
+ await self._call("clipboard.write", {"text": text})
611
+
612
+ # --- Notifications ---
613
+
614
+ async def show_notification(self, title: str, body: str, icon: Optional[str] = None) -> None:
615
+ await self._call("notification.show", {"title": title, "body": body, "icon": icon})
616
+
617
+ async def request_notification_permission(self) -> bool:
618
+ result = await self._call("notification.request_permission", {})
619
+ return result["granted"]
620
+
621
+ # --- Browser/URL ---
622
+
623
+ async def open_url(self, url: str) -> bool:
624
+ from js import window # type: ignore
625
+ try:
626
+ window.open(url, "_blank")
627
+ return True
628
+ except Exception:
629
+ return False
630
+
631
+ # --- Keyboard/Input ---
632
+
633
+ async def show_keyboard(self) -> None:
634
+ pass
635
+
636
+ async def hide_keyboard(self) -> None:
637
+ from js import document # type: ignore
638
+ try:
639
+ active = document.activeElement
640
+ if active:
641
+ active.blur()
642
+ except Exception:
643
+ pass
644
+
645
+ # --- Storage ---
646
+
647
+ async def get_storage_estimate(self) -> dict:
648
+ result = await self._call("storage.estimate", {})
649
+ return result
650
+
651
+ async def request_persistent_storage(self) -> bool:
652
+ result = await self._call("storage.request_persistent", {})
653
+ return result["granted"]
654
+
655
+ # --- App Lifecycle ---
656
+
657
+ async def minimize_app(self) -> None:
658
+ pass
659
+
660
+ async def close_app(self) -> None:
661
+ from js import window # type: ignore
662
+ window.close()
663
+
664
+ # --- Platform-specific ---
665
+
666
+ def get_env(self, key: str, default: Optional[str] = None) -> Optional[str]:
667
+ try:
668
+ from js import localStorage # type: ignore
669
+ return localStorage.getItem(f"cat_env_{key}") or default
670
+ except Exception:
671
+ return default
672
+
673
+ def set_env(self, key: str, value: str) -> None:
674
+ try:
675
+ from js import localStorage # type: ignore
676
+ localStorage.setItem(f"cat_env_{key}", value)
677
+ except Exception:
678
+ pass
679
+
680
+
681
+ class ServerBridgeFileHandle(FileHandle):
682
+ def __init__(self, platform: ServerBridgePlatform, handle_id: str, path: str, writable: bool = False):
683
+ self._platform = platform
684
+ self._handle_id = handle_id
685
+ self._path = path
686
+ self._writable = writable
687
+ self._position = 0
688
+ self._closed = False
689
+
690
+ async def read(self, size: int = -1) -> bytes:
691
+ result = await self._platform._call("fs.read", {"handle_id": self._handle_id, "position": self._position, "size": size})
692
+ data = bytes(result["data"])
693
+ self._position += len(data)
694
+ return data
695
+
696
+ async def write(self, data: Union[bytes, str]) -> int:
697
+ if not self._writable:
698
+ raise PermissionError("Not writable")
699
+ if isinstance(data, str):
700
+ data = data.encode("utf-8")
701
+ result = await self._platform._call("fs.write", {"handle_id": self._handle_id, "data": list(data), "position": self._position})
702
+ self._position += result["bytes_written"]
703
+ return result["bytes_written"]
704
+
705
+ async def close(self) -> None:
706
+ if not self._closed:
707
+ self._closed = True
708
+ await self._platform._call("fs.close", {"handle_id": self._handle_id})
709
+
710
+ async def seek(self, offset: int, whence: int = 0) -> int:
711
+ if whence == 0:
712
+ self._position = offset
713
+ elif whence == 1:
714
+ self._position += offset
715
+ elif whence == 2:
716
+ result = await self._platform._call("fs.seek_end", {"handle_id": self._handle_id, "offset": offset})
717
+ self._position = result["position"]
718
+ return self._position
719
+
720
+ def tell(self) -> int:
721
+ return self._position
722
+
723
+
724
+ class ServerBridgeDirectoryHandle(DirectoryHandle):
725
+ def __init__(self, platform: ServerBridgePlatform, handle_id: str, path: str):
726
+ self._platform = platform
727
+ self._handle_id = handle_id
728
+ self._path = path
729
+
730
+ async def get_file_handle(self, name: str, create: bool = False) -> FileHandle:
731
+ result = await self._platform._call("fs.get_file_handle", {"dir_handle_id": self._handle_id, "name": name, "create": create})
732
+ return ServerBridgeFileHandle(self._platform, result["handle_id"], f"{self._path}/{name}", writable=create)
733
+
734
+ async def get_directory_handle(self, name: str, create: bool = False) -> DirectoryHandle:
735
+ result = await self._platform._call("fs.get_dir_handle", {"dir_handle_id": self._handle_id, "name": name, "create": create})
736
+ return ServerBridgeDirectoryHandle(self._platform, result["handle_id"], f"{self._path}/{name}")
737
+
738
+ async def remove_entry(self, name: str, recursive: bool = False) -> None:
739
+ await self._platform._call("fs.remove_entry", {"dir_handle_id": self._handle_id, "name": name, "recursive": recursive})
740
+
741
+ async def list_entries(self) -> List[str]:
742
+ result = await self._platform._call("fs.list_dir", {"path": self._path})
743
+ return result["entries"]
744
+
745
+
746
+ class ServerBridgeProcessHandle(ProcessHandle):
747
+ def __init__(self, platform: ServerBridgePlatform, process_id: str):
748
+ self._platform = platform
749
+ self._process_id = process_id
750
+ self._returncode: Optional[int] = None
751
+
752
+ async def wait(self, timeout: Optional[float] = None) -> CommandResult:
753
+ try:
754
+ result = await asyncio.wait_for(
755
+ self._platform._call("process.wait", {"process_id": self._process_id}),
756
+ timeout=timeout,
757
+ )
758
+ self._returncode = result["returncode"]
759
+ return CommandResult(
760
+ returncode=result["returncode"],
761
+ stdout=result["stdout"],
762
+ stderr=result["stderr"],
763
+ success=result["returncode"] == 0,
764
+ )
765
+ except asyncio.TimeoutError:
766
+ await self.kill()
767
+ return CommandResult(returncode=-1, stdout="", stderr="Timeout", success=False)
768
+
769
+ async def terminate(self) -> None:
770
+ await self._platform._call("process.terminate", {"process_id": self._process_id})
771
+
772
+ async def kill(self) -> None:
773
+ await self._platform._call("process.kill", {"process_id": self._process_id})
774
+
775
+ @property
776
+ def pid(self) -> Optional[int]:
777
+ return None
778
+
779
+ @property
780
+ def returncode(self) -> Optional[int]:
781
+ return self._returncode