indent 0.1.21__py3-none-any.whl → 0.1.24__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 indent might be problematic. Click here for more details.

@@ -0,0 +1,73 @@
1
+ import time
2
+
3
+ import psutil
4
+
5
+ from exponent.core.remote_execution.types import PortInfo
6
+
7
+
8
+ def get_port_usage() -> list[PortInfo] | None:
9
+ """
10
+ Get information about all listening ports on the system.
11
+
12
+ Returns:
13
+ List of PortInfo objects containing process name, port, protocol, pid, and uptime.
14
+ Returns None if there's a permission error.
15
+ Returns empty list if no listening ports are found.
16
+ """
17
+ try:
18
+ connections = psutil.net_connections(kind="tcp")
19
+ except (psutil.AccessDenied, PermissionError):
20
+ # If we don't have permission to see connections, return None
21
+ return None
22
+ except Exception:
23
+ # For any other unexpected errors, return None
24
+ return None
25
+
26
+ port_info_list: list[PortInfo] = []
27
+ current_time = time.time()
28
+
29
+ for conn in connections:
30
+ # Only include TCP ports in LISTEN state
31
+ if conn.status != "LISTEN":
32
+ continue
33
+
34
+ # Skip if no local address (shouldn't happen for LISTEN, but be safe)
35
+ if not conn.laddr:
36
+ continue
37
+
38
+ port = conn.laddr.port
39
+ pid = conn.pid
40
+
41
+ # Try to get process information
42
+ process_name = "unknown"
43
+ uptime_seconds = None
44
+
45
+ if pid:
46
+ try:
47
+ process = psutil.Process(pid)
48
+ process_name = process.name()
49
+
50
+ # Calculate uptime
51
+ create_time = process.create_time()
52
+ uptime_seconds = current_time - create_time
53
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
54
+ # Process disappeared or we don't have permission
55
+ pass
56
+ except Exception:
57
+ # Any other unexpected error, just skip process info
58
+ pass
59
+
60
+ port_info = PortInfo(
61
+ process_name=process_name,
62
+ port=port,
63
+ protocol="TCP",
64
+ pid=pid,
65
+ uptime_seconds=uptime_seconds,
66
+ )
67
+ port_info_list.append(port_info)
68
+
69
+ # Limit to 50 ports to avoid bloating the heartbeat payload
70
+ if len(port_info_list) >= 50:
71
+ break
72
+
73
+ return port_info_list
@@ -4,6 +4,7 @@ import platform
4
4
 
5
5
  from exponent.core.remote_execution.git import get_git_info
6
6
  from exponent.core.remote_execution.languages import python_execution
7
+ from exponent.core.remote_execution.port_utils import get_port_usage
7
8
  from exponent.core.remote_execution.types import (
8
9
  SystemInfo,
9
10
  )
@@ -17,6 +18,7 @@ async def get_system_info(working_directory: str) -> SystemInfo:
17
18
  shell=_get_user_shell(),
18
19
  git=await get_git_info(working_directory),
19
20
  python_env=python_execution.get_python_env_info(),
21
+ port_usage=get_port_usage(),
20
22
  )
21
23
 
22
24
 
