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,941 @@
1
+ """
2
+ AIPTX Interactive Setup Wizard
3
+ ==============================
4
+
5
+ First-run setup wizard that guides users through configuration.
6
+ Collects API keys and settings interactively with a beautiful TUI.
7
+
8
+ NEW in v2.1:
9
+ - Automatic system detection (OS, package manager, architecture)
10
+ - Local security tool installation
11
+ - Ollama/local LLM support for offline operation
12
+ - Prerequisites verification and installation
13
+
14
+ Usage:
15
+ aiptx setup # Run setup wizard
16
+ aiptx scan example.com # Auto-triggers if not configured
17
+ """
18
+
19
+ import asyncio
20
+ import os
21
+ import shutil
22
+ import sys
23
+ from pathlib import Path
24
+ from typing import Optional, Tuple, Dict, List
25
+
26
+ from rich.console import Console
27
+ from rich.panel import Panel
28
+ from rich.prompt import Prompt, Confirm
29
+ from rich.table import Table
30
+ from rich.text import Text
31
+ from rich.progress import Progress, SpinnerColumn, TextColumn
32
+ from rich import box
33
+
34
+
35
+ console = Console()
36
+
37
+
38
+ # Lazy imports to avoid circular dependencies
39
+ def _get_system_detector():
40
+ from aipt_v2.system_detector import SystemDetector, SystemInfo
41
+ return SystemDetector, SystemInfo
42
+
43
+
44
+ def _get_tool_installer():
45
+ from aipt_v2.local_tool_installer import LocalToolInstaller, TOOLS, ToolCategory
46
+ return LocalToolInstaller, TOOLS, ToolCategory
47
+
48
+
49
+ # ============================================================================
50
+ # Configuration File Management
51
+ # ============================================================================
52
+
53
+ def get_config_path() -> Path:
54
+ """Get the path to the .env config file."""
55
+ # Check for existing .env in current directory
56
+ local_env = Path(".env")
57
+ if local_env.exists():
58
+ return local_env
59
+
60
+ # Check for global config in home directory
61
+ home_env = Path.home() / ".aiptx" / ".env"
62
+ if home_env.exists():
63
+ return home_env
64
+
65
+ # Default to home directory for new installations
66
+ return home_env
67
+
68
+
69
+ def load_existing_config() -> dict:
70
+ """Load existing configuration from .env file."""
71
+ config = {}
72
+ config_path = get_config_path()
73
+
74
+ if config_path.exists():
75
+ with open(config_path, "r") as f:
76
+ for line in f:
77
+ line = line.strip()
78
+ if line and not line.startswith("#") and "=" in line:
79
+ key, value = line.split("=", 1)
80
+ config[key.strip()] = value.strip().strip('"').strip("'")
81
+
82
+ return config
83
+
84
+
85
+ def save_config(config: dict, path: Optional[Path] = None) -> Path:
86
+ """Save configuration to .env file."""
87
+ if path is None:
88
+ path = Path.home() / ".aiptx" / ".env"
89
+
90
+ # Create directory if needed
91
+ path.parent.mkdir(parents=True, exist_ok=True)
92
+
93
+ # Build config content
94
+ lines = [
95
+ "# AIPTX Configuration",
96
+ "# Generated by 'aiptx setup'",
97
+ "# Edit this file or run 'aiptx setup' again to reconfigure",
98
+ "",
99
+ ]
100
+
101
+ # Group settings
102
+ sections = {
103
+ "LLM": [
104
+ "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "LLM_API_KEY",
105
+ "AIPT_LLM__PROVIDER", "AIPT_LLM__MODEL", "AIPT_LLM__OLLAMA_BASE_URL"
106
+ ],
107
+ "Acunetix": ["AIPT_SCANNERS__ACUNETIX_URL", "AIPT_SCANNERS__ACUNETIX_API_KEY"],
108
+ "Burp Suite": ["AIPT_SCANNERS__BURP_URL", "AIPT_SCANNERS__BURP_API_KEY"],
109
+ "Nessus": ["AIPT_SCANNERS__NESSUS_URL", "AIPT_SCANNERS__NESSUS_ACCESS_KEY",
110
+ "AIPT_SCANNERS__NESSUS_SECRET_KEY"],
111
+ "OWASP ZAP": ["AIPT_SCANNERS__ZAP_URL", "AIPT_SCANNERS__ZAP_API_KEY"],
112
+ "VPS": ["AIPT_VPS__HOST", "AIPT_VPS__USER", "AIPT_VPS__KEY_PATH", "AIPT_VPS__PORT"],
113
+ }
114
+
115
+ for section, keys in sections.items():
116
+ section_values = [(k, config.get(k)) for k in keys if config.get(k)]
117
+ if section_values:
118
+ lines.append(f"\n# {section} Configuration")
119
+ for key, value in section_values:
120
+ lines.append(f'{key}="{value}"')
121
+
122
+ # Write file
123
+ with open(path, "w") as f:
124
+ f.write("\n".join(lines) + "\n")
125
+
126
+ # Secure the file (readable only by owner)
127
+ os.chmod(path, 0o600)
128
+
129
+ return path
130
+
131
+
132
+ def is_configured() -> bool:
133
+ """Check if AIPTX has been configured with at least an LLM API key."""
134
+ # Check environment variables
135
+ for key in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "LLM_API_KEY"]:
136
+ if os.getenv(key):
137
+ return True
138
+
139
+ # Check .env files
140
+ config = load_existing_config()
141
+ for key in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "LLM_API_KEY"]:
142
+ if config.get(key):
143
+ return True
144
+
145
+ return False
146
+
147
+
148
+ # ============================================================================
149
+ # Interactive Setup Wizard
150
+ # ============================================================================
151
+
152
+ def print_welcome():
153
+ """Print welcome banner."""
154
+ banner = """
155
+ [bold cyan]╔═══════════════════════════════════════════════════════════════╗
156
+ ║ ║
157
+ ║ █████╗ ██╗██████╗ ████████╗██╗ ██╗ ║
158
+ ║ ██╔══██╗██║██╔══██╗╚══██╔══╝╚██╗██╔╝ ║
159
+ ║ ███████║██║██████╔╝ ██║ ╚███╔╝ ║
160
+ ║ ██╔══██║██║██╔═══╝ ██║ ██╔██╗ ║
161
+ ║ ██║ ██║██║██║ ██║ ██╔╝ ██╗ ║
162
+ ║ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ║
163
+ ║ ║
164
+ ║ AI-Powered Penetration Testing Framework ║
165
+ ║ ║
166
+ ╚═══════════════════════════════════════════════════════════════╝[/bold cyan]
167
+ """
168
+ console.print(banner)
169
+ console.print("[bold green]Welcome to AIPTX Setup![/bold green]")
170
+ console.print("This wizard will help you configure AIPTX for first use.\n")
171
+
172
+
173
+ async def detect_system() -> Optional[object]:
174
+ """
175
+ Detect and display system information.
176
+
177
+ Returns:
178
+ SystemInfo object or None if detection fails
179
+ """
180
+ console.print(Panel(
181
+ "[bold]System Detection[/bold]\n\n"
182
+ "Detecting your system configuration to optimize installation...",
183
+ title="🔍 Auto-Detection",
184
+ border_style="cyan"
185
+ ))
186
+
187
+ try:
188
+ SystemDetector, SystemInfo = _get_system_detector()
189
+ detector = SystemDetector()
190
+
191
+ with console.status("[bold cyan]Detecting system...[/bold cyan]"):
192
+ system_info = await detector.detect()
193
+
194
+ # Display results
195
+ table = Table(box=box.ROUNDED, show_header=False)
196
+ table.add_column("Property", style="cyan")
197
+ table.add_column("Value", style="green")
198
+
199
+ table.add_row("Operating System", system_info.os_name)
200
+ table.add_row("Version", f"{system_info.os_version}" +
201
+ (f" ({system_info.os_codename})" if system_info.os_codename else ""))
202
+ table.add_row("Architecture", system_info.architecture.value)
203
+ table.add_row("Package Manager", system_info.package_manager.value)
204
+
205
+ if system_info.is_wsl:
206
+ table.add_row("Environment", "WSL")
207
+ elif system_info.is_container:
208
+ table.add_row("Environment", "Container")
209
+
210
+ console.print(table)
211
+
212
+ # Show capabilities summary
213
+ caps = system_info.capabilities
214
+ cap_status = []
215
+ if caps.has_python3:
216
+ cap_status.append("[green]Python3[/green]")
217
+ if caps.has_go:
218
+ cap_status.append("[green]Go[/green]")
219
+ else:
220
+ cap_status.append("[yellow]Go (will install)[/yellow]")
221
+ if caps.has_docker:
222
+ cap_status.append("[green]Docker[/green]")
223
+ if caps.has_git:
224
+ cap_status.append("[green]Git[/green]")
225
+
226
+ console.print(f"\n[bold]Available runtimes:[/bold] {', '.join(cap_status)}")
227
+
228
+ return system_info
229
+
230
+ except Exception as e:
231
+ console.print(f"[yellow]Warning: Could not fully detect system: {e}[/yellow]")
232
+ return None
233
+
234
+
235
+ def check_ollama_installed() -> Tuple[bool, str]:
236
+ """Check if Ollama is installed and get version."""
237
+ ollama_path = shutil.which("ollama")
238
+ if ollama_path:
239
+ try:
240
+ import subprocess
241
+ result = subprocess.run(
242
+ ["ollama", "--version"],
243
+ capture_output=True,
244
+ text=True,
245
+ timeout=5
246
+ )
247
+ version = result.stdout.strip() or result.stderr.strip()
248
+ return True, version
249
+ except Exception:
250
+ return True, "unknown version"
251
+ return False, ""
252
+
253
+
254
+ async def check_ollama_running() -> bool:
255
+ """Check if Ollama server is running."""
256
+ try:
257
+ import asyncio
258
+ proc = await asyncio.create_subprocess_shell(
259
+ "curl -s http://localhost:11434/api/version",
260
+ stdout=asyncio.subprocess.PIPE,
261
+ stderr=asyncio.subprocess.PIPE,
262
+ )
263
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
264
+ return proc.returncode == 0 and b"version" in stdout
265
+ except Exception:
266
+ return False
267
+
268
+
269
+ async def get_ollama_models() -> List[str]:
270
+ """Get list of available Ollama models."""
271
+ try:
272
+ proc = await asyncio.create_subprocess_shell(
273
+ "ollama list",
274
+ stdout=asyncio.subprocess.PIPE,
275
+ stderr=asyncio.subprocess.PIPE,
276
+ )
277
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)
278
+ if proc.returncode == 0:
279
+ lines = stdout.decode().strip().split("\n")[1:] # Skip header
280
+ models = []
281
+ for line in lines:
282
+ if line.strip():
283
+ model_name = line.split()[0]
284
+ models.append(model_name)
285
+ return models
286
+ except Exception:
287
+ pass
288
+ return []
289
+
290
+
291
+ def setup_llm() -> dict:
292
+ """Configure LLM provider and API key."""
293
+ config = {}
294
+
295
+ console.print(Panel(
296
+ "[bold]Step 2: LLM Configuration[/bold]\n\n"
297
+ "AIPTX uses AI to guide penetration testing.\n"
298
+ "Choose a cloud provider or run locally with Ollama.",
299
+ title="🤖 AI Provider",
300
+ border_style="cyan"
301
+ ))
302
+
303
+ # Check Ollama status
304
+ ollama_installed, ollama_version = check_ollama_installed()
305
+
306
+ # Choose provider
307
+ console.print("\n[bold]Select your LLM provider:[/bold]")
308
+ console.print(" [1] Anthropic (Claude) - [green]Recommended for best results[/green]")
309
+ console.print(" [2] OpenAI (GPT-4)")
310
+ console.print(" [3] DeepSeek - [dim]Cost-effective option[/dim]")
311
+
312
+ if ollama_installed:
313
+ console.print(f" [4] Ollama (Local) - [green]✓ Installed ({ollama_version})[/green] - [bold]FREE, runs offline[/bold]")
314
+ else:
315
+ console.print(" [4] Ollama (Local) - [yellow]○ Not installed[/yellow] - [dim]Will install[/dim]")
316
+
317
+ console.print(" [5] Other (custom)")
318
+
319
+ choice = Prompt.ask(
320
+ "\nEnter choice",
321
+ choices=["1", "2", "3", "4", "5"],
322
+ default="1"
323
+ )
324
+
325
+ providers = {
326
+ "1": ("anthropic", "claude-sonnet-4-20250514", "ANTHROPIC_API_KEY"),
327
+ "2": ("openai", "gpt-4o", "OPENAI_API_KEY"),
328
+ "3": ("deepseek", "deepseek-chat", "DEEPSEEK_API_KEY"),
329
+ "4": ("ollama", "llama3.2", None), # No API key needed
330
+ "5": ("custom", "", "LLM_API_KEY"),
331
+ }
332
+
333
+ provider, model, key_name = providers[choice]
334
+
335
+ # Handle Ollama setup
336
+ if choice == "4":
337
+ config.update(_setup_ollama(ollama_installed))
338
+ return config
339
+
340
+ if choice == "5":
341
+ provider = Prompt.ask("Enter provider name")
342
+ model = Prompt.ask("Enter model name")
343
+ key_name = "LLM_API_KEY"
344
+
345
+ config["AIPT_LLM__PROVIDER"] = provider
346
+ config["AIPT_LLM__MODEL"] = model
347
+
348
+ # Get API key
349
+ console.print(f"\n[bold]Enter your {provider.title()} API key:[/bold]")
350
+
351
+ if provider == "anthropic":
352
+ console.print("[dim]Get one at: https://console.anthropic.com/settings/keys[/dim]")
353
+ elif provider == "openai":
354
+ console.print("[dim]Get one at: https://platform.openai.com/api-keys[/dim]")
355
+ elif provider == "deepseek":
356
+ console.print("[dim]Get one at: https://platform.deepseek.com/api_keys[/dim]")
357
+
358
+ api_key = Prompt.ask("API Key", password=True)
359
+
360
+ if api_key and key_name:
361
+ config[key_name] = api_key
362
+
363
+ return config
364
+
365
+
366
+ def _setup_ollama(ollama_installed: bool) -> dict:
367
+ """Configure Ollama for local LLM."""
368
+ config = {}
369
+
370
+ console.print("\n[bold cyan]═══ Ollama Local LLM Setup ═══[/bold cyan]")
371
+
372
+ if not ollama_installed:
373
+ console.print("\n[yellow]Ollama is not installed.[/yellow]")
374
+ console.print("Ollama allows you to run LLMs locally for FREE and offline.")
375
+
376
+ if Confirm.ask("\nWould you like to install Ollama now?", default=True):
377
+ _install_ollama()
378
+ else:
379
+ console.print("[dim]You can install Ollama later from: https://ollama.ai[/dim]")
380
+ console.print("[yellow]Falling back to cloud provider...[/yellow]")
381
+ return setup_llm() # Restart LLM setup
382
+
383
+ # Check if Ollama is running
384
+ loop = asyncio.new_event_loop()
385
+ try:
386
+ is_running = loop.run_until_complete(check_ollama_running())
387
+ finally:
388
+ loop.close()
389
+
390
+ if not is_running:
391
+ console.print("\n[yellow]Ollama server is not running.[/yellow]")
392
+ console.print("Start it with: [bold]ollama serve[/bold]")
393
+
394
+ if Confirm.ask("\nWould you like to start Ollama now?", default=True):
395
+ import subprocess
396
+ subprocess.Popen(
397
+ ["ollama", "serve"],
398
+ stdout=subprocess.DEVNULL,
399
+ stderr=subprocess.DEVNULL,
400
+ )
401
+ console.print("[green]✓ Ollama server started[/green]")
402
+ import time
403
+ time.sleep(2) # Wait for server to start
404
+
405
+ # Get available models
406
+ loop = asyncio.new_event_loop()
407
+ try:
408
+ models = loop.run_until_complete(get_ollama_models())
409
+ finally:
410
+ loop.close()
411
+
412
+ # Recommended models for pentesting
413
+ recommended_models = [
414
+ ("llama3.2", "Meta Llama 3.2 - Fast, good balance"),
415
+ ("qwen2.5:14b", "Qwen 2.5 14B - Excellent for code/reasoning"),
416
+ ("deepseek-r1:8b", "DeepSeek R1 8B - Strong reasoning"),
417
+ ("codellama:13b", "Code Llama 13B - Optimized for code"),
418
+ ]
419
+
420
+ console.print("\n[bold]Select a model:[/bold]")
421
+
422
+ # Show installed models first
423
+ if models:
424
+ console.print("\n[dim]Installed models:[/dim]")
425
+ for i, model in enumerate(models[:5], 1):
426
+ console.print(f" [{i}] {model} [green]✓ Ready[/green]")
427
+
428
+ console.print("\n[dim]Recommended models (will download):[/dim]")
429
+ offset = len(models[:5]) + 1
430
+ for i, (model, desc) in enumerate(recommended_models, offset):
431
+ installed = model.split(":")[0] in [m.split(":")[0] for m in models]
432
+ status = "[green]✓[/green]" if installed else "[dim]↓[/dim]"
433
+ console.print(f" [{i}] {model} - {desc} {status}")
434
+
435
+ # Let user choose
436
+ max_choice = offset + len(recommended_models) - 1
437
+ model_choice = Prompt.ask(
438
+ f"\nEnter choice (1-{max_choice}) or model name",
439
+ default="1"
440
+ )
441
+
442
+ try:
443
+ idx = int(model_choice)
444
+ if idx <= len(models[:5]):
445
+ selected_model = models[idx - 1]
446
+ else:
447
+ selected_model = recommended_models[idx - offset][0]
448
+ except ValueError:
449
+ selected_model = model_choice
450
+
451
+ # Check if model needs to be pulled
452
+ if selected_model not in models:
453
+ console.print(f"\n[cyan]Downloading model: {selected_model}[/cyan]")
454
+ console.print("[dim]This may take a few minutes...[/dim]")
455
+
456
+ import subprocess
457
+ try:
458
+ subprocess.run(
459
+ ["ollama", "pull", selected_model],
460
+ check=True
461
+ )
462
+ console.print(f"[green]✓ Model {selected_model} downloaded[/green]")
463
+ except subprocess.CalledProcessError:
464
+ console.print(f"[red]Failed to download {selected_model}[/red]")
465
+ console.print("[yellow]You can download it later with: ollama pull {selected_model}[/yellow]")
466
+
467
+ config["AIPT_LLM__PROVIDER"] = "ollama"
468
+ config["AIPT_LLM__MODEL"] = selected_model
469
+ config["AIPT_LLM__OLLAMA_BASE_URL"] = "http://localhost:11434"
470
+
471
+ console.print(Panel(
472
+ f"[green]✓ Ollama configured![/green]\n\n"
473
+ f"Model: [bold]{selected_model}[/bold]\n"
474
+ f"Server: http://localhost:11434\n\n"
475
+ f"[dim]Benefits: Free, runs offline, no API limits[/dim]",
476
+ title="🦙 Local LLM Ready",
477
+ border_style="green"
478
+ ))
479
+
480
+ return config
481
+
482
+
483
+ def _install_ollama():
484
+ """Install Ollama on the system."""
485
+ import platform
486
+ import subprocess
487
+
488
+ system = platform.system().lower()
489
+
490
+ console.print("\n[cyan]Installing Ollama...[/cyan]")
491
+
492
+ try:
493
+ if system == "darwin": # macOS
494
+ # Check if Homebrew is available
495
+ if shutil.which("brew"):
496
+ subprocess.run(["brew", "install", "ollama"], check=True)
497
+ else:
498
+ # Use curl installer
499
+ subprocess.run(
500
+ ["curl", "-fsSL", "https://ollama.ai/install.sh"],
501
+ stdout=subprocess.PIPE,
502
+ check=True
503
+ )
504
+ elif system == "linux":
505
+ subprocess.run(
506
+ "curl -fsSL https://ollama.ai/install.sh | sh",
507
+ shell=True,
508
+ check=True
509
+ )
510
+ elif system == "windows":
511
+ console.print("[yellow]Please install Ollama manually from: https://ollama.ai[/yellow]")
512
+ console.print("[dim]After installation, run 'ollama serve' to start the server[/dim]")
513
+ return
514
+
515
+ console.print("[green]✓ Ollama installed successfully[/green]")
516
+
517
+ except subprocess.CalledProcessError as e:
518
+ console.print(f"[red]Failed to install Ollama: {e}[/red]")
519
+ console.print("[dim]Please install manually from: https://ollama.ai[/dim]")
520
+
521
+
522
+ def setup_scanners() -> dict:
523
+ """Configure enterprise scanners (optional)."""
524
+ config = {}
525
+
526
+ console.print(Panel(
527
+ "[bold]Step 2: Enterprise Scanners (Optional)[/bold]\n\n"
528
+ "AIPTX integrates with enterprise DAST scanners for\n"
529
+ "comprehensive vulnerability assessment.",
530
+ title="🔍 Scanners",
531
+ border_style="cyan"
532
+ ))
533
+
534
+ if not Confirm.ask("\nDo you want to configure enterprise scanners?", default=False):
535
+ console.print("[dim]Skipping scanner configuration...[/dim]\n")
536
+ return config
537
+
538
+ # Acunetix
539
+ if Confirm.ask("\n[bold]Configure Acunetix?[/bold]", default=False):
540
+ url = Prompt.ask(" Acunetix URL (e.g., https://your-instance:3443)")
541
+ api_key = Prompt.ask(" Acunetix API Key", password=True)
542
+ if url:
543
+ config["AIPT_SCANNERS__ACUNETIX_URL"] = url
544
+ if api_key:
545
+ config["AIPT_SCANNERS__ACUNETIX_API_KEY"] = api_key
546
+
547
+ # Burp Suite
548
+ if Confirm.ask("\n[bold]Configure Burp Suite Enterprise?[/bold]", default=False):
549
+ url = Prompt.ask(" Burp Suite URL (e.g., https://your-burp:8080)")
550
+ api_key = Prompt.ask(" Burp Suite API Key", password=True)
551
+ if url:
552
+ config["AIPT_SCANNERS__BURP_URL"] = url
553
+ if api_key:
554
+ config["AIPT_SCANNERS__BURP_API_KEY"] = api_key
555
+
556
+ # Nessus
557
+ if Confirm.ask("\n[bold]Configure Nessus?[/bold]", default=False):
558
+ url = Prompt.ask(" Nessus URL (e.g., https://your-nessus:8834)")
559
+ access_key = Prompt.ask(" Nessus Access Key", password=True)
560
+ secret_key = Prompt.ask(" Nessus Secret Key", password=True)
561
+ if url:
562
+ config["AIPT_SCANNERS__NESSUS_URL"] = url
563
+ if access_key:
564
+ config["AIPT_SCANNERS__NESSUS_ACCESS_KEY"] = access_key
565
+ if secret_key:
566
+ config["AIPT_SCANNERS__NESSUS_SECRET_KEY"] = secret_key
567
+
568
+ # OWASP ZAP
569
+ if Confirm.ask("\n[bold]Configure OWASP ZAP?[/bold]", default=False):
570
+ url = Prompt.ask(" ZAP URL (e.g., http://localhost:8080)")
571
+ api_key = Prompt.ask(" ZAP API Key (leave empty if disabled)", password=True)
572
+ if url:
573
+ config["AIPT_SCANNERS__ZAP_URL"] = url
574
+ if api_key:
575
+ config["AIPT_SCANNERS__ZAP_API_KEY"] = api_key
576
+
577
+ return config
578
+
579
+
580
+ def setup_vps() -> dict:
581
+ """Configure VPS for remote execution (optional)."""
582
+ config = {}
583
+
584
+ console.print(Panel(
585
+ "[bold]Step 3: VPS Configuration (Optional)[/bold]\n\n"
586
+ "Run security tools on a remote VPS to avoid\n"
587
+ "network restrictions and maintain anonymity.",
588
+ title="🖥️ VPS",
589
+ border_style="cyan"
590
+ ))
591
+
592
+ if not Confirm.ask("\nDo you want to configure a VPS for remote execution?", default=False):
593
+ console.print("[dim]Skipping VPS configuration...[/dim]\n")
594
+ return config
595
+
596
+ host = Prompt.ask(" VPS IP or hostname")
597
+ user = Prompt.ask(" SSH username", default="ubuntu")
598
+ key_path = Prompt.ask(" Path to SSH private key (e.g., ~/.ssh/id_rsa)")
599
+ port = Prompt.ask(" SSH port", default="22")
600
+
601
+ if host:
602
+ config["AIPT_VPS__HOST"] = host
603
+ if user:
604
+ config["AIPT_VPS__USER"] = user
605
+ if key_path:
606
+ # Expand ~ to home directory
607
+ expanded_path = str(Path(key_path).expanduser())
608
+ config["AIPT_VPS__KEY_PATH"] = expanded_path
609
+ if port:
610
+ config["AIPT_VPS__PORT"] = port
611
+
612
+ return config
613
+
614
+
615
+ async def setup_security_tools(system_info=None) -> Dict[str, bool]:
616
+ """
617
+ Install security tools on the local system.
618
+
619
+ Args:
620
+ system_info: Pre-detected system info
621
+
622
+ Returns:
623
+ Dict mapping tool names to installation status
624
+ """
625
+ console.print(Panel(
626
+ "[bold]Step 4: Security Tools Installation[/bold]\n\n"
627
+ "AIPTX uses various security tools for penetration testing.\n"
628
+ "These tools will be installed on your local system.",
629
+ title="🔧 Security Tools",
630
+ border_style="cyan"
631
+ ))
632
+
633
+ try:
634
+ LocalToolInstaller, TOOLS, ToolCategory = _get_tool_installer()
635
+ except ImportError:
636
+ console.print("[yellow]Tool installer not available. Skipping...[/yellow]")
637
+ return {}
638
+
639
+ # Show what will be installed
640
+ core_tools = [name for name, tool in TOOLS.items() if tool.is_core]
641
+
642
+ console.print("\n[bold]Core tools to install:[/bold]")
643
+ for tool_name in core_tools[:8]:
644
+ tool = TOOLS.get(tool_name)
645
+ console.print(f" • {tool_name} - [dim]{tool.description[:50]}...[/dim]")
646
+
647
+ console.print(f"\n[dim]Total: {len(core_tools)} core tools, {len(TOOLS)} available[/dim]")
648
+
649
+ # Choose installation scope
650
+ console.print("\n[bold]Installation options:[/bold]")
651
+ console.print(" [1] Core tools only - [green]Recommended[/green] - Quick install of essential tools")
652
+ console.print(" [2] Full installation - Install all available security tools")
653
+ console.print(" [3] Custom selection - Choose categories to install")
654
+ console.print(" [4] Skip - Don't install any tools now")
655
+
656
+ choice = Prompt.ask("\nEnter choice", choices=["1", "2", "3", "4"], default="1")
657
+
658
+ if choice == "4":
659
+ console.print("[dim]Skipping tool installation. You can install later with: aiptx tools install[/dim]")
660
+ return {}
661
+
662
+ installer = LocalToolInstaller(system_info)
663
+
664
+ # Check if we need sudo
665
+ has_sudo = system_info.capabilities.has_sudo if system_info else True
666
+ if not has_sudo:
667
+ console.print("\n[yellow]Note: Some tools may require sudo/admin privileges.[/yellow]")
668
+ console.print("[dim]You may be prompted for your password.[/dim]")
669
+
670
+ results = {}
671
+
672
+ if choice == "1":
673
+ # Core tools only
674
+ console.print("\n[cyan]Installing core security tools...[/cyan]")
675
+ results = await installer.install_core_tools()
676
+
677
+ elif choice == "2":
678
+ # Full installation
679
+ console.print("\n[cyan]Installing all security tools...[/cyan]")
680
+ console.print("[dim]This may take 10-20 minutes...[/dim]")
681
+ results = await installer.install_all()
682
+
683
+ elif choice == "3":
684
+ # Custom selection
685
+ console.print("\n[bold]Select categories to install:[/bold]")
686
+ console.print(" [1] Recon - Subdomain discovery, port scanning, fingerprinting")
687
+ console.print(" [2] Scan - Vulnerability scanning, fuzzing, content discovery")
688
+ console.print(" [3] Exploit - SQL injection, brute forcing, exploitation")
689
+ console.print(" [4] Network - Fast port scanning, network analysis")
690
+ console.print(" [5] API - API security testing tools")
691
+
692
+ cat_choice = Prompt.ask(
693
+ "\nEnter categories (comma-separated, e.g., 1,2,3)",
694
+ default="1,2"
695
+ )
696
+
697
+ category_map = {
698
+ "1": "recon",
699
+ "2": "scan",
700
+ "3": "exploit",
701
+ "4": "network",
702
+ "5": "api",
703
+ }
704
+
705
+ categories = [
706
+ category_map[c.strip()]
707
+ for c in cat_choice.split(",")
708
+ if c.strip() in category_map
709
+ ]
710
+
711
+ if categories:
712
+ console.print(f"\n[cyan]Installing {', '.join(categories)} tools...[/cyan]")
713
+ results = await installer.install_tools(categories=categories)
714
+ else:
715
+ console.print("[yellow]No valid categories selected.[/yellow]")
716
+
717
+ # Show summary
718
+ if results:
719
+ installed = sum(1 for r in results.values() if r.success and not r.already_installed)
720
+ already = sum(1 for r in results.values() if r.already_installed)
721
+ failed = sum(1 for r in results.values() if not r.success)
722
+
723
+ console.print(Panel(
724
+ f"[bold]Installation Complete[/bold]\n\n"
725
+ f"[green]✓ Installed:[/green] {installed}\n"
726
+ f"[dim]○ Already installed:[/dim] {already}\n"
727
+ f"[red]✗ Failed:[/red] {failed}",
728
+ title="📊 Tool Installation Summary",
729
+ border_style="green" if failed == 0 else "yellow"
730
+ ))
731
+
732
+ return {name: result.success for name, result in results.items()} if results else {}
733
+
734
+
735
+ def show_summary(config: dict, tools_installed: Dict[str, bool] = None):
736
+ """Show configuration summary."""
737
+ console.print(Panel(
738
+ "[bold]Configuration Summary[/bold]",
739
+ title="📋 Summary",
740
+ border_style="green"
741
+ ))
742
+
743
+ table = Table(box=box.ROUNDED)
744
+ table.add_column("Setting", style="cyan")
745
+ table.add_column("Value", style="green")
746
+
747
+ # LLM
748
+ provider = config.get("AIPT_LLM__PROVIDER", "Not set")
749
+ model = config.get("AIPT_LLM__MODEL", "Not set")
750
+ has_key = any(k in config for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "LLM_API_KEY"])
751
+ is_ollama = provider == "ollama"
752
+
753
+ table.add_row("LLM Provider", provider)
754
+ table.add_row("LLM Model", model)
755
+ if is_ollama:
756
+ table.add_row("LLM Mode", "✓ Local (Ollama)")
757
+ else:
758
+ table.add_row("LLM API Key", "✓ Configured" if has_key else "✗ Not set")
759
+
760
+ # Scanners
761
+ table.add_row("─" * 20, "─" * 20)
762
+ table.add_row("Acunetix", "✓ Configured" if config.get("AIPT_SCANNERS__ACUNETIX_URL") else "○ Not configured")
763
+ table.add_row("Burp Suite", "✓ Configured" if config.get("AIPT_SCANNERS__BURP_URL") else "○ Not configured")
764
+ table.add_row("Nessus", "✓ Configured" if config.get("AIPT_SCANNERS__NESSUS_URL") else "○ Not configured")
765
+ table.add_row("OWASP ZAP", "✓ Configured" if config.get("AIPT_SCANNERS__ZAP_URL") else "○ Not configured")
766
+
767
+ # VPS
768
+ table.add_row("─" * 20, "─" * 20)
769
+ table.add_row("VPS", "✓ " + config.get("AIPT_VPS__HOST", "") if config.get("AIPT_VPS__HOST") else "○ Not configured")
770
+
771
+ # Tools
772
+ if tools_installed:
773
+ table.add_row("─" * 20, "─" * 20)
774
+ installed_count = sum(1 for v in tools_installed.values() if v)
775
+ table.add_row("Security Tools", f"✓ {installed_count} tools installed")
776
+
777
+ console.print(table)
778
+
779
+
780
+ def run_setup_wizard(force: bool = False) -> bool:
781
+ """
782
+ Run the interactive setup wizard.
783
+
784
+ Args:
785
+ force: Run even if already configured
786
+
787
+ Returns:
788
+ True if setup completed successfully
789
+ """
790
+ # Use asyncio to run the async version
791
+ try:
792
+ loop = asyncio.new_event_loop()
793
+ asyncio.set_event_loop(loop)
794
+ try:
795
+ return loop.run_until_complete(_run_setup_wizard_async(force))
796
+ finally:
797
+ loop.close()
798
+ except KeyboardInterrupt:
799
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
800
+ return False
801
+
802
+
803
+ async def _run_setup_wizard_async(force: bool = False) -> bool:
804
+ """
805
+ Async implementation of the setup wizard.
806
+
807
+ Args:
808
+ force: Run even if already configured
809
+
810
+ Returns:
811
+ True if setup completed successfully
812
+ """
813
+ try:
814
+ # Check if already configured
815
+ if not force and is_configured():
816
+ console.print("[yellow]AIPTX is already configured.[/yellow]")
817
+ if not Confirm.ask("Do you want to reconfigure?", default=False):
818
+ return False
819
+
820
+ print_welcome()
821
+
822
+ # Collect configuration
823
+ config = load_existing_config() # Start with existing config
824
+ tools_installed = {}
825
+
826
+ # Step 1: System Detection (NEW)
827
+ console.print()
828
+ system_info = await detect_system()
829
+
830
+ # Step 2: LLM Configuration
831
+ console.print()
832
+ llm_config = setup_llm()
833
+ config.update(llm_config)
834
+
835
+ # Check if we got an API key or using Ollama
836
+ is_ollama = config.get("AIPT_LLM__PROVIDER") == "ollama"
837
+ has_key = any(k in config for k in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "LLM_API_KEY"])
838
+
839
+ if not has_key and not is_ollama:
840
+ console.print("\n[bold red]Error:[/bold red] An LLM API key is required to use AIPTX.")
841
+ console.print("Please run [bold]aiptx setup[/bold] again with a valid API key.")
842
+ return False
843
+
844
+ # Step 3: Scanners (optional)
845
+ console.print()
846
+ scanner_config = setup_scanners()
847
+ config.update(scanner_config)
848
+
849
+ # Step 4: Security Tools Installation (NEW)
850
+ console.print()
851
+ if Confirm.ask("\nWould you like to install security tools now?", default=True):
852
+ tools_installed = await setup_security_tools(system_info)
853
+
854
+ # Step 5: VPS (optional)
855
+ console.print()
856
+ vps_config = setup_vps()
857
+ config.update(vps_config)
858
+
859
+ # Show summary
860
+ console.print()
861
+ show_summary(config, tools_installed)
862
+
863
+ # Confirm and save
864
+ console.print()
865
+ if Confirm.ask("[bold]Save this configuration?[/bold]", default=True):
866
+ config_path = save_config(config)
867
+
868
+ # Build dynamic completion message
869
+ next_steps = []
870
+ if is_ollama:
871
+ next_steps.append(" [bold]ollama serve[/bold] - Start Ollama (if not running)")
872
+ next_steps.extend([
873
+ " [bold]aiptx scan example.com[/bold] - Run a security scan",
874
+ " [bold]aiptx status[/bold] - Check configuration",
875
+ " [bold]aiptx tools install[/bold] - Install more security tools",
876
+ " [bold]aiptx setup[/bold] - Reconfigure AIPTX",
877
+ ])
878
+
879
+ console.print(Panel(
880
+ f"[bold green]✓ Configuration saved![/bold green]\n\n"
881
+ f"Config file: [cyan]{config_path}[/cyan]\n\n"
882
+ f"[bold]Next steps:[/bold]\n" +
883
+ "\n".join(next_steps),
884
+ title="🎉 Setup Complete",
885
+ border_style="green"
886
+ ))
887
+
888
+ # Load the config into environment for immediate use
889
+ for key, value in config.items():
890
+ os.environ[key] = value
891
+
892
+ return True
893
+ else:
894
+ console.print("[yellow]Setup cancelled. No changes saved.[/yellow]")
895
+ return False
896
+
897
+ except KeyboardInterrupt:
898
+ # Handle Ctrl+C gracefully
899
+ console.print("\n[yellow]Setup cancelled.[/yellow]")
900
+ return False
901
+
902
+
903
+ def prompt_first_run_setup() -> bool:
904
+ """
905
+ Prompt for setup on first run when configuration is missing.
906
+
907
+ Returns:
908
+ True if setup completed and user can proceed
909
+ """
910
+ try:
911
+ console.print(Panel(
912
+ "[bold yellow]⚠ AIPTX is not configured![/bold yellow]\n\n"
913
+ "This appears to be your first time running AIPTX.\n"
914
+ "You need to configure at least an LLM API key to proceed.",
915
+ title="First Run Setup Required",
916
+ border_style="yellow"
917
+ ))
918
+
919
+ if Confirm.ask("\nWould you like to run the setup wizard now?", default=True):
920
+ return run_setup_wizard(force=True)
921
+ else:
922
+ console.print("\n[dim]You can run setup later with: [bold]aiptx setup[/bold][/dim]")
923
+ console.print("[dim]Or set environment variables manually:[/dim]")
924
+ console.print("[dim] export ANTHROPIC_API_KEY=your-key-here[/dim]\n")
925
+ return False
926
+ except KeyboardInterrupt:
927
+ console.print("\n[yellow]Cancelled.[/yellow]")
928
+ return False
929
+
930
+
931
+ # ============================================================================
932
+ # CLI Entry Points
933
+ # ============================================================================
934
+
935
+ def main():
936
+ """Standalone setup wizard entry point."""
937
+ run_setup_wizard(force=True)
938
+
939
+
940
+ if __name__ == "__main__":
941
+ main()