portacode 0.2.2__tar.gz → 0.2.4.dev0__tar.gz

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 (38) hide show
  1. {portacode-0.2.2 → portacode-0.2.4.dev0}/PKG-INFO +1 -1
  2. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/_version.py +2 -2
  3. portacode-0.2.4.dev0/portacode/connection/handlers/README.md +427 -0
  4. portacode-0.2.4.dev0/portacode/connection/handlers/__init__.py +41 -0
  5. portacode-0.2.4.dev0/portacode/connection/handlers/base.py +115 -0
  6. portacode-0.2.4.dev0/portacode/connection/handlers/file_handlers.py +209 -0
  7. portacode-0.2.4.dev0/portacode/connection/handlers/registry.py +111 -0
  8. portacode-0.2.4.dev0/portacode/connection/handlers/session.py +269 -0
  9. portacode-0.2.4.dev0/portacode/connection/handlers/system_handlers.py +32 -0
  10. portacode-0.2.4.dev0/portacode/connection/handlers/terminal_handlers.py +139 -0
  11. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/multiplex.py +7 -7
  12. portacode-0.2.4.dev0/portacode/connection/terminal.py +177 -0
  13. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/PKG-INFO +1 -1
  14. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/SOURCES.txt +9 -1
  15. portacode-0.2.2/portacode/connection/terminal.py +0 -501
  16. {portacode-0.2.2 → portacode-0.2.4.dev0}/.gitignore +0 -0
  17. {portacode-0.2.2 → portacode-0.2.4.dev0}/.gitmodules +0 -0
  18. {portacode-0.2.2 → portacode-0.2.4.dev0}/MANIFEST.in +0 -0
  19. {portacode-0.2.2 → portacode-0.2.4.dev0}/Makefile +0 -0
  20. {portacode-0.2.2 → portacode-0.2.4.dev0}/README.md +0 -0
  21. {portacode-0.2.2 → portacode-0.2.4.dev0}/docker-compose.yaml +0 -0
  22. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/README.md +0 -0
  23. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/__init__.py +0 -0
  24. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/__main__.py +0 -0
  25. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/cli.py +0 -0
  26. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/README.md +0 -0
  27. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/__init__.py +0 -0
  28. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/client.py +0 -0
  29. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/data.py +0 -0
  30. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/keypair.py +0 -0
  31. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/service.py +0 -0
  32. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/dependency_links.txt +0 -0
  33. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/entry_points.txt +0 -0
  34. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/requires.txt +0 -0
  35. {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/top_level.txt +0 -0
  36. {portacode-0.2.2 → portacode-0.2.4.dev0}/pyproject.toml +0 -0
  37. {portacode-0.2.2 → portacode-0.2.4.dev0}/setup.cfg +0 -0
  38. {portacode-0.2.2 → portacode-0.2.4.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.2.2
3
+ Version: 0.2.4.dev0
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.2'
21
- __version_tuple__ = version_tuple = (0, 2, 2)
20
+ __version__ = version = '0.2.4.dev'
21
+ __version_tuple__ = version_tuple = (0, 2, 4, 'dev0')
@@ -0,0 +1,427 @@
1
+ # Portacode Handler System
2
+
3
+ This directory contains the modular command handler system for the Portacode client. The system provides a clean, extensible architecture for processing commands from the gateway.
4
+
5
+ ## Architecture Overview
6
+
7
+ The handler system consists of:
8
+
9
+ - **Base Handler Classes**: `BaseHandler`, `AsyncHandler`, `SyncHandler`
10
+ - **Command Registry**: `CommandRegistry` for managing and dispatching handlers
11
+ - **Session Management**: `SessionManager` for terminal session lifecycle
12
+ - **Specific Handlers**: Individual command implementations
13
+
14
+ ## Existing Handlers
15
+
16
+ ### Terminal Handlers (`terminal_handlers.py`)
17
+
18
+ 1. **`TerminalStartHandler`** - `terminal_start`
19
+ - Starts new terminal sessions
20
+ - Supports shell and cwd parameters
21
+ - Handles both Windows (ConPTY) and Unix (PTY) systems
22
+
23
+ 2. **`TerminalSendHandler`** - `terminal_send`
24
+ - Sends data to existing terminal sessions
25
+ - Requires terminal_id and data parameters
26
+
27
+ 3. **`TerminalStopHandler`** - `terminal_stop`
28
+ - Terminates terminal sessions
29
+ - Cleans up resources and sends exit events
30
+
31
+ 4. **`TerminalListHandler`** - `terminal_list`
32
+ - Lists all active terminal sessions
33
+ - Returns session metadata and buffer contents
34
+
35
+ ### System Handlers (`system_handlers.py`)
36
+
37
+ 1. **`SystemInfoHandler`** - `system_info`
38
+ - Provides system resource information
39
+ - Returns CPU, memory, and disk usage data
40
+
41
+ ## Adding New Handlers
42
+
43
+ ### 1. Asynchronous Handlers
44
+
45
+ For commands that need to perform I/O operations, use `AsyncHandler`:
46
+
47
+ ```python
48
+ from .base import AsyncHandler
49
+ from typing import Any, Dict
50
+
51
+ class FileReadHandler(AsyncHandler):
52
+ """Handler for reading file contents."""
53
+
54
+ @property
55
+ def command_name(self) -> str:
56
+ return "file_read"
57
+
58
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
59
+ """Read file contents asynchronously."""
60
+ file_path = message.get("path")
61
+ if not file_path:
62
+ raise ValueError("path parameter is required")
63
+
64
+ # Async file operations
65
+ import aiofiles
66
+ async with aiofiles.open(file_path, 'r') as f:
67
+ content = await f.read()
68
+
69
+ return {
70
+ "event": "file_read_response",
71
+ "path": file_path,
72
+ "content": content,
73
+ }
74
+ ```
75
+
76
+ ### 2. Synchronous Handlers
77
+
78
+ For CPU-bound operations or simple synchronous tasks, use `SyncHandler`:
79
+
80
+ ```python
81
+ from .base import SyncHandler
82
+ from typing import Any, Dict
83
+ import os
84
+
85
+ class DirectoryListHandler(SyncHandler):
86
+ """Handler for listing directory contents."""
87
+
88
+ @property
89
+ def command_name(self) -> str:
90
+ return "directory_list"
91
+
92
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
93
+ """List directory contents synchronously."""
94
+ path = message.get("path", ".")
95
+
96
+ try:
97
+ items = []
98
+ for item in os.listdir(path):
99
+ item_path = os.path.join(path, item)
100
+ items.append({
101
+ "name": item,
102
+ "is_dir": os.path.isdir(item_path),
103
+ "size": os.path.getsize(item_path) if os.path.isfile(item_path) else 0,
104
+ })
105
+
106
+ return {
107
+ "event": "directory_list_response",
108
+ "path": path,
109
+ "items": items,
110
+ }
111
+ except Exception as e:
112
+ raise RuntimeError(f"Failed to list directory: {e}")
113
+ ```
114
+
115
+ ### 3. Complex Handler with Context Access
116
+
117
+ Handlers can access shared context and services:
118
+
119
+ ```python
120
+ from .base import AsyncHandler
121
+ from typing import Any, Dict
122
+
123
+ class ProcessManagementHandler(AsyncHandler):
124
+ """Handler for managing processes on the device."""
125
+
126
+ @property
127
+ def command_name(self) -> str:
128
+ return "process_management"
129
+
130
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
131
+ """Manage processes with access to session manager."""
132
+ action = message.get("action") # "list", "kill", "start"
133
+
134
+ # Access session manager from context
135
+ session_manager = self.context.get("session_manager")
136
+ if not session_manager:
137
+ raise RuntimeError("Session manager not available")
138
+
139
+ if action == "list":
140
+ import psutil
141
+ processes = []
142
+ for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
143
+ try:
144
+ processes.append(proc.info)
145
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
146
+ continue
147
+
148
+ return {
149
+ "event": "process_list_response",
150
+ "processes": processes,
151
+ }
152
+
153
+ elif action == "kill":
154
+ pid = message.get("pid")
155
+ if not pid:
156
+ raise ValueError("pid parameter required for kill action")
157
+
158
+ import psutil
159
+ try:
160
+ process = psutil.Process(pid)
161
+ process.terminate()
162
+ return {
163
+ "event": "process_killed",
164
+ "pid": pid,
165
+ }
166
+ except psutil.NoSuchProcess:
167
+ raise ValueError(f"Process {pid} not found")
168
+
169
+ else:
170
+ raise ValueError(f"Unknown action: {action}")
171
+ ```
172
+
173
+ ### 4. Handler with Custom Response Handling
174
+
175
+ You can override the `handle` method for custom response behavior:
176
+
177
+ ```python
178
+ from .base import BaseHandler
179
+ from typing import Any, Dict, Optional
180
+
181
+ class StreamingHandler(BaseHandler):
182
+ """Handler that streams responses."""
183
+
184
+ @property
185
+ def command_name(self) -> str:
186
+ return "streaming_data"
187
+
188
+ async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
189
+ """Handle streaming data with custom response handling."""
190
+ try:
191
+ # Send initial response
192
+ await self.send_response({
193
+ "event": "streaming_started",
194
+ "message": "Starting data stream",
195
+ }, reply_channel)
196
+
197
+ # Stream data
198
+ for i in range(10):
199
+ await asyncio.sleep(0.1)
200
+ await self.send_response({
201
+ "event": "streaming_data",
202
+ "chunk": i,
203
+ "data": f"Data chunk {i}",
204
+ }, reply_channel)
205
+
206
+ # Send completion
207
+ await self.send_response({
208
+ "event": "streaming_completed",
209
+ "message": "Stream completed",
210
+ }, reply_channel)
211
+
212
+ except Exception as exc:
213
+ await self.send_error(str(exc), reply_channel)
214
+ ```
215
+
216
+ ## Registering Handlers
217
+
218
+ ### 1. Automatic Registration
219
+
220
+ Add your handler to the `__init__.py` file and register it in `TerminalManager._register_default_handlers()`:
221
+
222
+ ```python
223
+ # In __init__.py
224
+ from .file_handlers import FileReadHandler, DirectoryListHandler
225
+
226
+ # In terminal.py _register_default_handlers()
227
+ def _register_default_handlers(self) -> None:
228
+ """Register the default command handlers."""
229
+ # ... existing handlers ...
230
+ self._command_registry.register(FileReadHandler)
231
+ self._command_registry.register(DirectoryListHandler)
232
+ ```
233
+
234
+ ### 2. Dynamic Registration
235
+
236
+ Register handlers at runtime:
237
+
238
+ ```python
239
+ # In your code
240
+ terminal_manager.register_handler(MyCustomHandler)
241
+
242
+ # List all registered commands
243
+ commands = terminal_manager.list_commands()
244
+ print(f"Available commands: {commands}")
245
+
246
+ # Unregister if needed
247
+ terminal_manager.unregister_handler("my_command")
248
+ ```
249
+
250
+ ## Handler Guidelines
251
+
252
+ ### Best Practices
253
+
254
+ 1. **Command Naming**: Use descriptive, action-oriented names (`file_read`, `system_info`)
255
+ 2. **Error Handling**: Always validate input parameters and provide meaningful error messages
256
+ 3. **Response Format**: Use consistent response formats with `event` field
257
+ 4. **Resource Management**: Clean up resources in exception handlers
258
+ 5. **Logging**: Use appropriate logging levels for debugging and monitoring
259
+
260
+ ### Parameter Validation
261
+
262
+ ```python
263
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
264
+ # Validate required parameters
265
+ required_params = ["param1", "param2"]
266
+ for param in required_params:
267
+ if param not in message:
268
+ raise ValueError(f"Missing required parameter: {param}")
269
+
270
+ # Validate parameter types
271
+ if not isinstance(message.get("count"), int):
272
+ raise ValueError("count parameter must be an integer")
273
+
274
+ # Your logic here...
275
+ ```
276
+
277
+ ### Error Handling
278
+
279
+ ```python
280
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
281
+ try:
282
+ # Your logic here
283
+ result = await some_async_operation()
284
+ return {"event": "success", "result": result}
285
+ except FileNotFoundError:
286
+ raise ValueError("File not found")
287
+ except PermissionError:
288
+ raise RuntimeError("Permission denied")
289
+ except Exception as e:
290
+ raise RuntimeError(f"Unexpected error: {e}")
291
+ ```
292
+
293
+ ## Testing Handlers
294
+
295
+ ### Unit Testing
296
+
297
+ ```python
298
+ import pytest
299
+ from unittest.mock import Mock, AsyncMock
300
+ from portacode.connection.handlers.your_handler import YourHandler
301
+
302
+ @pytest.mark.asyncio
303
+ async def test_your_handler():
304
+ # Mock control channel
305
+ control_channel = AsyncMock()
306
+
307
+ # Mock context
308
+ context = {"session_manager": Mock()}
309
+
310
+ # Create handler
311
+ handler = YourHandler(control_channel, context)
312
+
313
+ # Test execute
314
+ message = {"param1": "value1"}
315
+ result = await handler.execute(message)
316
+
317
+ assert result["event"] == "expected_event"
318
+ assert result["data"] == "expected_data"
319
+ ```
320
+
321
+ ### Integration Testing
322
+
323
+ ```python
324
+ # Test with real TerminalManager
325
+ terminal_manager = TerminalManager(mock_multiplexer)
326
+ terminal_manager.register_handler(YourHandler)
327
+
328
+ # Simulate command
329
+ message = {"cmd": "your_command", "param1": "value1"}
330
+ # Process through control loop...
331
+ ```
332
+
333
+ ## Communication Protocol
334
+
335
+ ### Command Format
336
+
337
+ Commands from the gateway follow this format:
338
+
339
+ ```json
340
+ {
341
+ "cmd": "command_name",
342
+ "param1": "value1",
343
+ "param2": "value2",
344
+ "reply_channel": "optional_reply_channel"
345
+ }
346
+ ```
347
+
348
+ ### Response Format
349
+
350
+ Responses to the gateway should follow this format:
351
+
352
+ ```json
353
+ {
354
+ "event": "event_name",
355
+ "data": "response_data",
356
+ "reply_channel": "reply_channel_if_provided"
357
+ }
358
+ ```
359
+
360
+ ### Error Format
361
+
362
+ Error responses:
363
+
364
+ ```json
365
+ {
366
+ "event": "error",
367
+ "message": "Error description",
368
+ "reply_channel": "reply_channel_if_provided"
369
+ }
370
+ ```
371
+
372
+ ## Advanced Topics
373
+
374
+ ### Custom Context
375
+
376
+ You can extend the context with custom services:
377
+
378
+ ```python
379
+ # In TerminalManager._set_mux()
380
+ self._context = {
381
+ "session_manager": self._session_manager,
382
+ "mux": mux,
383
+ "file_manager": FileManager(),
384
+ "process_manager": ProcessManager(),
385
+ }
386
+ ```
387
+
388
+ ### Handler Chaining
389
+
390
+ Handlers can call other handlers:
391
+
392
+ ```python
393
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
394
+ # Get another handler
395
+ other_handler = self.context.get("command_registry").get_handler("other_command")
396
+ if other_handler:
397
+ intermediate_result = await other_handler.execute(message)
398
+ # Process intermediate result...
399
+
400
+ return final_result
401
+ ```
402
+
403
+ ### Middleware Pattern
404
+
405
+ Create middleware handlers that wrap other handlers:
406
+
407
+ ```python
408
+ class LoggingMiddleware(BaseHandler):
409
+ def __init__(self, control_channel, context, wrapped_handler):
410
+ super().__init__(control_channel, context)
411
+ self.wrapped_handler = wrapped_handler
412
+
413
+ async def handle(self, message, reply_channel=None):
414
+ logger.info(f"Executing command: {message.get('cmd')}")
415
+ start_time = time.time()
416
+
417
+ try:
418
+ result = await self.wrapped_handler.handle(message, reply_channel)
419
+ duration = time.time() - start_time
420
+ logger.info(f"Command completed in {duration:.2f}s")
421
+ return result
422
+ except Exception as e:
423
+ logger.error(f"Command failed: {e}")
424
+ raise
425
+ ```
426
+
427
+ This modular system provides a clean, extensible architecture for adding new functionality to the Portacode client while maintaining backward compatibility and code organization.
@@ -0,0 +1,41 @@
1
+ """Modular command handler system for Portacode client.
2
+
3
+ This package provides a flexible system for handling commands from the gateway.
4
+ Handlers can be easily added, removed, or modified without touching the main
5
+ terminal manager code.
6
+ """
7
+
8
+ from .base import BaseHandler, AsyncHandler, SyncHandler
9
+ from .registry import CommandRegistry
10
+ from .terminal_handlers import (
11
+ TerminalStartHandler,
12
+ TerminalSendHandler,
13
+ TerminalStopHandler,
14
+ TerminalListHandler,
15
+ )
16
+ from .system_handlers import SystemInfoHandler
17
+ from .file_handlers import (
18
+ FileReadHandler,
19
+ FileWriteHandler,
20
+ DirectoryListHandler,
21
+ FileInfoHandler,
22
+ FileDeleteHandler,
23
+ )
24
+
25
+ __all__ = [
26
+ "BaseHandler",
27
+ "AsyncHandler",
28
+ "SyncHandler",
29
+ "CommandRegistry",
30
+ "TerminalStartHandler",
31
+ "TerminalSendHandler",
32
+ "TerminalStopHandler",
33
+ "TerminalListHandler",
34
+ "SystemInfoHandler",
35
+ # File operation handlers (optional - register as needed)
36
+ "FileReadHandler",
37
+ "FileWriteHandler",
38
+ "DirectoryListHandler",
39
+ "FileInfoHandler",
40
+ "FileDeleteHandler",
41
+ ]
@@ -0,0 +1,115 @@
1
+ """Base handler classes for command processing."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..multiplex import Channel
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class BaseHandler(ABC):
15
+ """Base class for all command handlers."""
16
+
17
+ def __init__(self, control_channel: "Channel", context: Dict[str, Any]):
18
+ """Initialize the handler.
19
+
20
+ Args:
21
+ control_channel: The control channel for sending responses
22
+ context: Shared context containing terminal manager state
23
+ """
24
+ self.control_channel = control_channel
25
+ self.context = context
26
+
27
+ @property
28
+ @abstractmethod
29
+ def command_name(self) -> str:
30
+ """Return the command name this handler processes."""
31
+ pass
32
+
33
+ @abstractmethod
34
+ async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
35
+ """Handle the command message.
36
+
37
+ Args:
38
+ message: The command message dict
39
+ reply_channel: Optional reply channel for responses
40
+ """
41
+ pass
42
+
43
+ async def send_response(self, payload: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
44
+ """Send a response back to the gateway.
45
+
46
+ Args:
47
+ payload: Response payload
48
+ reply_channel: Optional reply channel
49
+ """
50
+ if reply_channel:
51
+ payload["reply_channel"] = reply_channel
52
+ await self.control_channel.send(payload)
53
+
54
+ async def send_error(self, message: str, reply_channel: Optional[str] = None) -> None:
55
+ """Send an error response.
56
+
57
+ Args:
58
+ message: Error message
59
+ reply_channel: Optional reply channel
60
+ """
61
+ payload = {"event": "error", "message": message}
62
+ if reply_channel:
63
+ payload["reply_channel"] = reply_channel
64
+ await self.control_channel.send(payload)
65
+
66
+
67
+ class AsyncHandler(BaseHandler):
68
+ """Base class for asynchronous command handlers."""
69
+
70
+ @abstractmethod
71
+ async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
72
+ """Execute the command logic asynchronously.
73
+
74
+ Args:
75
+ message: The command message dict
76
+
77
+ Returns:
78
+ Response payload dict
79
+ """
80
+ pass
81
+
82
+ async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
83
+ """Handle the command by executing it and sending the response."""
84
+ try:
85
+ response = await self.execute(message)
86
+ await self.send_response(response, reply_channel)
87
+ except Exception as exc:
88
+ logger.exception("Error in async handler %s: %s", self.command_name, exc)
89
+ await self.send_error(str(exc), reply_channel)
90
+
91
+
92
+ class SyncHandler(BaseHandler):
93
+ """Base class for synchronous command handlers."""
94
+
95
+ @abstractmethod
96
+ def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
97
+ """Execute the command logic synchronously.
98
+
99
+ Args:
100
+ message: The command message dict
101
+
102
+ Returns:
103
+ Response payload dict
104
+ """
105
+ pass
106
+
107
+ async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
108
+ """Handle the command by executing it in an executor and sending the response."""
109
+ try:
110
+ loop = asyncio.get_running_loop()
111
+ response = await loop.run_in_executor(None, self.execute, message)
112
+ await self.send_response(response, reply_channel)
113
+ except Exception as exc:
114
+ logger.exception("Error in sync handler %s: %s", self.command_name, exc)
115
+ await self.send_error(str(exc), reply_channel)