strix-agent 0.1.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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -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 {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 {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)
|
@@ -0,0 +1,34 @@
|
|
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 PythonRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "python_action"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
16
|
+
args = tool_data.get("args", {})
|
17
|
+
|
18
|
+
action = args.get("action", "")
|
19
|
+
code = args.get("code", "")
|
20
|
+
|
21
|
+
header = "</> [bold #3b82f6]Python[/]"
|
22
|
+
|
23
|
+
if code and action in ["new_session", "execute"]:
|
24
|
+
code_display = code[:250] + "..." if len(code) > 250 else code
|
25
|
+
content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
|
26
|
+
elif action == "close":
|
27
|
+
content_text = f"{header}\n [dim]Closing session...[/]"
|
28
|
+
elif action == "list_sessions":
|
29
|
+
content_text = f"{header}\n [dim]Listing sessions...[/]"
|
30
|
+
else:
|
31
|
+
content_text = f"{header}\n [dim]Running...[/]"
|
32
|
+
|
33
|
+
css_classes = cls.get_css_classes("completed")
|
34
|
+
return Static(content_text, classes=css_classes)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import Any, ClassVar
|
2
|
+
|
3
|
+
from textual.widgets import Static
|
4
|
+
|
5
|
+
from .base_renderer import BaseToolRenderer
|
6
|
+
|
7
|
+
|
8
|
+
class ToolTUIRegistry:
|
9
|
+
_renderers: ClassVar[dict[str, type[BaseToolRenderer]]] = {}
|
10
|
+
|
11
|
+
@classmethod
|
12
|
+
def register(cls, renderer_class: type[BaseToolRenderer]) -> None:
|
13
|
+
if not renderer_class.tool_name:
|
14
|
+
raise ValueError(f"Renderer {renderer_class.__name__} must define tool_name")
|
15
|
+
|
16
|
+
cls._renderers[renderer_class.tool_name] = renderer_class
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def get_renderer(cls, tool_name: str) -> type[BaseToolRenderer] | None:
|
20
|
+
return cls._renderers.get(tool_name)
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def list_tools(cls) -> list[str]:
|
24
|
+
return list(cls._renderers.keys())
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def has_renderer(cls, tool_name: str) -> bool:
|
28
|
+
return tool_name in cls._renderers
|
29
|
+
|
30
|
+
|
31
|
+
def register_tool_renderer(renderer_class: type[BaseToolRenderer]) -> type[BaseToolRenderer]:
|
32
|
+
ToolTUIRegistry.register(renderer_class)
|
33
|
+
return renderer_class
|
34
|
+
|
35
|
+
|
36
|
+
def get_tool_renderer(tool_name: str) -> type[BaseToolRenderer] | None:
|
37
|
+
return ToolTUIRegistry.get_renderer(tool_name)
|
38
|
+
|
39
|
+
|
40
|
+
def render_tool_widget(tool_data: dict[str, Any]) -> Static:
|
41
|
+
tool_name = tool_data.get("tool_name", "")
|
42
|
+
renderer = get_tool_renderer(tool_name)
|
43
|
+
|
44
|
+
if renderer:
|
45
|
+
return renderer.render(tool_data)
|
46
|
+
return _render_default_tool_widget(tool_data)
|
47
|
+
|
48
|
+
|
49
|
+
def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
|
50
|
+
tool_name = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool"))
|
51
|
+
args = tool_data.get("args", {})
|
52
|
+
status = tool_data.get("status", "unknown")
|
53
|
+
result = tool_data.get("result")
|
54
|
+
|
55
|
+
status_text = BaseToolRenderer.get_status_icon(status)
|
56
|
+
|
57
|
+
header = f"→ Using tool [bold blue]{tool_name}[/]"
|
58
|
+
content_parts = [header]
|
59
|
+
|
60
|
+
args_str = BaseToolRenderer.format_args(args)
|
61
|
+
if args_str:
|
62
|
+
content_parts.append(args_str)
|
63
|
+
|
64
|
+
if status in ["completed", "failed", "error"] and result is not None:
|
65
|
+
result_str = BaseToolRenderer.format_result(result)
|
66
|
+
if result_str:
|
67
|
+
content_parts.append(f"[bold]Result:[/] {result_str}")
|
68
|
+
else:
|
69
|
+
content_parts.append(status_text)
|
70
|
+
|
71
|
+
css_classes = BaseToolRenderer.get_css_classes(status)
|
72
|
+
return Static("\n".join(content_parts), classes=css_classes)
|
@@ -0,0 +1,53 @@
|
|
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 CreateVulnerabilityReportRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "create_vulnerability_report"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "reporting-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
|
+
severity = args.get("severity", "")
|
20
|
+
content = args.get("content", "")
|
21
|
+
|
22
|
+
header = "🐞 [bold #ea580c]Vulnerability Report[/]"
|
23
|
+
|
24
|
+
if title:
|
25
|
+
content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"]
|
26
|
+
|
27
|
+
if severity:
|
28
|
+
severity_color = cls._get_severity_color(severity.lower())
|
29
|
+
content_parts.append(
|
30
|
+
f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]"
|
31
|
+
)
|
32
|
+
|
33
|
+
if content:
|
34
|
+
content_preview = content[:100] + "..." if len(content) > 100 else content
|
35
|
+
content_parts.append(f" [dim]{cls.escape_markup(content_preview)}[/]")
|
36
|
+
|
37
|
+
content_text = "\n".join(content_parts)
|
38
|
+
else:
|
39
|
+
content_text = f"{header}\n [dim]Creating report...[/]"
|
40
|
+
|
41
|
+
css_classes = cls.get_css_classes("completed")
|
42
|
+
return Static(content_text, classes=css_classes)
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
def _get_severity_color(cls, severity: str) -> str:
|
46
|
+
severity_colors = {
|
47
|
+
"critical": "#dc2626",
|
48
|
+
"high": "#ea580c",
|
49
|
+
"medium": "#d97706",
|
50
|
+
"low": "#65a30d",
|
51
|
+
"info": "#0284c7",
|
52
|
+
}
|
53
|
+
return severity_colors.get(severity, "#6b7280")
|
@@ -0,0 +1,58 @@
|
|
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 ScanStartInfoRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "scan_start_info"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-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
|
+
target = args.get("target", {})
|
20
|
+
|
21
|
+
target_display = cls._build_target_display(target)
|
22
|
+
|
23
|
+
content = f"🚀 Starting scan on {target_display}"
|
24
|
+
|
25
|
+
css_classes = cls.get_css_classes(status)
|
26
|
+
return Static(content, classes=css_classes)
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def _build_target_display(cls, target: dict[str, Any]) -> str:
|
30
|
+
if target_url := target.get("target_url"):
|
31
|
+
return f"[bold #22c55e]{target_url}[/bold #22c55e]"
|
32
|
+
if target_repo := target.get("target_repo"):
|
33
|
+
return f"[bold #22c55e]{target_repo}[/bold #22c55e]"
|
34
|
+
if target_path := target.get("target_path"):
|
35
|
+
return f"[bold #22c55e]{target_path}[/bold #22c55e]"
|
36
|
+
return "[dim]unknown target[/dim]"
|
37
|
+
|
38
|
+
|
39
|
+
@register_tool_renderer
|
40
|
+
class SubagentStartInfoRenderer(BaseToolRenderer):
|
41
|
+
tool_name: ClassVar[str] = "subagent_start_info"
|
42
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"]
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
46
|
+
args = tool_data.get("args", {})
|
47
|
+
status = tool_data.get("status", "unknown")
|
48
|
+
|
49
|
+
name = args.get("name", "Unknown Agent")
|
50
|
+
task = args.get("task", "")
|
51
|
+
|
52
|
+
content = f"🤖 Spawned subagent [bold #22c55e]{name}[/bold #22c55e]"
|
53
|
+
if task:
|
54
|
+
display_task = task[:80] + "..." if len(task) > 80 else task
|
55
|
+
content += f"\n Task: [dim]{display_task}[/dim]"
|
56
|
+
|
57
|
+
css_classes = cls.get_css_classes(status)
|
58
|
+
return Static(content, classes=css_classes)
|
@@ -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 TerminalRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "terminal_action"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-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
|
+
result = tool_data.get("result", {})
|
19
|
+
|
20
|
+
action = args.get("action", "unknown")
|
21
|
+
inputs = args.get("inputs", [])
|
22
|
+
terminal_id = args.get("terminal_id", "default")
|
23
|
+
|
24
|
+
content = cls._build_sleek_content(action, inputs, terminal_id, result)
|
25
|
+
|
26
|
+
css_classes = cls.get_css_classes(status)
|
27
|
+
return Static(content, classes=css_classes)
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def _build_sleek_content(
|
31
|
+
cls,
|
32
|
+
action: str,
|
33
|
+
inputs: list[str],
|
34
|
+
terminal_id: str, # noqa: ARG003
|
35
|
+
result: dict[str, Any], # noqa: ARG003
|
36
|
+
) -> str:
|
37
|
+
terminal_icon = ">_"
|
38
|
+
|
39
|
+
if action in {"create", "new_terminal"}:
|
40
|
+
command = cls._format_command(inputs) if inputs else "bash"
|
41
|
+
return f"{terminal_icon} [#22c55e]${command}[/]"
|
42
|
+
|
43
|
+
if action == "send_input":
|
44
|
+
command = cls._format_command(inputs)
|
45
|
+
return f"{terminal_icon} [#22c55e]${command}[/]"
|
46
|
+
|
47
|
+
if action == "wait":
|
48
|
+
return f"{terminal_icon} [dim]waiting...[/]"
|
49
|
+
|
50
|
+
if action == "close":
|
51
|
+
return f"{terminal_icon} [dim]close[/]"
|
52
|
+
|
53
|
+
if action == "get_snapshot":
|
54
|
+
return f"{terminal_icon} [dim]snapshot[/]"
|
55
|
+
|
56
|
+
return f"{terminal_icon} [dim]{action}[/]"
|
57
|
+
|
58
|
+
@classmethod
|
59
|
+
def _format_command(cls, inputs: list[str]) -> str:
|
60
|
+
if not inputs:
|
61
|
+
return ""
|
62
|
+
|
63
|
+
command_parts = []
|
64
|
+
|
65
|
+
for input_item in inputs:
|
66
|
+
if input_item == "Enter":
|
67
|
+
break
|
68
|
+
if input_item.startswith("literal:"):
|
69
|
+
command_parts.append(input_item[8:])
|
70
|
+
elif input_item in [
|
71
|
+
"Space",
|
72
|
+
"Tab",
|
73
|
+
"Backspace",
|
74
|
+
"Up",
|
75
|
+
"Down",
|
76
|
+
"Left",
|
77
|
+
"Right",
|
78
|
+
"Home",
|
79
|
+
"End",
|
80
|
+
"PageUp",
|
81
|
+
"PageDown",
|
82
|
+
"Insert",
|
83
|
+
"Delete",
|
84
|
+
"Escape",
|
85
|
+
] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
|
86
|
+
if input_item == "Space":
|
87
|
+
command_parts.append(" ")
|
88
|
+
elif input_item == "Tab":
|
89
|
+
command_parts.append("\t")
|
90
|
+
continue
|
91
|
+
else:
|
92
|
+
command_parts.append(input_item)
|
93
|
+
|
94
|
+
command = "".join(command_parts).strip()
|
95
|
+
|
96
|
+
if len(command) > 200:
|
97
|
+
command = command[:197] + "..."
|
98
|
+
|
99
|
+
return cls.escape_markup(command) if command else "bash"
|
@@ -0,0 +1,29 @@
|
|
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 ThinkRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "think"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "thinking-tool"]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
16
|
+
args = tool_data.get("args", {})
|
17
|
+
|
18
|
+
thought = args.get("thought", "")
|
19
|
+
|
20
|
+
header = "🧠 [bold #a855f7]Thinking[/]"
|
21
|
+
|
22
|
+
if thought:
|
23
|
+
thought_display = thought[:200] + "..." if len(thought) > 200 else thought
|
24
|
+
content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
|
25
|
+
else:
|
26
|
+
content = f"{header}\n [italic dim]Thinking...[/]"
|
27
|
+
|
28
|
+
css_classes = cls.get_css_classes("completed")
|
29
|
+
return Static(content, classes=css_classes)
|
@@ -0,0 +1,43 @@
|
|
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 UserMessageRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "user_message"
|
12
|
+
css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def render(cls, message_data: dict[str, Any]) -> Static:
|
16
|
+
content = message_data.get("content", "")
|
17
|
+
|
18
|
+
if not content:
|
19
|
+
return Static("", classes=cls.css_classes)
|
20
|
+
|
21
|
+
if len(content) > 300:
|
22
|
+
content = content[:297] + "..."
|
23
|
+
|
24
|
+
lines = content.split("\n")
|
25
|
+
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
|
26
|
+
bordered_content = "\n".join(bordered_lines)
|
27
|
+
formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
|
28
|
+
|
29
|
+
css_classes = " ".join(cls.css_classes)
|
30
|
+
return Static(formatted_content, classes=css_classes)
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def render_simple(cls, content: str) -> str:
|
34
|
+
if not content:
|
35
|
+
return ""
|
36
|
+
|
37
|
+
if len(content) > 300:
|
38
|
+
content = content[:297] + "..."
|
39
|
+
|
40
|
+
lines = content.split("\n")
|
41
|
+
bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
|
42
|
+
bordered_content = "\n".join(bordered_lines)
|
43
|
+
return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
|
@@ -0,0 +1,28 @@
|
|
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 WebSearchRenderer(BaseToolRenderer):
|
11
|
+
tool_name: ClassVar[str] = "web_search"
|
12
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "web-search-tool"]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
16
|
+
args = tool_data.get("args", {})
|
17
|
+
query = args.get("query", "")
|
18
|
+
|
19
|
+
header = "🌐 [bold #60a5fa]Searching the web...[/]"
|
20
|
+
|
21
|
+
if query:
|
22
|
+
query_display = query[:100] + "..." if len(query) > 100 else query
|
23
|
+
content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]"
|
24
|
+
else:
|
25
|
+
content_text = f"{header}"
|
26
|
+
|
27
|
+
css_classes = cls.get_css_classes("completed")
|
28
|
+
return Static(content_text, classes=css_classes)
|