grasp-sdk 0.1.8__py3-none-any.whl → 0.2.0a1__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.

Potentially problematic release.


This version of grasp-sdk might be problematic. Click here for more details.

Files changed (38) hide show
  1. examples/example_async_context.py +122 -0
  2. examples/example_binary_file_support.py +127 -0
  3. examples/example_grasp_usage.py +82 -0
  4. examples/example_readfile_usage.py +136 -0
  5. examples/grasp_terminal.py +110 -0
  6. examples/grasp_usage.py +111 -0
  7. examples/test_async_context.py +64 -0
  8. examples/test_get_replay_screenshots.py +90 -0
  9. examples/test_grasp_classes.py +80 -0
  10. examples/test_python_script.py +160 -0
  11. examples/test_removed_methods.py +80 -0
  12. examples/test_shutdown_deprecation.py +62 -0
  13. examples/test_terminal_updates.py +196 -0
  14. grasp_sdk/__init__.py +131 -239
  15. grasp_sdk/grasp/__init__.py +26 -0
  16. grasp_sdk/grasp/browser.py +69 -0
  17. grasp_sdk/grasp/index.py +122 -0
  18. grasp_sdk/grasp/server.py +250 -0
  19. grasp_sdk/grasp/session.py +108 -0
  20. grasp_sdk/grasp/terminal.py +32 -0
  21. grasp_sdk/grasp/utils.py +90 -0
  22. grasp_sdk/models/__init__.py +1 -1
  23. grasp_sdk/services/__init__.py +8 -1
  24. grasp_sdk/services/browser.py +92 -63
  25. grasp_sdk/services/filesystem.py +94 -0
  26. grasp_sdk/services/sandbox.py +391 -20
  27. grasp_sdk/services/terminal.py +177 -0
  28. grasp_sdk/utils/auth.py +6 -8
  29. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/METADATA +2 -3
  30. grasp_sdk-0.2.0a1.dist-info/RECORD +36 -0
  31. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/top_level.txt +1 -0
  32. grasp_sdk/sandbox/bootstrap-chrome-stable.mjs +0 -69
  33. grasp_sdk/sandbox/chrome-stable.mjs +0 -424
  34. grasp_sdk/sandbox/chromium.mjs +0 -395
  35. grasp_sdk/sandbox/http-proxy.mjs +0 -324
  36. grasp_sdk-0.1.8.dist-info/RECORD +0 -18
  37. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/WHEEL +0 -0
  38. {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,69 @@
1
+ """GraspBrowser class for browser interface."""
2
+
3
+ import aiohttp
4
+ from typing import Optional, Dict, Any
5
+
6
+ from ..services.browser import CDPConnection
7
+
8
+
9
+ class GraspBrowser:
10
+ """Browser interface for Grasp session."""
11
+
12
+ def __init__(self, connection: CDPConnection):
13
+ """Initialize GraspBrowser.
14
+
15
+ Args:
16
+ connection: CDP connection instance
17
+ """
18
+ self.connection = connection
19
+
20
+ def get_endpoint(self) -> str:
21
+ """Get browser WebSocket endpoint URL.
22
+
23
+ Returns:
24
+ WebSocket URL for browser connection
25
+ """
26
+ return self.connection.ws_url
27
+
28
+ async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
29
+ """Get current page target information.
30
+
31
+ Warning:
32
+ This method is experimental and may change or be removed in future versions.
33
+ Use with caution in production environments.
34
+
35
+ Returns:
36
+ Dictionary containing target info and last screenshot, or None if failed
37
+ """
38
+ host = self.connection.http_url
39
+ api = f"{host}/api/page/info"
40
+
41
+ try:
42
+ async with aiohttp.ClientSession() as session:
43
+ async with session.get(api) as response:
44
+ if not response.ok:
45
+ return None
46
+
47
+ data = await response.json()
48
+ page_info = data.get('pageInfo', {})
49
+ last_screenshot = page_info.get('lastScreenshot')
50
+ session_id = page_info.get('sessionId')
51
+ targets = page_info.get('targets', {})
52
+
53
+ if session_id and session_id in targets:
54
+ target_info = targets[session_id].copy()
55
+ target_info['lastScreenshot'] = last_screenshot
56
+ return target_info
57
+
58
+ return None
59
+ except Exception:
60
+ return None
61
+
62
+ def get_liveview_url(self) -> Optional[str]:
63
+ """Get liveview URL (TODO: implementation pending).
64
+
65
+ Returns:
66
+ Liveview URL or None
67
+ """
68
+ # TODO: Implement liveview URL generation
69
+ return None
@@ -0,0 +1,122 @@
1
+ """Main Grasp class for SDK initialization."""
2
+
3
+ import os
4
+ from typing import Dict, Optional, Any
5
+
6
+ from .server import GraspServer
7
+ from .session import GraspSession
8
+
9
+
10
+ class Grasp:
11
+ """Main Grasp SDK class for creating and managing sessions."""
12
+
13
+ def __init__(self, options: Optional[Dict[str, Any]] = None):
14
+ """Initialize Grasp SDK.
15
+
16
+ Args:
17
+ options: Configuration options including apiKey
18
+ """
19
+ if options is None:
20
+ options = {}
21
+
22
+ self.key = options.get('apiKey', os.environ.get('GRASP_KEY', ''))
23
+ self._session: Optional[GraspSession] = None
24
+ self._launch_options: Optional[Dict[str, Any]] = None
25
+
26
+ async def launch(self, options: Dict[str, Any]) -> GraspSession:
27
+ """Launch a new browser session.
28
+
29
+ Args:
30
+ options: Launch options containing browser configuration
31
+
32
+ Returns:
33
+ GraspSession instance
34
+ """
35
+ browser_options = options.get('browser', {})
36
+ browser_options['key'] = self.key
37
+ browser_options['keepAliveMS'] = options.get('keepAliveMS', 10000)
38
+ browser_options['timeout'] = options.get('timeout', 900000)
39
+ browser_options['debug'] = options.get('debug', False)
40
+ browser_options['logLevel'] = options.get('logLevel', 'info')
41
+
42
+ server = GraspServer(browser_options)
43
+ cdp_connection = await server.create_browser_task()
44
+ return GraspSession(cdp_connection)
45
+
46
+ async def __aenter__(self) -> GraspSession:
47
+ """异步上下文管理器入口,启动会话。
48
+
49
+ Returns:
50
+ GraspSession 实例
51
+
52
+ Raises:
53
+ RuntimeError: 如果已经有活跃的会话
54
+ """
55
+ if self._session is not None:
56
+ raise RuntimeError('Session already active. Close existing session before creating a new one.')
57
+
58
+ if self._launch_options is None:
59
+ raise RuntimeError('No launch options provided. Use grasp.launch(options) as context manager.')
60
+
61
+ self._session = await self.launch(self._launch_options)
62
+ return self._session
63
+
64
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
65
+ """异步上下文管理器退出,清理会话。
66
+
67
+ Args:
68
+ exc_type: 异常类型
69
+ exc_val: 异常值
70
+ exc_tb: 异常回溯
71
+ """
72
+ if self._session is not None:
73
+ await self._session.close()
74
+ self._session = None
75
+ self._launch_options = None
76
+
77
+ def launch_context(self, options: Dict[str, Any]) -> 'Grasp':
78
+ """设置启动选项并返回自身,用于上下文管理器。
79
+
80
+ Args:
81
+ options: 启动选项
82
+
83
+ Returns:
84
+ 自身实例,用于 async with 语法
85
+
86
+ Example:
87
+ async with grasp.launch_context({
88
+ 'browser': {
89
+ 'type': 'chromium',
90
+ 'headless': True,
91
+ 'timeout': 30000
92
+ }
93
+ }) as session:
94
+ # 使用 session
95
+ pass
96
+ """
97
+ self._launch_options = options
98
+ return self
99
+
100
+ async def connect(self, session_id: str) -> GraspSession:
101
+ """Connect to an existing session.
102
+
103
+ Args:
104
+ session_id: ID of the session to connect to
105
+
106
+ Returns:
107
+ GraspSession instance
108
+ """
109
+ # Check if server already exists
110
+ from . import _servers
111
+ server = _servers.get(session_id)
112
+ cdp_connection = None
113
+
114
+ if server:
115
+ cdp_connection = server.browser_service.get_cdp_connection() if server.browser_service else None
116
+
117
+ if not cdp_connection:
118
+ # Create new server with no timeout for existing sessions
119
+ server = GraspServer({'timeout': 0})
120
+ cdp_connection = await server.connect_browser_task(session_id)
121
+
122
+ return GraspSession(cdp_connection)
@@ -0,0 +1,250 @@
1
+ """GraspServer class for browser automation."""
2
+
3
+ import os
4
+ from typing import Dict, Optional, Any
5
+
6
+ from ..utils.logger import init_logger, get_logger
7
+ from ..utils.config import get_config
8
+ from ..services.browser import BrowserService, CDPConnection
9
+ from ..services.sandbox import SandboxService
10
+ from ..models import (
11
+ ISandboxConfig,
12
+ IBrowserConfig,
13
+ SandboxStatus,
14
+ )
15
+
16
+
17
+ class GraspServer:
18
+ """Main Grasp E2B class for browser automation."""
19
+
20
+ def __init__(self, sandbox_config: Optional[Dict[str, Any]] = None):
21
+ """Initialize GraspServer with configuration.
22
+
23
+ Args:
24
+ sandbox_config: Optional sandbox configuration overrides
25
+ """
26
+ if sandbox_config is None:
27
+ sandbox_config = {}
28
+
29
+ # Extract browser-specific options
30
+ browser_type = sandbox_config.pop('type', 'chromium')
31
+ headless = sandbox_config.pop('headless', True)
32
+ adblock = sandbox_config.pop('adblock', False)
33
+ logLevel = sandbox_config.pop('logLevel', '')
34
+ keepAliveMS = sandbox_config.pop('keepAliveMS', 0)
35
+
36
+ # Set default log level
37
+ if not logLevel:
38
+ logLevel = 'debug' if sandbox_config.get('debug', False) else 'info'
39
+
40
+ self.__browser_type = browser_type
41
+
42
+ # Create browser task
43
+ self.__browser_config = {
44
+ 'headless': headless,
45
+ 'envs': {
46
+ 'ADBLOCK': 'true' if adblock else 'false',
47
+ 'KEEP_ALIVE_MS': str(keepAliveMS),
48
+ }
49
+ }
50
+
51
+ config = get_config()
52
+ config['sandbox'].update(sandbox_config)
53
+ self.config = config
54
+
55
+ logger = config['logger']
56
+ logger['level'] = logLevel
57
+
58
+ # Initialize logger first
59
+ init_logger(logger)
60
+ self.logger = get_logger().child('GraspServer')
61
+
62
+ self.browser_service: Optional[BrowserService] = None
63
+
64
+ self.logger.info('GraspServer initialized')
65
+
66
+ async def __aenter__(self):
67
+ connection = await self.create_browser_task()
68
+
69
+ # Register server
70
+ # if connection['id']:
71
+ # _servers[connection['id']] = self
72
+
73
+ return connection
74
+
75
+ async def __aexit__(self, exc_type, exc, tb):
76
+ if self.browser_service and self.browser_service.id:
77
+ service_id = self.browser_service.id
78
+ self.logger.info(f'Closing browser service {service_id}')
79
+ from . import _servers
80
+ await _servers[service_id].cleanup()
81
+
82
+ @property
83
+ def sandbox(self) -> Optional[SandboxService]:
84
+ """Get the underlying sandbox service.
85
+
86
+ Returns:
87
+ SandboxService instance or None
88
+ """
89
+ return self.browser_service.get_sandbox() if self.browser_service else None
90
+
91
+ def get_status(self) -> Optional[SandboxStatus]:
92
+ """Get current sandbox status.
93
+
94
+ Returns:
95
+ Sandbox status or None
96
+ """
97
+ return self.sandbox.get_status() if self.sandbox else None
98
+
99
+ def get_sandbox_id(self) -> Optional[str]:
100
+ """Get sandbox ID.
101
+
102
+ Returns:
103
+ Sandbox ID or None
104
+ """
105
+ return self.sandbox.get_sandbox_id() if self.sandbox else None
106
+
107
+ async def create_browser_task(
108
+ self,
109
+ ) -> CDPConnection:
110
+ """Create and launch a browser task.
111
+
112
+ Args:
113
+ browser_type: Type of browser to launch
114
+ config: Browser configuration overrides
115
+
116
+ Returns:
117
+ Dictionary containing browser connection info
118
+
119
+ Raises:
120
+ RuntimeError: If browser service is already initialized
121
+ """
122
+ if self.browser_service:
123
+ raise RuntimeError('Browser service can only be initialized once')
124
+
125
+ config = self.__browser_config
126
+ browser_type = self.__browser_type
127
+
128
+ if config is None:
129
+ config = {}
130
+
131
+ # Create base browser config
132
+ browser_config: IBrowserConfig = {
133
+ 'headless': True,
134
+ 'launchTimeout': 30000,
135
+ 'args': [
136
+ '--disable-web-security',
137
+ '--disable-features=VizDisplayCompositor',
138
+ ],
139
+ 'envs': {},
140
+ }
141
+
142
+ # Apply user config overrides with type safety
143
+ if 'headless' in config:
144
+ browser_config['headless'] = config['headless']
145
+ if 'launchTimeout' in config:
146
+ browser_config['launchTimeout'] = config['launchTimeout']
147
+ if 'args' in config:
148
+ browser_config['args'] = config['args']
149
+ if 'envs' in config:
150
+ browser_config['envs'] = config['envs']
151
+
152
+ self.browser_service = BrowserService(
153
+ self.config['sandbox'],
154
+ browser_config
155
+ )
156
+ await self.browser_service.initialize(browser_type)
157
+
158
+ # Register server
159
+ from . import _servers
160
+ _servers[str(self.browser_service.id)] = self
161
+ self.logger.info("🚀 Browser service initialized", {
162
+ 'id': self.browser_service.id,
163
+ })
164
+
165
+ self.logger.info('🌐 Launching Chromium browser with CDP...')
166
+ cdp_connection = await self.browser_service.launch_browser()
167
+
168
+ self.logger.info('✅ Browser launched successfully!')
169
+ self.logger.debug(
170
+ f'CDP Connection Info (wsUrl: {cdp_connection.ws_url}, httpUrl: {cdp_connection.http_url})'
171
+ )
172
+
173
+ return cdp_connection
174
+
175
+ async def connect_browser_task(self, session_id: str) -> CDPConnection:
176
+ """Connect to an existing browser session.
177
+
178
+ Args:
179
+ session_id: ID of the session to connect to
180
+
181
+ Returns:
182
+ CDPConnection instance for the existing session
183
+
184
+ Raises:
185
+ RuntimeError: If browser service is already initialized
186
+ """
187
+ if self.browser_service:
188
+ raise RuntimeError('Browser service can only be initialized once')
189
+
190
+ config = self.__browser_config
191
+ browser_type = self.__browser_type
192
+
193
+ if config is None:
194
+ config = {}
195
+
196
+ # Create base browser config
197
+ browser_config: IBrowserConfig = {
198
+ 'headless': True,
199
+ 'launchTimeout': 30000,
200
+ 'args': [
201
+ '--disable-web-security',
202
+ '--disable-features=VizDisplayCompositor',
203
+ ],
204
+ 'envs': {},
205
+ }
206
+
207
+ # Apply user config overrides with type safety
208
+ if 'headless' in config:
209
+ browser_config['headless'] = config['headless']
210
+ if 'launchTimeout' in config:
211
+ browser_config['launchTimeout'] = config['launchTimeout']
212
+ if 'args' in config:
213
+ browser_config['args'] = config['args']
214
+ if 'envs' in config:
215
+ browser_config['envs'] = config['envs']
216
+
217
+ self.browser_service = BrowserService(
218
+ self.config['sandbox'],
219
+ browser_config
220
+ )
221
+
222
+ # Connect to existing session
223
+ connection = await self.browser_service.connect(session_id)
224
+
225
+ # Register server
226
+ from . import _servers
227
+ _servers[connection.id] = self
228
+
229
+ return connection
230
+
231
+ async def cleanup(self) -> None:
232
+ """Cleanup resources.
233
+
234
+ Returns:
235
+ Promise that resolves when cleanup is complete
236
+ """
237
+ self.logger.info('Starting cleanup process')
238
+
239
+ try:
240
+ if self.browser_service:
241
+ id = self.browser_service.id
242
+ await self.browser_service.cleanup()
243
+ if id:
244
+ from . import _servers
245
+ del _servers[id]
246
+
247
+ self.logger.info('Cleanup completed successfully')
248
+ except Exception as error:
249
+ self.logger.error(f'Cleanup failed: {error}')
250
+ raise
@@ -0,0 +1,108 @@
1
+ """GraspSession class for session management."""
2
+
3
+ import asyncio
4
+ import websockets
5
+ from .browser import GraspBrowser
6
+ from .terminal import GraspTerminal
7
+ from ..services.browser import CDPConnection
8
+ from ..services.filesystem import FileSystemService
9
+
10
+
11
+ class GraspSession:
12
+ """Main session interface providing access to browser, terminal, and files."""
13
+
14
+ def __init__(self, connection: CDPConnection):
15
+ """Initialize GraspSession.
16
+
17
+ Args:
18
+ connection: CDP connection instance
19
+ """
20
+ self.connection = connection
21
+ self.browser = GraspBrowser(connection)
22
+ self.terminal = GraspTerminal(connection)
23
+
24
+ # Create files service
25
+ connection_id = connection.id
26
+ try:
27
+ from . import _servers
28
+ server = _servers[connection_id]
29
+ if server.sandbox is None:
30
+ raise RuntimeError('Sandbox is not available for file system service')
31
+ self.files = FileSystemService(server.sandbox, connection)
32
+ except (ImportError, KeyError) as e:
33
+ # In test environment or when server is not available, create a mock files service
34
+ print(f"Warning: Files service not available: {e}")
35
+ raise e
36
+
37
+ # WebSocket connection for keep-alive
38
+ self._ws = None
39
+ self._keep_alive_task = None
40
+
41
+ # Initialize WebSocket connection and start keep-alive
42
+ print('create session...')
43
+ asyncio.create_task(self._initialize_websocket())
44
+
45
+ async def _initialize_websocket(self) -> None:
46
+ """Initialize WebSocket connection and start keep-alive."""
47
+ try:
48
+ self._ws = await websockets.connect(self.connection.ws_url)
49
+ await self._keep_alive()
50
+ except Exception as e:
51
+ print(f"Failed to initialize WebSocket connection: {e}")
52
+
53
+ async def _keep_alive(self) -> None:
54
+ """Keep WebSocket connection alive with periodic ping."""
55
+ if self._ws is None:
56
+ return
57
+
58
+ async def ping_loop():
59
+ while self._ws and self._ws.close_code is None:
60
+ try:
61
+ await self._ws.ping()
62
+ await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
63
+ except Exception as e:
64
+ print(f"Keep-alive ping failed: {e}")
65
+ break
66
+
67
+ self._keep_alive_task = asyncio.create_task(ping_loop())
68
+
69
+ @property
70
+ def id(self) -> str:
71
+ """Get session ID.
72
+
73
+ Returns:
74
+ Session ID
75
+ """
76
+ return self.connection.id
77
+
78
+ async def close(self) -> None:
79
+ """Close the session and cleanup resources."""
80
+ # Stop keep-alive task
81
+ if self._keep_alive_task:
82
+ self._keep_alive_task.cancel()
83
+ try:
84
+ await self._keep_alive_task
85
+ except asyncio.CancelledError:
86
+ pass
87
+
88
+ # Close WebSocket connection with timeout
89
+ if self._ws and self._ws.close_code is None:
90
+ try:
91
+ # Add timeout to prevent hanging on close
92
+ await asyncio.wait_for(self._ws.close(), timeout=5.0)
93
+ print('✅ WebSocket closed successfully')
94
+ except asyncio.TimeoutError:
95
+ print('⚠️ WebSocket close timeout, forcing termination')
96
+ # Force close if timeout
97
+ if hasattr(self._ws, 'transport') and self._ws.transport:
98
+ self._ws.transport.close()
99
+ except Exception as e:
100
+ print(f"Failed to close WebSocket connection: {e}")
101
+
102
+ # Cleanup server resources
103
+ try:
104
+ from . import _servers
105
+ await _servers[self.id].cleanup()
106
+ except (ImportError, KeyError) as e:
107
+ # In test environment or when server is not available, skip cleanup
108
+ print(f"Warning: Server cleanup not available: {e}")
@@ -0,0 +1,32 @@
1
+ """GraspTerminal class for terminal interface."""
2
+
3
+ from ..services.browser import CDPConnection
4
+ from ..services.terminal import TerminalService
5
+
6
+
7
+ class GraspTerminal:
8
+ """Terminal interface for Grasp session."""
9
+
10
+ def __init__(self, connection: CDPConnection):
11
+ """Initialize GraspTerminal.
12
+
13
+ Args:
14
+ connection: CDP connection instance
15
+ """
16
+ self.connection = connection
17
+
18
+ def create(self) -> TerminalService:
19
+ """Create a new terminal service instance.
20
+
21
+ Returns:
22
+ TerminalService instance
23
+
24
+ Raises:
25
+ RuntimeError: If sandbox is not available
26
+ """
27
+ connection_id = self.connection.id
28
+ from . import _servers
29
+ server = _servers[connection_id]
30
+ if server.sandbox is None:
31
+ raise RuntimeError('Sandbox is not available')
32
+ return TerminalService(server.sandbox, self.connection)
@@ -0,0 +1,90 @@
1
+ """Utility functions for Grasp SDK."""
2
+
3
+ import os
4
+ import asyncio
5
+ import signal
6
+ from typing import Dict, Optional, List
7
+ import warnings
8
+
9
+ from ..utils.logger import get_logger
10
+ from .server import GraspServer
11
+ from .session import GraspSession
12
+
13
+ # Global server registry
14
+ _servers: Dict[str, GraspServer] = {}
15
+
16
+ async def _graceful_shutdown(reason: str = 'SIGTERM') -> None:
17
+ """Gracefully shutdown all servers.
18
+
19
+ Args:
20
+ reason: Reason for shutdown
21
+ """
22
+ logger = get_logger().child('shutdown')
23
+ logger.info(f'Graceful shutdown initiated: {reason}')
24
+
25
+ if not _servers:
26
+ logger.info('No active servers to shutdown')
27
+ return
28
+
29
+ logger.info(f'Shutting down {len(_servers)} active servers...')
30
+
31
+ # Create cleanup tasks for all servers
32
+ cleanup_tasks = []
33
+ for server_id, server in _servers.items():
34
+ logger.info(f'Scheduling cleanup for server {server_id}')
35
+ cleanup_tasks.append(server.cleanup())
36
+
37
+ # Wait for all cleanups to complete
38
+ try:
39
+ await asyncio.gather(*cleanup_tasks, return_exceptions=True)
40
+ logger.info('All servers cleaned up successfully')
41
+ except Exception as error:
42
+ logger.error(f'Error during cleanup: {error}')
43
+
44
+ # Clear the servers registry
45
+ _servers.clear()
46
+ logger.info('Graceful shutdown completed')
47
+
48
+
49
+ def _setup_signal_handlers() -> None:
50
+ """Setup signal handlers for graceful shutdown."""
51
+ def signal_handler(signum, frame):
52
+ signal_name = signal.Signals(signum).name
53
+ asyncio.create_task(_graceful_shutdown(signal_name))
54
+
55
+ # Register signal handlers
56
+ signal.signal(signal.SIGTERM, signal_handler)
57
+ signal.signal(signal.SIGINT, signal_handler)
58
+
59
+
60
+ async def shutdown(connection: Optional[str] = None) -> None:
61
+ """Shutdown Grasp servers (deprecated).
62
+
63
+ Args:
64
+ connection: Optional connection ID to shutdown specific server
65
+
66
+ Deprecated:
67
+ This function is deprecated. Use server.cleanup() or context managers instead.
68
+ """
69
+ warnings.warn(
70
+ "shutdown() is deprecated. Use server.cleanup() or context managers instead.",
71
+ DeprecationWarning,
72
+ stacklevel=2
73
+ )
74
+
75
+ logger = get_logger().child('shutdown')
76
+
77
+ if connection:
78
+ # Shutdown specific connection
79
+ if connection in _servers:
80
+ logger.info(f'Shutting down server {connection}')
81
+ await _servers[connection].cleanup()
82
+ else:
83
+ logger.warn(f'Server {connection} not found')
84
+ else:
85
+ # Shutdown all servers
86
+ await _graceful_shutdown('USER_SHUTDOWN')
87
+
88
+
89
+ # Setup signal handlers when module is imported
90
+ _setup_signal_handlers()
@@ -43,7 +43,7 @@ class ICommandOptions(TypedDict):
43
43
 
44
44
  class IScriptOptions(TypedDict):
45
45
  """Script execution options interface."""
46
- type: str # Required: Script type: 'cjs' for CommonJS, 'esm' for ES Modules
46
+ type: str # Required: Script type: 'cjs' for CommonJS, 'esm' for ES Modules, 'py' for Python
47
47
  cwd: NotRequired[str] # Working directory
48
48
  timeoutMs: NotRequired[int] # Timeout in milliseconds
49
49
  background: NotRequired[bool] # Run in background
@@ -4,5 +4,12 @@ This module contains core services for sandbox and browser management."""
4
4
 
5
5
  from .sandbox import SandboxService, CommandEventEmitter
6
6
  from .browser import BrowserService, CDPConnection
7
+ from .terminal import TerminalService, Commands, StreamableCommandResult
8
+ from .filesystem import FileSystemService
7
9
 
8
- __all__ = ['SandboxService', 'CommandEventEmitter', 'BrowserService', 'CDPConnection']
10
+ __all__ = [
11
+ 'SandboxService', 'CommandEventEmitter',
12
+ 'BrowserService', 'CDPConnection',
13
+ 'TerminalService', 'Commands', 'StreamableCommandResult',
14
+ 'FileSystemService'
15
+ ]