@@ -0,0 +1,428 @@
1
+ import asyncio
2
+ import fcntl
3
+ import logging
4
+ import os
5
+ import pty
6
+ import signal
7
+ import struct
8
+ import sys
9
+ import termios
10
+ import time
11
+ import traceback
12
+ from collections.abc import Callable
13
+
14
+ from exponent.core.remote_execution.terminal_types import (
15
+ TerminalMessage,
16
+ TerminalOutput,
17
+ TerminalResetSessions,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TerminalSession:
24
+ """
25
+ Manages a PTY session for terminal emulation.
26
+ Runs on the CLI machine and streams output back to server.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ session_id: str,
32
+ output_callback: Callable[[str], None],
33
+ cols: int = 80,
34
+ rows: int = 24,
35
+ ):
36
+ self.session_id = session_id
37
+ self.output_callback = output_callback # Called with terminal output
38
+ self.cols = cols
39
+ self.rows = rows
40
+ self.master_fd: int | None = None
41
+ self.pid: int | None = None
42
+ self._running = False
43
+ self._read_task: asyncio.Task[None] | None = None
44
+
45
+ async def start(
46
+ self,
47
+ command: list[str] | None = None,
48
+ env: dict[str, str] | None = None,
49
+ ) -> None:
50
+ """Start the terminal session with PTY"""
51
+ if self._running:
52
+ raise RuntimeError(f"Terminal session {self.session_id} already running")
53
+
54
+ # Default to bash if no command specified
55
+ if command is None:
56
+ command = ["/bin/bash"]
57
+
58
+ # Spawn process with PTY
59
+ try:
60
+ self.pid, self.master_fd = pty.fork()
61
+ except OSError as e:
62
+ logger.error(
63
+ "Failed to fork PTY",
64
+ session_id=self.session_id,
65
+ error=str(e),
66
+ )
67
+ raise RuntimeError(f"Failed to fork PTY: {e}") from e
68
+
69
+ if self.pid == 0:
70
+ # Child process - execute command
71
+ try:
72
+ # Set up environment
73
+ if env:
74
+ for key, value in env.items():
75
+ os.environ[key] = value
76
+
77
+ # Set terminal environment
78
+ os.environ["TERM"] = "xterm-256color"
79
+ os.environ["COLORTERM"] = "truecolor"
80
+
81
+ # Execute command
82
+ os.execvp(command[0], command)
83
+ except Exception as e:
84
+ # If exec fails, log and exit child process
85
+ traceback.print_exc()
86
+ sys.stderr.write(f"Failed to execute command {command}: {e}\n")
87
+ sys.stderr.flush()
88
+ os._exit(1)
89
+ else:
90
+ # Parent process - set up non-blocking I/O
91
+ flags = fcntl.fcntl(self.master_fd, fcntl.F_GETFL)
92
+ fcntl.fcntl(self.master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
93
+
94
+ # Set initial size
95
+ self.resize(self.cols, self.rows)
96
+
97
+ # Start reading from PTY
98
+ self._running = True
99
+ self._read_task = asyncio.create_task(self._read_from_pty())
100
+
101
+ logger.info(
102
+ "Terminal session started",
103
+ session_id=self.session_id,
104
+ pid=self.pid,
105
+ command=command,
106
+ )
107
+
108
+ async def _read_from_pty(self) -> None:
109
+ """Continuously read from PTY using event loop's add_reader (non-blocking)"""
110
+ if self.master_fd is None:
111
+ return
112
+
113
+ loop = asyncio.get_event_loop()
114
+ read_queue: asyncio.Queue[bytes | None] = asyncio.Queue()
115
+
116
+ def read_callback() -> None:
117
+ """Called by event loop when data is available on the FD"""
118
+ if self.master_fd is None:
119
+ return
120
+ try:
121
+ data = os.read(self.master_fd, 4096)
122
+ if data:
123
+ # Put data in queue to be processed by async task
124
+ read_queue.put_nowait(data)
125
+ else:
126
+ # EOF - PTY closed
127
+ read_queue.put_nowait(None)
128
+ except OSError as e:
129
+ if e.errno == 11: # EAGAIN - shouldn't happen with add_reader
130
+ pass
131
+ else:
132
+ # PTY closed or error
133
+ logger.info(
134
+ "PTY read error in callback",
135
+ session_id=self.session_id,
136
+ error=str(e),
137
+ errno=e.errno,
138
+ )
139
+ read_queue.put_nowait(None)
140
+ except Exception as e:
141
+ logger.error(
142
+ "Unexpected error in PTY read callback",
143
+ session_id=self.session_id,
144
+ error=str(e),
145
+ )
146
+ read_queue.put_nowait(None)
147
+
148
+ # Register the FD with the event loop
149
+ loop.add_reader(self.master_fd, read_callback)
150
+
151
+ try:
152
+ while self._running:
153
+ # Wait for data from the queue (non-blocking for event loop)
154
+ data = await read_queue.get()
155
+
156
+ if data is None:
157
+ # EOF or error
158
+ logger.info(
159
+ "PTY closed (EOF)",
160
+ session_id=self.session_id,
161
+ )
162
+ break
163
+
164
+ # Process the data
165
+ decoded = data.decode("utf-8", errors="replace")
166
+ self.output_callback(decoded)
167
+ finally:
168
+ # Unregister the FD from the event loop
169
+ loop.remove_reader(self.master_fd)
170
+ logger.info(
171
+ "PTY read loop exited",
172
+ session_id=self.session_id,
173
+ running=self._running,
174
+ )
175
+
176
+ async def write_input(self, data: str) -> None:
177
+ """Write user input to PTY"""
178
+ if not self._running or self.master_fd is None:
179
+ raise RuntimeError(f"Terminal session {self.session_id} not running")
180
+
181
+ try:
182
+ os.write(self.master_fd, data.encode("utf-8"))
183
+ except OSError as e:
184
+ logger.error(
185
+ "Error writing to PTY",
186
+ session_id=self.session_id,
187
+ error=str(e),
188
+ )
189
+ raise
190
+
191
+ def resize(self, cols: int, rows: int) -> None:
192
+ """Resize the PTY to match terminal dimensions"""
193
+ if self.master_fd is None:
194
+ return
195
+
196
+ self.cols = cols
197
+ self.rows = rows
198
+
199
+ try:
200
+ size = struct.pack("HHHH", rows, cols, 0, 0)
201
+ fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, size)
202
+ logger.debug(
203
+ "Terminal resized",
204
+ session_id=self.session_id,
205
+ cols=cols,
206
+ rows=rows,
207
+ )
208
+ except Exception as e:
209
+ logger.error(
210
+ "Error resizing PTY",
211
+ session_id=self.session_id,
212
+ error=str(e),
213
+ )
214
+
215
+ async def stop(self) -> tuple[bool, int | None]:
216
+ """
217
+ Stop the terminal session and clean up resources.
218
+ Returns (success, exit_code)
219
+ """
220
+ if not self._running:
221
+ return True, None
222
+
223
+ self._running = False
224
+
225
+ # Cancel read task
226
+ if self._read_task and not self._read_task.done():
227
+ self._read_task.cancel()
228
+ try:
229
+ await self._read_task
230
+ except asyncio.CancelledError:
231
+ pass
232
+
233
+ exit_code = None
234
+
235
+ # Close file descriptor
236
+ if self.master_fd is not None:
237
+ try:
238
+ os.close(self.master_fd)
239
+ except Exception as e:
240
+ logger.error(
241
+ "Error closing PTY fd",
242
+ session_id=self.session_id,
243
+ error=str(e),
244
+ )
245
+ self.master_fd = None
246
+
247
+ # Kill child process
248
+ if self.pid is not None:
249
+ try:
250
+ os.kill(self.pid, signal.SIGTERM)
251
+ # Wait for process to terminate (with timeout)
252
+ for _ in range(10): # Wait up to 1 second
253
+ try:
254
+ pid, status = os.waitpid(self.pid, os.WNOHANG)
255
+ if pid != 0:
256
+ exit_code = os.WEXITSTATUS(status)
257
+ break
258
+ except ChildProcessError:
259
+ break
260
+ await asyncio.sleep(0.1)
261
+ else:
262
+ # Force kill if still running
263
+ try:
264
+ os.kill(self.pid, signal.SIGKILL)
265
+ os.waitpid(self.pid, 0)
266
+ except Exception:
267
+ pass
268
+ except Exception as e:
269
+ logger.error(
270
+ "Error killing PTY process",
271
+ session_id=self.session_id,
272
+ error=str(e),
273
+ )
274
+ self.pid = None
275
+
276
+ logger.info(
277
+ "Terminal session stopped",
278
+ session_id=self.session_id,
279
+ exit_code=exit_code,
280
+ )
281
+
282
+ return True, exit_code
283
+
284
+ @property
285
+ def is_running(self) -> bool:
286
+ """Check if terminal session is running"""
287
+ return self._running and self.master_fd is not None
288
+
289
+
290
+ class TerminalSessionManager:
291
+ """Manages multiple terminal sessions"""
292
+
293
+ def __init__(self, output_queue: asyncio.Queue[TerminalMessage]) -> None:
294
+ self._sessions: dict[str, TerminalSession] = {}
295
+ self._lock = asyncio.Lock()
296
+ self._websocket: object | None = None
297
+ self._output_queue = output_queue
298
+
299
+ # Send reset message immediately to clear stale sessions
300
+ try:
301
+ reset_message = TerminalResetSessions()
302
+ self._output_queue.put_nowait(reset_message)
303
+ logger.info("Sent TerminalResetSessions message")
304
+ except asyncio.QueueFull:
305
+ logger.error("Failed to queue terminal reset message - queue full")
306
+
307
+ def set_websocket(self, websocket: object) -> None:
308
+ """Set the websocket for sending output"""
309
+ self._websocket = websocket
310
+
311
+ async def start_session(
312
+ self,
313
+ websocket: object,
314
+ session_id: str,
315
+ command: list[str] | None = None,
316
+ cols: int = 80,
317
+ rows: int = 24,
318
+ env: dict[str, str] | None = None,
319
+ ) -> str:
320
+ """Start a new terminal session"""
321
+ async with self._lock:
322
+ if session_id in self._sessions:
323
+ raise RuntimeError(f"Terminal session {session_id} already exists")
324
+
325
+ # Store websocket reference
326
+ self._websocket = websocket
327
+
328
+ # Create output callback that queues data to be sent
329
+ def output_callback(data: str) -> None:
330
+ # Queue the output to be sent asynchronously
331
+ try:
332
+ terminal_output = TerminalOutput(
333
+ session_id=session_id,
334
+ data=data,
335
+ timestamp=time.time(),
336
+ )
337
+ self._output_queue.put_nowait(terminal_output)
338
+ except asyncio.QueueFull:
339
+ logger.error(
340
+ "Terminal output queue full",
341
+ session_id=session_id,
342
+ )
343
+
344
+ session = TerminalSession(
345
+ session_id=session_id,
346
+ output_callback=output_callback,
347
+ cols=cols,
348
+ rows=rows,
349
+ )
350
+
351
+ await session.start(command=command, env=env)
352
+ self._sessions[session_id] = session
353
+
354
+ return session_id
355
+
356
+ async def send_input(self, session_id: str, data: str) -> bool:
357
+ """Send input to a terminal session"""
358
+ async with self._lock:
359
+ session = self._sessions.get(session_id)
360
+ if session is None:
361
+ return False
362
+
363
+ try:
364
+ await session.write_input(data)
365
+ return True
366
+ except Exception as e:
367
+ logger.error(
368
+ "Failed to send input to terminal",
369
+ session_id=session_id,
370
+ error=str(e),
371
+ )
372
+ return False
373
+
374
+ async def resize_terminal(self, session_id: str, rows: int, cols: int) -> bool:
375
+ """Resize a terminal session"""
376
+ async with self._lock:
377
+ session = self._sessions.get(session_id)
378
+ if session is None:
379
+ return False
380
+
381
+ try:
382
+ session.resize(cols, rows)
383
+ return True
384
+ except Exception as e:
385
+ logger.error(
386
+ "Failed to resize terminal",
387
+ session_id=session_id,
388
+ error=str(e),
389
+ )
390
+ return False
391
+
392
+ async def stop_session(self, session_id: str) -> bool:
393
+ """Stop a terminal session"""
394
+ async with self._lock:
395
+ session = self._sessions.pop(session_id, None)
396
+ if session is None:
397
+ return True # Already stopped
398
+
399
+ try:
400
+ await session.stop()
401
+ return True
402
+ except Exception as e:
403
+ logger.error(
404
+ "Failed to stop terminal",
405
+ session_id=session_id,
406
+ error=str(e),
407
+ )
408
+ return False
409
+
410
+ async def stop_all_sessions(self) -> None:
411
+ """Stop all terminal sessions (cleanup on disconnect)"""
412
+ async with self._lock:
413
+ session_ids = list(self._sessions.keys())
414
+ for session_id in session_ids:
415
+ session = self._sessions.pop(session_id, None)
416
+ if session:
417
+ try:
418
+ await session.stop()
419
+ logger.info(
420
+ "Stopped terminal session on cleanup",
421
+ session_id=session_id,
422
+ )
423
+ except Exception as e:
424
+ logger.error(
425
+ "Error stopping terminal session on cleanup",
426
+ session_id=session_id,
427
+ error=str(e),
428
+ )
@@ -0,0 +1,29 @@
1
+ """Type definitions for terminal output streaming."""
2
+
3
+ import msgspec
4
+
5
+
6
+ class TerminalOutput(msgspec.Struct, tag="terminal_output"):
7
+ """Terminal output data from CLI to web client."""
8
+
9
+ session_id: str
10
+ data: str
11
+ timestamp: float
12
+
13
+
14
+ class TerminalStatus(msgspec.Struct, tag="terminal_status"):
15
+ """Terminal status update from CLI to web client."""
16
+
17
+ session_id: str
18
+ status: str
19
+ message: str
20
+ exit_code: int | None = None
21
+
22
+
23
+ class TerminalResetSessions(msgspec.Struct, tag="terminal_reset_sessions"):
24
+ """Sent from CLI when terminal session manager starts to clear stale sessions."""
25
+
26
+ # No fields needed - just a signal
27
+
28
+
29
+ TerminalMessage = TerminalOutput | TerminalStatus | TerminalResetSessions
@@ -32,15 +32,15 @@ from exponent.core.remote_execution.cli_rpc_types import (
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  from exponent.core.remote_execution.client import RemoteExecutionClient
35
+ from exponent.core.remote_execution.cli_rpc_types import (
36
+ StreamingCodeExecutionRequest,
37
+ StreamingCodeExecutionResponse,
38
+ )
35
39
  from exponent.core.remote_execution.code_execution import (
36
40
  execute_code_streaming,
37
41
  )
38
42
  from exponent.core.remote_execution.file_write import execute_full_file_rewrite
39
43
  from exponent.core.remote_execution.truncation import truncate_tool_result
40
- from exponent.core.remote_execution.types import (
41
- StreamingCodeExecutionRequest,
42
- StreamingCodeExecutionResponse,
43
- )
44
44
  from exponent.core.remote_execution.utils import (
45
45
  assert_unreachable,
46
46
  safe_get_file_metadata,
@@ -17,7 +17,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
17
17
  )
18
18
  from exponent.core.remote_execution.utils import truncate_output
19
19
 
20
- DEFAULT_CHARACTER_LIMIT = 90_000
20
+ DEFAULT_CHARACTER_LIMIT = 50_000
21
21
  DEFAULT_LIST_ITEM_LIMIT = 1000
22
22
  DEFAULT_LIST_PREVIEW_ITEMS = 10
23
23
 
@@ -120,6 +120,14 @@ class PythonEnvInfo(BaseModel):
120
120
  provider: Literal["venv", "pyenv", "pipenv", "conda"] | None = "pyenv"
121
121
 
122
122
 
123
+ class PortInfo(BaseModel):
124
+ process_name: str
125
+ port: int
126
+ protocol: str
127
+ pid: int | None
128
+ uptime_seconds: float | None
129
+
130
+
123
131
  class SystemInfo(BaseModel):
124
132
  name: str
125
133
  cwd: str
@@ -127,6 +135,7 @@ class SystemInfo(BaseModel):
127
135
  shell: str
128
136
  git: GitInfo | None
129
137
  python_env: PythonEnvInfo | None
138
+ port_usage: list[PortInfo] | None = None
130
139
 
131
140
 
132
141
  class HeartbeatInfo(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.21
3
+ Version: 0.1.24
4
4
  Summary: Indent is an AI Pair Programmer
5
5
  Author-email: Sashank Thupukari <sashank@exponent.run>
6
6
  Requires-Python: <3.13,>=3.10
@@ -22,6 +22,7 @@ Requires-Dist: msgspec>=0.19.0
22
22
  Requires-Dist: packaging~=24.1
23
23
  Requires-Dist: pip<26,>=25.0.1
24
24
  Requires-Dist: prompt-toolkit<4,>=3.0.36
25
+ Requires-Dist: psutil<7,>=5.9.0
25
26
  Requires-Dist: pydantic-ai==0.0.30
26
27
  Requires-Dist: pydantic-settings<3,>=2.2.1
27
28
  Requires-Dist: pydantic[email]<3,>=2.6.4
@@ -1,7 +1,7 @@
1
- exponent/__init__.py,sha256=QwRysHnO-cJwIFSvXReHDj93DEPhlibOTtnnb25J9-c,706
1
+ exponent/__init__.py,sha256=IV4a2R7tlzuACf6FAyPEbprLKNroeE-n_UPSKi1QJSc,706
2
2
  exponent/cli.py,sha256=QnIeDTgWaQJrRs5WESCkQpVEQiJiAO4qWgB0rYlkd78,3344
3
3
  exponent/py.typed,sha256=9XZl5avs8yHp89XP_1Fjtbeg_2rjYorCC9I0k_j-h2c,334
4
- exponent/commands/cloud_commands.py,sha256=_qivNDIHIJXxLgVUm5vUNyuzG15llMqB8xY2MZ7F_lc,16710
4
+ exponent/commands/cloud_commands.py,sha256=yd0d7l8AaFZIgrFPRYspsKlmcyFqkK_ovSQKhK-YpVU,16773
5
5
  exponent/commands/common.py,sha256=M2KI9yKjB8fecPoDBphMa123c35-iNeaE9q4DxhkaFU,12817
6
6
  exponent/commands/config_commands.py,sha256=mmQYuyRosODgawoHWsn9xnWnV37GiQaxJjMv-_xreAU,8902
7
7
  exponent/commands/run_commands.py,sha256=xn0SJX0PPrmHu8Nh-kG-lJBhGC4qFCc04aLIzdVyFho,6389
@@ -13,13 +13,13 @@ exponent/core/config.py,sha256=TNFLUgLnfSocRMVSav_7E4VcaNHXZ_3Mg5Lp1smP46U,5731
13
13
  exponent/core/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  exponent/core/graphql/client.py,sha256=SRagD3YPyoYZSO1RtfO-OXD7b5dm1NvgoL6CTbN380o,2009
15
15
  exponent/core/graphql/get_chats_query.py,sha256=9-2N1VfapXUZB3IFIKw5U_gKdmfyviJp5JSUntB_Yyk,1177
16
- exponent/core/graphql/mutations.py,sha256=eVoazBZowqLk3Jz-67tR_ATqrQKPhk5Teeq7YV-70b0,2931
16
+ exponent/core/graphql/mutations.py,sha256=Szs8wS_5EpVuZdt09QbstIm_8i-_-EGT4Z17pou5714,2971
17
17
  exponent/core/graphql/queries.py,sha256=RYsk8bub0esspqgakilhzX07yJf2652Ey9tBZK1l_lY,3297
18
18
  exponent/core/graphql/subscriptions.py,sha256=SQngv_nYVNJjiZ_P2k0UcLIu1pzc4vi7q7lhH89NCZM,393
19
19
  exponent/core/remote_execution/checkpoints.py,sha256=3QGYMLa8vT7XmxMYTRcGrW8kNGHwRC0AkUfULribJWg,6354
20
- exponent/core/remote_execution/cli_rpc_types.py,sha256=1HPLLtzpeGJfkh8nXlo8d_zDajzfDIwLu83Cfm60Cnc,8642
21
- exponent/core/remote_execution/client.py,sha256=dG2ueANYJH8CznDT5L3-UApkWZ25ANheSvv62JCMq9c,32038
22
- exponent/core/remote_execution/code_execution.py,sha256=jYPB_7dJzS9BTPLX9fKQpsFPatwjbXuaFFSxT9tDTfI,2388
20
+ exponent/core/remote_execution/cli_rpc_types.py,sha256=RPlvZYb7KLYlb65I8wcVAlkv3cSB7n2okZqMeWz_V4c,11791
21
+ exponent/core/remote_execution/client.py,sha256=bUl2w28c_okWndW9jEreVyYKeFNyL8h8OMZNBb4lf3g,40307
22
+ exponent/core/remote_execution/code_execution.py,sha256=QL78v2yHMrIcbNWIczAICWRceziXgG4pzw7gvhUOLxs,3328
23
23
  exponent/core/remote_execution/default_env.py,sha256=s44A1Cz9EgYuhF17WO3ESVNSLQw57EoOLyi9k6qliIo,911
24
24
  exponent/core/remote_execution/error_info.py,sha256=Rd7OA3ps06qYejPVcOaMBB9AtftP3wqQoOfiILFASnc,1378
25
25
  exponent/core/remote_execution/exceptions.py,sha256=eT57lBnBhvh-KJ5lsKWcfgGA5-WisAxhjZx-Z6OupZY,135
@@ -27,15 +27,18 @@ exponent/core/remote_execution/file_write.py,sha256=8Sa70ANIDHGxIAq4_Uy2Qoo55K7-
27
27
  exponent/core/remote_execution/files.py,sha256=mIVjhStaEKETW6y3pCVeV8eJKNaPtroWGP_kBK1x8uA,8776
28
28
  exponent/core/remote_execution/git.py,sha256=dGjBpeoKJZsYgRwctSq29GmbsNIN9tbSA3VwBnRD0IQ,7810
29
29
  exponent/core/remote_execution/http_fetch.py,sha256=aFEyXd0S-MRfisSMuIFiEyc1AEAj9nUZ9Rj_P_YRows,2827
30
+ exponent/core/remote_execution/port_utils.py,sha256=kWje8ikCzBXMeS7qr6NZZOzQOMoMuacgPUDYyloYgwM,2183
30
31
  exponent/core/remote_execution/session.py,sha256=jlQIdeUj0f7uOk3BgzlJtBJ_GyTIjCchBp5ApQuF2-I,3847
31
- exponent/core/remote_execution/system_context.py,sha256=QY1zY8_fWj3sh-fmLYtewvgxh7uTX4ITIJqlUTDkj6c,648
32
- exponent/core/remote_execution/tool_execution.py,sha256=DVO7jPS6JUGjY3GTevvNn5NlieKDOcx8id_nhi9x6Og,15783
32
+ exponent/core/remote_execution/system_context.py,sha256=I4RNuM60isS-529EuRrrEPPwJssNFC2TZ_7MhBTWEd0,754
33
+ exponent/core/remote_execution/terminal_session.py,sha256=Bva8D9lPjq-HXktxvjtK6xn7z3CsDVKyl1hxJV2-67M,14213
34
+ exponent/core/remote_execution/terminal_types.py,sha256=t4snBiTtidAEJZTvy789x-5HFqjkV9rlonjDO30PfAY,731
35
+ exponent/core/remote_execution/tool_execution.py,sha256=tDVW1c4ZysfTZqbR-wd7et41Mfv1lFQJCBvQShWnWq4,15791
33
36
  exponent/core/remote_execution/tool_type_utils.py,sha256=7qi6Qd8fvHts019ZSLPbtiy17BUqgqBg3P_gdfvFf7w,1301
34
- exponent/core/remote_execution/truncation.py,sha256=crHzjcUxL3tVe8yuoWNAE5r-uQKjtL-GzZczd-ucrd0,9849
35
- exponent/core/remote_execution/types.py,sha256=fUT5-3TE3sA2dUqWaen9VgpEllz4RA0B3i-FJpEEXOs,15601
37
+ exponent/core/remote_execution/truncation.py,sha256=0zFnmqXES2vtQCSpfXIQn5hgg6bZK4Sad_Cfh27xTZU,9849
38
+ exponent/core/remote_execution/types.py,sha256=2tp73g6WLhL3x-5FyP9jhadcRHIswt4wfJJlEvNwlvk,15782
36
39
  exponent/core/remote_execution/utils.py,sha256=6PlBqYJ3OQwZ0dgXiIu3br04a-d-glDeDZpD0XGGPAE,14793
37
40
  exponent/core/remote_execution/languages/python_execution.py,sha256=nsX_LsXcUcHhiEHpSTjOTVNd7CxM146al0kw_iQX5OU,7724
38
- exponent/core/remote_execution/languages/shell_streaming.py,sha256=gBACa5uFMGuQzEFpKE61ww6niHWFe58NToI1HaIYGVU,7662
41
+ exponent/core/remote_execution/languages/shell_streaming.py,sha256=MpE1XQiu18xWUGp1wD_Hb1nuPCQE-i5-_XO6FnkcNvo,7675
39
42
  exponent/core/remote_execution/languages/types.py,sha256=f7FjSRNRSga-ZaE3LddDhxCirUVjlSYMEdoskG6Pta4,314
40
43
  exponent/core/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
44
  exponent/core/types/command_data.py,sha256=_HqQsnamRZeVoVaTpeO3ecVUzNBdG62WXlFy6Q7rtUM,5294
@@ -46,7 +49,7 @@ exponent/migration-docs/login.md,sha256=KIeXy3m2nzSUgw-4PW1XzXfHael1D4Zu93CplLMb
46
49
  exponent/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
50
  exponent/utils/colors.py,sha256=HBkqe_ZmhJ9YiL2Fpulqek4KvLS5mwBTY4LQSM5N8SM,2762
48
51
  exponent/utils/version.py,sha256=GHZ9ET1kMyDubJZU3w2sah5Pw8XpiEakS5IOlt3wUnQ,8888
49
- indent-0.1.21.dist-info/METADATA,sha256=kUlmIYOdjCG1iPkNC0oFscWTh9tsFMVuFUG_MPmNltQ,1308
50
- indent-0.1.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
51
- indent-0.1.21.dist-info/entry_points.txt,sha256=q8q1t1sbl4NULGOR0OV5RmSG4KEjkpEQRU_RUXEGzcs,44
52
- indent-0.1.21.dist-info/RECORD,,
52
+ indent-0.1.24.dist-info/METADATA,sha256=ifMj-YDEk36Exlumd1dldLvl6FdXZfWWZPI_BSTHO-Y,1340
53
+ indent-0.1.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
54
+ indent-0.1.24.dist-info/entry_points.txt,sha256=q8q1t1sbl4NULGOR0OV5RmSG4KEjkpEQRU_RUXEGzcs,44
55
+ indent-0.1.24.dist-info/RECORD,,