portacode 0.3.12.dev6__tar.gz → 0.3.12.dev7__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 (44) hide show
  1. portacode-0.3.12.dev7/.claude/agents/communication-manager.md +5 -0
  2. portacode-0.3.12.dev7/.claude/settings.local.json +9 -0
  3. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/PKG-INFO +1 -1
  4. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/docker-compose.yaml +0 -24
  5. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/_version.py +2 -2
  6. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +112 -3
  7. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/session.py +11 -7
  8. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/terminal.py +33 -8
  9. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/PKG-INFO +1 -1
  10. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/SOURCES.txt +1 -0
  11. portacode-0.3.12.dev6/.claude/settings.local.json +0 -8
  12. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/.gitignore +0 -0
  13. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/.gitmodules +0 -0
  14. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/LICENSE +0 -0
  15. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/MANIFEST.in +0 -0
  16. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/Makefile +0 -0
  17. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/README.md +0 -0
  18. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/backup.sh +0 -0
  19. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/README.md +0 -0
  20. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/__init__.py +0 -0
  21. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/__main__.py +0 -0
  22. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/cli.py +0 -0
  23. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/README.md +0 -0
  24. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/__init__.py +0 -0
  25. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/client.py +0 -0
  26. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/README.md +0 -0
  27. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/__init__.py +0 -0
  28. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/base.py +0 -0
  29. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/file_handlers.py +0 -0
  30. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/registry.py +0 -0
  31. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/system_handlers.py +0 -0
  32. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/handlers/terminal_handlers.py +0 -0
  33. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/connection/multiplex.py +0 -0
  34. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/data.py +0 -0
  35. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/keypair.py +0 -0
  36. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode/service.py +0 -0
  37. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/dependency_links.txt +0 -0
  38. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/entry_points.txt +0 -0
  39. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/requires.txt +0 -0
  40. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/portacode.egg-info/top_level.txt +0 -0
  41. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/pyproject.toml +0 -0
  42. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/restore.sh +0 -0
  43. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/setup.cfg +0 -0
  44. {portacode-0.3.12.dev6 → portacode-0.3.12.dev7}/setup.py +0 -0
@@ -0,0 +1,5 @@
1
+ ---
2
+ name: communication-manager
3
+ description: Expert code protocol engineer. Accurately defines and improves simple yet straightforward and concise scallable communication protocols for websocket communication between devices and user sessions
4
+ tools: Read, Grep, Glob
5
+ ---
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:stackoverflow.com)",
5
+ "Bash(docker-compose logs:*)"
6
+ ],
7
+ "deny": []
8
+ }
9
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.12.dev6
3
+ Version: 0.3.12.dev7
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -32,30 +32,6 @@ services:
32
32
  volumes:
33
33
  - ./server/portacode_django:/app
34
34
 
35
- portacode-gateway:
36
- build:
37
- context: ./server
38
- dockerfile: Dockerfile
39
- container_name: portacode-gateway
40
- restart: unless-stopped
41
- env_file:
42
- - main.env
43
- depends_on:
44
- db:
45
- condition: service_healthy
46
- redis:
47
- condition: service_started
48
- ports:
49
- - "8000:8000"
50
- volumes:
51
- - ./server:/app
52
-
53
- redis:
54
- image: redis:7-alpine
55
- container_name: portacode-redis
56
- restart: unless-stopped
57
- ports:
58
- - "6379:6379"
59
35
 
60
36
  volumes:
61
37
  pgdata:
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.3.12.dev6'
21
- __version_tuple__ = version_tuple = (0, 3, 12, 'dev6')
20
+ __version__ = version = '0.3.12.dev7'
21
+ __version_tuple__ = version_tuple = (0, 3, 12, 'dev7')
@@ -2,6 +2,51 @@
2
2
 
3
3
  This document outlines the WebSocket communication protocol between the Portacode server and the connected client devices.
4
4
 
