strix-agent 0.4.0__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 (118) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +89 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +404 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +518 -0
  7. strix/agents/state.py +163 -0
  8. strix/interface/__init__.py +4 -0
  9. strix/interface/assets/tui_styles.tcss +694 -0
  10. strix/interface/cli.py +230 -0
  11. strix/interface/main.py +500 -0
  12. strix/interface/tool_components/__init__.py +39 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +123 -0
  14. strix/interface/tool_components/base_renderer.py +62 -0
  15. strix/interface/tool_components/browser_renderer.py +120 -0
  16. strix/interface/tool_components/file_edit_renderer.py +99 -0
  17. strix/interface/tool_components/finish_renderer.py +31 -0
  18. strix/interface/tool_components/notes_renderer.py +108 -0
  19. strix/interface/tool_components/proxy_renderer.py +255 -0
  20. strix/interface/tool_components/python_renderer.py +34 -0
  21. strix/interface/tool_components/registry.py +72 -0
  22. strix/interface/tool_components/reporting_renderer.py +53 -0
  23. strix/interface/tool_components/scan_info_renderer.py +64 -0
  24. strix/interface/tool_components/terminal_renderer.py +131 -0
  25. strix/interface/tool_components/thinking_renderer.py +29 -0
  26. strix/interface/tool_components/user_message_renderer.py +43 -0
  27. strix/interface/tool_components/web_search_renderer.py +28 -0
  28. strix/interface/tui.py +1274 -0
  29. strix/interface/utils.py +559 -0
  30. strix/llm/__init__.py +15 -0
  31. strix/llm/config.py +20 -0
  32. strix/llm/llm.py +465 -0
  33. strix/llm/memory_compressor.py +212 -0
  34. strix/llm/request_queue.py +87 -0
  35. strix/llm/utils.py +87 -0
  36. strix/prompts/README.md +64 -0
  37. strix/prompts/__init__.py +109 -0
  38. strix/prompts/cloud/.gitkeep +0 -0
  39. strix/prompts/coordination/root_agent.jinja +41 -0
  40. strix/prompts/custom/.gitkeep +0 -0
  41. strix/prompts/frameworks/fastapi.jinja +142 -0
  42. strix/prompts/frameworks/nextjs.jinja +126 -0
  43. strix/prompts/protocols/graphql.jinja +215 -0
  44. strix/prompts/reconnaissance/.gitkeep +0 -0
  45. strix/prompts/technologies/firebase_firestore.jinja +177 -0
  46. strix/prompts/technologies/supabase.jinja +189 -0
  47. strix/prompts/vulnerabilities/authentication_jwt.jinja +147 -0
  48. strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
  49. strix/prompts/vulnerabilities/business_logic.jinja +171 -0
  50. strix/prompts/vulnerabilities/csrf.jinja +174 -0
  51. strix/prompts/vulnerabilities/idor.jinja +195 -0
  52. strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
  53. strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
  54. strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
  55. strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
  56. strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
  57. strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
  58. strix/prompts/vulnerabilities/rce.jinja +154 -0
  59. strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
  60. strix/prompts/vulnerabilities/ssrf.jinja +135 -0
  61. strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
  62. strix/prompts/vulnerabilities/xss.jinja +169 -0
  63. strix/prompts/vulnerabilities/xxe.jinja +184 -0
  64. strix/runtime/__init__.py +19 -0
  65. strix/runtime/docker_runtime.py +399 -0
  66. strix/runtime/runtime.py +29 -0
  67. strix/runtime/tool_server.py +205 -0
  68. strix/telemetry/__init__.py +4 -0
  69. strix/telemetry/tracer.py +337 -0
  70. strix/tools/__init__.py +64 -0
  71. strix/tools/agents_graph/__init__.py +16 -0
  72. strix/tools/agents_graph/agents_graph_actions.py +621 -0
  73. strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
  74. strix/tools/argument_parser.py +121 -0
  75. strix/tools/browser/__init__.py +4 -0
  76. strix/tools/browser/browser_actions.py +236 -0
  77. strix/tools/browser/browser_actions_schema.xml +183 -0
  78. strix/tools/browser/browser_instance.py +533 -0
  79. strix/tools/browser/tab_manager.py +342 -0
  80. strix/tools/executor.py +305 -0
  81. strix/tools/file_edit/__init__.py +4 -0
  82. strix/tools/file_edit/file_edit_actions.py +141 -0
  83. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  84. strix/tools/finish/__init__.py +4 -0
  85. strix/tools/finish/finish_actions.py +174 -0
  86. strix/tools/finish/finish_actions_schema.xml +45 -0
  87. strix/tools/notes/__init__.py +14 -0
  88. strix/tools/notes/notes_actions.py +191 -0
  89. strix/tools/notes/notes_actions_schema.xml +150 -0
  90. strix/tools/proxy/__init__.py +20 -0
  91. strix/tools/proxy/proxy_actions.py +101 -0
  92. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  93. strix/tools/proxy/proxy_manager.py +785 -0
  94. strix/tools/python/__init__.py +4 -0
  95. strix/tools/python/python_actions.py +47 -0
  96. strix/tools/python/python_actions_schema.xml +131 -0
  97. strix/tools/python/python_instance.py +172 -0
  98. strix/tools/python/python_manager.py +131 -0
  99. strix/tools/registry.py +196 -0
  100. strix/tools/reporting/__init__.py +6 -0
  101. strix/tools/reporting/reporting_actions.py +63 -0
  102. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  103. strix/tools/terminal/__init__.py +4 -0
  104. strix/tools/terminal/terminal_actions.py +35 -0
  105. strix/tools/terminal/terminal_actions_schema.xml +146 -0
  106. strix/tools/terminal/terminal_manager.py +151 -0
  107. strix/tools/terminal/terminal_session.py +447 -0
  108. strix/tools/thinking/__init__.py +4 -0
  109. strix/tools/thinking/thinking_actions.py +18 -0
  110. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  111. strix/tools/web_search/__init__.py +4 -0
  112. strix/tools/web_search/web_search_actions.py +80 -0
  113. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  114. strix_agent-0.4.0.dist-info/LICENSE +201 -0
  115. strix_agent-0.4.0.dist-info/METADATA +282 -0
  116. strix_agent-0.4.0.dist-info/RECORD +118 -0
  117. strix_agent-0.4.0.dist-info/WHEEL +4 -0
  118. strix_agent-0.4.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,62 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, ClassVar, cast
