strix-agent 0.1.19__py3-none-any.whl → 0.3.1__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.

Potentially problematic release.


This version of strix-agent might be problematic. Click here for more details.

Files changed (41) hide show
  1. strix/agents/StrixAgent/strix_agent.py +49 -40
  2. strix/agents/StrixAgent/system_prompt.jinja +15 -0
  3. strix/agents/base_agent.py +71 -11
  4. strix/agents/state.py +5 -1
  5. strix/interface/cli.py +171 -0
  6. strix/interface/main.py +482 -0
  7. strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
  8. strix/{cli/app.py → interface/tui.py} +15 -16
  9. strix/interface/utils.py +435 -0
  10. strix/runtime/docker_runtime.py +28 -7
  11. strix/runtime/runtime.py +4 -1
  12. strix/telemetry/__init__.py +4 -0
  13. strix/{cli → telemetry}/tracer.py +21 -9
  14. strix/tools/agents_graph/agents_graph_actions.py +13 -9
  15. strix/tools/executor.py +1 -1
  16. strix/tools/finish/finish_actions.py +1 -1
  17. strix/tools/reporting/reporting_actions.py +1 -1
  18. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/METADATA +45 -4
  19. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/RECORD +39 -36
  20. strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
  21. strix/cli/main.py +0 -703
  22. strix_agent-0.1.19.dist-info/entry_points.txt +0 -3
  23. /strix/{cli → interface}/__init__.py +0 -0
  24. /strix/{cli/assets/cli.tcss → interface/assets/tui_styles.tcss} +0 -0
  25. /strix/{cli → interface}/tool_components/__init__.py +0 -0
  26. /strix/{cli → interface}/tool_components/agents_graph_renderer.py +0 -0
  27. /strix/{cli → interface}/tool_components/base_renderer.py +0 -0
  28. /strix/{cli → interface}/tool_components/browser_renderer.py +0 -0
  29. /strix/{cli → interface}/tool_components/file_edit_renderer.py +0 -0
  30. /strix/{cli → interface}/tool_components/finish_renderer.py +0 -0
  31. /strix/{cli → interface}/tool_components/notes_renderer.py +0 -0
  32. /strix/{cli → interface}/tool_components/proxy_renderer.py +0 -0
  33. /strix/{cli → interface}/tool_components/python_renderer.py +0 -0
  34. /strix/{cli → interface}/tool_components/registry.py +0 -0
  35. /strix/{cli → interface}/tool_components/reporting_renderer.py +0 -0
  36. /strix/{cli → interface}/tool_components/terminal_renderer.py +0 -0
  37. /strix/{cli → interface}/tool_components/thinking_renderer.py +0 -0
  38. /strix/{cli → interface}/tool_components/user_message_renderer.py +0 -0
  39. /strix/{cli → interface}/tool_components/web_search_renderer.py +0 -0
  40. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
  41. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Strix Agent Interface
