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,559 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPTX Interactive Shell
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
Provides an interactive shell environment for running security tools
|
|
6
|
+
with enhanced features like command history, output logging, and
|
|
7
|
+
real-time display.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Interactive command execution
|
|
11
|
+
- Tab completion for tool names
|
|
12
|
+
- Command history with readline
|
|
13
|
+
- Real-time output streaming
|
|
14
|
+
- Session logging
|
|
15
|
+
- Tool discovery and help
|
|
16
|
+
- Environment management
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
aiptx shell # Start interactive shell
|
|
20
|
+
aiptx shell --log session.log # With logging
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import os
|
|
25
|
+
import pty
|
|
26
|
+
import readline
|
|
27
|
+
import select
|
|
28
|
+
import shlex
|
|
29
|
+
import shutil
|
|
30
|
+
import signal
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import termios
|
|
34
|
+
import tty
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Dict, List, Optional, Callable, Tuple
|
|
38
|
+
|
|
39
|
+
from rich.console import Console
|
|
40
|
+
from rich.panel import Panel
|
|
41
|
+
from rich.table import Table
|
|
42
|
+
from rich.syntax import Syntax
|
|
43
|
+
from rich.live import Live
|
|
44
|
+
from rich.text import Text
|
|
45
|
+
from rich import box
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
console = Console()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Command history file
|
|
52
|
+
HISTORY_FILE = Path.home() / ".aiptx" / "shell_history"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ToolCompleter:
|
|
56
|
+
"""Tab completion for tool names and commands."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, tools: List[str]):
|
|
59
|
+
self.tools = sorted(tools)
|
|
60
|
+
self.commands = [
|
|
61
|
+
"help", "tools", "exit", "quit", "clear", "history",
|
|
62
|
+
"env", "cd", "pwd", "run", "scan", "export", "log"
|
|
63
|
+
]
|
|
64
|
+
self.all_completions = self.commands + self.tools
|
|
65
|
+
|
|
66
|
+
def complete(self, text: str, state: int) -> Optional[str]:
|
|
67
|
+
"""Readline completion function."""
|
|
68
|
+
if state == 0:
|
|
69
|
+
if text:
|
|
70
|
+
self.matches = [
|
|
71
|
+
s for s in self.all_completions
|
|
72
|
+
if s.startswith(text)
|
|
73
|
+
]
|
|
74
|
+
else:
|
|
75
|
+
self.matches = self.all_completions[:]
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
return self.matches[state]
|
|
79
|
+
except IndexError:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InteractiveShell:
|
|
84
|
+
"""
|
|
85
|
+
Interactive shell for running security tools.
|
|
86
|
+
|
|
87
|
+
Provides a REPL-like environment with:
|
|
88
|
+
- Tool execution with real-time output
|
|
89
|
+
- Command history and tab completion
|
|
90
|
+
- Session logging
|
|
91
|
+
- Environment management
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
log_file: Optional[Path] = None,
|
|
97
|
+
working_dir: Optional[Path] = None,
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Initialize the interactive shell.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
log_file: Optional file to log session output
|
|
104
|
+
working_dir: Working directory (defaults to current)
|
|
105
|
+
"""
|
|
106
|
+
self.log_file = log_file
|
|
107
|
+
self.working_dir = working_dir or Path.cwd()
|
|
108
|
+
self.running = False
|
|
109
|
+
self.env = os.environ.copy()
|
|
110
|
+
self.history: List[str] = []
|
|
111
|
+
self._tools: Dict[str, str] = {}
|
|
112
|
+
self._load_tools()
|
|
113
|
+
self._setup_readline()
|
|
114
|
+
|
|
115
|
+
def _load_tools(self):
|
|
116
|
+
"""Load available tools from the installer."""
|
|
117
|
+
try:
|
|
118
|
+
from aipt_v2.local_tool_installer import TOOLS
|
|
119
|
+
self._tools = {
|
|
120
|
+
name: tool.description
|
|
121
|
+
for name, tool in TOOLS.items()
|
|
122
|
+
}
|
|
123
|
+
except ImportError:
|
|
124
|
+
self._tools = {}
|
|
125
|
+
|
|
126
|
+
def _setup_readline(self):
|
|
127
|
+
"""Configure readline for history and completion."""
|
|
128
|
+
# Load history
|
|
129
|
+
HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
if HISTORY_FILE.exists():
|
|
131
|
+
try:
|
|
132
|
+
readline.read_history_file(str(HISTORY_FILE))
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
# Set up completion
|
|
137
|
+
tool_names = list(self._tools.keys())
|
|
138
|
+
completer = ToolCompleter(tool_names)
|
|
139
|
+
readline.set_completer(completer.complete)
|
|
140
|
+
readline.parse_and_bind("tab: complete")
|
|
141
|
+
readline.set_completer_delims(" \t\n;")
|
|
142
|
+
|
|
143
|
+
# Limit history size
|
|
144
|
+
readline.set_history_length(1000)
|
|
145
|
+
|
|
146
|
+
def _save_history(self):
|
|
147
|
+
"""Save command history to file."""
|
|
148
|
+
try:
|
|
149
|
+
readline.write_history_file(str(HISTORY_FILE))
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
def _log(self, message: str):
|
|
154
|
+
"""Log message to file if logging is enabled."""
|
|
155
|
+
if self.log_file:
|
|
156
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
157
|
+
with open(self.log_file, "a") as f:
|
|
158
|
+
f.write(f"[{timestamp}] {message}\n")
|
|
159
|
+
|
|
160
|
+
def print_banner(self):
|
|
161
|
+
"""Print welcome banner."""
|
|
162
|
+
banner = """
|
|
163
|
+
[bold cyan]╔═══════════════════════════════════════════════════════════════╗
|
|
164
|
+
║ AIPTX Interactive Shell ║
|
|
165
|
+
║ AI-Powered Penetration Testing ║
|
|
166
|
+
╚═══════════════════════════════════════════════════════════════╝[/bold cyan]
|
|
167
|
+
"""
|
|
168
|
+
console.print(banner)
|
|
169
|
+
console.print("[dim]Type 'help' for commands, 'tools' to list available tools[/dim]")
|
|
170
|
+
console.print(f"[dim]Working directory: {self.working_dir}[/dim]")
|
|
171
|
+
console.print()
|
|
172
|
+
|
|
173
|
+
def print_help(self):
|
|
174
|
+
"""Print help information."""
|
|
175
|
+
help_text = """
|
|
176
|
+
[bold cyan]AIPTX Interactive Shell Commands[/bold cyan]
|
|
177
|
+
|
|
178
|
+
[bold]Built-in Commands:[/bold]
|
|
179
|
+
help Show this help message
|
|
180
|
+
tools List available security tools
|
|
181
|
+
tools <category> List tools in a category
|
|
182
|
+
clear Clear the screen
|
|
183
|
+
history Show command history
|
|
184
|
+
env Show environment variables
|
|
185
|
+
env SET KEY=VAL Set an environment variable
|
|
186
|
+
cd <path> Change working directory
|
|
187
|
+
pwd Print working directory
|
|
188
|
+
log <file> Start logging to file
|
|
189
|
+
exit, quit Exit the shell
|
|
190
|
+
|
|
191
|
+
[bold]Running Tools:[/bold]
|
|
192
|
+
Just type the tool name and arguments:
|
|
193
|
+
nmap -sV target.com
|
|
194
|
+
nuclei -u https://target.com
|
|
195
|
+
sqlmap -u "http://target.com?id=1"
|
|
196
|
+
|
|
197
|
+
[bold]Tips:[/bold]
|
|
198
|
+
• Use TAB for auto-completion
|
|
199
|
+
• Use UP/DOWN arrows for history
|
|
200
|
+
• Ctrl+C interrupts running command
|
|
201
|
+
• Ctrl+D exits the shell
|
|
202
|
+
"""
|
|
203
|
+
console.print(help_text)
|
|
204
|
+
|
|
205
|
+
def list_tools(self, category: Optional[str] = None):
|
|
206
|
+
"""List available tools, optionally filtered by category."""
|
|
207
|
+
try:
|
|
208
|
+
from aipt_v2.local_tool_installer import TOOLS, ToolCategory
|
|
209
|
+
|
|
210
|
+
table = Table(title="Available Security Tools", box=box.ROUNDED)
|
|
211
|
+
table.add_column("Tool", style="cyan")
|
|
212
|
+
table.add_column("Category", style="dim")
|
|
213
|
+
table.add_column("Installed", justify="center")
|
|
214
|
+
table.add_column("Description")
|
|
215
|
+
|
|
216
|
+
for name, tool in sorted(TOOLS.items()):
|
|
217
|
+
if category and tool.category.value != category:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
is_installed = shutil.which(name) is not None
|
|
221
|
+
status = "[green]✓[/green]" if is_installed else "[dim]○[/dim]"
|
|
222
|
+
|
|
223
|
+
desc = tool.description[:45] + "..." if len(tool.description) > 45 else tool.description
|
|
224
|
+
|
|
225
|
+
table.add_row(name, tool.category.value, status, desc)
|
|
226
|
+
|
|
227
|
+
console.print()
|
|
228
|
+
console.print(table)
|
|
229
|
+
console.print()
|
|
230
|
+
|
|
231
|
+
if not category:
|
|
232
|
+
console.print("[dim]Filter by category: tools <category>[/dim]")
|
|
233
|
+
categories = sorted(set(t.category.value for t in TOOLS.values()))
|
|
234
|
+
console.print(f"[dim]Categories: {', '.join(categories)}[/dim]")
|
|
235
|
+
|
|
236
|
+
except ImportError:
|
|
237
|
+
console.print("[yellow]Tool catalog not available[/yellow]")
|
|
238
|
+
|
|
239
|
+
def show_history(self, count: int = 20):
|
|
240
|
+
"""Show command history."""
|
|
241
|
+
history_len = readline.get_current_history_length()
|
|
242
|
+
start = max(1, history_len - count + 1)
|
|
243
|
+
|
|
244
|
+
console.print(f"\n[bold]Last {min(count, history_len)} commands:[/bold]")
|
|
245
|
+
for i in range(start, history_len + 1):
|
|
246
|
+
cmd = readline.get_history_item(i)
|
|
247
|
+
console.print(f" {i:4d} {cmd}")
|
|
248
|
+
console.print()
|
|
249
|
+
|
|
250
|
+
def show_env(self):
|
|
251
|
+
"""Show relevant environment variables."""
|
|
252
|
+
relevant_prefixes = ["AIPT", "PATH", "HOME", "USER", "SHELL", "GOPATH", "GOBIN"]
|
|
253
|
+
table = Table(title="Environment Variables", box=box.ROUNDED)
|
|
254
|
+
table.add_column("Variable", style="cyan")
|
|
255
|
+
table.add_column("Value", style="green", overflow="fold")
|
|
256
|
+
|
|
257
|
+
for key, value in sorted(self.env.items()):
|
|
258
|
+
if any(key.startswith(p) for p in relevant_prefixes):
|
|
259
|
+
# Truncate long values
|
|
260
|
+
display_val = value[:80] + "..." if len(value) > 80 else value
|
|
261
|
+
table.add_row(key, display_val)
|
|
262
|
+
|
|
263
|
+
console.print()
|
|
264
|
+
console.print(table)
|
|
265
|
+
console.print()
|
|
266
|
+
|
|
267
|
+
def set_env(self, key: str, value: str):
|
|
268
|
+
"""Set an environment variable."""
|
|
269
|
+
self.env[key] = value
|
|
270
|
+
os.environ[key] = value
|
|
271
|
+
console.print(f"[green]Set {key}={value}[/green]")
|
|
272
|
+
|
|
273
|
+
def change_dir(self, path: str):
|
|
274
|
+
"""Change working directory."""
|
|
275
|
+
try:
|
|
276
|
+
new_path = Path(path).expanduser().resolve()
|
|
277
|
+
if new_path.is_dir():
|
|
278
|
+
os.chdir(new_path)
|
|
279
|
+
self.working_dir = new_path
|
|
280
|
+
console.print(f"[green]Changed to: {new_path}[/green]")
|
|
281
|
+
else:
|
|
282
|
+
console.print(f"[red]Not a directory: {path}[/red]")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
285
|
+
|
|
286
|
+
async def run_command(self, command: str) -> int:
|
|
287
|
+
"""
|
|
288
|
+
Run a command with real-time output.
|
|
289
|
+
|
|
290
|
+
Uses PTY for proper terminal handling, allowing interactive
|
|
291
|
+
tools to work correctly.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
command: Command to execute
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Exit code of the command
|
|
298
|
+
"""
|
|
299
|
+
self._log(f"$ {command}")
|
|
300
|
+
|
|
301
|
+
# Parse the command
|
|
302
|
+
try:
|
|
303
|
+
parts = shlex.split(command)
|
|
304
|
+
if not parts:
|
|
305
|
+
return 0
|
|
306
|
+
program = parts[0]
|
|
307
|
+
except ValueError as e:
|
|
308
|
+
console.print(f"[red]Parse error: {e}[/red]")
|
|
309
|
+
return 1
|
|
310
|
+
|
|
311
|
+
# Check if tool exists
|
|
312
|
+
if not shutil.which(program):
|
|
313
|
+
console.print(f"[yellow]Command not found: {program}[/yellow]")
|
|
314
|
+
|
|
315
|
+
# Check if it's a known tool that's not installed
|
|
316
|
+
if program in self._tools:
|
|
317
|
+
console.print(f"[dim]This tool is not installed. Run: aiptx tools install -t {program}[/dim]")
|
|
318
|
+
return 127
|
|
319
|
+
|
|
320
|
+
# Run with PTY for full terminal support
|
|
321
|
+
console.print()
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
# Create pseudo-terminal
|
|
325
|
+
master_fd, slave_fd = pty.openpty()
|
|
326
|
+
|
|
327
|
+
# Start process
|
|
328
|
+
process = subprocess.Popen(
|
|
329
|
+
command,
|
|
330
|
+
shell=True,
|
|
331
|
+
stdin=slave_fd,
|
|
332
|
+
stdout=slave_fd,
|
|
333
|
+
stderr=slave_fd,
|
|
334
|
+
env=self.env,
|
|
335
|
+
cwd=str(self.working_dir),
|
|
336
|
+
preexec_fn=os.setsid,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
os.close(slave_fd)
|
|
340
|
+
|
|
341
|
+
# Read output in real-time
|
|
342
|
+
output_lines = []
|
|
343
|
+
try:
|
|
344
|
+
while True:
|
|
345
|
+
ready, _, _ = select.select([master_fd], [], [], 0.1)
|
|
346
|
+
if ready:
|
|
347
|
+
try:
|
|
348
|
+
data = os.read(master_fd, 1024)
|
|
349
|
+
if not data:
|
|
350
|
+
break
|
|
351
|
+
text = data.decode("utf-8", errors="replace")
|
|
352
|
+
sys.stdout.write(text)
|
|
353
|
+
sys.stdout.flush()
|
|
354
|
+
output_lines.append(text)
|
|
355
|
+
self._log(text.rstrip())
|
|
356
|
+
except OSError:
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
# Check if process finished
|
|
360
|
+
if process.poll() is not None:
|
|
361
|
+
# Read any remaining output
|
|
362
|
+
try:
|
|
363
|
+
while True:
|
|
364
|
+
ready, _, _ = select.select([master_fd], [], [], 0.1)
|
|
365
|
+
if not ready:
|
|
366
|
+
break
|
|
367
|
+
data = os.read(master_fd, 1024)
|
|
368
|
+
if not data:
|
|
369
|
+
break
|
|
370
|
+
text = data.decode("utf-8", errors="replace")
|
|
371
|
+
sys.stdout.write(text)
|
|
372
|
+
sys.stdout.flush()
|
|
373
|
+
output_lines.append(text)
|
|
374
|
+
except OSError:
|
|
375
|
+
pass
|
|
376
|
+
break
|
|
377
|
+
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
# Send SIGINT to process group
|
|
380
|
+
os.killpg(os.getpgid(process.pid), signal.SIGINT)
|
|
381
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
382
|
+
|
|
383
|
+
finally:
|
|
384
|
+
os.close(master_fd)
|
|
385
|
+
process.wait()
|
|
386
|
+
|
|
387
|
+
return process.returncode or 0
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
console.print(f"[red]Error running command: {e}[/red]")
|
|
391
|
+
return 1
|
|
392
|
+
|
|
393
|
+
def process_builtin(self, command: str) -> Tuple[bool, int]:
|
|
394
|
+
"""
|
|
395
|
+
Process built-in commands.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (is_builtin, exit_code)
|
|
399
|
+
"""
|
|
400
|
+
parts = command.strip().split(maxsplit=1)
|
|
401
|
+
if not parts:
|
|
402
|
+
return True, 0
|
|
403
|
+
|
|
404
|
+
cmd = parts[0].lower()
|
|
405
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
406
|
+
|
|
407
|
+
if cmd in ("exit", "quit"):
|
|
408
|
+
self.running = False
|
|
409
|
+
return True, 0
|
|
410
|
+
|
|
411
|
+
elif cmd == "help":
|
|
412
|
+
self.print_help()
|
|
413
|
+
return True, 0
|
|
414
|
+
|
|
415
|
+
elif cmd == "tools":
|
|
416
|
+
self.list_tools(args if args else None)
|
|
417
|
+
return True, 0
|
|
418
|
+
|
|
419
|
+
elif cmd == "clear":
|
|
420
|
+
console.clear()
|
|
421
|
+
return True, 0
|
|
422
|
+
|
|
423
|
+
elif cmd == "history":
|
|
424
|
+
count = int(args) if args.isdigit() else 20
|
|
425
|
+
self.show_history(count)
|
|
426
|
+
return True, 0
|
|
427
|
+
|
|
428
|
+
elif cmd == "env":
|
|
429
|
+
if args.startswith("SET ") or args.startswith("set "):
|
|
430
|
+
# Set environment variable
|
|
431
|
+
var_part = args[4:].strip()
|
|
432
|
+
if "=" in var_part:
|
|
433
|
+
key, value = var_part.split("=", 1)
|
|
434
|
+
self.set_env(key.strip(), value.strip())
|
|
435
|
+
else:
|
|
436
|
+
console.print("[yellow]Usage: env SET KEY=VALUE[/yellow]")
|
|
437
|
+
else:
|
|
438
|
+
self.show_env()
|
|
439
|
+
return True, 0
|
|
440
|
+
|
|
441
|
+
elif cmd == "cd":
|
|
442
|
+
if args:
|
|
443
|
+
self.change_dir(args)
|
|
444
|
+
else:
|
|
445
|
+
self.change_dir(str(Path.home()))
|
|
446
|
+
return True, 0
|
|
447
|
+
|
|
448
|
+
elif cmd == "pwd":
|
|
449
|
+
console.print(str(self.working_dir))
|
|
450
|
+
return True, 0
|
|
451
|
+
|
|
452
|
+
elif cmd == "log":
|
|
453
|
+
if args:
|
|
454
|
+
self.log_file = Path(args).expanduser()
|
|
455
|
+
console.print(f"[green]Logging to: {self.log_file}[/green]")
|
|
456
|
+
else:
|
|
457
|
+
if self.log_file:
|
|
458
|
+
console.print(f"[dim]Currently logging to: {self.log_file}[/dim]")
|
|
459
|
+
else:
|
|
460
|
+
console.print("[dim]Logging disabled. Use: log <filename>[/dim]")
|
|
461
|
+
return True, 0
|
|
462
|
+
|
|
463
|
+
return False, 0
|
|
464
|
+
|
|
465
|
+
async def run(self):
|
|
466
|
+
"""Run the interactive shell."""
|
|
467
|
+
self.running = True
|
|
468
|
+
self.print_banner()
|
|
469
|
+
|
|
470
|
+
while self.running:
|
|
471
|
+
try:
|
|
472
|
+
# Build prompt
|
|
473
|
+
cwd = str(self.working_dir)
|
|
474
|
+
home = str(Path.home())
|
|
475
|
+
if cwd.startswith(home):
|
|
476
|
+
cwd = "~" + cwd[len(home):]
|
|
477
|
+
|
|
478
|
+
prompt = f"\n[bold green]aiptx[/bold green]:[bold blue]{cwd}[/bold blue]$ "
|
|
479
|
+
console.print(prompt, end="")
|
|
480
|
+
|
|
481
|
+
# Read input
|
|
482
|
+
try:
|
|
483
|
+
command = input().strip()
|
|
484
|
+
except EOFError:
|
|
485
|
+
console.print()
|
|
486
|
+
break
|
|
487
|
+
|
|
488
|
+
if not command:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
# Add to history
|
|
492
|
+
self.history.append(command)
|
|
493
|
+
|
|
494
|
+
# Check for built-in commands
|
|
495
|
+
is_builtin, exit_code = self.process_builtin(command)
|
|
496
|
+
if is_builtin:
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
# Run external command
|
|
500
|
+
exit_code = await self.run_command(command)
|
|
501
|
+
|
|
502
|
+
if exit_code != 0:
|
|
503
|
+
console.print(f"[dim]Exit code: {exit_code}[/dim]")
|
|
504
|
+
|
|
505
|
+
except KeyboardInterrupt:
|
|
506
|
+
console.print("\n[dim]Use 'exit' or Ctrl+D to quit[/dim]")
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
# Save history on exit
|
|
510
|
+
self._save_history()
|
|
511
|
+
console.print("\n[dim]Goodbye![/dim]")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
async def start_interactive_shell(
|
|
515
|
+
log_file: Optional[str] = None,
|
|
516
|
+
working_dir: Optional[str] = None,
|
|
517
|
+
) -> int:
|
|
518
|
+
"""
|
|
519
|
+
Start the interactive shell.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
log_file: Optional log file path
|
|
523
|
+
working_dir: Optional working directory
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Exit code
|
|
527
|
+
"""
|
|
528
|
+
shell = InteractiveShell(
|
|
529
|
+
log_file=Path(log_file) if log_file else None,
|
|
530
|
+
working_dir=Path(working_dir) if working_dir else None,
|
|
531
|
+
)
|
|
532
|
+
await shell.run()
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def main():
|
|
537
|
+
"""CLI entry point for interactive shell."""
|
|
538
|
+
import argparse
|
|
539
|
+
|
|
540
|
+
parser = argparse.ArgumentParser(description="AIPTX Interactive Shell")
|
|
541
|
+
parser.add_argument(
|
|
542
|
+
"--log", "-l",
|
|
543
|
+
help="Log session to file"
|
|
544
|
+
)
|
|
545
|
+
parser.add_argument(
|
|
546
|
+
"--dir", "-d",
|
|
547
|
+
help="Working directory"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
args = parser.parse_args()
|
|
551
|
+
|
|
552
|
+
asyncio.run(start_interactive_shell(
|
|
553
|
+
log_file=args.log,
|
|
554
|
+
working_dir=args.dir,
|
|
555
|
+
))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
if __name__ == "__main__":
|
|
559
|
+
main()
|