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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +89 -0
- strix/agents/StrixAgent/system_prompt.jinja +404 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +518 -0
- strix/agents/state.py +163 -0
- strix/interface/__init__.py +4 -0
- strix/interface/assets/tui_styles.tcss +694 -0
- strix/interface/cli.py +230 -0
- strix/interface/main.py +500 -0
- strix/interface/tool_components/__init__.py +39 -0
- strix/interface/tool_components/agents_graph_renderer.py +123 -0
- strix/interface/tool_components/base_renderer.py +62 -0
- strix/interface/tool_components/browser_renderer.py +120 -0
- strix/interface/tool_components/file_edit_renderer.py +99 -0
- strix/interface/tool_components/finish_renderer.py +31 -0
- strix/interface/tool_components/notes_renderer.py +108 -0
- strix/interface/tool_components/proxy_renderer.py +255 -0
- strix/interface/tool_components/python_renderer.py +34 -0
- strix/interface/tool_components/registry.py +72 -0
- strix/interface/tool_components/reporting_renderer.py +53 -0
- strix/interface/tool_components/scan_info_renderer.py +64 -0
- strix/interface/tool_components/terminal_renderer.py +131 -0
- strix/interface/tool_components/thinking_renderer.py +29 -0
- strix/interface/tool_components/user_message_renderer.py +43 -0
- strix/interface/tool_components/web_search_renderer.py +28 -0
- strix/interface/tui.py +1274 -0
- strix/interface/utils.py +559 -0
- strix/llm/__init__.py +15 -0
- strix/llm/config.py +20 -0
- strix/llm/llm.py +465 -0
- strix/llm/memory_compressor.py +212 -0
- strix/llm/request_queue.py +87 -0
- strix/llm/utils.py +87 -0
- strix/prompts/README.md +64 -0
- strix/prompts/__init__.py +109 -0
- strix/prompts/cloud/.gitkeep +0 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/custom/.gitkeep +0 -0
- strix/prompts/frameworks/fastapi.jinja +142 -0
- strix/prompts/frameworks/nextjs.jinja +126 -0
- strix/prompts/protocols/graphql.jinja +215 -0
- strix/prompts/reconnaissance/.gitkeep +0 -0
- strix/prompts/technologies/firebase_firestore.jinja +177 -0
- strix/prompts/technologies/supabase.jinja +189 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +147 -0
- strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
- strix/prompts/vulnerabilities/business_logic.jinja +171 -0
- strix/prompts/vulnerabilities/csrf.jinja +174 -0
- strix/prompts/vulnerabilities/idor.jinja +195 -0
- strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
- strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
- strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
- strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
- strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
- strix/prompts/vulnerabilities/rce.jinja +154 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
- strix/prompts/vulnerabilities/ssrf.jinja +135 -0
- strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
- strix/prompts/vulnerabilities/xss.jinja +169 -0
- strix/prompts/vulnerabilities/xxe.jinja +184 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +399 -0
- strix/runtime/runtime.py +29 -0
- strix/runtime/tool_server.py +205 -0
- strix/telemetry/__init__.py +4 -0
- strix/telemetry/tracer.py +337 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +621 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
- strix/tools/argument_parser.py +121 -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 +305 -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 +174 -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 +35 -0
- strix/tools/terminal/terminal_actions_schema.xml +146 -0
- strix/tools/terminal/terminal_manager.py +151 -0
- strix/tools/terminal/terminal_session.py +447 -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.4.0.dist-info/LICENSE +201 -0
- strix_agent-0.4.0.dist-info/METADATA +282 -0
- strix_agent-0.4.0.dist-info/RECORD +118 -0
- strix_agent-0.4.0.dist-info/WHEEL +4 -0
- 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)
|