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.
Files changed (187) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +46 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/exploit_agent.py +688 -0
  8. aipt_v2/agents/ptt.py +406 -0
  9. aipt_v2/agents/state.py +168 -0
  10. aipt_v2/app.py +957 -0
  11. aipt_v2/browser/__init__.py +31 -0
  12. aipt_v2/browser/automation.py +458 -0
  13. aipt_v2/browser/crawler.py +453 -0
  14. aipt_v2/cli.py +2933 -0
  15. aipt_v2/compliance/__init__.py +71 -0
  16. aipt_v2/compliance/compliance_report.py +449 -0
  17. aipt_v2/compliance/framework_mapper.py +424 -0
  18. aipt_v2/compliance/nist_mapping.py +345 -0
  19. aipt_v2/compliance/owasp_mapping.py +330 -0
  20. aipt_v2/compliance/pci_mapping.py +297 -0
  21. aipt_v2/config.py +341 -0
  22. aipt_v2/core/__init__.py +43 -0
  23. aipt_v2/core/agent.py +630 -0
  24. aipt_v2/core/llm.py +395 -0
  25. aipt_v2/core/memory.py +305 -0
  26. aipt_v2/core/ptt.py +329 -0
  27. aipt_v2/database/__init__.py +14 -0
  28. aipt_v2/database/models.py +232 -0
  29. aipt_v2/database/repository.py +384 -0
  30. aipt_v2/docker/__init__.py +23 -0
  31. aipt_v2/docker/builder.py +260 -0
  32. aipt_v2/docker/manager.py +222 -0
  33. aipt_v2/docker/sandbox.py +371 -0
  34. aipt_v2/evasion/__init__.py +58 -0
  35. aipt_v2/evasion/request_obfuscator.py +272 -0
  36. aipt_v2/evasion/tls_fingerprint.py +285 -0
  37. aipt_v2/evasion/ua_rotator.py +301 -0
  38. aipt_v2/evasion/waf_bypass.py +439 -0
  39. aipt_v2/execution/__init__.py +23 -0
  40. aipt_v2/execution/executor.py +302 -0
  41. aipt_v2/execution/parser.py +544 -0
  42. aipt_v2/execution/terminal.py +337 -0
  43. aipt_v2/health.py +437 -0
  44. aipt_v2/intelligence/__init__.py +194 -0
  45. aipt_v2/intelligence/adaptation.py +474 -0
  46. aipt_v2/intelligence/auth.py +520 -0
  47. aipt_v2/intelligence/chaining.py +775 -0
  48. aipt_v2/intelligence/correlation.py +536 -0
  49. aipt_v2/intelligence/cve_aipt.py +334 -0
  50. aipt_v2/intelligence/cve_info.py +1111 -0
  51. aipt_v2/intelligence/knowledge_graph.py +590 -0
  52. aipt_v2/intelligence/learning.py +626 -0
  53. aipt_v2/intelligence/llm_analyzer.py +502 -0
  54. aipt_v2/intelligence/llm_tool_selector.py +518 -0
  55. aipt_v2/intelligence/payload_generator.py +562 -0
  56. aipt_v2/intelligence/rag.py +239 -0
  57. aipt_v2/intelligence/scope.py +442 -0
  58. aipt_v2/intelligence/searchers/__init__.py +5 -0
  59. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  60. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  61. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  62. aipt_v2/intelligence/tools.json +443 -0
  63. aipt_v2/intelligence/triage.py +670 -0
  64. aipt_v2/interactive_shell.py +559 -0
  65. aipt_v2/interface/__init__.py +5 -0
  66. aipt_v2/interface/cli.py +230 -0
  67. aipt_v2/interface/main.py +501 -0
  68. aipt_v2/interface/tui.py +1276 -0
  69. aipt_v2/interface/utils.py +583 -0
  70. aipt_v2/llm/__init__.py +39 -0
  71. aipt_v2/llm/config.py +26 -0
  72. aipt_v2/llm/llm.py +514 -0
  73. aipt_v2/llm/memory.py +214 -0
  74. aipt_v2/llm/request_queue.py +89 -0
  75. aipt_v2/llm/utils.py +89 -0
  76. aipt_v2/local_tool_installer.py +1467 -0
  77. aipt_v2/models/__init__.py +15 -0
  78. aipt_v2/models/findings.py +295 -0
  79. aipt_v2/models/phase_result.py +224 -0
  80. aipt_v2/models/scan_config.py +207 -0
  81. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  82. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  83. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  84. aipt_v2/monitoring/prometheus.yml +60 -0
  85. aipt_v2/orchestration/__init__.py +52 -0
  86. aipt_v2/orchestration/pipeline.py +398 -0
  87. aipt_v2/orchestration/progress.py +300 -0
  88. aipt_v2/orchestration/scheduler.py +296 -0
  89. aipt_v2/orchestrator.py +2427 -0
  90. aipt_v2/payloads/__init__.py +27 -0
  91. aipt_v2/payloads/cmdi.py +150 -0
  92. aipt_v2/payloads/sqli.py +263 -0
  93. aipt_v2/payloads/ssrf.py +204 -0
  94. aipt_v2/payloads/templates.py +222 -0
  95. aipt_v2/payloads/traversal.py +166 -0
  96. aipt_v2/payloads/xss.py +204 -0
  97. aipt_v2/prompts/__init__.py +60 -0
  98. aipt_v2/proxy/__init__.py +29 -0
  99. aipt_v2/proxy/history.py +352 -0
  100. aipt_v2/proxy/interceptor.py +452 -0
  101. aipt_v2/recon/__init__.py +44 -0
  102. aipt_v2/recon/dns.py +241 -0
  103. aipt_v2/recon/osint.py +367 -0
  104. aipt_v2/recon/subdomain.py +372 -0
  105. aipt_v2/recon/tech_detect.py +311 -0
  106. aipt_v2/reports/__init__.py +17 -0
  107. aipt_v2/reports/generator.py +313 -0
  108. aipt_v2/reports/html_report.py +378 -0
  109. aipt_v2/runtime/__init__.py +53 -0
  110. aipt_v2/runtime/base.py +30 -0
  111. aipt_v2/runtime/docker.py +401 -0
  112. aipt_v2/runtime/local.py +346 -0
  113. aipt_v2/runtime/tool_server.py +205 -0
  114. aipt_v2/runtime/vps.py +830 -0
  115. aipt_v2/scanners/__init__.py +28 -0
  116. aipt_v2/scanners/base.py +273 -0
  117. aipt_v2/scanners/nikto.py +244 -0
  118. aipt_v2/scanners/nmap.py +402 -0
  119. aipt_v2/scanners/nuclei.py +273 -0
  120. aipt_v2/scanners/web.py +454 -0
  121. aipt_v2/scripts/security_audit.py +366 -0
  122. aipt_v2/setup_wizard.py +941 -0
  123. aipt_v2/skills/__init__.py +80 -0
  124. aipt_v2/skills/agents/__init__.py +14 -0
  125. aipt_v2/skills/agents/api_tester.py +706 -0
  126. aipt_v2/skills/agents/base.py +477 -0
  127. aipt_v2/skills/agents/code_review.py +459 -0
  128. aipt_v2/skills/agents/security_agent.py +336 -0
  129. aipt_v2/skills/agents/web_pentest.py +818 -0
  130. aipt_v2/skills/prompts/__init__.py +647 -0
  131. aipt_v2/system_detector.py +539 -0
  132. aipt_v2/telemetry/__init__.py +7 -0
  133. aipt_v2/telemetry/tracer.py +347 -0
  134. aipt_v2/terminal/__init__.py +28 -0
  135. aipt_v2/terminal/executor.py +400 -0
  136. aipt_v2/terminal/sandbox.py +350 -0
  137. aipt_v2/tools/__init__.py +44 -0
  138. aipt_v2/tools/active_directory/__init__.py +78 -0
  139. aipt_v2/tools/active_directory/ad_config.py +238 -0
  140. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  141. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  142. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  143. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  144. aipt_v2/tools/agents_graph/__init__.py +19 -0
  145. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  146. aipt_v2/tools/api_security/__init__.py +76 -0
  147. aipt_v2/tools/api_security/api_discovery.py +608 -0
  148. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  149. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  150. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  151. aipt_v2/tools/browser/__init__.py +5 -0
  152. aipt_v2/tools/browser/browser_actions.py +238 -0
  153. aipt_v2/tools/browser/browser_instance.py +535 -0
  154. aipt_v2/tools/browser/tab_manager.py +344 -0
  155. aipt_v2/tools/cloud/__init__.py +70 -0
  156. aipt_v2/tools/cloud/cloud_config.py +273 -0
  157. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  158. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  159. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  160. aipt_v2/tools/executor.py +307 -0
  161. aipt_v2/tools/parser.py +408 -0
  162. aipt_v2/tools/proxy/__init__.py +5 -0
  163. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  164. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  165. aipt_v2/tools/registry.py +196 -0
  166. aipt_v2/tools/scanners/__init__.py +343 -0
  167. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  168. aipt_v2/tools/scanners/burp_tool.py +631 -0
  169. aipt_v2/tools/scanners/config.py +156 -0
  170. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  171. aipt_v2/tools/scanners/zap_tool.py +612 -0
  172. aipt_v2/tools/terminal/__init__.py +5 -0
  173. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  174. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  175. aipt_v2/tools/terminal/terminal_session.py +449 -0
  176. aipt_v2/tools/tool_processing.py +108 -0
  177. aipt_v2/utils/__init__.py +17 -0
  178. aipt_v2/utils/logging.py +202 -0
  179. aipt_v2/utils/model_manager.py +187 -0
  180. aipt_v2/utils/searchers/__init__.py +269 -0
  181. aipt_v2/verify_install.py +793 -0
  182. aiptx-2.0.7.dist-info/METADATA +345 -0
  183. aiptx-2.0.7.dist-info/RECORD +187 -0
  184. aiptx-2.0.7.dist-info/WHEEL +5 -0
  185. aiptx-2.0.7.dist-info/entry_points.txt +7 -0
  186. aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
  187. aiptx-2.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,400 @@
