aiptx 2.0.7__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.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +2933 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +341 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interactive_shell.py +559 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/local_tool_installer.py +1467 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2427 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +53 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/runtime/vps.py +830 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Terminal - Subprocess execution wrapper
|
|
3
|
+
|
|
4
|
+
Handles tool execution with timeout, output capture, and error handling.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Configurable timeout
|
|
8
|
+
- Output streaming
|
|
9
|
+
- Error capture
|
|
10
|
+
- Working directory management
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import subprocess
|
|
15
|
+
import shlex
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import time
|
|
19
|
+
import asyncio
|
|
20
|
+
from typing import Optional, Callable, Dict, Any
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ExecutionResult:
|
|
27
|
+
"""Result of command execution"""
|
|
28
|
+
command: str
|
|
29
|
+
output: str
|
|
30
|
+
error: Optional[str]
|
|
31
|
+
return_code: int
|
|
32
|
+
timed_out: bool
|
|
33
|
+
duration: float
|
|
34
|
+
working_dir: str = ""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def success(self) -> bool:
|
|
38
|
+
"""Check if execution was successful"""
|
|
39
|
+
return self.return_code == 0 and not self.timed_out
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
42
|
+
return {
|
|
43
|
+
"command": self.command,
|
|
44
|
+
"output": self.output,
|
|
45
|
+
"error": self.error,
|
|
46
|
+
"return_code": self.return_code,
|
|
47
|
+
"timed_out": self.timed_out,
|
|
48
|
+
"duration": self.duration,
|
|
49
|
+
"success": self.success,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Terminal:
|
|
54
|
+
"""
|
|
55
|
+
Terminal execution wrapper.
|
|
56
|
+
|
|
57
|
+
Handles:
|
|
58
|
+
- Command execution with timeout
|
|
59
|
+
- Output capture (stdout + stderr)
|
|
60
|
+
- Working directory management
|
|
61
|
+
- Signal handling for cleanup
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
default_timeout: int = 300,
|
|
67
|
+
max_output: int = 50000,
|
|
68
|
+
shell: str = "/bin/bash",
|
|
69
|
+
working_dir: Optional[str] = None,
|
|
70
|
+
env: Optional[Dict[str, str]] = None,
|
|
71
|
+
):
|
|
72
|
+
self.default_timeout = default_timeout
|
|
73
|
+
self.max_output = max_output
|
|
74
|
+
self.shell = shell
|
|
75
|
+
self.working_dir = working_dir or os.getcwd()
|
|
76
|
+
self.default_env = env or {}
|
|
77
|
+
|
|
78
|
+
# Ensure working directory exists
|
|
79
|
+
Path(self.working_dir).mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
def execute(
|
|
82
|
+
self,
|
|
83
|
+
command: str,
|
|
84
|
+
timeout: Optional[int] = None,
|
|
85
|
+
working_dir: Optional[str] = None,
|
|
86
|
+
env: Optional[Dict[str, str]] = None,
|
|
87
|
+
capture_stderr: bool = True,
|
|
88
|
+
) -> ExecutionResult:
|
|
89
|
+
"""
|
|
90
|
+
Execute a command and capture output.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
command: Command to execute
|
|
94
|
+
timeout: Timeout in seconds (uses default if not specified)
|
|
95
|
+
working_dir: Working directory (uses instance default if not specified)
|
|
96
|
+
env: Additional environment variables
|
|
97
|
+
capture_stderr: Whether to capture stderr
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
ExecutionResult with output and status
|
|
101
|
+
"""
|
|
102
|
+
timeout = timeout or self.default_timeout
|
|
103
|
+
cwd = working_dir or self.working_dir
|
|
104
|
+
|
|
105
|
+
# Prepare environment
|
|
106
|
+
full_env = os.environ.copy()
|
|
107
|
+
full_env.update(self.default_env)
|
|
108
|
+
if env:
|
|
109
|
+
full_env.update(env)
|
|
110
|
+
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
process = subprocess.Popen(
|
|
115
|
+
command,
|
|
116
|
+
shell=True,
|
|
117
|
+
executable=self.shell,
|
|
118
|
+
stdout=subprocess.PIPE,
|
|
119
|
+
stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
|
|
120
|
+
cwd=cwd,
|
|
121
|
+
env=full_env,
|
|
122
|
+
preexec_fn=os.setsid, # Create new process group for cleanup
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
|
127
|
+
timed_out = False
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
130
|
+
stdout, stderr = process.communicate()
|
|
131
|
+
timed_out = True
|
|
132
|
+
|
|
133
|
+
duration = time.time() - start_time
|
|
134
|
+
|
|
135
|
+
output = self._decode_output(stdout)
|
|
136
|
+
error_output = self._decode_output(stderr) if stderr else None
|
|
137
|
+
|
|
138
|
+
error = error_output if process.returncode != 0 or error_output else None
|
|
139
|
+
|
|
140
|
+
return ExecutionResult(
|
|
141
|
+
command=command,
|
|
142
|
+
output=output,
|
|
143
|
+
error=error,
|
|
144
|
+
return_code=process.returncode,
|
|
145
|
+
timed_out=timed_out,
|
|
146
|
+
duration=duration,
|
|
147
|
+
working_dir=cwd,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
duration = time.time() - start_time
|
|
152
|
+
return ExecutionResult(
|
|
153
|
+
command=command,
|
|
154
|
+
output="",
|
|
155
|
+
error=str(e),
|
|
156
|
+
return_code=-1,
|
|
157
|
+
timed_out=False,
|
|
158
|
+
duration=duration,
|
|
159
|
+
working_dir=cwd,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
async def execute_async(
|
|
163
|
+
self,
|
|
164
|
+
command: str,
|
|
165
|
+
timeout: Optional[int] = None,
|
|
166
|
+
**kwargs
|
|
167
|
+
) -> ExecutionResult:
|
|
168
|
+
"""Async version of execute"""
|
|
169
|
+
loop = asyncio.get_event_loop()
|
|
170
|
+
return await loop.run_in_executor(
|
|
171
|
+
None,
|
|
172
|
+
lambda: self.execute(command, timeout, **kwargs)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def execute_streaming(
|
|
176
|
+
self,
|
|
177
|
+
command: str,
|
|
178
|
+
callback: Callable[[str], None],
|
|
179
|
+
timeout: Optional[int] = None,
|
|
180
|
+
working_dir: Optional[str] = None,
|
|
181
|
+
) -> ExecutionResult:
|
|
182
|
+
"""
|
|
183
|
+
Execute command with real-time output streaming.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
command: Command to execute
|
|
187
|
+
callback: Function called with each output line
|
|
188
|
+
timeout: Timeout in seconds
|
|
189
|
+
working_dir: Working directory
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
ExecutionResult with full output
|
|
193
|
+
"""
|
|
194
|
+
import select
|
|
195
|
+
|
|
196
|
+
timeout = timeout or self.default_timeout
|
|
197
|
+
cwd = working_dir or self.working_dir
|
|
198
|
+
start_time = time.time()
|
|
199
|
+
output_lines = []
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
process = subprocess.Popen(
|
|
203
|
+
command,
|
|
204
|
+
shell=True,
|
|
205
|
+
executable=self.shell,
|
|
206
|
+
stdout=subprocess.PIPE,
|
|
207
|
+
stderr=subprocess.STDOUT,
|
|
208
|
+
cwd=cwd,
|
|
209
|
+
preexec_fn=os.setsid,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
while True:
|
|
213
|
+
elapsed = time.time() - start_time
|
|
214
|
+
if elapsed > timeout:
|
|
215
|
+
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
|
|
216
|
+
return ExecutionResult(
|
|
217
|
+
command=command,
|
|
218
|
+
output="\n".join(output_lines),
|
|
219
|
+
error="Timeout exceeded",
|
|
220
|
+
return_code=-1,
|
|
221
|
+
timed_out=True,
|
|
222
|
+
duration=elapsed,
|
|
223
|
+
working_dir=cwd,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
ready, _, _ = select.select([process.stdout], [], [], 0.1)
|
|
227
|
+
if ready:
|
|
228
|
+
line = process.stdout.readline()
|
|
229
|
+
if line:
|
|
230
|
+
decoded = line.decode("utf-8", errors="replace").rstrip()
|
|
231
|
+
output_lines.append(decoded)
|
|
232
|
+
callback(decoded)
|
|
233
|
+
|
|
234
|
+
if process.poll() is not None:
|
|
235
|
+
remaining = process.stdout.read()
|
|
236
|
+
if remaining:
|
|
237
|
+
decoded = remaining.decode("utf-8", errors="replace")
|
|
238
|
+
for line in decoded.split("\n"):
|
|
239
|
+
if line:
|
|
240
|
+
output_lines.append(line)
|
|
241
|
+
callback(line)
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
duration = time.time() - start_time
|
|
245
|
+
output = "\n".join(output_lines)
|
|
246
|
+
|
|
247
|
+
return ExecutionResult(
|
|
248
|
+
command=command,
|
|
249
|
+
output=output[:self.max_output],
|
|
250
|
+
error=None if process.returncode == 0 else f"Exit code: {process.returncode}",
|
|
251
|
+
return_code=process.returncode,
|
|
252
|
+
timed_out=False,
|
|
253
|
+
duration=duration,
|
|
254
|
+
working_dir=cwd,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return ExecutionResult(
|
|
259
|
+
command=command,
|
|
260
|
+
output="\n".join(output_lines),
|
|
261
|
+
error=str(e),
|
|
262
|
+
return_code=-1,
|
|
263
|
+
timed_out=False,
|
|
264
|
+
duration=time.time() - start_time,
|
|
265
|
+
working_dir=cwd,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def execute_background(
|
|
269
|
+
self,
|
|
270
|
+
command: str,
|
|
271
|
+
log_file: Optional[str] = None,
|
|
272
|
+
) -> int:
|
|
273
|
+
"""
|
|
274
|
+
Execute command in background.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
command: Command to execute
|
|
278
|
+
log_file: Optional file to log output
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Process ID
|
|
282
|
+
"""
|
|
283
|
+
if log_file:
|
|
284
|
+
command = f"{command} > {log_file} 2>&1"
|
|
285
|
+
|
|
286
|
+
process = subprocess.Popen(
|
|
287
|
+
command,
|
|
288
|
+
shell=True,
|
|
289
|
+
executable=self.shell,
|
|
290
|
+
stdout=subprocess.DEVNULL,
|
|
291
|
+
stderr=subprocess.DEVNULL,
|
|
292
|
+
cwd=self.working_dir,
|
|
293
|
+
start_new_session=True,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
return process.pid
|
|
297
|
+
|
|
298
|
+
def _decode_output(self, data: bytes) -> str:
|
|
299
|
+
"""Decode output and truncate if necessary"""
|
|
300
|
+
try:
|
|
301
|
+
decoded = data.decode("utf-8", errors="replace")
|
|
302
|
+
except Exception:
|
|
303
|
+
decoded = str(data)
|
|
304
|
+
|
|
305
|
+
if len(decoded) > self.max_output:
|
|
306
|
+
decoded = decoded[:self.max_output] + f"\n\n[Output truncated at {self.max_output} chars]"
|
|
307
|
+
|
|
308
|
+
return decoded
|
|
309
|
+
|
|
310
|
+
def check_tool_available(self, tool_name: str) -> bool:
|
|
311
|
+
"""Check if a tool is available in PATH"""
|
|
312
|
+
result = self.execute(f"which {tool_name}", timeout=5)
|
|
313
|
+
return result.return_code == 0
|
|
314
|
+
|
|
315
|
+
def get_tool_version(self, tool_name: str) -> Optional[str]:
|
|
316
|
+
"""Get version of a tool"""
|
|
317
|
+
for flag in ["--version", "-v", "-V", "version"]:
|
|
318
|
+
result = self.execute(f"{tool_name} {flag} 2>&1 | head -1", timeout=5)
|
|
319
|
+
if result.return_code == 0 and result.output:
|
|
320
|
+
return result.output.strip().split("\n")[0]
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
def list_available_tools(self, tools: list[str]) -> Dict[str, bool]:
|
|
324
|
+
"""Check availability of multiple tools"""
|
|
325
|
+
return {tool: self.check_tool_available(tool) for tool in tools}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# Singleton instance
|
|
329
|
+
_terminal: Optional[Terminal] = None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def get_terminal(**kwargs) -> Terminal:
|
|
333
|
+
"""Get singleton terminal instance"""
|
|
334
|
+
global _terminal
|
|
335
|
+
if _terminal is None:
|
|
336
|
+
_terminal = Terminal(**kwargs)
|
|
337
|
+
return _terminal
|