5
+ ## Table of Contents
6
+
7
+ - [Raw Message Format](#raw-message-format)
8
+ - [Actions](#actions)
9
+ - [Terminal Actions](#terminal-actions)
10
+ - [`terminal_start`](#terminal_start)
11
+ - [`terminal_send`](#terminal_send)
12
+ - [`terminal_stop`](#terminal_stop)
13
+ - [`terminal_list`](#terminal_list)
14
+ - [System Actions](#system-actions)
15
+ - [`system_info`](#system_info)
16
+ - [File Actions](#file-actions)
17
+ - [`file_read`](#file_read)
18
+ - [`file_write`](#file_write)
19
+ - [`directory_list`](#directory_list)
20
+ - [`file_info`](#file_info)
21
+ - [`file_delete`](#file_delete)
22
+ - [Client Session Management](#client-session-management)
23
+ - [`client_sessions_update`](#client_sessions_update)
24
+ - [Events](#events)
25
+ - [Error Events](#error-events)
26
+ - [`error`](#error)
27
+ - [Terminal Events](#terminal-events)
28
+ - [`terminal_started`](#terminal_started)
29
+ - [`terminal_exit`](#terminal_exit)
30
+ - [`terminal_send_ack`](#terminal_send_ack)
31
+ - [`terminal_stopped`](#terminal_stopped)
32
+ - [`terminal_stop_completed`](#terminal_stop_completed)
33
+ - [`terminal_list`](#terminal_list-event)
34
+ - [System Events](#system-events)
35
+ - [`system_info`](#system_info-event)
36
+ - [File Events](#file-events)
37
+ - [`file_read_response`](#file_read_response)
38
+ - [`file_write_response`](#file_write_response)
39
+ - [`directory_list_response`](#directory_list_response)
40
+ - [`file_info_response`](#file_info_response)
41
+ - [`file_delete_response`](#file_delete_response)
42
+ - [Client Session Events](#client-session-events)
43
+ - [`request_client_sessions`](#request_client_sessions)
44
+ - [Terminal Data](#terminal-data)
45
+ - [Terminal I/O Data](#terminal_data)
46
+ - [Server-Side Events](#server-side-events)
47
+ - [`device_status`](#device_status)
48
+ - [`devices`](#devices)
49
+
5
50
  ## Raw Message Format
6
51
 
7
52
  All communication over the WebSocket is managed by a [multiplexer](./multiplex.py) that wraps every message in a JSON object with a `channel` and a `payload`. This allows for multiple virtual communication channels over a single connection.
@@ -41,7 +86,7 @@ Actions are messages sent from the server to the device, placed within the `payl
41
86
 
42
87
  * `command` (string, mandatory): The name of the action to be executed (e.g., `terminal_start`).
43
88
  * `payload` (object, mandatory): An object containing the specific arguments for the action.
44
- * `reply_channel` (string, optional): A channel name that the device will echo back in its response. The difference between this field and the `channel` field handled in the device connection [multiplexer](./multiplex.py), is simply that `channel` defines which application in the device is handeling this communication, while `reply_channel` defines which client session on the server side is communicating with the device. That's why the [multiplexer](./multiplex.py) for `channel` is here, while the multiplexer for the `reply_channel` is on the other side of the conversation. A lot like the source port number and destination port number in TCP/IP.
89
+ * `reply_channel` (string, optional): **DEPRECATED** - A channel name for backward compatibility. Modern implementations should use the `client_sessions` mechanism instead.
45
90
 
46
91
  ### `terminal_start`
47
92
 
@@ -177,6 +222,20 @@ Deletes a file or directory. Handled by [`file_delete`](./file_handlers.py).
177
222
  * On success, the device will respond with a [`file_delete_response`](#file_delete_response) event.
178
223
  * On error, a generic [`error`](#error) event is sent.
179
224
 
225
+ ### Client Session Management
226
+
227
+ ### `client_sessions_update`
228
+
229
+ Sends updated client session information to the device. This is a special internal action used by the server to inform devices about connected client sessions.
230
+
231
+ **Payload Fields:**
232
+
233
+ * `sessions` (array, mandatory): Array of client session objects containing connection information.
234
+
235
+ **Responses:**
236
+
237
+ This action does not generate a response event.
238
+
180
239
  ---
181
240
 
182
241
  ## Events
@@ -194,7 +253,8 @@ Events are messages sent from the device to the server, placed within the `paylo
194
253
  ```
195
254
 
196
255
  * `event` (string, mandatory): The name of the event being sent (e.g., `terminal_started`).
197
- * <a name="reply_channel"></a>`reply_channel` (string, optional): If the event is a direct response to an action that included a `reply_channel`, this field will contain the same value. This allows the server to correlate responses with their original requests.
256
+ * <a name="reply_channel"></a>`reply_channel` (string, optional): **DEPRECATED** - For backward compatibility only. Modern events include `client_sessions` array for targeting.
257
+ * `client_sessions` (array, optional): Array of client session channel names that should receive this event. This is the modern way to target specific connected clients.
198
258
 
199
259
  ### <a name="error"></a>`error`
200
260
 
@@ -328,4 +388,53 @@ Confirms that a file or directory has been deleted in response to a `file_delete
328
388
 
329
389
  * `path` (string, mandatory): The path of the deleted file or directory.
330
390
  * `deleted_type` (string, mandatory): The type of the deleted item ("file" or "directory").
331
- * `success` (boolean, mandatory): Indicates whether the deletion was successful.
391
+ * `success` (boolean, mandatory): Indicates whether the deletion was successful.
392
+
393
+ ### Client Session Events
394
+
395
+ ### <a name="request_client_sessions"></a>`request_client_sessions`
396
+
397
+ Sent by the device to request the current list of connected client sessions from the server. This is an internal event used during device initialization and reconnection.
398
+
399
+ **Event Fields:**
400
+
401
+ This event carries no additional fields.
402
+
403
+ ### Terminal Data
404
+
405
+ ### <a name="terminal_data"></a>Terminal I/O Data
406
+
407
+ Terminal input/output data is sent directly on terminal channels (not on the control channel). Each terminal session has its own dedicated channel identified by the terminal's UUID.
408
+
409
+ **Terminal Data Format:**
410
+
411
+ ```json
412
+ {
413
+ "channel": "<terminal_uuid>",
414
+ "payload": "<terminal_output_string>"
415
+ }
416
+ ```
417
+
418
+ * Terminal output is sent as raw string data in the payload
419
+ * Input to terminals is sent the same way but in the opposite direction
420
+ * No event wrapper is used for terminal I/O data
421
+
422
+ ### Server-Side Events
423
+
424
+ ### <a name="device_status"></a>`device_status`
425
+
426
+ Sent by the server to clients to indicate device online/offline status changes.
427
+
428
+ **Event Fields:**
429
+
430
+ * `device` (object, mandatory): Device status information
431
+ * `id` (integer, mandatory): Device ID
432
+ * `online` (boolean, mandatory): Whether the device is online
433
+
434
+ ### <a name="devices"></a>`devices`
435
+
436
+ Sent by the server to clients to provide initial device list snapshot.
437
+
438
+ **Event Fields:**
439
+
440
+ * `devices` (array, mandatory): Array of device objects with status information
@@ -78,8 +78,15 @@ class TerminalSession:
78
78
  logger.warning("stdin pipe closed for terminal %s", self.id)
79
79
  return
80
80
  try:
81
- self.proc.stdin.write(data.encode())
82
- await self.proc.stdin.drain()
81
+ if hasattr(self.proc.stdin, 'write') and hasattr(self.proc.stdin, 'drain'):
82
+ # StreamWriter (pipe fallback)
83
+ self.proc.stdin.write(data.encode())
84
+ await self.proc.stdin.drain()
85
+ else:
86
+ # File object (PTY)
87
+ loop = asyncio.get_running_loop()
88
+ await loop.run_in_executor(None, self.proc.stdin.write, data.encode())
89
+ await loop.run_in_executor(None, self.proc.stdin.flush)
83
90
  except Exception as exc:
84
91
  logger.warning("Failed to write to terminal %s: %s", self.id, exc)
85
92
 
@@ -301,11 +308,8 @@ class SessionManager:
301
308
  protocol = asyncio.StreamReaderProtocol(reader)
302
309
  await loop.connect_read_pipe(lambda: protocol, os.fdopen(master_fd, "rb", buffering=0))
303
310
  proc.stdout = reader
304
- # Use writer for stdin
305
- writer_transport, writer_protocol = await loop.connect_write_pipe(
306
- lambda: asyncio.Protocol(), os.fdopen(master_fd, "wb", buffering=0)
307
- )
308
- proc.stdin = asyncio.StreamWriter(writer_transport, writer_protocol, reader, loop)
311
+ # Use writer for stdin - create a simple file-like wrapper
312
+ proc.stdin = os.fdopen(master_fd, "wb", buffering=0)
309
313
  except Exception:
310
314
  logger.warning("Failed to allocate PTY, falling back to pipes")
311
315
  proc = await asyncio.create_subprocess_exec(
@@ -92,7 +92,12 @@ class ClientSessionManager:
92
92
 
93
93
  target_sessions = []
94
94
  for session in self._client_sessions.values():
95
- # If project_id specified, filter by project
95
+ # Dashboard sessions should receive ALL events regardless of project_id
96
+ if session.get("connection_type") == "dashboard":
97
+ target_sessions.append(session.get("channel_name"))
98
+ continue
99
+
100
+ # For project sessions, filter by project_id if specified
96
101
  if project_id and session.get("project_id") != project_id:
97
102
  continue
98
103
  target_sessions.append(session.get("channel_name"))
@@ -241,13 +246,17 @@ class TerminalManager:
241
246
  # Handle client sessions update directly (special case)
242
247
  if cmd == "client_sessions_update":
243
248
  sessions = message.get("sessions", [])
244
- logger.info("terminal_manager: Handling client_sessions_update with %d sessions", len(sessions))
249
+ logger.info("terminal_manager: 🔔 RECEIVED client_sessions_update with %d sessions", len(sessions))
250
+ logger.debug("terminal_manager: Session details: %s", sessions)
245
251
  self._client_session_manager.update_sessions(sessions)
246
- logger.info("terminal_manager: Updated client sessions (%d sessions)", len(sessions))
252
+ logger.info("terminal_manager: Updated client sessions (%d sessions)", len(sessions))
247
253
 
248
254
  # Auto-send initial data to new clients
249
255
  if len(sessions) > 0:
256
+ logger.info("terminal_manager: 🚀 Triggering auto-send of initial data to clients")
250
257
  await self._send_initial_data_to_clients()
258
+ else:
259
+ logger.info("terminal_manager: ℹ️ No sessions to send data to")
251
260
  continue
252
261
 
253
262
  # Dispatch command through registry
@@ -263,37 +272,53 @@ class TerminalManager:
263
272
 
264
273
  async def _send_initial_data_to_clients(self):
265
274
  """Send initial system info and terminal list to connected clients."""
266
- logger.info("terminal_manager: Sending initial data to connected clients")
275
+ logger.info("terminal_manager: 📤 Starting to send initial data to connected clients")
267
276
 
268
277
  try:
269
278
  # Send system_info
270
- logger.info("terminal_manager: Sending system_info to clients")
279
+ logger.info("terminal_manager: 📊 Dispatching system_info command")
271
280
  await self._command_registry.dispatch("system_info", {}, None)
281
+ logger.info("terminal_manager: ✅ system_info dispatch completed")
272
282
 
273
283
  # Send terminal_list for each project that has connected clients
274
- logger.info("terminal_manager: Sending terminal_list to clients")
284
+ logger.info("terminal_manager: 📋 Preparing to send terminal_list to clients")
275
285
 
276
286
  # Get unique project IDs from connected clients
277
287
  project_ids = set()
278
- for session in self._client_session_manager.get_sessions().values():
288
+ all_sessions = self._client_session_manager.get_sessions()
289
+ logger.info(f"terminal_manager: Analyzing {len(all_sessions)} client sessions for project IDs")
290
+
291
+ for session in all_sessions.values():
279
292
  project_id = session.get("project_id")
293
+ connection_type = session.get("connection_type", "unknown")
294
+ logger.debug(f"terminal_manager: Session {session.get('channel_name')}: project_id={project_id}, type={connection_type}")
280
295
  if project_id:
281
296
  project_ids.add(project_id)
282
297
 
298
+ logger.info(f"terminal_manager: Found {len(project_ids)} unique project IDs: {list(project_ids)}")
299
+
283
300
  # Send terminal_list for each project, plus one without project_id for general sessions
284
301
  if not project_ids:
285
302
  # No specific projects, send general terminal_list
303
+ logger.info("terminal_manager: 📋 Dispatching general terminal_list (no specific projects)")
286
304
  await self._command_registry.dispatch("terminal_list", {}, None)
305
+ logger.info("terminal_manager: ✅ General terminal_list dispatch completed")
287
306
  else:
288
307
  # Send terminal_list for each project
289
308
  for project_id in project_ids:
309
+ logger.info(f"terminal_manager: 📋 Dispatching terminal_list for project {project_id}")
290
310
  await self._command_registry.dispatch("terminal_list", {"project_id": project_id}, None)
311
+ logger.info(f"terminal_manager: ✅ Project {project_id} terminal_list dispatch completed")
291
312
 
292
313
  # Also send general terminal_list for dashboard connections
314
+ logger.info("terminal_manager: 📋 Dispatching general terminal_list for dashboard connections")
293
315
  await self._command_registry.dispatch("terminal_list", {}, None)
316
+ logger.info("terminal_manager: ✅ General terminal_list for dashboard dispatch completed")
317
+
318
+ logger.info("terminal_manager: 🎉 All initial data sent successfully")
294
319
 
295
320
  except Exception as exc:
296
- logger.exception("terminal_manager: Error sending initial data to clients: %s", exc)
321
+ logger.exception("terminal_manager: Error sending initial data to clients: %s", exc)
297
322
 
298
323
  # ------------------------------------------------------------------
299
324
  # Extension API
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portacode
3
- Version: 0.3.12.dev6
3
+ Version: 0.3.12.dev7
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -10,6 +10,7 @@ pyproject.toml
10
10
  restore.sh
11
11
  setup.py
12
12
  .claude/settings.local.json
13
+ .claude/agents/communication-manager.md
13
14
  portacode/README.md
14
15
  portacode/__init__.py
15
16
  portacode/__main__.py
@@ -1,8 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "WebFetch(domain:stackoverflow.com)"
5
- ],
6
- "deny": []
7
- }
8
- }
File without changes