replx 1.1.1__tar.gz → 1.3__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 (152) hide show
  1. {replx-1.1.1 → replx-1.3}/LICENSE +1 -1
  2. replx-1.3/PKG-INFO +25 -0
  3. {replx-1.1.1 → replx-1.3}/pyproject.toml +10 -3
  4. {replx-1.1.1 → replx-1.3}/replx/__init__.py +1 -1
  5. replx-1.3/replx/cli/__init__.py +13 -0
  6. replx-1.3/replx/cli/agent/__init__.py +14 -0
  7. replx-1.3/replx/cli/agent/client/__init__.py +14 -0
  8. replx-1.3/replx/cli/agent/client/core.py +373 -0
  9. replx-1.3/replx/cli/agent/client/session.py +148 -0
  10. replx-1.3/replx/cli/agent/protocol.py +131 -0
  11. replx-1.3/replx/cli/agent/server/__init__.py +3 -0
  12. replx-1.3/replx/cli/agent/server/__main__.py +4 -0
  13. replx-1.3/replx/cli/agent/server/command_dispatcher.py +44 -0
  14. replx-1.3/replx/cli/agent/server/connection_manager.py +538 -0
  15. replx-1.3/replx/cli/agent/server/core.py +701 -0
  16. replx-1.3/replx/cli/agent/server/handlers/__init__.py +14 -0
  17. replx-1.3/replx/cli/agent/server/handlers/exec.py +485 -0
  18. replx-1.3/replx/cli/agent/server/handlers/filesystem.py +381 -0
  19. replx-1.3/replx/cli/agent/server/handlers/repl.py +123 -0
  20. replx-1.3/replx/cli/agent/server/handlers/session.py +223 -0
  21. replx-1.3/replx/cli/agent/server/handlers/transfer.py +552 -0
  22. replx-1.3/replx/cli/agent/server/session_manager.py +277 -0
  23. replx-1.3/replx/cli/app.py +574 -0
  24. replx-1.3/replx/cli/commands/__init__.py +18 -0
  25. replx-1.3/replx/cli/commands/device.py +1782 -0
  26. replx-1.3/replx/cli/commands/exec.py +1647 -0
  27. replx-1.3/replx/cli/commands/file.py +2225 -0
  28. replx-1.3/replx/cli/commands/firmware.py +713 -0
  29. replx-1.3/replx/cli/commands/package.py +1668 -0
  30. replx-1.3/replx/cli/commands/utility.py +1322 -0
  31. replx-1.3/replx/cli/config.py +462 -0
  32. replx-1.3/replx/cli/connection.py +437 -0
  33. replx-1.3/replx/cli/helpers/__init__.py +59 -0
  34. replx-1.3/replx/cli/helpers/compiler.py +122 -0
  35. replx-1.3/replx/cli/helpers/environment.py +24 -0
  36. replx-1.3/replx/cli/helpers/output.py +205 -0
  37. replx-1.3/replx/cli/helpers/registry.py +385 -0
  38. replx-1.3/replx/cli/helpers/scanner.py +222 -0
  39. replx-1.3/replx/cli/helpers/store.py +99 -0
  40. replx-1.3/replx/cli/helpers/updater.py +69 -0
  41. replx-1.3/replx/commands.py +97 -0
  42. replx-1.3/replx/protocol/__init__.py +12 -0
  43. replx-1.3/replx/protocol/repl.py +933 -0
  44. replx-1.3/replx/protocol/storage.py +932 -0
  45. replx-1.3/replx/terminal.py +244 -0
  46. replx-1.3/replx/tests/__init__.py +0 -0
  47. replx-1.3/replx/tests/test_compiler_arch.py +17 -0
  48. replx-1.3/replx/tests/test_connection_info_lookup.py +30 -0
  49. replx-1.3/replx/tests/test_device_info_esp_multi_core.py +56 -0
  50. replx-1.3/replx/tests/test_pkg_local_version.py +44 -0
  51. replx-1.3/replx/tests/test_session_id_fallback.py +32 -0
  52. replx-1.3/replx/transport/__init__.py +17 -0
  53. replx-1.3/replx/transport/base.py +43 -0
  54. replx-1.3/replx/transport/serial.py +156 -0
  55. replx-1.3/replx/typehints/comm/_thread.pyi +219 -0
  56. replx-1.3/replx/typehints/comm/aioble/__init__.pyi +816 -0
  57. replx-1.3/replx/typehints/comm/array.pyi +183 -0
  58. replx-1.3/replx/typehints/comm/asyncio/__init__.pyi +833 -0
  59. replx-1.3/replx/typehints/comm/binascii.pyi +132 -0
  60. replx-1.3/replx/typehints/comm/bluetooth.pyi +580 -0
  61. replx-1.3/replx/typehints/comm/builtins.pyi +2013 -0
  62. replx-1.3/replx/typehints/comm/cmath.pyi +279 -0
  63. replx-1.3/replx/typehints/comm/collections.pyi +243 -0
  64. replx-1.3/replx/typehints/comm/cryptolib.pyi +117 -0
  65. replx-1.3/replx/typehints/comm/deflate.pyi +185 -0
  66. replx-1.3/replx/typehints/comm/errno.pyi +96 -0
  67. replx-1.3/replx/typehints/comm/framebuf.pyi +376 -0
  68. replx-1.3/replx/typehints/comm/gc.pyi +168 -0
  69. replx-1.3/replx/typehints/comm/hashlib.pyi +205 -0
  70. replx-1.3/replx/typehints/comm/heapq.pyi +87 -0
  71. replx-1.3/replx/typehints/comm/io.pyi +434 -0
  72. replx-1.3/replx/typehints/comm/json.pyi +121 -0
  73. replx-1.3/replx/typehints/comm/lwip.pyi +48 -0
  74. replx-1.3/replx/typehints/comm/machine.pyi +1594 -0
  75. replx-1.3/replx/typehints/comm/math.pyi +816 -0
  76. replx-1.3/replx/typehints/comm/micropython.pyi +340 -0
  77. replx-1.3/replx/typehints/comm/mip/__init__.pyi +81 -0
  78. replx-1.3/replx/typehints/comm/network.pyi +435 -0
  79. replx-1.3/replx/typehints/comm/ntptime.pyi +76 -0
  80. replx-1.3/replx/typehints/comm/os.pyi +372 -0
  81. replx-1.3/replx/typehints/comm/platform.pyi +71 -0
  82. replx-1.3/replx/typehints/comm/random.pyi +189 -0
  83. replx-1.3/replx/typehints/comm/re.pyi +314 -0
  84. replx-1.3/replx/typehints/comm/requests/__init__.pyi +347 -0
  85. replx-1.3/replx/typehints/comm/select.pyi +180 -0
  86. replx-1.3/replx/typehints/comm/socket.pyi +577 -0
  87. replx-1.3/replx/typehints/comm/ssl.pyi +336 -0
  88. replx-1.3/replx/typehints/comm/struct.pyi +145 -0
  89. replx-1.3/replx/typehints/comm/sys.pyi +169 -0
  90. replx-1.3/replx/typehints/comm/time.pyi +308 -0
  91. replx-1.3/replx/typehints/comm/tls.pyi +25 -0
  92. replx-1.3/replx/typehints/comm/uasyncio.pyi +23 -0
  93. replx-1.3/replx/typehints/comm/uctypes.pyi +200 -0
  94. replx-1.3/replx/typehints/comm/urequests.pyi +272 -0
  95. replx-1.3/replx/typehints/comm/vfs.pyi +320 -0
  96. replx-1.3/replx/typehints/comm_separate/EFR32MG/binascii.pyi +21 -0
  97. replx-1.3/replx/typehints/comm_separate/EFR32MG/errno.pyi +21 -0
  98. replx-1.3/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +21 -0
  99. replx-1.3/replx/typehints/comm_separate/EFR32MG/io.pyi +21 -0
  100. replx-1.3/replx/typehints/comm_separate/EFR32MG/json.pyi +21 -0
  101. replx-1.3/replx/typehints/comm_separate/EFR32MG/machine.pyi +21 -0
  102. replx-1.3/replx/typehints/comm_separate/EFR32MG/math.pyi +174 -0
  103. replx-1.3/replx/typehints/comm_separate/EFR32MG/micropython.pyi +16 -0
  104. replx-1.3/replx/typehints/comm_separate/EFR32MG/network.pyi +286 -0
  105. replx-1.3/replx/typehints/comm_separate/EFR32MG/os.pyi +21 -0
  106. replx-1.3/replx/typehints/comm_separate/EFR32MG/select.pyi +21 -0
  107. replx-1.3/replx/typehints/comm_separate/EFR32MG/socket.pyi +21 -0
  108. replx-1.3/replx/typehints/comm_separate/EFR32MG/ssl.pyi +21 -0
  109. replx-1.3/replx/typehints/comm_separate/EFR32MG/struct.pyi +21 -0
  110. replx-1.3/replx/typehints/comm_separate/EFR32MG/sys.pyi +57 -0
  111. replx-1.3/replx/typehints/comm_separate/EFR32MG/time.pyi +21 -0
  112. replx-1.3/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +74 -0
  113. replx-1.3/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +83 -0
  114. replx-1.3/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +46 -0
  115. replx-1.3/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +52 -0
  116. replx-1.3/replx/typehints/comm_separate/EFR32MG/uio.pyi +297 -0
  117. replx-1.3/replx/typehints/comm_separate/EFR32MG/ujson.pyi +72 -0
  118. replx-1.3/replx/typehints/comm_separate/EFR32MG/umachine.pyi +792 -0
  119. replx-1.3/replx/typehints/comm_separate/EFR32MG/uos.pyi +226 -0
  120. replx-1.3/replx/typehints/comm_separate/EFR32MG/uselect.pyi +187 -0
  121. replx-1.3/replx/typehints/comm_separate/EFR32MG/usocket.pyi +271 -0
  122. replx-1.3/replx/typehints/comm_separate/EFR32MG/ussl.pyi +69 -0
  123. replx-1.3/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +82 -0
  124. replx-1.3/replx/typehints/comm_separate/EFR32MG/utime.pyi +163 -0
  125. replx-1.3/replx/typehints/core/ESP32/aioespnow.pyi +137 -0
  126. replx-1.3/replx/typehints/core/ESP32/esp.pyi +482 -0
  127. replx-1.3/replx/typehints/core/ESP32/esp32.pyi +1196 -0
  128. replx-1.3/replx/typehints/core/ESP32/espnow.pyi +525 -0
  129. replx-1.3/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +148 -0
  130. replx-1.3/replx/typehints/core/RP2350/rp2.pyi +852 -0
  131. replx-1.3/replx/utils/__init__.py +82 -0
  132. replx-1.3/replx/utils/constants.py +55 -0
  133. replx-1.3/replx/utils/device_info.py +121 -0
  134. replx-1.3/replx/utils/exceptions.py +55 -0
  135. replx-1.3/replx.egg-info/PKG-INFO +25 -0
  136. replx-1.3/replx.egg-info/SOURCES.txt +139 -0
  137. replx-1.3/replx.egg-info/entry_points.txt +3 -0
  138. {replx-1.1.1 → replx-1.3}/replx.egg-info/requires.txt +1 -0
  139. replx-1.1.1/PKG-INFO +0 -531
  140. replx-1.1.1/README.md +0 -485
  141. replx-1.1.1/replx/exceptions.py +0 -14
  142. replx-1.1.1/replx/file_system.py +0 -550
  143. replx-1.1.1/replx/helpers.py +0 -812
  144. replx-1.1.1/replx/repl_protocol.py +0 -974
  145. replx-1.1.1/replx/replx.py +0 -1683
  146. replx-1.1.1/replx/terminal.py +0 -241
  147. replx-1.1.1/replx.egg-info/PKG-INFO +0 -531
  148. replx-1.1.1/replx.egg-info/SOURCES.txt +0 -16
  149. replx-1.1.1/replx.egg-info/entry_points.txt +0 -2
  150. {replx-1.1.1 → replx-1.3}/replx.egg-info/dependency_links.txt +0 -0
  151. {replx-1.1.1 → replx-1.3}/replx.egg-info/top_level.txt +0 -0
  152. {replx-1.1.1 → replx-1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 chanmin.park
3
+ Copyright (c) 2024-2026 chanmin.park
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
replx-1.3/PKG-INFO ADDED
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: replx
3
+ Version: 1.3
4
+ Summary: replx is a fast, modern MicroPython CLI: turbo REPL, robust file sync (put/get), project install, mpy-cross integration, and smart port discovery.
5
+ Author-email: "chanmin.park" <devcamp@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/PlanXLab/replx
8
+ Project-URL: Repository, https://github.com/PlanXLab/replx
9
+ Project-URL: Issues, https://github.com/PlanXLab/replx/issues
10
+ Keywords: micropython,repl,serial,pyserial,typer,mpy-cross,deploy
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Software Development :: Embedded Systems
16
+ Classifier: Topic :: System :: Hardware :: Universal Serial Bus (USB)
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: typer>=0.12
21
+ Requires-Dist: rich>=13.0
22
+ Requires-Dist: pyserial>=3.5
23
+ Requires-Dist: mpy-cross>=1.26
24
+ Requires-Dist: psutil>=5.9.0
25
+ Dynamic: license-file
@@ -10,13 +10,13 @@ readme = "README.md"
10
10
  authors = [{ name = "chanmin.park", email = "devcamp@gmail.com" }]
11
11
  requires-python = ">=3.10"
12
12
  keywords = ["micropython", "repl", "serial", "pyserial", "typer", "mpy-cross", "deploy"]
13
- license = { file = "LICENSE" }
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
14
15
 
15
16
  classifiers = [
16
17
  "Environment :: Console",
17
18
  "Programming Language :: Python :: 3",
18
19
  "Programming Language :: Python :: 3 :: Only",
19
- "License :: OSI Approved :: MIT License",
20
20
  "Operating System :: OS Independent",
21
21
  "Topic :: Software Development :: Embedded Systems",
22
22
  "Topic :: System :: Hardware :: Universal Serial Bus (USB)",
@@ -27,19 +27,26 @@ dependencies = [
27
27
  "rich>=13.0",
28
28
  "pyserial>=3.5",
29
29
  "mpy-cross>=1.26",
30
+ "psutil>=5.9.0",
30
31
  ]
31
32
 
33
+ [project.optional-dependencies]
34
+
32
35
  [project.urls]
33
36
  Homepage = "https://github.com/PlanXLab/replx"
34
37
  Repository = "https://github.com/PlanXLab/replx"
35
38
  Issues = "https://github.com/PlanXLab/replx/issues"
36
39
 
37
40
  [project.scripts]
38
- replx = "replx.replx:main"
41
+ replx = "replx.cli.app:main"
42
+ rx = "replx.cli.app:main"
39
43
 
40
44
  [tool.setuptools.packages.find]
41
45
  where = ["."]
42
46
  include = ["replx*"]
43
47
 
48
+ [tool.setuptools.package-data]
49
+ replx = ["typehints/**/*.pyi"]
50
+
44
51
  [tool.setuptools.dynamic]
45
52
  version = { attr = "replx.__init__.__version__" }
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.1.1"
2
+ __version__ = "1.3"
3
3
  __description__ = "Fast, modern MicroPython CLI with REPL, file sync, install, and smart port detection."
4
4
  __author__ = "PlanX Lab Development Team"
5
5
 
@@ -0,0 +1,13 @@
1
+ from .config import (
2
+ RuntimeState, STATE, GLOBAL_OPTIONS,
3
+ ConfigManager, AgentPortManager, ConnectionResolver,
4
+ )
5
+ from replx.utils.constants import DEFAULT_AGENT_PORT, MAX_AGENT_PORT
6
+ from .app import app, main
7
+
8
+ __all__ = [
9
+ 'RuntimeState', 'STATE', 'GLOBAL_OPTIONS',
10
+ 'ConfigManager', 'AgentPortManager', 'ConnectionResolver',
11
+ 'DEFAULT_AGENT_PORT', 'MAX_AGENT_PORT',
12
+ 'app', 'main'
13
+ ]
@@ -0,0 +1,14 @@
1
+
2
+ from .protocol import AgentProtocol
3
+ from .client import AgentClient, get_session_id, get_cached_session_id, clear_session_cache
4
+ from .server import AgentServer, main as agent_main
5
+
6
+ __all__ = [
7
+ 'AgentProtocol',
8
+ 'AgentClient',
9
+ 'get_session_id',
10
+ 'get_cached_session_id',
11
+ 'clear_session_cache',
12
+ 'AgentServer',
13
+ 'agent_main',
14
+ ]
@@ -0,0 +1,14 @@
1
+
2
+ from .core import AgentClient
3
+ from .session import (
4
+ get_session_id,
5
+ get_cached_session_id,
6
+ clear_session_cache
7
+ )
8
+
9
+ __all__ = [
10
+ 'AgentClient',
11
+ 'get_session_id',
12
+ 'get_cached_session_id',
13
+ 'clear_session_cache'
14
+ ]
@@ -0,0 +1,373 @@
1
+ import os
2
+ import sys
3
+ import socket
4
+ import time
5
+ from typing import Dict, Any, Optional, Callable
6
+
7
+ from replx.utils.constants import DEFAULT_AGENT_PORT, AGENT_HOST, MAX_UDP_SIZE
8
+ from replx.commands import Cmd
9
+ from replx.cli.agent.protocol import AgentProtocol
10
+ from .session import get_cached_session_id
11
+
12
+ LOCAL_PATH_PARAMS = frozenset({'local_path', 'local'})
13
+
14
+
15
+ class AgentClient:
16
+ TIMEOUT = 5.0
17
+ MAX_RETRIES = 3
18
+
19
+ def __init__(self, port: int = None, device_port: str = None):
20
+ self.agent_port = port or DEFAULT_AGENT_PORT
21
+ self.device_port = device_port
22
+ self.sock: Optional[socket.socket] = None
23
+
24
+ self._ppid = get_cached_session_id()
25
+
26
+ def connect(self):
27
+ if not self.sock:
28
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
29
+ self.sock.settimeout(self.TIMEOUT)
30
+
31
+ def disconnect(self):
32
+ if self.sock:
33
+ self.sock.close()
34
+ self.sock = None
35
+
36
+ def send_command(self, command: str, timeout: float = None, **args) -> Dict[str, Any]:
37
+ if not self.sock:
38
+ self.connect()
39
+
40
+ effective_timeout = timeout if timeout else self.TIMEOUT
41
+
42
+ if timeout:
43
+ self.sock.settimeout(timeout)
44
+
45
+ port_to_use = args.pop('port', None) or self.device_port
46
+
47
+ for param in LOCAL_PATH_PARAMS:
48
+ if param in args and args[param]:
49
+ args[param] = os.path.abspath(args[param])
50
+
51
+ request = AgentProtocol.create_request(
52
+ command,
53
+ ppid=self._ppid,
54
+ port=port_to_use,
55
+ **args
56
+ )
57
+ seq = request['seq']
58
+ request_data = AgentProtocol.encode_message(request)
59
+
60
+ response = None
61
+
62
+ max_attempts = 1 if effective_timeout < 1.0 else self.MAX_RETRIES
63
+
64
+ for attempt in range(max_attempts):
65
+ try:
66
+ self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
67
+
68
+ start_time = time.time()
69
+ while time.time() - start_time < effective_timeout:
70
+ try:
71
+ data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
72
+
73
+ msg = AgentProtocol.decode_message(data)
74
+ if not msg or msg.get('seq') != seq:
75
+ continue
76
+
77
+ if msg.get('type') == 'ack':
78
+ continue
79
+
80
+ if msg.get('type') == 'response':
81
+ response = msg
82
+ break
83
+
84
+ except socket.timeout:
85
+ break
86
+
87
+ if response:
88
+ break
89
+
90
+ if attempt < max_attempts - 1 and effective_timeout >= 1.0:
91
+ time.sleep(0.1 * (attempt + 1))
92
+
93
+ except socket.timeout:
94
+ if attempt < max_attempts - 1:
95
+ continue
96
+ raise RuntimeError(f"Agent timeout after {max_attempts} attempts")
97
+
98
+ if not response:
99
+ raise RuntimeError("No response from agent")
100
+
101
+ if response.get('error'):
102
+ raise RuntimeError(response['error'])
103
+
104
+ return response.get('result', {})
105
+
106
+ def run_interactive(self, script_path: str = None, script_content: str = None,
107
+ echo: bool = False,
108
+ output_callback: Callable[[bytes, str], None] = None,
109
+ input_provider: Callable[[], Optional[bytes]] = None,
110
+ stop_check: Callable[[], bool] = None) -> Dict[str, Any]:
111
+ if not self.sock:
112
+ self.connect()
113
+
114
+ if script_path:
115
+ script_path = os.path.abspath(script_path)
116
+
117
+ request = AgentProtocol.create_request(
118
+ 'run_interactive',
119
+ ppid=self._ppid,
120
+ port=self.device_port,
121
+ script_path=script_path,
122
+ script_content=script_content,
123
+ echo=echo
124
+ )
125
+ seq = request['seq']
126
+ request_data = AgentProtocol.encode_message(request)
127
+
128
+ self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
129
+
130
+ self.sock.settimeout(5.0)
131
+ ack_received = False
132
+ error_response = None
133
+
134
+ while True:
135
+ try:
136
+ data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
137
+ msg = AgentProtocol.decode_message(data)
138
+ if msg and msg.get('seq') == seq:
139
+ if msg.get('type') == 'ack':
140
+ ack_received = True
141
+ break
142
+ elif msg.get('type') == 'response' and msg.get('error'):
143
+ error_response = msg
144
+ break
145
+ except socket.timeout:
146
+ break
147
+
148
+ if error_response:
149
+ raise RuntimeError(error_response['error'])
150
+
151
+ if not ack_received:
152
+ raise RuntimeError("No ACK from agent - run_interactive failed to start")
153
+
154
+ self.sock.settimeout(0.01)
155
+ input_interval = 0.001
156
+ last_input_time = 0
157
+ error_check_until = time.time() + 0.1
158
+
159
+ try:
160
+ while True:
161
+ if stop_check and stop_check():
162
+ try:
163
+ self.send_command(Cmd.RUN_STOP, timeout=0.5)
164
+ except Exception:
165
+ pass
166
+ break
167
+
168
+ now = time.time()
169
+
170
+ if now - last_input_time >= input_interval:
171
+ last_input_time = now
172
+ if input_provider:
173
+ try:
174
+ input_data = input_provider()
175
+ if input_data:
176
+ input_msg = AgentProtocol.create_input(seq, input_data, ppid=self._ppid, port=self.device_port)
177
+ input_data_encoded = AgentProtocol.encode_message(input_msg)
178
+ self.sock.sendto(input_data_encoded, (AGENT_HOST, self.agent_port))
179
+ except Exception:
180
+ pass
181
+
182
+ try:
183
+ data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
184
+ msg = AgentProtocol.decode_message(data)
185
+
186
+ if msg and msg.get('seq') == seq:
187
+ if now < error_check_until and msg.get('type') == 'response' and msg.get('error'):
188
+ raise RuntimeError(msg['error'])
189
+
190
+ if msg.get('type') == 'stream':
191
+ output = msg.get('output', '')
192
+ if output and output_callback:
193
+ output_callback(output.encode('utf-8'), 'stdout')
194
+
195
+ if msg.get('completed'):
196
+ error = msg.get('error')
197
+ if error and output_callback:
198
+ output_callback(error.encode('utf-8'), 'stderr')
199
+ break
200
+
201
+ except socket.timeout:
202
+ pass
203
+ except Exception:
204
+ pass
205
+
206
+ except KeyboardInterrupt:
207
+ try:
208
+ self.send_command(Cmd.RUN_STOP, timeout=0.5)
209
+ except Exception:
210
+ pass
211
+ raise
212
+
213
+ self.sock.settimeout(self.TIMEOUT)
214
+
215
+ return {"run": True, "completed": True}
216
+
217
+ def send_command_streaming(self, command: str, timeout: float = None,
218
+ progress_callback: Callable[[Dict[str, Any]], None] = None,
219
+ **args) -> Dict[str, Any]:
220
+ if not self.sock:
221
+ self.connect()
222
+
223
+ effective_timeout = timeout if timeout else 60.0
224
+
225
+ port_to_use = args.pop('port', None) or self.device_port
226
+
227
+ for param in LOCAL_PATH_PARAMS:
228
+ if param in args and args[param]:
229
+ args[param] = os.path.abspath(args[param])
230
+
231
+ request = AgentProtocol.create_request(
232
+ command,
233
+ ppid=self._ppid,
234
+ port=port_to_use,
235
+ **args
236
+ )
237
+ seq = request['seq']
238
+ request_data = AgentProtocol.encode_message(request)
239
+
240
+ self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
241
+
242
+ self.sock.settimeout(0.1)
243
+
244
+ ack_received = False
245
+ response = None
246
+ start_time = time.time()
247
+
248
+ while time.time() - start_time < effective_timeout:
249
+ try:
250
+ data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
251
+ msg = AgentProtocol.decode_message(data)
252
+
253
+ if not msg or msg.get('seq') != seq:
254
+ continue
255
+
256
+ msg_type = msg.get('type')
257
+
258
+ if msg_type == 'ack':
259
+ ack_received = True
260
+ continue
261
+
262
+ elif msg_type == 'stream':
263
+ if progress_callback:
264
+ stream_data = msg.get('data', {})
265
+ progress_callback(stream_data)
266
+ continue
267
+
268
+ elif msg_type == 'response':
269
+ response = msg
270
+ break
271
+
272
+ except socket.timeout:
273
+ if not ack_received and time.time() - start_time > 5.0:
274
+ raise RuntimeError("No response from agent")
275
+ continue
276
+ except Exception as e:
277
+ raise RuntimeError(f"Communication error: {e}")
278
+
279
+ self.sock.settimeout(self.TIMEOUT)
280
+
281
+ if not response:
282
+ raise RuntimeError("No response from agent (timeout)")
283
+
284
+ if response.get('error'):
285
+ raise RuntimeError(response['error'])
286
+
287
+ return response.get('result', {})
288
+
289
+ def ping(self) -> bool:
290
+ try:
291
+ result = self.send_command(Cmd.PING, timeout=0.3)
292
+ return result.get('pong', False)
293
+ except Exception:
294
+ return False
295
+
296
+ def __enter__(self):
297
+ self.connect()
298
+ return self
299
+
300
+ def __exit__(self, _exc_type, _exc_val, _exc_tb):
301
+ self.disconnect()
302
+
303
+ @staticmethod
304
+ def is_agent_running(port: int = None) -> bool:
305
+ try:
306
+ client = AgentClient(port=port)
307
+ return client.ping()
308
+ except Exception:
309
+ return False
310
+
311
+ @staticmethod
312
+ def start_agent(port: int = None, background: bool = True) -> bool:
313
+ if AgentClient.is_agent_running(port=port):
314
+ return False
315
+
316
+ import subprocess
317
+ python_exe = sys.executable
318
+ agent_module = 'replx.cli.agent.server'
319
+
320
+ cmd = [python_exe, '-m', agent_module]
321
+ if port:
322
+ cmd.append(str(port))
323
+
324
+ if background:
325
+ if sys.platform == 'win32':
326
+ startupinfo = subprocess.STARTUPINFO()
327
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
328
+ startupinfo.wShowWindow = 0
329
+
330
+ subprocess.Popen(
331
+ cmd,
332
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
333
+ stdout=subprocess.DEVNULL,
334
+ stderr=subprocess.DEVNULL,
335
+ startupinfo=startupinfo
336
+ )
337
+ else:
338
+ subprocess.Popen(
339
+ cmd,
340
+ stdout=subprocess.DEVNULL,
341
+ stderr=subprocess.DEVNULL,
342
+ stdin=subprocess.DEVNULL,
343
+ start_new_session=True,
344
+ close_fds=True
345
+ )
346
+ else:
347
+ subprocess.Popen(cmd)
348
+
349
+ for i in range(30):
350
+ time.sleep(0.1)
351
+ if AgentClient.is_agent_running(port=port):
352
+ return True
353
+
354
+ raise RuntimeError("Failed to start agent (timeout)")
355
+
356
+ @staticmethod
357
+ def stop_agent(port: int = None, timeout: float = 1.5) -> bool:
358
+ if not AgentClient.is_agent_running(port=port):
359
+ return False
360
+
361
+ try:
362
+ client = AgentClient(port=port)
363
+ client.send_command(Cmd.SHUTDOWN, timeout=0.5)
364
+ except Exception:
365
+ pass
366
+
367
+ start_time = time.time()
368
+ while time.time() - start_time < timeout:
369
+ time.sleep(0.05)
370
+ if not AgentClient.is_agent_running(port=port):
371
+ return True
372
+
373
+ return not AgentClient.is_agent_running(port=port)
@@ -0,0 +1,148 @@
1
+ import os
2
+ from typing import Optional
3
+ import psutil
4
+
5
+ def _find_terminal_process() -> Optional[dict]:
6
+ shell_names = {
7
+ 'powershell.exe', 'pwsh.exe', 'cmd.exe', 'bash.exe', 'zsh.exe', 'sh.exe', 'fish.exe',
8
+ 'windowsterminal.exe',
9
+ }
10
+
11
+ ide_names = {
12
+ 'code.exe',
13
+ 'conemu64.exe', 'conemu.exe',
14
+ 'pycharm.exe', 'pycharm64.exe', 'idea.exe', 'idea64.exe',
15
+ }
16
+
17
+ try:
18
+ parent_pid = os.getppid()
19
+ if parent_pid and parent_pid > 0:
20
+ parent = psutil.Process(parent_pid)
21
+ pname = (parent.name() or '').lower()
22
+ if pname in shell_names:
23
+ return {
24
+ 'pid': parent.pid,
25
+ 'name': parent.name(),
26
+ 'create_time': parent.create_time(),
27
+ 'cwd': None,
28
+ 'level': 0,
29
+ }
30
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, OSError):
31
+ pass
32
+
33
+ try:
34
+ current = psutil.Process()
35
+ best_ide = None
36
+
37
+ for level in range(12):
38
+ if current is None:
39
+ break
40
+
41
+ try:
42
+ name = (current.name() or '').lower()
43
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
44
+ break
45
+
46
+ if name in shell_names:
47
+ return {
48
+ 'pid': current.pid,
49
+ 'name': current.name(),
50
+ 'create_time': current.create_time(),
51
+ 'cwd': None,
52
+ 'level': level,
53
+ }
54
+
55
+ if name in ide_names and best_ide is None:
56
+ try:
57
+ best_ide = {
58
+ 'pid': current.pid,
59
+ 'name': current.name(),
60
+ 'create_time': current.create_time(),
61
+ 'cwd': None,
62
+ 'level': level,
63
+ }
64
+ except Exception:
65
+ best_ide = None
66
+
67
+ try:
68
+ if current.ppid() == 0:
69
+ break
70
+ except Exception:
71
+ break
72
+
73
+ try:
74
+ parent = current.parent()
75
+ except Exception:
76
+ parent = None
77
+
78
+ if parent is None:
79
+ break
80
+ current = parent
81
+
82
+ if best_ide is not None:
83
+ return best_ide
84
+
85
+ except Exception:
86
+ pass
87
+
88
+ return None
89
+
90
+ def _find_jupyter_kernel() -> Optional[dict]:
91
+ try:
92
+ current = psutil.Process()
93
+ for level in range(10):
94
+ if current is None:
95
+ break
96
+
97
+ cmdline = ' '.join(current.cmdline()).lower()
98
+
99
+ if any(keyword in cmdline for keyword in ['jupyter', 'ipykernel', 'ipython']):
100
+ return {
101
+ 'pid': current.pid,
102
+ 'name': current.name(),
103
+ 'cmdline': cmdline,
104
+ 'level': level
105
+ }
106
+
107
+ if current.ppid() == 0:
108
+ break
109
+
110
+ parent = current.parent()
111
+ if parent is None:
112
+ break
113
+ current = parent
114
+
115
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
116
+ pass
117
+
118
+ return None
119
+
120
+ def get_session_id() -> int:
121
+ terminal = _find_terminal_process()
122
+ if terminal:
123
+ return terminal['pid']
124
+
125
+ jupyter = _find_jupyter_kernel()
126
+ if jupyter:
127
+ return jupyter['pid']
128
+
129
+ ppid = os.getppid()
130
+ if ppid and ppid > 0:
131
+ return ppid
132
+
133
+ workspace_hash = abs(hash(os.getcwd())) % (10**8)
134
+ return workspace_hash
135
+
136
+ _session_id_cache: Optional[int] = None
137
+
138
+ def get_cached_session_id() -> int:
139
+ global _session_id_cache
140
+
141
+ if _session_id_cache is None:
142
+ _session_id_cache = get_session_id()
143
+
144
+ return _session_id_cache
145
+
146
+ def clear_session_cache():
147
+ global _session_id_cache
148
+ _session_id_cache = None