iridet-bot 0.1.1a1__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.
- iribot/.env.example +4 -0
- iribot/__init__.py +5 -0
- iribot/__main__.py +7 -0
- iribot/ag_ui_protocol.py +247 -0
- iribot/agent.py +155 -0
- iribot/cli.py +33 -0
- iribot/config.py +45 -0
- iribot/executor.py +73 -0
- iribot/main.py +300 -0
- iribot/models.py +79 -0
- iribot/prompt_generator.py +104 -0
- iribot/session_manager.py +194 -0
- iribot/templates/system_prompt.j2 +185 -0
- iribot/tools/__init__.py +27 -0
- iribot/tools/base.py +80 -0
- iribot/tools/execute_command.py +572 -0
- iribot/tools/list_directory.py +49 -0
- iribot/tools/read_file.py +43 -0
- iribot/tools/write_file.py +49 -0
- iridet_bot-0.1.1a1.dist-info/METADATA +369 -0
- iridet_bot-0.1.1a1.dist-info/RECORD +24 -0
- iridet_bot-0.1.1a1.dist-info/WHEEL +5 -0
- iridet_bot-0.1.1a1.dist-info/entry_points.txt +2 -0
- iridet_bot-0.1.1a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Execute command tool with interactive shell sessions"""
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import platform
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from collections import deque
|
|
9
|
+
from typing import Any, Dict, Optional, Deque, Tuple, List
|
|
10
|
+
from .base import BaseTool, BaseToolGroup, BaseStatus
|
|
11
|
+
from ..config import settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ShellSession:
|
|
15
|
+
"""Persistent shell session with async output capture"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, working_dir: Optional[str] = None):
|
|
18
|
+
self.working_dir = working_dir
|
|
19
|
+
self._output: Deque[Tuple[str, str]] = deque()
|
|
20
|
+
self._log: Deque[Dict[str, str]] = deque()
|
|
21
|
+
self._output_event = threading.Event()
|
|
22
|
+
self._lock = threading.Lock()
|
|
23
|
+
self._running_marker: Optional[str] = None # Track if command is running
|
|
24
|
+
|
|
25
|
+
env = os.environ.copy()
|
|
26
|
+
env["PYTHONIOENCODING"] = "utf-8"
|
|
27
|
+
|
|
28
|
+
# Use UTF-8 encoding for both Windows and Unix
|
|
29
|
+
# Git Bash and Python both support UTF-8 well
|
|
30
|
+
encoding = "utf-8"
|
|
31
|
+
|
|
32
|
+
# Disable colors and set simple terminal to avoid ANSI escape sequences
|
|
33
|
+
env["TERM"] = "dumb"
|
|
34
|
+
env["PS1"] = "$ " # Simple prompt without colors
|
|
35
|
+
env["NO_COLOR"] = "1" # Disable colors in many tools
|
|
36
|
+
env["PYTHONUNBUFFERED"] = "1" # Disable Python output buffering
|
|
37
|
+
|
|
38
|
+
# Always use bash, get path from config
|
|
39
|
+
bash_cmd = settings.bash_path
|
|
40
|
+
# Use --norc --noprofile to avoid loading user configs that add colors
|
|
41
|
+
# Don't use -i to avoid "no job control" warnings in non-TTY environment
|
|
42
|
+
cmd = [bash_cmd, "--norc", "--noprofile"]
|
|
43
|
+
|
|
44
|
+
self.process = subprocess.Popen(
|
|
45
|
+
cmd,
|
|
46
|
+
stdin=subprocess.PIPE,
|
|
47
|
+
stdout=subprocess.PIPE,
|
|
48
|
+
stderr=subprocess.PIPE,
|
|
49
|
+
cwd=working_dir if working_dir else None,
|
|
50
|
+
text=True,
|
|
51
|
+
bufsize=1,
|
|
52
|
+
env=env,
|
|
53
|
+
encoding=encoding,
|
|
54
|
+
errors="replace",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self._stdout_thread = threading.Thread(
|
|
58
|
+
target=self._read_stream,
|
|
59
|
+
args=("stdout", self.process.stdout),
|
|
60
|
+
daemon=True,
|
|
61
|
+
)
|
|
62
|
+
self._stderr_thread = threading.Thread(
|
|
63
|
+
target=self._read_stream,
|
|
64
|
+
args=("stderr", self.process.stderr),
|
|
65
|
+
daemon=True,
|
|
66
|
+
)
|
|
67
|
+
self._stdout_thread.start()
|
|
68
|
+
self._stderr_thread.start()
|
|
69
|
+
|
|
70
|
+
def _read_stream(self, stream_name: str, stream):
|
|
71
|
+
try:
|
|
72
|
+
for line in iter(stream.readline, ""):
|
|
73
|
+
if line:
|
|
74
|
+
with self._lock:
|
|
75
|
+
self._output.append((stream_name, line))
|
|
76
|
+
self._log.append({"stream": stream_name, "data": line})
|
|
77
|
+
self._output_event.set()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
finally:
|
|
81
|
+
stream.close()
|
|
82
|
+
|
|
83
|
+
def is_alive(self) -> bool:
|
|
84
|
+
return self.process and self.process.poll() is None
|
|
85
|
+
|
|
86
|
+
def write(self, data: str) -> None:
|
|
87
|
+
if not self.is_alive():
|
|
88
|
+
raise RuntimeError("Shell session is not running")
|
|
89
|
+
if not data.endswith("\n"):
|
|
90
|
+
data += "\n"
|
|
91
|
+
with self._lock:
|
|
92
|
+
self._log.append({"stream": "stdin", "data": data})
|
|
93
|
+
self.process.stdin.write(data)
|
|
94
|
+
self.process.stdin.flush()
|
|
95
|
+
|
|
96
|
+
def read(self, wait_ms: int = 0, max_chars: int = 20000) -> Dict[str, Any]:
|
|
97
|
+
output = []
|
|
98
|
+
stderr = []
|
|
99
|
+
|
|
100
|
+
if wait_ms > 0:
|
|
101
|
+
self._output_event.wait(wait_ms / 1000)
|
|
102
|
+
|
|
103
|
+
with self._lock:
|
|
104
|
+
char_count = 0
|
|
105
|
+
while self._output and char_count <= max_chars:
|
|
106
|
+
stream_name, line = self._output.popleft()
|
|
107
|
+
char_count += len(line)
|
|
108
|
+
if stream_name == "stdout":
|
|
109
|
+
output.append(line)
|
|
110
|
+
else:
|
|
111
|
+
stderr.append(line)
|
|
112
|
+
self._output_event.clear()
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"stdout": "".join(output),
|
|
116
|
+
"stderr": "".join(stderr),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def terminate(self) -> None:
|
|
120
|
+
if self.is_alive():
|
|
121
|
+
self.process.terminate()
|
|
122
|
+
|
|
123
|
+
def get_log(self) -> list:
|
|
124
|
+
with self._lock:
|
|
125
|
+
return list(self._log)
|
|
126
|
+
|
|
127
|
+
def is_running(self) -> bool:
|
|
128
|
+
"""Check if a command is currently running (marker not yet seen)"""
|
|
129
|
+
if self._running_marker is None:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Check if marker is already in the buffered output
|
|
133
|
+
# This handles the case where command completed but we didn't wait long enough
|
|
134
|
+
with self._lock:
|
|
135
|
+
new_output = deque()
|
|
136
|
+
marker_found = False
|
|
137
|
+
for stream_name, line in self._output:
|
|
138
|
+
if self._running_marker in line:
|
|
139
|
+
# Marker found in buffer, command has completed
|
|
140
|
+
marker_found = True
|
|
141
|
+
# Remove the marker line from output
|
|
142
|
+
continue
|
|
143
|
+
new_output.append((stream_name, line))
|
|
144
|
+
|
|
145
|
+
if marker_found:
|
|
146
|
+
self._output = new_output
|
|
147
|
+
self._running_marker = None
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
def set_running_marker(self, marker: str) -> None:
|
|
153
|
+
"""Set the marker for currently running command"""
|
|
154
|
+
with self._lock:
|
|
155
|
+
self._running_marker = marker
|
|
156
|
+
|
|
157
|
+
def clear_running_marker(self) -> None:
|
|
158
|
+
"""Clear the running marker when command completes"""
|
|
159
|
+
with self._lock:
|
|
160
|
+
self._running_marker = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
_shell_sessions: Dict[str, ShellSession] = {}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _collect_output_until_marker(
|
|
167
|
+
session: ShellSession,
|
|
168
|
+
marker: str,
|
|
169
|
+
wait_ms: int,
|
|
170
|
+
max_chars: int,
|
|
171
|
+
) -> Tuple[str, str, bool]:
|
|
172
|
+
"""
|
|
173
|
+
Collect output from session until marker is found or timeout.
|
|
174
|
+
|
|
175
|
+
Returns: (stdout, stderr, marker_found)
|
|
176
|
+
"""
|
|
177
|
+
max_wait_time = wait_ms if wait_ms > 0 else 100000
|
|
178
|
+
start_time = time.time() * 1000
|
|
179
|
+
all_stdout = []
|
|
180
|
+
all_stderr = []
|
|
181
|
+
marker_found = False
|
|
182
|
+
|
|
183
|
+
while (time.time() * 1000 - start_time) < max_wait_time:
|
|
184
|
+
output = session.read(wait_ms=100, max_chars=max_chars)
|
|
185
|
+
stdout_chunk = output.get("stdout", "")
|
|
186
|
+
stderr_chunk = output.get("stderr", "")
|
|
187
|
+
|
|
188
|
+
if marker in stdout_chunk:
|
|
189
|
+
# Remove the marker and everything after it (including the marker line)
|
|
190
|
+
lines = stdout_chunk.split('\n')
|
|
191
|
+
filtered_lines = []
|
|
192
|
+
for line in lines:
|
|
193
|
+
if marker in line:
|
|
194
|
+
marker_found = True
|
|
195
|
+
break
|
|
196
|
+
filtered_lines.append(line)
|
|
197
|
+
stdout_chunk = '\n'.join(filtered_lines)
|
|
198
|
+
# Add final newline if there were lines
|
|
199
|
+
if filtered_lines and stdout_chunk and not stdout_chunk.endswith('\n'):
|
|
200
|
+
stdout_chunk += '\n'
|
|
201
|
+
|
|
202
|
+
if stdout_chunk:
|
|
203
|
+
all_stdout.append(stdout_chunk)
|
|
204
|
+
if stderr_chunk:
|
|
205
|
+
all_stderr.append(stderr_chunk)
|
|
206
|
+
|
|
207
|
+
if marker_found:
|
|
208
|
+
# Clear running marker when command completes
|
|
209
|
+
session.clear_running_marker()
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not stdout_chunk and not stderr_chunk:
|
|
213
|
+
time.sleep(0.01)
|
|
214
|
+
|
|
215
|
+
return "".join(all_stdout), "".join(all_stderr), marker_found
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _ensure_session(session_id: str, working_dir: Optional[str] = None) -> ShellSession:
|
|
219
|
+
if session_id not in _shell_sessions or not _shell_sessions[session_id].is_alive():
|
|
220
|
+
_shell_sessions[session_id] = ShellSession(working_dir=working_dir)
|
|
221
|
+
if working_dir:
|
|
222
|
+
_shell_sessions[session_id].write(f'cd "{working_dir}"')
|
|
223
|
+
return _shell_sessions[session_id]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _get_sessions_status() -> List[Dict[str, Any]]:
|
|
227
|
+
sessions = []
|
|
228
|
+
for session_id, session in _shell_sessions.items():
|
|
229
|
+
sessions.append({
|
|
230
|
+
"session_id": session_id,
|
|
231
|
+
"working_dir": session.working_dir,
|
|
232
|
+
"alive": session.is_alive(),
|
|
233
|
+
"pid": session.process.pid if session.process else None,
|
|
234
|
+
"log": session.get_log(),
|
|
235
|
+
})
|
|
236
|
+
return sessions
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class ShellStartTool(BaseTool):
|
|
240
|
+
"""Start a persistent shell session"""
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def name(self) -> str:
|
|
244
|
+
return "shell_start"
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def description(self) -> str:
|
|
248
|
+
return "Start a persistent bash shell session."
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def parameters(self) -> Dict[str, Any]:
|
|
252
|
+
return {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"properties": {
|
|
255
|
+
"session_id": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"description": "Session/agent identifier for persistent shell",
|
|
258
|
+
},
|
|
259
|
+
"working_dir": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"description": "Working directory to start shell (optional)",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
"required": [],
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
def execute(
|
|
268
|
+
self,
|
|
269
|
+
session_id: Optional[str] = None,
|
|
270
|
+
working_dir: Optional[str] = None,
|
|
271
|
+
) -> Dict[str, Any]:
|
|
272
|
+
session_id = session_id or "default"
|
|
273
|
+
_ensure_session(session_id, working_dir=working_dir)
|
|
274
|
+
return {
|
|
275
|
+
"success": True,
|
|
276
|
+
"session_id": session_id,
|
|
277
|
+
"status": "started",
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ShellRunTool(BaseTool):
|
|
282
|
+
"""Run a command in a shell session"""
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def name(self) -> str:
|
|
286
|
+
return "shell_run"
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def description(self) -> str:
|
|
290
|
+
return "Run a command in a persistent bash session. **IMPORTANT**: If 'background' is set to true, the command will run in the background and the tool will return immediately. This will occupy the shell session; start a new session for other commands."
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def parameters(self) -> Dict[str, Any]:
|
|
294
|
+
return {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"properties": {
|
|
297
|
+
"session_id": {
|
|
298
|
+
"type": "string",
|
|
299
|
+
"description": "Session/agent identifier for persistent shell",
|
|
300
|
+
},
|
|
301
|
+
"command": {
|
|
302
|
+
"type": "string",
|
|
303
|
+
"description": "Command to run",
|
|
304
|
+
},
|
|
305
|
+
"wait_ms": {
|
|
306
|
+
"type": "integer",
|
|
307
|
+
"description": "Max wait time in milliseconds for command completion",
|
|
308
|
+
},
|
|
309
|
+
"max_chars": {
|
|
310
|
+
"type": "integer",
|
|
311
|
+
"description": "Max characters to return from buffered output",
|
|
312
|
+
"default": 20000,
|
|
313
|
+
},
|
|
314
|
+
"background": {
|
|
315
|
+
"type": "boolean",
|
|
316
|
+
"description": "Run command in background and return immediately. **IMPORTANT** This will occupy the shell session; start a new session for other commands.",
|
|
317
|
+
"default": False,
|
|
318
|
+
},
|
|
319
|
+
"working_dir": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"description": "Working directory to start shell (optional)",
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
"required": ["command", "background"],
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
def execute(
|
|
328
|
+
self,
|
|
329
|
+
command: str,
|
|
330
|
+
session_id: Optional[str] = None,
|
|
331
|
+
wait_ms: Optional[int] = None,
|
|
332
|
+
max_chars: int = 20000,
|
|
333
|
+
background: bool = False,
|
|
334
|
+
working_dir: Optional[str] = None,
|
|
335
|
+
) -> Dict[str, Any]:
|
|
336
|
+
session_id = session_id or "default"
|
|
337
|
+
session = _ensure_session(session_id, working_dir=working_dir)
|
|
338
|
+
|
|
339
|
+
# Check if session is already running a command
|
|
340
|
+
if session.is_running():
|
|
341
|
+
return {
|
|
342
|
+
"success": False,
|
|
343
|
+
"error": f"Session '{session_id}' is already running a command. Please wait for it to complete, use shell_read to check status, or start a new session to run another command.",
|
|
344
|
+
"session_id": session_id,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if wait_ms is None:
|
|
348
|
+
wait_ms = 10000 if background else 100000
|
|
349
|
+
|
|
350
|
+
marker = f"__CMD_DONE_{uuid.uuid4().hex[:8]}__"
|
|
351
|
+
|
|
352
|
+
# Mark session as running before executing command
|
|
353
|
+
session.set_running_marker(marker)
|
|
354
|
+
|
|
355
|
+
session.write(command)
|
|
356
|
+
session.write(f"echo {marker}")
|
|
357
|
+
|
|
358
|
+
if background:
|
|
359
|
+
# For background mode, wait for marker with a reasonable timeout
|
|
360
|
+
# If marker is found, command completed and marker will be cleared
|
|
361
|
+
# If marker is not found, command is still running, keep marker set
|
|
362
|
+
# (is_running() will check buffer for marker before rejecting new commands)
|
|
363
|
+
stdout, stderr, marker_found = _collect_output_until_marker(
|
|
364
|
+
session, marker, wait_ms, max_chars
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
status = "completed" if marker_found else "running"
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"success": True,
|
|
371
|
+
"session_id": session_id,
|
|
372
|
+
"status": status,
|
|
373
|
+
"stdout": stdout,
|
|
374
|
+
"stderr": stderr,
|
|
375
|
+
}
|
|
376
|
+
else:
|
|
377
|
+
# For normal mode, wait until completion or timeout
|
|
378
|
+
max_wait_time = wait_ms if wait_ms > 0 else 100000
|
|
379
|
+
stdout, stderr, marker_found = _collect_output_until_marker(
|
|
380
|
+
session, marker, max_wait_time, max_chars
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"success": True,
|
|
385
|
+
"session_id": session_id,
|
|
386
|
+
"stdout": stdout,
|
|
387
|
+
"stderr": stderr,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class ShellWriteTool(BaseTool):
|
|
392
|
+
"""Write input to a shell session"""
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def name(self) -> str:
|
|
396
|
+
return "shell_write"
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def description(self) -> str:
|
|
400
|
+
return "Write input to stdin of a persistent bash session."
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def parameters(self) -> Dict[str, Any]:
|
|
404
|
+
return {
|
|
405
|
+
"type": "object",
|
|
406
|
+
"properties": {
|
|
407
|
+
"session_id": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"description": "Session/agent identifier for persistent shell",
|
|
410
|
+
},
|
|
411
|
+
"input": {
|
|
412
|
+
"type": "string",
|
|
413
|
+
"description": "Input to write to stdin",
|
|
414
|
+
},
|
|
415
|
+
"working_dir": {
|
|
416
|
+
"type": "string",
|
|
417
|
+
"description": "Working directory to start shell (optional)",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
"required": ["input"],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
def execute(
|
|
424
|
+
self,
|
|
425
|
+
input: str,
|
|
426
|
+
session_id: Optional[str] = None,
|
|
427
|
+
working_dir: Optional[str] = None,
|
|
428
|
+
) -> Dict[str, Any]:
|
|
429
|
+
session_id = session_id or "default"
|
|
430
|
+
session = _ensure_session(session_id, working_dir=working_dir)
|
|
431
|
+
session.write(input)
|
|
432
|
+
return {
|
|
433
|
+
"success": True,
|
|
434
|
+
"session_id": session_id,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class ShellReadTool(BaseTool):
|
|
439
|
+
"""Read output from a shell session"""
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def name(self) -> str:
|
|
443
|
+
return "shell_read"
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def description(self) -> str:
|
|
447
|
+
return "Read buffered output from a persistent bash session."
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def parameters(self) -> Dict[str, Any]:
|
|
451
|
+
return {
|
|
452
|
+
"type": "object",
|
|
453
|
+
"properties": {
|
|
454
|
+
"session_id": {
|
|
455
|
+
"type": "string",
|
|
456
|
+
"description": "Session/agent identifier for persistent shell",
|
|
457
|
+
},
|
|
458
|
+
"wait_ms": {
|
|
459
|
+
"type": "integer",
|
|
460
|
+
"description": "Wait time in milliseconds before reading output",
|
|
461
|
+
"default": 0,
|
|
462
|
+
},
|
|
463
|
+
"max_chars": {
|
|
464
|
+
"type": "integer",
|
|
465
|
+
"description": "Max characters to return from buffered output",
|
|
466
|
+
"default": 20000,
|
|
467
|
+
},
|
|
468
|
+
"working_dir": {
|
|
469
|
+
"type": "string",
|
|
470
|
+
"description": "Working directory to start shell (optional)",
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
"required": [],
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def execute(
|
|
477
|
+
self,
|
|
478
|
+
session_id: Optional[str] = None,
|
|
479
|
+
wait_ms: int = 0,
|
|
480
|
+
max_chars: int = 20000,
|
|
481
|
+
working_dir: Optional[str] = None,
|
|
482
|
+
) -> Dict[str, Any]:
|
|
483
|
+
session_id = session_id or "default"
|
|
484
|
+
session = _ensure_session(session_id, working_dir=working_dir)
|
|
485
|
+
default_wait_ms = wait_ms if wait_ms > 0 else 1000
|
|
486
|
+
output = session.read(wait_ms=default_wait_ms, max_chars=max_chars)
|
|
487
|
+
return {
|
|
488
|
+
"success": True,
|
|
489
|
+
"session_id": session_id,
|
|
490
|
+
**output,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class ShellStopTool(BaseTool):
|
|
495
|
+
"""Stop a shell session"""
|
|
496
|
+
|
|
497
|
+
@property
|
|
498
|
+
def name(self) -> str:
|
|
499
|
+
return "shell_stop"
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def description(self) -> str:
|
|
503
|
+
return "Stop a persistent bash session."
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def parameters(self) -> Dict[str, Any]:
|
|
507
|
+
return {
|
|
508
|
+
"type": "object",
|
|
509
|
+
"properties": {
|
|
510
|
+
"session_id": {
|
|
511
|
+
"type": "string",
|
|
512
|
+
"description": "Session/agent identifier for persistent shell",
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
"required": [],
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
def execute(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
519
|
+
session_id = session_id or "default"
|
|
520
|
+
session = _shell_sessions.get(session_id)
|
|
521
|
+
if not session or not session.is_alive():
|
|
522
|
+
return {
|
|
523
|
+
"success": True,
|
|
524
|
+
"session_id": session_id,
|
|
525
|
+
"status": "stopped",
|
|
526
|
+
}
|
|
527
|
+
session.terminate()
|
|
528
|
+
return {
|
|
529
|
+
"success": True,
|
|
530
|
+
"session_id": session_id,
|
|
531
|
+
"status": "stopped",
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
class ShellToolGroup(BaseToolGroup):
|
|
536
|
+
"""Shell tool group"""
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def name(self) -> str:
|
|
540
|
+
return "shell"
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def description(self) -> str:
|
|
544
|
+
return "Persistent bash shell tools."
|
|
545
|
+
|
|
546
|
+
def get_tools(self) -> List[BaseTool]:
|
|
547
|
+
return [
|
|
548
|
+
ShellStartTool(),
|
|
549
|
+
ShellRunTool(),
|
|
550
|
+
ShellWriteTool(),
|
|
551
|
+
ShellReadTool(),
|
|
552
|
+
ShellStopTool(),
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class ShellStatus(BaseStatus):
|
|
557
|
+
"""Status provider for shell sessions"""
|
|
558
|
+
|
|
559
|
+
@property
|
|
560
|
+
def name(self) -> str:
|
|
561
|
+
return "shell"
|
|
562
|
+
|
|
563
|
+
@property
|
|
564
|
+
def description(self) -> str:
|
|
565
|
+
return "Persistent bash shell status."
|
|
566
|
+
|
|
567
|
+
def get_status(self) -> Dict[str, Any]:
|
|
568
|
+
return {
|
|
569
|
+
"name": self.name,
|
|
570
|
+
"status": "ok",
|
|
571
|
+
"sessions": _get_sessions_status(),
|
|
572
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""List directory tool"""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
from .base import BaseTool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ListDirectoryTool(BaseTool):
|
|
8
|
+
"""List files and directories"""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
return "list_directory"
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def description(self) -> str:
|
|
16
|
+
return "List files and directories in a path"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def parameters(self) -> Dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"path": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Path to list"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": ["path"]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def execute(self, path: str) -> Dict[str, Any]:
|
|
32
|
+
"""List directory contents"""
|
|
33
|
+
try:
|
|
34
|
+
items = []
|
|
35
|
+
for item in Path(path).iterdir():
|
|
36
|
+
items.append({
|
|
37
|
+
"name": item.name,
|
|
38
|
+
"type": "directory" if item.is_dir() else "file",
|
|
39
|
+
"path": str(item)
|
|
40
|
+
})
|
|
41
|
+
return {
|
|
42
|
+
"success": True,
|
|
43
|
+
"items": items
|
|
44
|
+
}
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return {
|
|
47
|
+
"success": False,
|
|
48
|
+
"error": str(e)
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Read file tool"""
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
from .base import BaseTool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReadFileTool(BaseTool):
|
|
7
|
+
"""Read content from files"""
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "read_file"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def description(self) -> str:
|
|
15
|
+
return "Read content from a file"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def parameters(self) -> Dict[str, Any]:
|
|
19
|
+
return {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"file_path": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Path to the file to read"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["file_path"]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def execute(self, file_path: str) -> Dict[str, Any]:
|
|
31
|
+
"""Read file content"""
|
|
32
|
+
try:
|
|
33
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
return {
|
|
36
|
+
"success": True,
|
|
37
|
+
"content": content
|
|
38
|
+
}
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return {
|
|
41
|
+
"success": False,
|
|
42
|
+
"error": str(e)
|
|
43
|
+
}
|