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