4
+ """
5
+
6
+ import argparse
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import shutil
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import litellm
15
+ from docker.errors import DockerException
16
+ from rich.console import Console
17
+ from rich.panel import Panel
18
+ from rich.text import Text
19
+
20
+ from strix.interface.cli import run_cli
21
+ from strix.interface.tui import run_tui
22
+ from strix.interface.utils import (
23
+ assign_workspace_subdirs,
24
+ build_llm_stats_text,
25
+ build_stats_text,
26
+ check_docker_connection,
27
+ clone_repository,
28
+ collect_local_sources,
29
+ generate_run_name,
30
+ image_exists,
31
+ infer_target_type,
32
+ process_pull_line,
33
+ validate_llm_response,
34
+ )
35
+ from strix.runtime.docker_runtime import STRIX_IMAGE
36
+ from strix.telemetry.tracer import get_global_tracer
37
+
38
+
39
+ logging.getLogger().setLevel(logging.ERROR)
40
+
41
+
42
+ def validate_environment() -> None: # noqa: PLR0912, PLR0915
43
+ console = Console()
44
+ missing_required_vars = []
45
+ missing_optional_vars = []
46
+
47
+ if not os.getenv("STRIX_LLM"):
48
+ missing_required_vars.append("STRIX_LLM")
49
+
50
+ has_base_url = any(
51
+ [
52
+ os.getenv("LLM_API_BASE"),
53
+ os.getenv("OPENAI_API_BASE"),
54
+ os.getenv("LITELLM_BASE_URL"),
55
+ os.getenv("OLLAMA_API_BASE"),
56
+ ]
57
+ )
58
+
59
+ if not os.getenv("LLM_API_KEY"):
60
+ if not has_base_url:
61
+ missing_required_vars.append("LLM_API_KEY")
62
+ else:
63
+ missing_optional_vars.append("LLM_API_KEY")
64
+
65
+ if not has_base_url:
66
+ missing_optional_vars.append("LLM_API_BASE")
67
+
68
+ if not os.getenv("PERPLEXITY_API_KEY"):
69
+ missing_optional_vars.append("PERPLEXITY_API_KEY")
70
+
71
+ if missing_required_vars:
72
+ error_text = Text()
73
+ error_text.append("❌ ", style="bold red")
74
+ error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
75
+ error_text.append("\n\n", style="white")
76
+
77
+ for var in missing_required_vars:
78
+ error_text.append(f"• {var}", style="bold yellow")
79
+ error_text.append(" is not set\n", style="white")
80
+
81
+ if missing_optional_vars:
82
+ error_text.append("\nOptional environment variables:\n", style="dim white")
83
+ for var in missing_optional_vars:
84
+ error_text.append(f"• {var}", style="dim yellow")
85
+ error_text.append(" is not set\n", style="dim white")
86
+
87
+ error_text.append("\nRequired environment variables:\n", style="white")
88
+ for var in missing_required_vars:
89
+ if var == "STRIX_LLM":
90
+ error_text.append("• ", style="white")
91
+ error_text.append("STRIX_LLM", style="bold cyan")
92
+ error_text.append(
93
+ " - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
94
+ style="white",
95
+ )
96
+ elif var == "LLM_API_KEY":
97
+ error_text.append("• ", style="white")
98
+ error_text.append("LLM_API_KEY", style="bold cyan")
99
+ error_text.append(
100
+ " - API key for the LLM provider (required for cloud providers)\n",
101
+ style="white",
102
+ )
103
+
104
+ if missing_optional_vars:
105
+ error_text.append("\nOptional environment variables:\n", style="white")
106
+ for var in missing_optional_vars:
107
+ if var == "LLM_API_KEY":
108
+ error_text.append("• ", style="white")
109
+ error_text.append("LLM_API_KEY", style="bold cyan")
110
+ error_text.append(" - API key for the LLM provider\n", style="white")
111
+ elif var == "LLM_API_BASE":
112
+ error_text.append("• ", style="white")
113
+ error_text.append("LLM_API_BASE", style="bold cyan")
114
+ error_text.append(
115
+ " - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
116
+ style="white",
117
+ )
118
+ elif var == "PERPLEXITY_API_KEY":
119
+ error_text.append("• ", style="white")
120
+ error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
121
+ error_text.append(
122
+ " - API key for Perplexity AI web search (enables real-time research)\n",
123
+ style="white",
124
+ )
125
+
126
+ error_text.append("\nExample setup:\n", style="white")
127
+ error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
128
+
129
+ if "LLM_API_KEY" in missing_required_vars:
130
+ error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
131
+
132
+ if missing_optional_vars:
133
+ for var in missing_optional_vars:
134
+ if var == "LLM_API_KEY":
135
+ error_text.append(
136
+ "export LLM_API_KEY='your-api-key-here' # optional with local models\n",
137
+ style="dim white",
138
+ )
139
+ elif var == "LLM_API_BASE":
140
+ error_text.append(
141
+ "export LLM_API_BASE='http://localhost:11434' "
142
+ "# needed for local models only\n",
143
+ style="dim white",
144
+ )
145
+ elif var == "PERPLEXITY_API_KEY":
146
+ error_text.append(
147
+ "export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
148
+ )
149
+
150
+ panel = Panel(
151
+ error_text,
152
+ title="[bold red]🛡️ STRIX CONFIGURATION ERROR",
153
+ title_align="center",
154
+ border_style="red",
155
+ padding=(1, 2),
156
+ )
157
+
158
+ console.print("\n")
159
+ console.print(panel)
160
+ console.print()
161
+ sys.exit(1)
162
+
163
+
164
+ def check_docker_installed() -> None:
165
+ if shutil.which("docker") is None:
166
+ console = Console()
167
+ error_text = Text()
168
+ error_text.append("❌ ", style="bold red")
169
+ error_text.append("DOCKER NOT INSTALLED", style="bold red")
170
+ error_text.append("\n\n", style="white")
171
+ error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white")
172
+ error_text.append(
173
+ "Please install Docker and ensure the 'docker' command is available.\n\n", style="white"
174
+ )
175
+
176
+ panel = Panel(
177
+ error_text,
178
+ title="[bold red]🛡️ STRIX STARTUP ERROR",
179
+ title_align="center",
180
+ border_style="red",
181
+ padding=(1, 2),
182
+ )
183
+ console.print("\n", panel, "\n")
184
+ sys.exit(1)
185
+
186
+
187
+ async def warm_up_llm() -> None:
188
+ console = Console()
189
+
190
+ try:
191
+ model_name = os.getenv("STRIX_LLM", "openai/gpt-5")
192
+ api_key = os.getenv("LLM_API_KEY")
193
+
194
+ if api_key:
195
+ litellm.api_key = api_key
196
+
197
+ api_base = (
198
+ os.getenv("LLM_API_BASE")
199
+ or os.getenv("OPENAI_API_BASE")
200
+ or os.getenv("LITELLM_BASE_URL")
201
+ or os.getenv("OLLAMA_API_BASE")
202
+ )
203
+ if api_base:
204
+ litellm.api_base = api_base
205
+
206
+ test_messages = [
207
+ {"role": "system", "content": "You are a helpful assistant."},
208
+ {"role": "user", "content": "Reply with just 'OK'."},
209
+ ]
210
+
211
+ response = litellm.completion(
212
+ model=model_name,
213
+ messages=test_messages,
214
+ )
215
+
216
+ validate_llm_response(response)
217
+
218
+ except Exception as e: # noqa: BLE001
219
+ error_text = Text()
220
+ error_text.append("❌ ", style="bold red")
221
+ error_text.append("LLM CONNECTION FAILED", style="bold red")
222
+ error_text.append("\n\n", style="white")
223
+ error_text.append("Could not establish connection to the language model.\n", style="white")
224
+ error_text.append("Please check your configuration and try again.\n", style="white")
225
+ error_text.append(f"\nError: {e}", style="dim white")
226
+
227
+ panel = Panel(
228
+ error_text,
229
+ title="[bold red]🛡️ STRIX STARTUP ERROR",
230
+ title_align="center",
231
+ border_style="red",
232
+ padding=(1, 2),
233
+ )
234
+
235
+ console.print("\n")
236
+ console.print(panel)
237
+ console.print()
238
+ sys.exit(1)
239
+
240
+
241
+ def parse_arguments() -> argparse.Namespace:
242
+ parser = argparse.ArgumentParser(
243
+ description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
244
+ formatter_class=argparse.RawDescriptionHelpFormatter,
245
+ epilog="""
246
+ Examples:
247
+ # Web application penetration test
248
+ strix --target https://example.com
249
+
250
+ # GitHub repository analysis
251
+ strix --target https://github.com/user/repo
252
+ strix --target git@github.com:user/repo.git
253
+
254
+ # Local code analysis
255
+ strix --target ./my-project
256
+
257
+ # Domain penetration test
258
+ strix --target example.com
259
+
260
+ # Multiple targets (e.g., white-box testing with source and deployed app)
261
+ strix --target https://github.com/user/repo --target https://example.com
262
+ strix --target ./my-project --target https://staging.example.com --target https://prod.example.com
263
+
264
+ # Custom instructions
265
+ strix --target example.com --instruction "Focus on authentication vulnerabilities"
266
+ """,
267
+ )
268
+
269
+ parser.add_argument(
270
+ "-t",
271
+ "--target",
272
+ type=str,
273
+ required=True,
274
+ action="append",
275
+ help="Target to test (URL, repository, local directory path, or domain name). "
276
+ "Can be specified multiple times for multi-target scans.",
277
+ )
278
+ parser.add_argument(
279
+ "--instruction",
280
+ type=str,
281
+ help="Custom instructions for the penetration test. This can be "
282
+ "specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), "
283
+ "testing approaches (e.g., 'Perform thorough authentication testing'), "
284
+ "test credentials (e.g., 'Use the following credentials to access the app: "
285
+ "admin:password123'), "
286
+ "or areas of interest (e.g., 'Check login API endpoint for security issues')",
287
+ )
288
+
289
+ parser.add_argument(
290
+ "--run-name",
291
+ type=str,
292
+ help="Custom name for this penetration test run",
293
+ )
294
+
295
+ parser.add_argument(
296
+ "-n",
297
+ "--non-interactive",
298
+ action="store_true",
299
+ help=(
300
+ "Run in non-interactive mode (no TUI, exits on completion). "
301
+ "Default is interactive mode with TUI."
302
+ ),
303
+ )
304
+
305
+ args = parser.parse_args()
306
+
307
+ args.targets_info = []
308
+ for target in args.target:
309
+ try:
310
+ target_type, target_dict = infer_target_type(target)
311
+
312
+ if target_type == "local_code":
313
+ display_target = target_dict.get("target_path", target)
314
+ else:
315
+ display_target = target
316
+
317
+ args.targets_info.append(
318
+ {"type": target_type, "details": target_dict, "original": display_target}
319
+ )
320
+ except ValueError:
321
+ parser.error(f"Invalid target '{target}'")
322
+
323
+ assign_workspace_subdirs(args.targets_info)
324
+
325
+ return args
326
+
327
+
328
+ def display_completion_message(args: argparse.Namespace, results_path: Path) -> None:
329
+ console = Console()
330
+ tracer = get_global_tracer()
331
+
332
+ scan_completed = False
333
+ if tracer and tracer.scan_results:
334
+ scan_completed = tracer.scan_results.get("scan_completed", False)
335
+
336
+ has_vulnerabilities = tracer and len(tracer.vulnerability_reports) > 0
337
+
338
+ completion_text = Text()
339
+ if scan_completed:
340
+ completion_text.append("🦉 ", style="bold white")
341
+ completion_text.append("AGENT FINISHED", style="bold green")
342
+ completion_text.append(" • ", style="dim white")
343
+ completion_text.append("Penetration test completed", style="white")
344
+ else:
345
+ completion_text.append("🦉 ", style="bold white")
346
+ completion_text.append("SESSION ENDED", style="bold yellow")
347
+ completion_text.append(" • ", style="dim white")
348
+ completion_text.append("Penetration test interrupted by user", style="white")
349
+
350
+ stats_text = build_stats_text(tracer)
351
+ llm_stats_text = build_llm_stats_text(tracer)
352
+
353
+ target_text = Text()
354
+ if len(args.targets_info) == 1:
355
+ target_text.append("🎯 Target: ", style="bold cyan")
356
+ target_text.append(args.targets_info[0]["original"], style="bold white")
357
+ else:
358
+ target_text.append("🎯 Targets: ", style="bold cyan")
359
+ target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
360
+ for i, target_info in enumerate(args.targets_info):
361
+ target_text.append(" • ", style="dim white")
362
+ target_text.append(target_info["original"], style="white")
363
+ if i < len(args.targets_info) - 1:
364
+ target_text.append("\n")
365
+
366
+ panel_parts = [completion_text, "\n\n", target_text]
367
+
368
+ if stats_text.plain:
369
+ panel_parts.extend(["\n", stats_text])
370
+
371
+ if llm_stats_text.plain:
372
+ panel_parts.extend(["\n", llm_stats_text])
373
+
374
+ if scan_completed or has_vulnerabilities:
375
+ results_text = Text()
376
+ results_text.append("📊 Results Saved To: ", style="bold cyan")
377
+ results_text.append(str(results_path), style="bold yellow")
378
+ panel_parts.extend(["\n\n", results_text])
379
+
380
+ panel_content = Text.assemble(*panel_parts)
381
+
382
+ border_style = "green" if scan_completed else "yellow"
383
+
384
+ panel = Panel(
385
+ panel_content,
386
+ title="[bold green]🛡️ STRIX CYBERSECURITY AGENT",
387
+ title_align="center",
388
+ border_style=border_style,
389
+ padding=(1, 2),
390
+ )
391
+
392
+ console.print("\n")
393
+ console.print(panel)
394
+ console.print()
395
+
396
+
397
+ def pull_docker_image() -> None:
398
+ console = Console()
399
+ client = check_docker_connection()
400
+
401
+ if image_exists(client, STRIX_IMAGE):
402
+ return
403
+
404
+ console.print()
405
+ console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {STRIX_IMAGE}")
406
+ console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
407
+ console.print()
408
+
409
+ with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
410
+ try:
411
+ layers_info: dict[str, str] = {}
412
+ last_update = ""
413
+
414
+ for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True):
415
+ last_update = process_pull_line(line, layers_info, status, last_update)
416
+
417
+ except DockerException as e:
418
+ console.print()
419
+ error_text = Text()
420
+ error_text.append("❌ ", style="bold red")
421
+ error_text.append("FAILED TO PULL IMAGE", style="bold red")
422
+ error_text.append("\n\n", style="white")
423
+ error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white")
424
+ error_text.append(str(e), style="dim red")
425
+
426
+ panel = Panel(
427
+ error_text,
428
+ title="[bold red]🛡️ DOCKER PULL ERROR",
429
+ title_align="center",
430
+ border_style="red",
431
+ padding=(1, 2),
432
+ )
433
+ console.print(panel, "\n")
434
+ sys.exit(1)
435
+
436
+ success_text = Text()
437
+ success_text.append("✅ ", style="bold green")
438
+ success_text.append("Successfully pulled Docker image", style="green")
439
+ console.print(success_text)
440
+ console.print()
441
+
442
+
443
+ def main() -> None:
444
+ if sys.platform == "win32":
445
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
446
+
447
+ args = parse_arguments()
448
+
449
+ check_docker_installed()
450
+ pull_docker_image()
451
+
452
+ validate_environment()
453
+ asyncio.run(warm_up_llm())
454
+
455
+ if not args.run_name:
456
+ args.run_name = generate_run_name()
457
+
458
+ for target_info in args.targets_info:
459
+ if target_info["type"] == "repository":
460
+ repo_url = target_info["details"]["target_repo"]
461
+ dest_name = target_info["details"].get("workspace_subdir")
462
+ cloned_path = clone_repository(repo_url, args.run_name, dest_name)
463
+ target_info["details"]["cloned_repo_path"] = cloned_path
464
+
465
+ args.local_sources = collect_local_sources(args.targets_info)
466
+
467
+ if args.non_interactive:
468
+ asyncio.run(run_cli(args))
469
+ else:
470
+ asyncio.run(run_tui(args))
471
+
472
+ results_path = Path("agent_runs") / args.run_name
473
+ display_completion_message(args, results_path)
474
+
475
+ if args.non_interactive:
476
+ tracer = get_global_tracer()
477
+ if tracer and tracer.vulnerability_reports:
478
+ sys.exit(2)
479
+
480
+
481
+ if __name__ == "__main__":
482
+ main()
@@ -16,23 +16,28 @@ class ScanStartInfoRenderer(BaseToolRenderer):
16
16
  args = tool_data.get("args", {})
17
17
  status = tool_data.get("status", "unknown")
18
18
 
19
- target = args.get("target", {})
20
-
21
- target_display = cls._build_target_display(target)
22
-
23
- content = f"🚀 Starting scan on {target_display}"
19
+ targets = args.get("targets", [])
20
+
21
+ if len(targets) == 1:
22
+ target_display = cls._build_single_target_display(targets[0])
23
+ content = f"🚀 Starting penetration test on {target_display}"
24
+ elif len(targets) > 1:
25
+ content = f"🚀 Starting penetration test on {len(targets)} targets"
26
+ for target_info in targets:
27
+ target_display = cls._build_single_target_display(target_info)
28
+ content += f"\n • {target_display}"
29
+ else:
30
+ content = "🚀 Starting penetration test"
24
31
 
25
32
  css_classes = cls.get_css_classes(status)
26
33
  return Static(content, classes=css_classes)
27
34
 
28
35
  @classmethod
29
- def _build_target_display(cls, target: dict[str, Any]) -> str:
30
- if target_url := target.get("target_url"):
31
- return cls.escape_markup(str(target_url))
32
- if target_repo := target.get("target_repo"):
33
- return cls.escape_markup(str(target_repo))
34
- if target_path := target.get("target_path"):
35
- return cls.escape_markup(str(target_path))
36
+ def _build_single_target_display(cls, target_info: dict[str, Any]) -> str:
37
+ original = target_info.get("original")
38
+ if original:
39
+ return cls.escape_markup(str(original))
40
+
36
41
  return "unknown target"
37
42
 
38
43
 
@@ -11,6 +11,7 @@ from importlib.metadata import PackageNotFoundError
11
11
  from importlib.metadata import version as pkg_version
12
12
  from typing import TYPE_CHECKING, Any, ClassVar, cast
13
13
 
14
+
14
15
  if TYPE_CHECKING:
15
16
  from textual.timer import Timer
16
17
 
@@ -30,8 +31,8 @@ from textual.widgets import Button, Label, Static, TextArea, Tree
30
31
  from textual.widgets.tree import TreeNode
31
32
 
32
33
  from strix.agents.StrixAgent import StrixAgent
33
- from strix.cli.tracer import Tracer, set_global_tracer
34
34
  from strix.llm.config import LLMConfig
35
+ from strix.telemetry.tracer import Tracer, set_global_tracer
35
36
 
36
37
 
37
38
  def escape_markup(text: str) -> str:
@@ -48,9 +49,9 @@ def get_package_version() -> str:
48
49
  class ChatTextArea(TextArea): # type: ignore[misc]
49
50
  def __init__(self, *args: Any, **kwargs: Any) -> None:
50
51
  super().__init__(*args, **kwargs)
51
- self._app_reference: StrixCLIApp | None = None
52
+ self._app_reference: StrixTUIApp | None = None
52
53
 
53
- def set_app_reference(self, app: "StrixCLIApp") -> None:
54
+ def set_app_reference(self, app: "StrixTUIApp") -> None:
54
55
  self._app_reference = app
55
56
 
56
57
  def _on_key(self, event: events.Key) -> None:
@@ -259,8 +260,8 @@ class QuitScreen(ModalScreen): # type: ignore[misc]
259
260
  self.app.pop_screen()
260
261
 
261
262
 
262
- class StrixCLIApp(App): # type: ignore[misc]
263
- CSS_PATH = "assets/cli.tcss"
263
+ class StrixTUIApp(App): # type: ignore[misc]
264
+ CSS_PATH = "assets/tui_styles.tcss"
264
265
 
265
266
  selected_agent_id: reactive[str | None] = reactive(default=None)
266
267
  show_splash: reactive[bool] = reactive(default=True)
@@ -311,8 +312,7 @@ class StrixCLIApp(App): # type: ignore[misc]
311
312
  def _build_scan_config(self, args: argparse.Namespace) -> dict[str, Any]:
312
313
  return {
313
314
  "scan_id": args.run_name,
314
- "scan_type": args.target_type,
315
- "target": args.target_dict,
315
+ "targets": args.targets_info,
316
316
  "user_instructions": args.instruction or "",
317
317
  "run_name": args.run_name,
318
318
  }
@@ -322,13 +322,11 @@ class StrixCLIApp(App): # type: ignore[misc]
322
322
 
323
323
  config = {
324
324
  "llm_config": llm_config,
325
- "max_iterations": 200,
325
+ "max_iterations": 300,
326
326
  }
327
327
 
328
- if args.target_type == "local_code" and "target_path" in args.target_dict:
329
- config["local_source_path"] = args.target_dict["target_path"]
330
- elif args.target_type == "repository" and "cloned_repo_path" in args.target_dict:
331
- config["local_source_path"] = args.target_dict["cloned_repo_path"]
328
+ if getattr(args, "local_sources", None):
329
+ config["local_sources"] = args.local_sources
332
330
 
333
331
  return config
334
332
 
@@ -961,7 +959,7 @@ class StrixCLIApp(App): # type: ignore[misc]
961
959
  return ""
962
960
 
963
961
  if role == "user":
964
- from strix.cli.tool_components.user_message_renderer import UserMessageRenderer
962
+ from strix.interface.tool_components.user_message_renderer import UserMessageRenderer
965
963
 
966
964
  return UserMessageRenderer.render_simple(content)
967
965
  return content
@@ -991,7 +989,7 @@ class StrixCLIApp(App): # type: ignore[misc]
991
989
 
992
990
  color = tool_colors.get(tool_name, "#737373")
993
991
 
994
- from strix.cli.tool_components.registry import get_tool_renderer
992
+ from strix.interface.tool_components.registry import get_tool_renderer
995
993
 
996
994
  renderer = get_tool_renderer(tool_name)
997
995
 
@@ -1236,6 +1234,7 @@ class StrixCLIApp(App): # type: ignore[misc]
1236
1234
  widget.update(plain_text)
1237
1235
 
1238
1236
 
1239
- async def run_strix_cli(args: argparse.Namespace) -> None:
1240
- app = StrixCLIApp(args)
1237
+ async def run_tui(args: argparse.Namespace) -> None:
1238
+ """Run strix in interactive TUI mode with textual."""
1239
+ app = StrixTUIApp(args)
1241
1240
  await app.run_async()