replx-agent 2.0.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.

Potentially problematic release.


This version of replx-agent might be problematic. Click here for more details.

Files changed (46) hide show
  1. replx_agent/__init__.py +3 -0
  2. replx_agent/__main__.py +5 -0
  3. replx_agent/commands.py +131 -0
  4. replx_agent/helpers/__init__.py +3 -0
  5. replx_agent/helpers/scanner.py +308 -0
  6. replx_agent/pkg/__init__.py +11 -0
  7. replx_agent/pkg/compiler.py +153 -0
  8. replx_agent/pkg/registry.py +387 -0
  9. replx_agent/pkg/service.py +690 -0
  10. replx_agent/pkg/store.py +83 -0
  11. replx_agent/protocol/__init__.py +11 -0
  12. replx_agent/protocol/repl.py +1029 -0
  13. replx_agent/protocol/storage.py +1018 -0
  14. replx_agent/rpc/__init__.py +3 -0
  15. replx_agent/rpc/protocol.py +220 -0
  16. replx_agent/server/__init__.py +3 -0
  17. replx_agent/server/__main__.py +4 -0
  18. replx_agent/server/command_dispatcher.py +49 -0
  19. replx_agent/server/connection_manager.py +654 -0
  20. replx_agent/server/core.py +1221 -0
  21. replx_agent/server/handlers/__init__.py +24 -0
  22. replx_agent/server/handlers/exec.py +553 -0
  23. replx_agent/server/handlers/filesystem.py +383 -0
  24. replx_agent/server/handlers/firmware.py +118 -0
  25. replx_agent/server/handlers/i2c.py +23 -0
  26. replx_agent/server/handlers/package.py +182 -0
  27. replx_agent/server/handlers/repl.py +188 -0
  28. replx_agent/server/handlers/session.py +205 -0
  29. replx_agent/server/handlers/spi.py +23 -0
  30. replx_agent/server/handlers/transfer.py +600 -0
  31. replx_agent/server/handlers/uart.py +32 -0
  32. replx_agent/server/session_manager.py +391 -0
  33. replx_agent/server/tcp.py +207 -0
  34. replx_agent/terminal.py +566 -0
  35. replx_agent/transport/__init__.py +32 -0
  36. replx_agent/transport/serial.py +220 -0
  37. replx_agent/utils/__init__.py +95 -0
  38. replx_agent/utils/constants.py +78 -0
  39. replx_agent/utils/device_info.py +183 -0
  40. replx_agent/utils/exceptions.py +31 -0
  41. replx_agent-2.0.0.dist-info/METADATA +74 -0
  42. replx_agent-2.0.0.dist-info/RECORD +46 -0
  43. replx_agent-2.0.0.dist-info/WHEEL +5 -0
  44. replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
  45. replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
  46. replx_agent-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ from .protocol import AgentProtocol
