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,63 @@
1
+ from typing import Any
2
+
3
+ from strix.tools.registry import register_tool
4
+
5
+
6
+ @register_tool(sandbox_execution=False)
7
+ def create_vulnerability_report(
8
+ title: str,
9
+ content: str,
10
+ severity: str,
11
+ ) -> dict[str, Any]:
12
+ validation_error = None
13
+ if not title or not title.strip():
14
+ validation_error = "Title cannot be empty"
15
+ elif not content or not content.strip():
16
+ validation_error = "Content cannot be empty"
17
+ elif not severity or not severity.strip():
18
+ validation_error = "Severity cannot be empty"
19
+ else:
20
+ valid_severities = ["critical", "high", "medium", "low", "info"]
21
+ if severity.lower() not in valid_severities:
22
+ validation_error = (
23
+ f"Invalid severity '{severity}'. Must be one of: {', '.join(valid_severities)}"
24
+ )
25
+
26
+ if validation_error:
27
+ return {"success": False, "message": validation_error}
28
+
29
+ try:
30
+ from strix.telemetry.tracer import get_global_tracer
31
+
32
+ tracer = get_global_tracer()
33
+ if tracer:
34
+ report_id = tracer.add_vulnerability_report(
35
+ title=title,
36
+ content=content,
37
+ severity=severity,
38
+ )
39
+
40
+ return {
41
+ "success": True,
42
+ "message": f"Vulnerability report '{title}' created successfully",
43
+ "report_id": report_id,
44
+ "severity": severity.lower(),
45
+ }
46
+ import logging
47
+
48
+ logging.warning("Global tracer not available - vulnerability report not stored")
49
+
50
+ return { # noqa: TRY300
51
+ "success": True,
52
+ "message": f"Vulnerability report '{title}' created successfully (not persisted)",
53
+ "warning": "Report could not be persisted - tracer unavailable",
54
+ }
55
+
56
+ except ImportError:
57
+ return {
58
+ "success": True,
59
+ "message": f"Vulnerability report '{title}' created successfully (not persisted)",
60
+ "warning": "Report could not be persisted - tracer module unavailable",
61
+ }
62
+ except (ValueError, TypeError) as e:
63
+ return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
@@ -0,0 +1,30 @@
1
+ <tools>
2
+ <tool name="create_vulnerability_report">
3
+ <description>Create a vulnerability report for a discovered security issue.
4
+
5
+ Use this tool to document a specific verified security vulnerability.
6
+ Put ALL details in the content field - affected URLs, parameters, proof of concept, remediation steps, CVE references, CVSS scores, technical details, impact assessment, etc.
7
+
8
+ DO NOT USE:
9
+ - For general security observations without specific vulnerabilities
10
+ - When you don't have concrete vulnerability details
11
+ - When you don't have a proof of concept, or still not 100% sure if it's a vulnerability
12
+ - For tracking multiple vulnerabilities (create separate reports)
13
+ - For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
14
+ </description>
15
+ <parameters>
16
+ <parameter name="title" type="string" required="true">
17
+ <description>Clear, concise title of the vulnerability</description>
18
+ </parameter>
19
+ <parameter name="content" type="string" required="true">
20
+ <description>Complete vulnerability details including affected URLs, technical details, impact, proof of concept, remediation steps, and any relevant references. Be comprehensive and include everything relevant.</description>
21
+ </parameter>
22
+ <parameter name="severity" type="string" required="true">
23
+ <description>Severity level: critical, high, medium, low, or info</description>
24
+ </parameter>
25
+ </parameters>
26
+ <returns type="Dict[str, Any]">
27
+ <description>Response containing success status and message</description>
28
+ </returns>
29
+ </tool>
30
+ </tools>
@@ -0,0 +1,4 @@
1
+ from .terminal_actions import terminal_execute
2
+
3
+
4
+ __all__ = ["terminal_execute"]
@@ -0,0 +1,35 @@
1
+ from typing import Any
2
+
3
+ from strix.tools.registry import register_tool
4
+
5
+ from .terminal_manager import get_terminal_manager
6
+
7
+
8
+ @register_tool
9
+ def terminal_execute(
10
+ command: str,
11
+ is_input: bool = False,
12
+ timeout: float | None = None,
13
+ terminal_id: str | None = None,
14
+ no_enter: bool = False,
15
+ ) -> dict[str, Any]:
16
+ manager = get_terminal_manager()
17
+
18
+ try:
19
+ return manager.execute_command(
20
+ command=command,
21
+ is_input=is_input,
22
+ timeout=timeout,
23
+ terminal_id=terminal_id,
24
+ no_enter=no_enter,
25
+ )
26
+ except (ValueError, RuntimeError) as e:
27
+ return {
28
+ "error": str(e),
29
+ "command": command,
30
+ "terminal_id": terminal_id or "default",
31
+ "content": "",
32
+ "status": "error",
33
+ "exit_code": None,
34
+ "working_dir": None,
35
+ }
@@ -0,0 +1,146 @@
1
+ <tools>
2
+ <tool name="terminal_execute">
3
+ <description>Execute a bash command in a persistent terminal session. The terminal maintains state (environment variables, current directory, running processes) between commands.</description>
4
+ <parameters>
5
+ <parameter name="command" type="string" required="true">
6
+ <description>The bash command to execute. Can be empty to check output of running commands (will wait for timeout period to collect output).
7
+
8
+ Supported special keys and sequences (based on official tmux key names):
9
+ - Control sequences: C-c, C-d, C-z, C-a, C-e, C-k, C-l, C-u, C-w, etc. (also ^c, ^d, etc.)
10
+ - Navigation keys: Up, Down, Left, Right, Home, End
11
+ - Page keys: PageUp, PageDown, PgUp, PgDn, PPage, NPage
12
+ - Function keys: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12
13
+ - Special keys: Enter, Escape, Space, Tab, BTab, BSpace, DC, IC
14
+ - Note: Use official tmux names (BSpace not Backspace, DC not Delete, IC not Insert, Escape not Esc)
15
+ - Meta/Alt sequences: M-key (e.g., M-f, M-b) - tmux official modifier
16
+ - Shift sequences: S-key (e.g., S-F6, S-Tab, S-Left)
17
+ - Combined modifiers: C-S-key, C-M-key, S-M-key, etc.
18
+
19
+ Special keys work automatically - no need to set is_input=true for keys like C-c, C-d, etc.
20
+ These are useful for interacting with vim, emacs, REPLs, and other interactive applications.</description>
21
+ </parameter>
22
+ <parameter name="is_input" type="boolean" required="false">
23
+ <description>If true, the command is sent as input to a currently running process. If false (default), the command is executed as a new bash command.
24
+ Note: Special keys (C-c, C-d, etc.) automatically work when a process is running - you don't need to set is_input=true for them.
25
+ Use is_input=true for regular text input to running processes.</description>
26
+ </parameter>
27
+ <parameter name="timeout" type="number" required="false">
28
+ <description>Optional timeout in seconds for command execution. CAPPED AT 60 SECONDS. If not provided, uses default wait (30s). On timeout, the command keeps running and the tool returns with status 'running'. For truly long-running tasks, prefer backgrounding with '&'.</description>
29
+ </parameter>
30
+ <parameter name="terminal_id" type="string" required="false">
31
+ <description>Identifier for the terminal session. Defaults to "default". Use different IDs to manage multiple concurrent terminal sessions.</description>
32
+ </parameter>
33
+ <parameter name="no_enter" type="boolean" required="false">
34
+ <description>If true, don't automatically add Enter/newline after the command. Useful for:
35
+ - Interactive prompts where you want to send keys without submitting
36
+ - Navigation keys in full-screen applications
37
+
38
+ Examples:
39
+ - terminal_execute("gg", is_input=true, no_enter=true) # Vim: go to top
40
+ - terminal_execute("5j", is_input=true, no_enter=true) # Vim: move down 5 lines
41
+ - terminal_execute("i", is_input=true, no_enter=true) # Vim: insert mode</description>
42
+ </parameter>
43
+ </parameters>
44
+ <returns type="Dict[str, Any]">
45
+ <description>Response containing:
46
+ - content: Command output
47
+ - exit_code: Exit code of the command (only for completed commands)
48
+ - command: The executed command
49
+ - terminal_id: The terminal session ID
50
+ - status: Command status ('completed' or 'running')
51
+ - working_dir: Current working directory after command execution</description>
52
+ </returns>
53
+ <notes>
54
+ Important usage rules:
55
+ 1. PERSISTENT SESSION: The terminal maintains state between commands. Environment variables,
56
+ current directory, and running processes persist across multiple tool calls.
57
+
58
+ 2. COMMAND EXECUTION:
59
+ - AVOID: Long pipelines, complex bash scripts, or convoluted one-liners
60
+ - Break complex operations into multiple simple tool calls for clarity and debugging
61
+ - For multiple commands, prefer separate tool calls over chaining with && or ;
62
+
63
+ 3. LONG-RUNNING COMMANDS:
64
+ - Commands never get killed automatically - they keep running in background
65
+ - Set timeout to control how long to wait for output before returning
66
+ - For daemons/servers or very long jobs, append '&' to run in background
67
+ - Use empty command "" to check progress (waits for timeout period to collect output)
68
+ - Use C-c, C-d, C-z to interrupt processes (works automatically, no is_input needed)
69
+
70
+ 4. TIMEOUT HANDLING:
71
+ - Timeout controls how long to wait before returning current output (max 60s cap)
72
+ - Commands are NEVER killed on timeout - they keep running
73
+ - After timeout, you can run new commands or check progress with empty command
74
+ - On timeout, status is 'running'; on completion, status is 'completed'
75
+
76
+ 5. MULTIPLE TERMINALS: Use different terminal_id values to run multiple concurrent sessions.
77
+
78
+ 6. INTERACTIVE PROCESSES:
79
+ - Special keys (C-c, C-d, etc.) work automatically when a process is running
80
+ - Use is_input=true for regular text input to running processes like:
81
+ * Interactive shells, REPLs, or prompts
82
+ * Long-running applications waiting for input
83
+ * Background processes that need interaction
84
+ - Use no_enter=true for stuff like Vim navigation, password typing, or multi-step commands
85
+
86
+ 7. WORKING DIRECTORY: The terminal tracks and returns the current working directory.
87
+ Use absolute paths or cd commands to change directories as needed.
88
+
89
+ 8. OUTPUT HANDLING: Large outputs are automatically truncated. The tool provides
90
+ the most relevant parts of the output for analysis.
91
+ </notes>
92
+ <examples>
93
+ # Execute a simple command
94
+ <function=terminal_execute>
95
+ <parameter=command>ls -la</parameter>
96
+ </function>
97
+
98
+ # Run a command with custom timeout
99
+ <function=terminal_execute>
100
+ <parameter=command>npm install</parameter>
101
+ <parameter=timeout>60</parameter>
102
+ </function>
103
+
104
+ # Check progress of running command (waits for timeout to collect output)
105
+ <function=terminal_execute>
106
+ <parameter=command></parameter>
107
+ <parameter=timeout>5</parameter>
108
+ </function>
109
+
110
+ # Start a background service
111
+ <function=terminal_execute>
112
+ <parameter=command>python app.py > server.log 2>&1 &</parameter>
113
+ </function>
114
+
115
+ # Interact with a running process
116
+ <function=terminal_execute>
117
+ <parameter=command>y</parameter>
118
+ <parameter=is_input>true</parameter>
119
+ </function>
120
+
121
+ # Interrupt a running process (special keys work automatically)
122
+ <function=terminal_execute>
123
+ <parameter=command>C-c</parameter>
124
+ </function>
125
+
126
+ # Send Escape key (use official tmux name)
127
+ <function=terminal_execute>
128
+ <parameter=command>Escape</parameter>
129
+ <parameter=is_input>true</parameter>
130
+ </function>
131
+
132
+ # Use a different terminal session
133
+ <function=terminal_execute>
134
+ <parameter=command>python3</parameter>
135
+ <parameter=terminal_id>python_session</parameter>
136
+ </function>
137
+
138
+ # Send input to Python REPL in specific session
139
+ <function=terminal_execute>
140
+ <parameter=command>print("Hello World")</parameter>
141
+ <parameter=is_input>true</parameter>
142
+ <parameter=terminal_id>python_session</parameter>
143
+ </function>
144
+ </examples>
145
+ </tool>
146
+ </tools>
@@ -0,0 +1,151 @@
1
+ import atexit
2
+ import contextlib
3
+ import signal
4
+ import sys
5
+ import threading
6
+ from typing import Any
7
+
8
+ from .terminal_session import TerminalSession
9
+
10
+
11
+ class TerminalManager:
12
+ def __init__(self) -> None:
13
+ self.sessions: dict[str, TerminalSession] = {}
14
+ self._lock = threading.Lock()
15
+ self.default_terminal_id = "default"
16
+ self.default_timeout = 30.0
17
+
18
+ self._register_cleanup_handlers()
19
+
20
+ def execute_command(
21
+ self,
22
+ command: str,
23
+ is_input: bool = False,
24
+ timeout: float | None = None,
25
+ terminal_id: str | None = None,
26
+ no_enter: bool = False,
27
+ ) -> dict[str, Any]:
28
+ if terminal_id is None:
29
+ terminal_id = self.default_terminal_id
30
+
31
+ session = self._get_or_create_session(terminal_id)
32
+
33
+ try:
34
+ result = session.execute(command, is_input, timeout or self.default_timeout, no_enter)
35
+
36
+ return {
37
+ "content": result["content"],
38
+ "command": command,
39
+ "terminal_id": terminal_id,
40
+ "status": result["status"],
41
+ "exit_code": result.get("exit_code"),
42
+ "working_dir": result.get("working_dir"),
43
+ }
44
+
45
+ except RuntimeError as e:
46
+ return {
47
+ "error": str(e),
48
+ "command": command,
49
+ "terminal_id": terminal_id,
50
+ "content": "",
51
+ "status": "error",
52
+ "exit_code": None,
53
+ "working_dir": None,
54
+ }
55
+ except OSError as e:
56
+ return {
57
+ "error": f"System error: {e}",
58
+ "command": command,
59
+ "terminal_id": terminal_id,
60
+ "content": "",
61
+ "status": "error",
62
+ "exit_code": None,
63
+ "working_dir": None,
64
+ }
65
+
66
+ def _get_or_create_session(self, terminal_id: str) -> TerminalSession:
67
+ with self._lock:
68
+ if terminal_id not in self.sessions:
69
+ self.sessions[terminal_id] = TerminalSession(terminal_id)
70
+ return self.sessions[terminal_id]
71
+
72
+ def close_session(self, terminal_id: str | None = None) -> dict[str, Any]:
73
+ if terminal_id is None:
74
+ terminal_id = self.default_terminal_id
75
+
76
+ with self._lock:
77
+ if terminal_id not in self.sessions:
78
+ return {
79
+ "terminal_id": terminal_id,
80
+ "message": f"Terminal '{terminal_id}' not found",
81
+ "status": "not_found",
82
+ }
83
+
84
+ session = self.sessions.pop(terminal_id)
85
+
86
+ try:
87
+ session.close()
88
+ except (RuntimeError, OSError) as e:
89
+ return {
90
+ "terminal_id": terminal_id,
91
+ "error": f"Failed to close terminal '{terminal_id}': {e}",
92
+ "status": "error",
93
+ }
94
+ else:
95
+ return {
96
+ "terminal_id": terminal_id,
97
+ "message": f"Terminal '{terminal_id}' closed successfully",
98
+ "status": "closed",
99
+ }
100
+
101
+ def list_sessions(self) -> dict[str, Any]:
102
+ with self._lock:
103
+ session_info: dict[str, dict[str, Any]] = {}
104
+ for tid, session in self.sessions.items():
105
+ session_info[tid] = {
106
+ "is_running": session.is_running(),
107
+ "working_dir": session.get_working_dir(),
108
+ }
109
+
110
+ return {"sessions": session_info, "total_count": len(session_info)}
111
+
112
+ def cleanup_dead_sessions(self) -> None:
113
+ with self._lock:
114
+ dead_sessions: list[str] = []
115
+ for tid, session in self.sessions.items():
116
+ if not session.is_running():
117
+ dead_sessions.append(tid)
118
+
119
+ for tid in dead_sessions:
120
+ session = self.sessions.pop(tid)
121
+ with contextlib.suppress(Exception):
122
+ session.close()
123
+
124
+ def close_all_sessions(self) -> None:
125
+ with self._lock:
126
+ sessions_to_close = list(self.sessions.values())
127
+ self.sessions.clear()
128
+
129
+ for session in sessions_to_close:
130
+ with contextlib.suppress(Exception):
131
+ session.close()
132
+
133
+ def _register_cleanup_handlers(self) -> None:
134
+ atexit.register(self.close_all_sessions)
135
+
136
+ signal.signal(signal.SIGTERM, self._signal_handler)
137
+ signal.signal(signal.SIGINT, self._signal_handler)
138
+
139
+ if hasattr(signal, "SIGHUP"):
140
+ signal.signal(signal.SIGHUP, self._signal_handler)
141
+
142
+ def _signal_handler(self, _signum: int, _frame: Any) -> None:
143
+ self.close_all_sessions()
144
+ sys.exit(0)
145
+
146
+
147
+ _terminal_manager = TerminalManager()
148
+
149
+
150
+ def get_terminal_manager() -> TerminalManager:
151
+ return _terminal_manager