3
+
4
+ from rich.markup import escape as rich_escape
5
+ from textual.widgets import Static
6
+
7
+
8
+ class BaseToolRenderer(ABC):
9
+ tool_name: ClassVar[str] = ""
10
+
11
+ css_classes: ClassVar[list[str]] = ["tool-call"]
12
+
13
+ @classmethod
14
+ @abstractmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ pass
17
+
18
+ @classmethod
19
+ def escape_markup(cls, text: str) -> str:
20
+ return cast("str", rich_escape(text))
21
+
22
+ @classmethod
23
+ def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
24
+ if not args:
25
+ return ""
26
+
27
+ args_parts = []
28
+ for k, v in args.items():
29
+ str_v = str(v)
30
+ if len(str_v) > max_length:
31
+ str_v = str_v[: max_length - 3] + "..."
32
+ args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}")
33
+ return "\n".join(args_parts)
34
+
35
+ @classmethod
36
+ def format_result(cls, result: Any, max_length: int = 1000) -> str:
37
+ if result is None:
38
+ return ""
39
+
40
+ str_result = str(result).strip()
41
+ if not str_result:
42
+ return ""
43
+
44
+ if len(str_result) > max_length:
45
+ str_result = str_result[: max_length - 3] + "..."
46
+ return cls.escape_markup(str_result)
47
+
48
+ @classmethod
49
+ def get_status_icon(cls, status: str) -> str:
50
+ status_icons = {
51
+ "running": "[#f59e0b]●[/#f59e0b] In progress...",
52
+ "completed": "[#22c55e]βœ“[/#22c55e] Done",
53
+ "failed": "[#dc2626]βœ—[/#dc2626] Failed",
54
+ "error": "[#dc2626]βœ—[/#dc2626] Error",
55
+ }
56
+ return status_icons.get(status, "[dim]β—‹[/dim] Unknown")
57
+
58
+ @classmethod
59
+ def get_css_classes(cls, status: str) -> str:
60
+ base_classes = cls.css_classes.copy()
61
+ base_classes.append(f"status-{status}")
62
+ return " ".join(base_classes)
@@ -0,0 +1,120 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class BrowserRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "browser_action"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ status = tool_data.get("status", "unknown")
18
+
19
+ action = args.get("action", "unknown")
20
+
21
+ content = cls._build_sleek_content(action, args)
22
+
23
+ css_classes = cls.get_css_classes(status)
24
+ return Static(content, classes=css_classes)
25
+
26
+ @classmethod
27
+ def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str:
28
+ browser_icon = "🌐"
29
+
30
+ url = args.get("url")
31
+ text = args.get("text")
32
+ js_code = args.get("js_code")
33
+ key = args.get("key")
34
+ file_path = args.get("file_path")
35
+
36
+ if action in [
37
+ "launch",
38
+ "goto",
39
+ "new_tab",
40
+ "type",
41
+ "execute_js",
42
+ "click",
43
+ "double_click",
44
+ "hover",
45
+ "press_key",
46
+ "save_pdf",
47
+ ]:
48
+ if action == "launch":
49
+ display_url = cls._format_url(url) if url else None
50
+ message = (
51
+ f"launching {display_url} on browser" if display_url else "launching browser"
52
+ )
53
+ elif action == "goto":
54
+ display_url = cls._format_url(url) if url else None
55
+ message = f"navigating to {display_url}" if display_url else "navigating"
56
+ elif action == "new_tab":
57
+ display_url = cls._format_url(url) if url else None
58
+ message = f"opening tab {display_url}" if display_url else "opening tab"
59
+ elif action == "type":
60
+ display_text = cls._format_text(text) if text else None
61
+ message = f"typing {display_text}" if display_text else "typing"
62
+ elif action == "execute_js":
63
+ display_js = cls._format_js(js_code) if js_code else None
64
+ message = (
65
+ f"executing javascript\n{display_js}" if display_js else "executing javascript"
66
+ )
67
+ elif action == "press_key":
68
+ display_key = cls.escape_markup(key) if key else None
69
+ message = f"pressing key {display_key}" if display_key else "pressing key"
70
+ elif action == "save_pdf":
71
+ display_path = cls.escape_markup(file_path) if file_path else None
72
+ message = f"saving PDF to {display_path}" if display_path else "saving PDF"
73
+ else:
74
+ action_words = {
75
+ "click": "clicking",
76
+ "double_click": "double clicking",
77
+ "hover": "hovering",
78
+ }
79
+ message = cls.escape_markup(action_words[action])
80
+
81
+ return f"{browser_icon} [#06b6d4]{message}[/]"
82
+
83
+ simple_actions = {
84
+ "back": "going back in browser history",
85
+ "forward": "going forward in browser history",
86
+ "scroll_down": "scrolling down",
87
+ "scroll_up": "scrolling up",
88
+ "refresh": "refreshing browser tab",
89
+ "close_tab": "closing browser tab",
90
+ "switch_tab": "switching browser tab",
91
+ "list_tabs": "listing browser tabs",
92
+ "view_source": "viewing page source",
93
+ "get_console_logs": "getting console logs",
94
+ "screenshot": "taking screenshot of browser tab",
95
+ "wait": "waiting...",
96
+ "close": "closing browser",
97
+ }
98
+
99
+ if action in simple_actions:
100
+ return f"{browser_icon} [#06b6d4]{cls.escape_markup(simple_actions[action])}[/]"
101
+
102
+ return f"{browser_icon} [#06b6d4]{cls.escape_markup(action)}[/]"
103
+
104
+ @classmethod
105
+ def _format_url(cls, url: str) -> str:
106
+ if len(url) > 300:
107
+ url = url[:297] + "..."
108
+ return cls.escape_markup(url)
109
+
110
+ @classmethod
111
+ def _format_text(cls, text: str) -> str:
112
+ if len(text) > 200:
113
+ text = text[:197] + "..."
114
+ return cls.escape_markup(text)
115
+
116
+ @classmethod
117
+ def _format_js(cls, js_code: str) -> str:
118
+ if len(js_code) > 200:
119
+ js_code = js_code[:197] + "..."
120
+ return f"[white]{cls.escape_markup(js_code)}[/white]"
@@ -0,0 +1,99 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class StrReplaceEditorRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "str_replace_editor"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ result = tool_data.get("result")
18
+
19
+ command = args.get("command", "")
20
+ path = args.get("path", "")
21
+
22
+ if command == "view":
23
+ header = "πŸ“– [bold #10b981]Reading file[/]"
24
+ elif command == "str_replace":
25
+ header = "✏️ [bold #10b981]Editing file[/]"
26
+ elif command == "create":
27
+ header = "πŸ“ [bold #10b981]Creating file[/]"
28
+ elif command == "insert":
29
+ header = "✏️ [bold #10b981]Inserting text[/]"
30
+ elif command == "undo_edit":
31
+ header = "↩️ [bold #10b981]Undoing edit[/]"
32
+ else:
33
+ header = "πŸ“„ [bold #10b981]File operation[/]"
34
+
35
+ if (result and isinstance(result, dict) and "content" in result) or path:
36
+ path_display = path[-60:] if len(path) > 60 else path
37
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
38
+ else:
39
+ content_text = f"{header} [dim]Processing...[/]"
40
+
41
+ css_classes = cls.get_css_classes("completed")
42
+ return Static(content_text, classes=css_classes)
43
+
44
+
45
+ @register_tool_renderer
46
+ class ListFilesRenderer(BaseToolRenderer):
47
+ tool_name: ClassVar[str] = "list_files"
48
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
49
+
50
+ @classmethod
51
+ def render(cls, tool_data: dict[str, Any]) -> Static:
52
+ args = tool_data.get("args", {})
53
+
54
+ path = args.get("path", "")
55
+
56
+ header = "πŸ“‚ [bold #10b981]Listing files[/]"
57
+
58
+ if path:
59
+ path_display = path[-60:] if len(path) > 60 else path
60
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
61
+ else:
62
+ content_text = f"{header} [dim]Current directory[/]"
63
+
64
+ css_classes = cls.get_css_classes("completed")
65
+ return Static(content_text, classes=css_classes)
66
+
67
+
68
+ @register_tool_renderer
69
+ class SearchFilesRenderer(BaseToolRenderer):
70
+ tool_name: ClassVar[str] = "search_files"
71
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
72
+
73
+ @classmethod
74
+ def render(cls, tool_data: dict[str, Any]) -> Static:
75
+ args = tool_data.get("args", {})
76
+
77
+ path = args.get("path", "")
78
+ regex = args.get("regex", "")
79
+
80
+ header = "πŸ” [bold purple]Searching files[/]"
81
+
82
+ if path and regex:
83
+ path_display = path[-30:] if len(path) > 30 else path
84
+ regex_display = regex[:30] if len(regex) > 30 else regex
85
+ content_text = (
86
+ f"{header} [dim]{cls.escape_markup(path_display)} for "
87
+ f"'{cls.escape_markup(regex_display)}'[/]"
88
+ )
89
+ elif path:
90
+ path_display = path[-60:] if len(path) > 60 else path
91
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
92
+ elif regex:
93
+ regex_display = regex[:60] if len(regex) > 60 else regex
94
+ content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]"
95
+ else:
96
+ content_text = f"{header} [dim]Searching...[/]"
97
+
98
+ css_classes = cls.get_css_classes("completed")
99
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,31 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class FinishScanRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "finish_scan"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ content = args.get("content", "")
19
+ success = args.get("success", True)
20
+
21
+ header = (
22
+ "🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]"
23
+ )
24
+
25
+ if content:
26
+ content_text = f"{header}\n [bold]{cls.escape_markup(content)}[/]"
27
+ else:
28
+ content_text = f"{header}\n [dim]Generating final report...[/]"
29
+
30
+ css_classes = cls.get_css_classes("completed")
31
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,108 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class CreateNoteRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "create_note"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ title = args.get("title", "")
19
+ content = args.get("content", "")
20
+
21
+ header = "πŸ“ [bold #fbbf24]Note[/]"
22
+
23
+ if title:
24
+ title_display = title[:100] + "..." if len(title) > 100 else title
25
+ note_parts = [f"{header}\n [bold]{cls.escape_markup(title_display)}[/]"]
26
+
27
+ if content:
28
+ content_display = content[:200] + "..." if len(content) > 200 else content
29
+ note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
30
+
31
+ content_text = "\n".join(note_parts)
32
+ else:
33
+ content_text = f"{header}\n [dim]Creating note...[/]"
34
+
35
+ css_classes = cls.get_css_classes("completed")
36
+ return Static(content_text, classes=css_classes)
37
+
38
+
39
+ @register_tool_renderer
40
+ class DeleteNoteRenderer(BaseToolRenderer):
41
+ tool_name: ClassVar[str] = "delete_note"
42
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
43
+
44
+ @classmethod
45
+ def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
46
+ header = "πŸ—‘οΈ [bold #fbbf24]Delete Note[/]"
47
+ content_text = f"{header}\n [dim]Deleting...[/]"
48
+
49
+ css_classes = cls.get_css_classes("completed")
50
+ return Static(content_text, classes=css_classes)
51
+
52
+
53
+ @register_tool_renderer
54
+ class UpdateNoteRenderer(BaseToolRenderer):
55
+ tool_name: ClassVar[str] = "update_note"
56
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
57
+
58
+ @classmethod
59
+ def render(cls, tool_data: dict[str, Any]) -> Static:
60
+ args = tool_data.get("args", {})
61
+
62
+ title = args.get("title", "")
63
+ content = args.get("content", "")
64
+
65
+ header = "✏️ [bold #fbbf24]Update Note[/]"
66
+
67
+ if title or content:
68
+ note_parts = [header]
69
+
70
+ if title:
71
+ title_display = title[:100] + "..." if len(title) > 100 else title
72
+ note_parts.append(f" [bold]{cls.escape_markup(title_display)}[/]")
73
+
74
+ if content:
75
+ content_display = content[:200] + "..." if len(content) > 200 else content
76
+ note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
77
+
78
+ content_text = "\n".join(note_parts)
79
+ else:
80
+ content_text = f"{header}\n [dim]Updating...[/]"
81
+
82
+ css_classes = cls.get_css_classes("completed")
83
+ return Static(content_text, classes=css_classes)
84
+
85
+
86
+ @register_tool_renderer
87
+ class ListNotesRenderer(BaseToolRenderer):
88
+ tool_name: ClassVar[str] = "list_notes"
89
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
90
+
91
+ @classmethod
92
+ def render(cls, tool_data: dict[str, Any]) -> Static:
93
+ result = tool_data.get("result")
94
+
95
+ header = "πŸ“‹ [bold #fbbf24]Listing notes[/]"
96
+
97
+ if result and isinstance(result, dict) and "notes" in result:
98
+ notes = result["notes"]
99
+ if isinstance(notes, list):
100
+ count = len(notes)
101
+ content_text = f"{header}\n [dim]{count} notes found[/]"
102
+ else:
103
+ content_text = f"{header}\n [dim]No notes found[/]"
104
+ else:
105
+ content_text = f"{header}\n [dim]Listing notes...[/]"
106
+
107
+ css_classes = cls.get_css_classes("completed")
108
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,255 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ListRequestsRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "list_requests"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ result = tool_data.get("result")
18
+
19
+ httpql_filter = args.get("httpql_filter")
20
+
21
+ header = "πŸ“‹ [bold #06b6d4]Listing requests[/]"
22
+
23
+ if result and isinstance(result, dict) and "requests" in result:
24
+ requests = result["requests"]
25
+ if isinstance(requests, list) and requests:
26
+ request_lines = []
27
+ for req in requests[:3]:
28
+ if isinstance(req, dict):
29
+ method = req.get("method", "?")
30
+ path = req.get("path", "?")
31
+ response = req.get("response") or {}
32
+ status = response.get("statusCode", "?")
33
+ line = f"{method} {path} β†’ {status}"
34
+ request_lines.append(line)
35
+
36
+ if len(requests) > 3:
37
+ request_lines.append(f"... +{len(requests) - 3} more")
38
+
39
+ escaped_lines = [cls.escape_markup(line) for line in request_lines]
40
+ content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
41
+ else:
42
+ content_text = f"{header}\n [dim]No requests found[/]"
43
+ elif httpql_filter:
44
+ filter_display = (
45
+ httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter
46
+ )
47
+ content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]"
48
+ else:
49
+ content_text = f"{header}\n [dim]All requests[/]"
50
+
51
+ css_classes = cls.get_css_classes("completed")
52
+ return Static(content_text, classes=css_classes)
53
+
54
+
55
+ @register_tool_renderer
56
+ class ViewRequestRenderer(BaseToolRenderer):
57
+ tool_name: ClassVar[str] = "view_request"
58
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
59
+
60
+ @classmethod
61
+ def render(cls, tool_data: dict[str, Any]) -> Static:
62
+ args = tool_data.get("args", {})
63
+ result = tool_data.get("result")
64
+
65
+ part = args.get("part", "request")
66
+
67
+ header = f"πŸ‘€ [bold #06b6d4]Viewing {cls.escape_markup(part)}[/]"
68
+
69
+ if result and isinstance(result, dict):
70
+ if "content" in result:
71
+ content = result["content"]
72
+ content_preview = content[:500] + "..." if len(content) > 500 else content
73
+ content_text = f"{header}\n [dim]{cls.escape_markup(content_preview)}[/]"
74
+ elif "matches" in result:
75
+ matches = result["matches"]
76
+ if isinstance(matches, list) and matches:
77
+ match_lines = [
78
+ match["match"]
79
+ for match in matches[:3]
80
+ if isinstance(match, dict) and "match" in match
81
+ ]
82
+ if len(matches) > 3:
83
+ match_lines.append(f"... +{len(matches) - 3} more matches")
84
+ escaped_lines = [cls.escape_markup(line) for line in match_lines]
85
+ content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
86
+ else:
87
+ content_text = f"{header}\n [dim]No matches found[/]"
88
+ else:
89
+ content_text = f"{header}\n [dim]Viewing content...[/]"
90
+ else:
91
+ content_text = f"{header}\n [dim]Loading...[/]"
92
+
93
+ css_classes = cls.get_css_classes("completed")
94
+ return Static(content_text, classes=css_classes)
95
+
96
+
97
+ @register_tool_renderer
98
+ class SendRequestRenderer(BaseToolRenderer):
99
+ tool_name: ClassVar[str] = "send_request"
100
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
101
+
102
+ @classmethod
103
+ def render(cls, tool_data: dict[str, Any]) -> Static:
104
+ args = tool_data.get("args", {})
105
+ result = tool_data.get("result")
106
+
107
+ method = args.get("method", "GET")
108
+ url = args.get("url", "")
109
+
110
+ header = f"πŸ“€ [bold #06b6d4]Sending {cls.escape_markup(method)}[/]"
111
+
112
+ if result and isinstance(result, dict):
113
+ status_code = result.get("status_code")
114
+ response_body = result.get("body", "")
115
+
116
+ if status_code:
117
+ response_preview = f"Status: {status_code}"
118
+ if response_body:
119
+ body_preview = (
120
+ response_body[:300] + "..." if len(response_body) > 300 else response_body
121
+ )
122
+ response_preview += f"\n{body_preview}"
123
+ content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
124
+ else:
125
+ content_text = f"{header}\n [dim]Response received[/]"
126
+ elif url:
127
+ url_display = url[:400] + "..." if len(url) > 400 else url
128
+ content_text = f"{header}\n [dim]{cls.escape_markup(url_display)}[/]"
129
+ else:
130
+ content_text = f"{header}\n [dim]Sending...[/]"
131
+
132
+ css_classes = cls.get_css_classes("completed")
133
+ return Static(content_text, classes=css_classes)
134
+
135
+
136
+ @register_tool_renderer
137
+ class RepeatRequestRenderer(BaseToolRenderer):
138
+ tool_name: ClassVar[str] = "repeat_request"
139
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
140
+
141
+ @classmethod
142
+ def render(cls, tool_data: dict[str, Any]) -> Static:
143
+ args = tool_data.get("args", {})
144
+ result = tool_data.get("result")
145
+
146
+ modifications = args.get("modifications", {})
147
+
148
+ header = "πŸ”„ [bold #06b6d4]Repeating request[/]"
149
+
150
+ if result and isinstance(result, dict):
151
+ status_code = result.get("status_code")
152
+ response_body = result.get("body", "")
153
+
154
+ if status_code:
155
+ response_preview = f"Status: {status_code}"
156
+ if response_body:
157
+ body_preview = (
158
+ response_body[:300] + "..." if len(response_body) > 300 else response_body
159
+ )
160
+ response_preview += f"\n{body_preview}"
161
+ content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
162
+ else:
163
+ content_text = f"{header}\n [dim]Response received[/]"
164
+ elif modifications:
165
+ mod_text = str(modifications)
166
+ mod_display = mod_text[:400] + "..." if len(mod_text) > 400 else mod_text
167
+ content_text = f"{header}\n [dim]{cls.escape_markup(mod_display)}[/]"
168
+ else:
169
+ content_text = f"{header}\n [dim]No modifications[/]"
170
+
171
+ css_classes = cls.get_css_classes("completed")
172
+ return Static(content_text, classes=css_classes)
173
+
174
+
175
+ @register_tool_renderer
176
+ class ScopeRulesRenderer(BaseToolRenderer):
177
+ tool_name: ClassVar[str] = "scope_rules"
178
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
179
+
180
+ @classmethod
181
+ def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
182
+ header = "βš™οΈ [bold #06b6d4]Updating proxy scope[/]"
183
+ content_text = f"{header}\n [dim]Configuring...[/]"
184
+
185
+ css_classes = cls.get_css_classes("completed")
186
+ return Static(content_text, classes=css_classes)
187
+
188
+
189
+ @register_tool_renderer
190
+ class ListSitemapRenderer(BaseToolRenderer):
191
+ tool_name: ClassVar[str] = "list_sitemap"
192
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
193
+
194
+ @classmethod
195
+ def render(cls, tool_data: dict[str, Any]) -> Static:
196
+ result = tool_data.get("result")
197
+
198
+ header = "πŸ—ΊοΈ [bold #06b6d4]Listing sitemap[/]"
199
+
200
+ if result and isinstance(result, dict) and "entries" in result:
201
+ entries = result["entries"]
202
+ if isinstance(entries, list) and entries:
203
+ entry_lines = []
204
+ for entry in entries[:4]:
205
+ if isinstance(entry, dict):
206
+ label = entry.get("label", "?")
207
+ kind = entry.get("kind", "?")
208
+ line = f"{kind}: {label}"
209
+ entry_lines.append(line)
210
+
211
+ if len(entries) > 4:
212
+ entry_lines.append(f"... +{len(entries) - 4} more")
213
+
214
+ escaped_lines = [cls.escape_markup(line) for line in entry_lines]
215
+ content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
216
+ else:
217
+ content_text = f"{header}\n [dim]No entries found[/]"
218
+ else:
219
+ content_text = f"{header}\n [dim]Loading...[/]"
220
+
221
+ css_classes = cls.get_css_classes("completed")
222
+ return Static(content_text, classes=css_classes)
223
+
224
+
225
+ @register_tool_renderer
226
+ class ViewSitemapEntryRenderer(BaseToolRenderer):
227
+ tool_name: ClassVar[str] = "view_sitemap_entry"
228
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
229
+
230
+ @classmethod
231
+ def render(cls, tool_data: dict[str, Any]) -> Static:
232
+ result = tool_data.get("result")
233
+
234
+ header = "πŸ“ [bold #06b6d4]Viewing sitemap entry[/]"
235
+
236
+ if result and isinstance(result, dict):
237
+ if "entry" in result:
238
+ entry = result["entry"]
239
+ if isinstance(entry, dict):
240
+ label = entry.get("label", "")
241
+ kind = entry.get("kind", "")
242
+ if label and kind:
243
+ entry_info = f"{kind}: {label}"
244
+ content_text = f"{header}\n [dim]{cls.escape_markup(entry_info)}[/]"
245
+ else:
246
+ content_text = f"{header}\n [dim]Entry details loaded[/]"
247
+ else:
248
+ content_text = f"{header}\n [dim]Entry details loaded[/]"
249
+ else:
250
+ content_text = f"{header}\n [dim]Loading entry...[/]"
251
+ else:
252
+ content_text = f"{header}\n [dim]Loading...[/]"
253
+
254
+ css_classes = cls.get_css_classes("completed")
255
+ return Static(content_text, classes=css_classes)