2
+
3
+ __all__ = ["AgentProtocol"]
@@ -0,0 +1,220 @@
1
+ import base64
2
+ import json
3
+ import socket
4
+ import struct
5
+ import threading
6
+ import time
7
+ from enum import IntEnum
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from replx_agent.utils.constants import MAX_PAYLOAD_SIZE
11
+
12
+ _MAGIC = b"RPLX"
13
+ _VERSION = 2
14
+ _HEADER = struct.Struct("!4sBBII")
15
+ _MAX_BINARY_FRAME_SIZE = 256 * 1024
16
+ _READ_SIZE = 64 * 1024
17
+
18
+
19
+ class FrameType(IntEnum):
20
+ JSON = 1
21
+ BINARY = 2
22
+
23
+
24
+ _seq_lock = threading.Lock()
25
+ _seq_counter = int(time.time() * 1000000) & 0xFFFFFFFE
26
+
27
+
28
+ def _next_seq() -> int:
29
+ global _seq_counter
30
+ with _seq_lock:
31
+ _seq_counter = (_seq_counter + 1) & 0xFFFFFFFF
32
+ return _seq_counter
33
+
34
+
35
+ class FrameReader:
36
+ def __init__(self, sock: socket.socket):
37
+ self._sock = sock
38
+ self._buffer = bytearray()
39
+
40
+ def _extract_frame(self) -> Optional[bytes]:
41
+ if len(self._buffer) < _HEADER.size:
42
+ return None
43
+
44
+ magic, version, frame_type, _seq, length = _HEADER.unpack_from(self._buffer)
45
+ if magic != _MAGIC or version != _VERSION:
46
+ raise ValueError("Invalid agent frame header")
47
+ try:
48
+ kind = FrameType(frame_type)
49
+ except ValueError as exc:
50
+ raise ValueError(f"Unknown agent frame type: {frame_type}") from exc
51
+
52
+ maximum = MAX_PAYLOAD_SIZE if kind is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
53
+ if length > maximum:
54
+ raise ValueError(f"Agent frame exceeds {maximum} bytes")
55
+
56
+ frame_size = _HEADER.size + length
57
+ if len(self._buffer) < frame_size:
58
+ return None
59
+
60
+ frame = bytes(self._buffer[:frame_size])
61
+ del self._buffer[:frame_size]
62
+ return frame
63
+
64
+ def receive(self, timeout: Optional[float] = None) -> Optional[bytes]:
65
+ self._sock.settimeout(timeout)
66
+ while True:
67
+ frame = self._extract_frame()
68
+ if frame is not None:
69
+ return frame
70
+
71
+ try:
72
+ chunk = self._sock.recv(_READ_SIZE)
73
+ except socket.timeout:
74
+ return None
75
+
76
+ if not chunk:
77
+ if self._buffer:
78
+ raise ConnectionError("Agent closed the connection mid-frame")
79
+ raise ConnectionError("Agent closed the connection")
80
+ self._buffer.extend(chunk)
81
+
82
+
83
+ class AgentProtocol:
84
+ @staticmethod
85
+ def _encode_frame(frame_type: FrameType, seq: int, payload: bytes) -> bytes:
86
+ maximum = MAX_PAYLOAD_SIZE if frame_type is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
87
+ if len(payload) > maximum:
88
+ raise ValueError(f"Agent frame too large: {len(payload)} bytes")
89
+ return _HEADER.pack(_MAGIC, _VERSION, int(frame_type), seq & 0xFFFFFFFF, len(payload)) + payload
90
+
91
+ @staticmethod
92
+ def encode_message(msg: Dict[str, Any]) -> bytes:
93
+ payload = json.dumps(msg, separators=(",", ":")).encode("utf-8")
94
+ return AgentProtocol._encode_frame(
95
+ FrameType.JSON, int(msg.get("seq", 0)), payload
96
+ )
97
+
98
+ @staticmethod
99
+ def encode_binary(seq: int, payload: bytes) -> bytes:
100
+ return AgentProtocol._encode_frame(FrameType.BINARY, seq, payload)
101
+
102
+ @staticmethod
103
+ def decode_frame(data: bytes) -> tuple[FrameType, int, bytes]:
104
+ if len(data) < _HEADER.size:
105
+ raise ValueError("Agent frame header is incomplete")
106
+
107
+ magic, version, frame_type, seq, length = _HEADER.unpack_from(data)
108
+ if magic != _MAGIC or version != _VERSION:
109
+ raise ValueError("Invalid agent frame header")
110
+ try:
111
+ kind = FrameType(frame_type)
112
+ except ValueError as exc:
113
+ raise ValueError(f"Unknown agent frame type: {frame_type}") from exc
114
+
115
+ maximum = MAX_PAYLOAD_SIZE if kind is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
116
+ if length > maximum:
117
+ raise ValueError(f"Agent frame exceeds {maximum} bytes")
118
+ if len(data) != _HEADER.size + length:
119
+ raise ValueError("Agent frame length does not match its header")
120
+
121
+ return kind, seq, data[_HEADER.size:]
122
+
123
+ @staticmethod
124
+ def decode_message(data: bytes) -> Optional[Dict[str, Any]]:
125
+ try:
126
+ frame_type, _seq, payload = AgentProtocol.decode_frame(data)
127
+ if frame_type is not FrameType.JSON:
128
+ return None
129
+ message = json.loads(payload.decode("utf-8"))
130
+ return message if isinstance(message, dict) else None
131
+ except (UnicodeDecodeError, ValueError, json.JSONDecodeError):
132
+ return None
133
+
134
+ @staticmethod
135
+ def create_request(
136
+ command: str,
137
+ seq: int = None,
138
+ ppid: int = None,
139
+ port: str = None,
140
+ auth_token: str = None,
141
+ **args,
142
+ ) -> Dict[str, Any]:
143
+ if seq is None:
144
+ seq = _next_seq()
145
+
146
+ request = {
147
+ "seq": seq,
148
+ "type": "request",
149
+ "command": command,
150
+ "args": args,
151
+ }
152
+ if ppid is not None:
153
+ request["ppid"] = ppid
154
+ if port is not None:
155
+ request["port"] = port
156
+ if auth_token:
157
+ request["auth_token"] = auth_token
158
+ return request
159
+
160
+ @staticmethod
161
+ def create_response(seq: int, result: Any = None, error: str = None) -> Dict[str, Any]:
162
+ return {
163
+ "seq": seq,
164
+ "type": "response",
165
+ "result": result,
166
+ "error": error,
167
+ }
168
+
169
+ @staticmethod
170
+ def create_ack(seq: int) -> Dict[str, Any]:
171
+ return {"seq": seq, "type": "ack"}
172
+
173
+ @staticmethod
174
+ def create_stream(seq: int, data: bytes, stream_type: str = "stdout") -> Dict[str, Any]:
175
+ return {
176
+ "seq": seq,
177
+ "type": "stream",
178
+ "data": base64.b64encode(data).decode("ascii"),
179
+ "stream_type": stream_type,
180
+ }
181
+
182
+ @staticmethod
183
+ def create_progress_stream(seq: int, progress_data: Dict[str, Any]) -> Dict[str, Any]:
184
+ return {
185
+ "seq": seq,
186
+ "type": "stream",
187
+ "data": progress_data,
188
+ "stream_type": "progress",
189
+ }
190
+
191
+ @staticmethod
192
+ def create_input(seq: int, data: bytes, ppid: int = None, port: str = None) -> Dict[str, Any]:
193
+ message = {
194
+ "seq": seq,
195
+ "type": "input",
196
+ "data": base64.b64encode(data).decode("ascii"),
197
+ }
198
+ if ppid is not None:
199
+ message["ppid"] = ppid
200
+ if port is not None:
201
+ message["port"] = port
202
+ return message
203
+
204
+ @staticmethod
205
+ def create_stream_ack(seq: int) -> Dict[str, Any]:
206
+ return {"seq": seq, "type": "stream_ack"}
207
+
208
+ @staticmethod
209
+ def decode_stream_data(msg: Dict[str, Any]) -> bytes:
210
+ data_b64 = msg.get("data", "")
211
+ return base64.b64decode(data_b64) if data_b64 else b""
212
+
213
+ @staticmethod
214
+ def chunk_large_data(data: str, max_size: int = MAX_PAYLOAD_SIZE) -> List[str]:
215
+ data_bytes = data.encode("utf-8")
216
+ chunk_size = max_size - 100
217
+ return [
218
+ data_bytes[offset:offset + chunk_size].decode("utf-8", errors="ignore")
219
+ for offset in range(0, len(data_bytes), chunk_size)
220
+ ]
@@ -0,0 +1,3 @@
1
+ from .core import AgentServer, main
2
+
3
+ __all__ = ['AgentServer', 'main']
@@ -0,0 +1,4 @@
1
+ from .core import main
2
+
3
+ if __name__ == '__main__':
4
+ main()
@@ -0,0 +1,49 @@
1
+ from typing import Optional, Hashable
2
+
3
+ from .connection_manager import BoardConnection
4
+ from replx_agent.commands import CmdGroups
5
+
6
+ class CommandContext:
7
+ def __init__(
8
+ self,
9
+ connection: Optional[BoardConnection] = None,
10
+ ppid: Optional[Hashable] = None,
11
+ sid: Optional[int] = None,
12
+ client_ip: Optional[str] = None,
13
+ explicit_port: Optional[str] = None,
14
+ seq: int = 0,
15
+ client_addr: tuple = None
16
+ ):
17
+ self.connection = connection
18
+ self.ppid = ppid
19
+ self.sid = sid if sid is not None else (ppid[1] if isinstance(ppid, tuple) else ppid)
20
+ address = getattr(client_addr, 'address', client_addr)
21
+ self.client_ip = client_ip or (address[0] if address else None)
22
+ self.explicit_port = explicit_port
23
+ self.seq = seq
24
+ self.client_addr = client_addr
25
+
26
+ if connection:
27
+ self.repl_protocol = connection.repl_protocol
28
+ self.file_system = connection.file_system
29
+ self.port = connection.port
30
+ self.core = connection.core
31
+ self.device = connection.device
32
+ self.manufacturer = connection.manufacturer
33
+ self.version = connection.version
34
+ self.device_root_fs = connection.device_root_fs
35
+ else:
36
+ self.repl_protocol = None
37
+ self.file_system = None
38
+ self.port = ""
39
+ self.core = ""
40
+ self.device = ""
41
+ self.manufacturer = "?"
42
+ self.version = "?"
43
+ self.device_root_fs = "/"
44
+
45
+ NON_REPL_COMMANDS = CmdGroups.NON_REPL
46
+ READ_ONLY_COMMANDS = CmdGroups.READ_ONLY
47
+ SESSION_COMMANDS = CmdGroups.SESSION
48
+ PERSISTENT_BUSY_COMMANDS = CmdGroups.PERSISTENT_BUSY
49
+ STREAMING_COMMANDS = CmdGroups.STREAMING