1
+ """
2
+ AIPT Terminal Executor
3
+
4
+ Async command execution with streaming output, timeouts, and safety controls.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import logging
10
+ import os
11
+ import re
12
+ import shlex
13
+ import signal
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from enum import Enum
18
+ from typing import AsyncIterator, Callable, Optional
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ExecutionStatus(Enum):
24
+ """Command execution status"""
25
+ PENDING = "pending"
26
+ RUNNING = "running"
27
+ SUCCESS = "success"
28
+ FAILED = "failed"
29
+ TIMEOUT = "timeout"
30
+ KILLED = "killed"
31
+
32
+
33
+ @dataclass
34
+ class ExecutionConfig:
35
+ """Configuration for command execution"""
36
+ timeout: float = 300.0 # 5 minutes default
37
+ working_dir: Optional[str] = None
38
+ environment: dict[str, str] = field(default_factory=dict)
39
+ shell: bool = False # Run in shell (less secure, but needed for pipes)
40
+ capture_output: bool = True
41
+ stream_output: bool = False
42
+ max_output_size: int = 10 * 1024 * 1024 # 10MB
43
+
44
+ # Safety settings
45
+ allow_sudo: bool = False
46
+ blocked_commands: list[str] = field(default_factory=lambda: [
47
+ "rm -rf /",
48
+ "mkfs",
49
+ "dd if=/dev/zero",
50
+ ":(){:|:&};:", # Fork bomb
51
+ ])
52
+
53
+ # Resource limits
54
+ max_memory_mb: int = 1024
55
+ max_cpu_time: int = 300
56
+
57
+
58
+ @dataclass
59
+ class CommandResult:
60
+ """Result of a command execution"""
61
+ command: str
62
+ status: ExecutionStatus
63
+ exit_code: Optional[int] = None
64
+ stdout: str = ""
65
+ stderr: str = ""
66
+ start_time: Optional[datetime] = None
67
+ end_time: Optional[datetime] = None
68
+ duration_seconds: float = 0.0
69
+
70
+ # Metadata
71
+ working_dir: str = ""
72
+ pid: Optional[int] = None
73
+
74
+ @property
75
+ def success(self) -> bool:
76
+ return self.status == ExecutionStatus.SUCCESS and self.exit_code == 0
77
+
78
+ @property
79
+ def output(self) -> str:
80
+ """Combined stdout and stderr"""
81
+ parts = []
82
+ if self.stdout:
83
+ parts.append(self.stdout)
84
+ if self.stderr:
85
+ parts.append(f"[STDERR]\n{self.stderr}")
86
+ return "\n".join(parts)
87
+
88
+ def to_dict(self) -> dict:
89
+ return {
90
+ "command": self.command,
91
+ "status": self.status.value,
92
+ "exit_code": self.exit_code,
93
+ "stdout": self.stdout[:10000] if self.stdout else "",
94
+ "stderr": self.stderr[:5000] if self.stderr else "",
95
+ "duration_seconds": self.duration_seconds,
96
+ "success": self.success,
97
+ }
98
+
99
+
100
+ class TerminalExecutor:
101
+ """
102
+ Async terminal command executor with safety controls.
103
+
104
+ Features:
105
+ - Async execution with streaming output
106
+ - Timeout handling
107
+ - Command sanitization
108
+ - Output size limits
109
+ - Resource control
110
+
111
+ Example:
112
+ executor = TerminalExecutor()
113
+ result = await executor.run("nmap -sV target.com")
114
+ print(result.stdout)
115
+
116
+ # Stream output
117
+ async for line in executor.stream("nikto -h target.com"):
118
+ print(line)
119
+ """
120
+
121
+ def __init__(self, config: Optional[ExecutionConfig] = None):
122
+ self.config = config or ExecutionConfig()
123
+ self._processes: dict[int, asyncio.subprocess.Process] = {}
124
+ self._history: list[CommandResult] = []
125
+
126
+ async def run(
127
+ self,
128
+ command: str,
129
+ timeout: Optional[float] = None,
130
+ working_dir: Optional[str] = None,
131
+ env: Optional[dict[str, str]] = None,
132
+ ) -> CommandResult:
133
+ """
134
+ Execute a command and return the result.
135
+
136
+ Args:
137
+ command: Command to execute
138
+ timeout: Override default timeout
139
+ working_dir: Working directory
140
+ env: Additional environment variables
141
+
142
+ Returns:
143
+ CommandResult with output and status
144
+ """
145
+ # Validate command
146
+ validation_error = self._validate_command(command)
147
+ if validation_error:
148
+ return CommandResult(
149
+ command=command,
150
+ status=ExecutionStatus.FAILED,
151
+ stderr=validation_error,
152
+ )
153
+
154
+ timeout = timeout or self.config.timeout
155
+ cwd = working_dir or self.config.working_dir or os.getcwd()
156
+
157
+ # Build environment
158
+ environment = os.environ.copy()
159
+ environment.update(self.config.environment)
160
+ if env:
161
+ environment.update(env)
162
+
163
+ result = CommandResult(
164
+ command=command,
165
+ status=ExecutionStatus.RUNNING,
166
+ start_time=datetime.utcnow(),
167
+ working_dir=cwd,
168
+ )
169
+
170
+ try:
171
+ # Create process
172
+ if self.config.shell:
173
+ process = await asyncio.create_subprocess_shell(
174
+ command,
175
+ stdout=asyncio.subprocess.PIPE if self.config.capture_output else None,
176
+ stderr=asyncio.subprocess.PIPE if self.config.capture_output else None,
177
+ cwd=cwd,
178
+ env=environment,
179
+ )
180
+ else:
181
+ args = shlex.split(command)
182
+ process = await asyncio.create_subprocess_exec(
183
+ *args,
184
+ stdout=asyncio.subprocess.PIPE if self.config.capture_output else None,
185
+ stderr=asyncio.subprocess.PIPE if self.config.capture_output else None,
186
+ cwd=cwd,
187
+ env=environment,
188
+ )
189
+
190
+ result.pid = process.pid
191
+ self._processes[process.pid] = process
192
+
193
+ logger.info(f"Executing: {command[:100]}... (PID: {process.pid})")
194
+
195
+ # Wait for completion with timeout
196
+ try:
197
+ stdout, stderr = await asyncio.wait_for(
198
+ process.communicate(),
199
+ timeout=timeout,
200
+ )
201
+
202
+ result.exit_code = process.returncode
203
+ result.status = ExecutionStatus.SUCCESS if process.returncode == 0 else ExecutionStatus.FAILED
204
+
205
+ if stdout:
206
+ result.stdout = self._limit_output(stdout.decode("utf-8", errors="replace"))
207
+ if stderr:
208
+ result.stderr = self._limit_output(stderr.decode("utf-8", errors="replace"))
209
+
210
+ except asyncio.TimeoutError:
211
+ logger.warning(f"Command timed out after {timeout}s: {command[:50]}")
212
+ process.kill()
213
+ await process.wait()
214
+ result.status = ExecutionStatus.TIMEOUT
215
+ result.stderr = f"Command timed out after {timeout} seconds"
216
+
217
+ except FileNotFoundError as e:
218
+ result.status = ExecutionStatus.FAILED
219
+ result.stderr = f"Command not found: {e}"
220
+ except PermissionError as e:
221
+ result.status = ExecutionStatus.FAILED
222
+ result.stderr = f"Permission denied: {e}"
223
+ except Exception as e:
224
+ logger.error(f"Execution error: {e}")
225
+ result.status = ExecutionStatus.FAILED
226
+ result.stderr = str(e)
227
+ finally:
228
+ result.end_time = datetime.utcnow()
229
+ if result.start_time:
230
+ result.duration_seconds = (result.end_time - result.start_time).total_seconds()
231
+
232
+ # Cleanup
233
+ if result.pid and result.pid in self._processes:
234
+ del self._processes[result.pid]
235
+
236
+ self._history.append(result)
237
+ return result
238
+
239
+ async def stream(
240
+ self,
241
+ command: str,
242
+ timeout: Optional[float] = None,
243
+ callback: Optional[Callable[[str], None]] = None,
244
+ ) -> AsyncIterator[str]:
245
+ """
246
+ Execute command and stream output line by line.
247
+
248
+ Args:
249
+ command: Command to execute
250
+ timeout: Timeout in seconds
251
+ callback: Optional callback for each line
252
+
253
+ Yields:
254
+ Output lines as they're produced
255
+ """
256
+ validation_error = self._validate_command(command)
257
+ if validation_error:
258
+ yield f"[ERROR] {validation_error}"
259
+ return
260
+
261
+ timeout = timeout or self.config.timeout
262
+ cwd = self.config.working_dir or os.getcwd()
263
+
264
+ environment = os.environ.copy()
265
+ environment.update(self.config.environment)
266
+
267
+ try:
268
+ if self.config.shell:
269
+ process = await asyncio.create_subprocess_shell(
270
+ command,
271
+ stdout=asyncio.subprocess.PIPE,
272
+ stderr=asyncio.subprocess.STDOUT,
273
+ cwd=cwd,
274
+ env=environment,
275
+ )
276
+ else:
277
+ args = shlex.split(command)
278
+ process = await asyncio.create_subprocess_exec(
279
+ *args,
280
+ stdout=asyncio.subprocess.PIPE,
281
+ stderr=asyncio.subprocess.STDOUT,
282
+ cwd=cwd,
283
+ env=environment,
284
+ )
285
+
286
+ self._processes[process.pid] = process
287
+ start_time = time.time()
288
+ output_size = 0
289
+
290
+ async for line in process.stdout:
291
+ # Check timeout
292
+ if time.time() - start_time > timeout:
293
+ process.kill()
294
+ yield "[TIMEOUT] Command execution timed out"
295
+ break
296
+
297
+ # Check output size
298
+ output_size += len(line)
299
+ if output_size > self.config.max_output_size:
300
+ process.kill()
301
+ yield "[TRUNCATED] Output exceeded maximum size"
302
+ break
303
+
304
+ decoded = line.decode("utf-8", errors="replace").rstrip()
305
+ if callback:
306
+ callback(decoded)
307
+ yield decoded
308
+
309
+ await process.wait()
310
+
311
+ except Exception as e:
312
+ yield f"[ERROR] {str(e)}"
313
+ finally:
314
+ if process.pid in self._processes:
315
+ del self._processes[process.pid]
316
+
317
+ async def run_multiple(
318
+ self,
319
+ commands: list[str],
320
+ parallel: bool = False,
321
+ stop_on_error: bool = True,
322
+ ) -> list[CommandResult]:
323
+ """
324
+ Execute multiple commands.
325
+
326
+ Args:
327
+ commands: List of commands
328
+ parallel: Run in parallel (True) or sequential (False)
329
+ stop_on_error: Stop on first error (sequential only)
330
+
331
+ Returns:
332
+ List of results
333
+ """
334
+ if parallel:
335
+ tasks = [self.run(cmd) for cmd in commands]
336
+ return await asyncio.gather(*tasks)
337
+ else:
338
+ results = []
339
+ for cmd in commands:
340
+ result = await self.run(cmd)
341
+ results.append(result)
342
+ if stop_on_error and not result.success:
343
+ break
344
+ return results
345
+
346
+ async def kill(self, pid: int) -> bool:
347
+ """Kill a running process by PID"""
348
+ if pid in self._processes:
349
+ try:
350
+ self._processes[pid].kill()
351
+ await self._processes[pid].wait()
352
+ del self._processes[pid]
353
+ return True
354
+ except Exception as e:
355
+ logger.error(f"Failed to kill process {pid}: {e}")
356
+ return False
357
+
358
+ async def kill_all(self) -> int:
359
+ """Kill all running processes"""
360
+ killed = 0
361
+ for pid in list(self._processes.keys()):
362
+ if await self.kill(pid):
363
+ killed += 1
364
+ return killed
365
+
366
+ def _validate_command(self, command: str) -> Optional[str]:
367
+ """Validate command for safety"""
368
+ cmd_lower = command.lower()
369
+
370
+ # Check blocked commands
371
+ for blocked in self.config.blocked_commands:
372
+ if blocked.lower() in cmd_lower:
373
+ return f"Blocked command pattern: {blocked}"
374
+
375
+ # Check sudo
376
+ if not self.config.allow_sudo and cmd_lower.strip().startswith("sudo"):
377
+ return "sudo commands are not allowed"
378
+
379
+ return None
380
+
381
+ def _limit_output(self, output: str) -> str:
382
+ """Limit output size"""
383
+ if len(output) > self.config.max_output_size:
384
+ return output[:self.config.max_output_size] + "\n[OUTPUT TRUNCATED]"
385
+ return output
386
+
387
+ def get_history(self, limit: int = 100) -> list[CommandResult]:
388
+ """Get command execution history"""
389
+ return self._history[-limit:]
390
+
391
+ def clear_history(self) -> None:
392
+ """Clear execution history"""
393
+ self._history.clear()
394
+
395
+
396
+ # Convenience function
397
+ async def run_command(command: str, timeout: float = 60.0) -> CommandResult:
398
+ """Quick command execution"""
399
+ executor = TerminalExecutor()
400
+ return await executor.run(command, timeout=timeout)