strix-agent 0.1.9__py3-none-any.whl → 0.1.10__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.
@@ -1,231 +0,0 @@
1
- import contextlib
2
- import os
3
- import pty
4
- import select
5
- import signal
6
- import subprocess
7
- import threading
8
- import time
9
- from typing import Any
10
-
11
- import pyte
12
-
13
-
14
- MAX_TERMINAL_SNAPSHOT_LENGTH = 10_000
15
-
16
-
17
- class TerminalInstance:
18
- def __init__(self, terminal_id: str, initial_command: str | None = None) -> None:
19
- self.terminal_id = terminal_id
20
- self.process: subprocess.Popen[bytes] | None = None
21
- self.master_fd: int | None = None
22
- self.is_running = False
23
- self._output_lock = threading.Lock()
24
- self._reader_thread: threading.Thread | None = None
25
-
26
- self.screen = pyte.HistoryScreen(80, 24, history=1000)
27
- self.stream = pyte.ByteStream()
28
- self.stream.attach(self.screen)
29
-
30
- self._start_terminal(initial_command)
31
-
32
- def _start_terminal(self, initial_command: str | None = None) -> None:
33
- try:
34
- self.master_fd, slave_fd = pty.openpty()
35
-
36
- shell = "/bin/bash"
37
-
38
- self.process = subprocess.Popen( # noqa: S603
39
- [shell, "-i"],
40
- stdin=slave_fd,
41
- stdout=slave_fd,
42
- stderr=slave_fd,
43
- cwd="/workspace",
44
- preexec_fn=os.setsid, # noqa: PLW1509 - Required for PTY functionality
45
- )
46
-
47
- os.close(slave_fd)
48
-
49
- self.is_running = True
50
-
51
- self._reader_thread = threading.Thread(target=self._read_output, daemon=True)
52
- self._reader_thread.start()
53
-
54
- time.sleep(0.5)
55
-
56
- if initial_command:
57
- self._write_to_terminal(initial_command)
58
-
59
- except (OSError, ValueError) as e:
60
- raise RuntimeError(f"Failed to start terminal: {e}") from e
61
-
62
- def _read_output(self) -> None:
63
- while self.is_running and self.master_fd:
64
- try:
65
- ready, _, _ = select.select([self.master_fd], [], [], 0.1)
66
- if ready:
67
- data = os.read(self.master_fd, 4096)
68
- if data:
69
- with self._output_lock, contextlib.suppress(TypeError):
70
- self.stream.feed(data)
71
- else:
72
- break
73
- except (OSError, ValueError):
74
- break
75
-
76
- def _write_to_terminal(self, data: str) -> None:
77
- if self.master_fd and self.is_running:
78
- try:
79
- os.write(self.master_fd, data.encode("utf-8"))
80
- except (OSError, ValueError) as e:
81
- raise RuntimeError("Terminal is no longer available") from e
82
-
83
- def send_input(self, inputs: list[str]) -> None:
84
- if not self.is_running:
85
- raise RuntimeError("Terminal is not running")
86
-
87
- for i, input_item in enumerate(inputs):
88
- if input_item.startswith("literal:"):
89
- literal_text = input_item[8:]
90
- self._write_to_terminal(literal_text)
91
- else:
92
- key_sequence = self._get_key_sequence(input_item)
93
- if key_sequence:
94
- self._write_to_terminal(key_sequence)
95
- else:
96
- self._write_to_terminal(input_item)
97
-
98
- time.sleep(0.05)
99
-
100
- if (
101
- i < len(inputs) - 1
102
- and not input_item.startswith("literal:")
103
- and not self._is_special_key(input_item)
104
- and not inputs[i + 1].startswith("literal:")
105
- and not self._is_special_key(inputs[i + 1])
106
- ):
107
- self._write_to_terminal(" ")
108
-
109
- def get_snapshot(self) -> dict[str, Any]:
110
- with self._output_lock:
111
- history_lines = [
112
- "".join(char.data for char in line_dict.values())
113
- for line_dict in self.screen.history.top
114
- ]
115
-
116
- current_lines = self.screen.display
117
-
118
- all_lines = history_lines + current_lines
119
- rendered_output = "\n".join(all_lines)
120
-
121
- if len(rendered_output) > MAX_TERMINAL_SNAPSHOT_LENGTH:
122
- rendered_output = rendered_output[-MAX_TERMINAL_SNAPSHOT_LENGTH:]
123
- truncated = True
124
- else:
125
- truncated = False
126
-
127
- return {
128
- "terminal_id": self.terminal_id,
129
- "snapshot": rendered_output,
130
- "is_running": self.is_running,
131
- "process_id": self.process.pid if self.process else None,
132
- "truncated": truncated,
133
- }
134
-
135
- def wait(self, duration: float) -> dict[str, Any]:
136
- time.sleep(duration)
137
- return self.get_snapshot()
138
-
139
- def close(self) -> None:
140
- self.is_running = False
141
-
142
- if self.process:
143
- with contextlib.suppress(OSError, ProcessLookupError):
144
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
145
-
146
- try:
147
- self.process.wait(timeout=2)
148
- except subprocess.TimeoutExpired:
149
- os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
150
- self.process.wait()
151
-
152
- if self.master_fd:
153
- with contextlib.suppress(OSError):
154
- os.close(self.master_fd)
155
- self.master_fd = None
156
-
157
- if self._reader_thread and self._reader_thread.is_alive():
158
- self._reader_thread.join(timeout=1)
159
-
160
- def _is_special_key(self, key: str) -> bool:
161
- special_keys = {
162
- "Enter",
163
- "Space",
164
- "Backspace",
165
- "Tab",
166
- "Escape",
167
- "Up",
168
- "Down",
169
- "Left",
170
- "Right",
171
- "Home",
172
- "End",
173
- "PageUp",
174
- "PageDown",
175
- "Insert",
176
- "Delete",
177
- } | {f"F{i}" for i in range(1, 13)}
178
-
179
- if key in special_keys:
180
- return True
181
-
182
- return bool(key.startswith(("^", "C-", "S-", "A-")))
183
-
184
- def _get_key_sequence(self, key: str) -> str | None:
185
- key_map = {
186
- "Enter": "\r",
187
- "Space": " ",
188
- "Backspace": "\x08",
189
- "Tab": "\t",
190
- "Escape": "\x1b",
191
- "Up": "\x1b[A",
192
- "Down": "\x1b[B",
193
- "Right": "\x1b[C",
194
- "Left": "\x1b[D",
195
- "Home": "\x1b[H",
196
- "End": "\x1b[F",
197
- "PageUp": "\x1b[5~",
198
- "PageDown": "\x1b[6~",
199
- "Insert": "\x1b[2~",
200
- "Delete": "\x1b[3~",
201
- "F1": "\x1b[11~",
202
- "F2": "\x1b[12~",
203
- "F3": "\x1b[13~",
204
- "F4": "\x1b[14~",
205
- "F5": "\x1b[15~",
206
- "F6": "\x1b[17~",
207
- "F7": "\x1b[18~",
208
- "F8": "\x1b[19~",
209
- "F9": "\x1b[20~",
210
- "F10": "\x1b[21~",
211
- "F11": "\x1b[23~",
212
- "F12": "\x1b[24~",
213
- }
214
-
215
- if key in key_map:
216
- return key_map[key]
217
-
218
- if key.startswith("^") and len(key) == 2:
219
- char = key[1].lower()
220
- return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
221
-
222
- if key.startswith("C-") and len(key) == 3:
223
- char = key[2].lower()
224
- return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
225
-
226
- return None
227
-
228
- def is_alive(self) -> bool:
229
- if not self.process:
230
- return False
231
- return self.process.poll() is None