superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,259 @@
1
+ """
2
+ Shell Tools - Simple Command Execution.
3
+
4
+ NO command parsing, NO permission trees, NO complex safety checks.
5
+ Just run the command and return output.
6
+
7
+ Safety is handled at a higher level (user confirmation if enabled).
8
+ Git operations are blocked during QE sessions to maintain immutable repo guarantee.
9
+
10
+ Performance features:
11
+ - Streaming output as command runs (via ctx.on_output callback)
12
+ - Non-blocking execution with proper timeout handling
13
+ """
14
+
15
+ import asyncio
16
+ from pathlib import Path
17
+ from typing import Any, Dict
18
+
19
+ from .base import Tool, ToolResult, ToolContext
20
+ from .validation import validate_working_dir_parameter
21
+
22
+
23
+ class BashTool(Tool):
24
+ """Execute shell commands.
25
+
26
+ Simple, transparent shell execution with streaming output.
27
+ Git operations are blocked during QE sessions.
28
+
29
+ Performance:
30
+ When ctx.on_output is set, output is streamed in real-time
31
+ as it's produced, instead of waiting for command completion.
32
+ """
33
+
34
+ DEFAULT_TIMEOUT = 120 # 2 minutes
35
+ MAX_OUTPUT = 50000 # 50KB output limit
36
+ CHUNK_SIZE = 1024 # Read chunks for streaming
37
+
38
+ def __init__(self, git_guard_enabled: bool = True):
39
+ """
40
+ Initialize BashTool.
41
+
42
+ Args:
43
+ git_guard_enabled: If True, block git write operations during QE.
44
+ """
45
+ self._git_guard_enabled = git_guard_enabled
46
+
47
+ @property
48
+ def name(self) -> str:
49
+ return "bash"
50
+
51
+ @property
52
+ def description(self) -> str:
53
+ return "Execute a shell command and return its output."
54
+
55
+ @property
56
+ def parameters(self) -> Dict[str, Any]:
57
+ return {
58
+ "type": "object",
59
+ "properties": {
60
+ "command": {"type": "string", "description": "The shell command to execute"},
61
+ "working_dir": {
62
+ "type": "string",
63
+ "description": "Working directory for the command (optional)",
64
+ },
65
+ "timeout": {"type": "integer", "description": "Timeout in seconds (default: 120)"},
66
+ },
67
+ "required": ["command"],
68
+ }
69
+
70
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
71
+ command = args.get("command", "")
72
+ working_dir = args.get("working_dir")
73
+ timeout = args.get("timeout", self.DEFAULT_TIMEOUT)
74
+
75
+ if not command.strip():
76
+ return ToolResult(success=False, output="", error="Empty command")
77
+
78
+ # Check Git Guard - block git write operations during QE
79
+ if self._git_guard_enabled:
80
+ try:
81
+ from superqode.workspace.git_guard import get_git_guard, GitOperationBlocked
82
+
83
+ guard = get_git_guard()
84
+ if guard.enabled:
85
+ guard.check_command(command)
86
+ except GitOperationBlocked as e:
87
+ return ToolResult(
88
+ success=False,
89
+ output="",
90
+ error=f"🛡️ Git operation blocked: {e.reason}\n\n"
91
+ f"💡 {e.suggestion}\n\n"
92
+ "SuperQode runs in ephemeral mode - all changes are "
93
+ "automatically tracked and reverted after QE completes. "
94
+ "Findings are preserved in .superqode/qe-artifacts/",
95
+ metadata={"blocked_by": "git_guard", "command": command},
96
+ )
97
+ except ImportError:
98
+ pass # Git guard not available, continue
99
+
100
+ # Validate and resolve working directory - ensures it stays within ctx.working_directory
101
+ try:
102
+ cwd = validate_working_dir_parameter(working_dir, ctx.working_directory)
103
+ except ValueError as e:
104
+ return ToolResult(success=False, output="", error=str(e))
105
+
106
+ # Emit initial progress
107
+ await ctx.emit_progress(0.0, f"Running: {command[:50]}...")
108
+
109
+ try:
110
+ # PERFORMANCE: Use streaming mode if callback is set
111
+ if ctx.on_output:
112
+ return await self._execute_streaming(command, cwd, timeout, ctx)
113
+ else:
114
+ return await self._execute_buffered(command, cwd, timeout, ctx)
115
+
116
+ except Exception as e:
117
+ return ToolResult(success=False, output="", error=str(e))
118
+
119
+ async def _execute_buffered(
120
+ self,
121
+ command: str,
122
+ cwd: Path,
123
+ timeout: int,
124
+ ctx: ToolContext,
125
+ ) -> ToolResult:
126
+ """Execute command and buffer all output (original behavior)."""
127
+ process = await asyncio.create_subprocess_shell(
128
+ command,
129
+ stdout=asyncio.subprocess.PIPE,
130
+ stderr=asyncio.subprocess.PIPE,
131
+ cwd=str(cwd),
132
+ )
133
+
134
+ try:
135
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
136
+ except asyncio.TimeoutError:
137
+ process.kill()
138
+ return ToolResult(
139
+ success=False, output="", error=f"Command timed out after {timeout} seconds"
140
+ )
141
+
142
+ # Decode output
143
+ stdout_str = stdout.decode("utf-8", errors="replace")
144
+ stderr_str = stderr.decode("utf-8", errors="replace")
145
+
146
+ # Combine output
147
+ output = stdout_str
148
+ if stderr_str:
149
+ output += f"\n[stderr]\n{stderr_str}" if output else stderr_str
150
+
151
+ # Truncate if too long
152
+ if len(output) > self.MAX_OUTPUT:
153
+ output = (
154
+ output[: self.MAX_OUTPUT] + f"\n\n[Output truncated at {self.MAX_OUTPUT} bytes]"
155
+ )
156
+
157
+ success = process.returncode == 0
158
+ await ctx.emit_progress(1.0, "Complete" if success else "Failed")
159
+
160
+ return ToolResult(
161
+ success=success,
162
+ output=output,
163
+ error=None if success else f"Exit code: {process.returncode}",
164
+ metadata={"exit_code": process.returncode, "command": command, "cwd": str(cwd)},
165
+ )
166
+
167
+ async def _execute_streaming(
168
+ self,
169
+ command: str,
170
+ cwd: Path,
171
+ timeout: int,
172
+ ctx: ToolContext,
173
+ ) -> ToolResult:
174
+ """Execute command with streaming output to callback."""
175
+ process = await asyncio.create_subprocess_shell(
176
+ command,
177
+ stdout=asyncio.subprocess.PIPE,
178
+ stderr=asyncio.subprocess.PIPE,
179
+ cwd=str(cwd),
180
+ )
181
+
182
+ output_chunks = []
183
+ total_bytes = 0
184
+ truncated = False
185
+
186
+ async def read_stream(stream, is_stderr: bool = False):
187
+ """Read from a stream and emit chunks."""
188
+ nonlocal total_bytes, truncated
189
+
190
+ while True:
191
+ try:
192
+ chunk = await asyncio.wait_for(
193
+ stream.read(self.CHUNK_SIZE),
194
+ timeout=1.0, # Check timeout every second
195
+ )
196
+ except asyncio.TimeoutError:
197
+ continue # Keep reading
198
+
199
+ if not chunk:
200
+ break
201
+
202
+ text = chunk.decode("utf-8", errors="replace")
203
+
204
+ # Check size limit
205
+ if total_bytes + len(text) > self.MAX_OUTPUT:
206
+ remaining = self.MAX_OUTPUT - total_bytes
207
+ if remaining > 0:
208
+ text = text[:remaining]
209
+ output_chunks.append(text)
210
+ await ctx.emit_output(text)
211
+ truncated = True
212
+ break
213
+
214
+ total_bytes += len(text)
215
+
216
+ # Prefix stderr
217
+ if is_stderr and text.strip():
218
+ text = f"[stderr] {text}"
219
+
220
+ output_chunks.append(text)
221
+ await ctx.emit_output(text)
222
+
223
+ # Create timeout task
224
+ try:
225
+ # Read both streams concurrently
226
+ await asyncio.wait_for(
227
+ asyncio.gather(
228
+ read_stream(process.stdout, is_stderr=False),
229
+ read_stream(process.stderr, is_stderr=True),
230
+ ),
231
+ timeout=timeout,
232
+ )
233
+ await process.wait()
234
+ except asyncio.TimeoutError:
235
+ process.kill()
236
+ return ToolResult(
237
+ success=False,
238
+ output="".join(output_chunks),
239
+ error=f"Command timed out after {timeout} seconds",
240
+ )
241
+
242
+ output = "".join(output_chunks)
243
+ if truncated:
244
+ output += f"\n\n[Output truncated at {self.MAX_OUTPUT} bytes]"
245
+
246
+ success = process.returncode == 0
247
+ await ctx.emit_progress(1.0, "Complete" if success else "Failed")
248
+
249
+ return ToolResult(
250
+ success=success,
251
+ output=output,
252
+ error=None if success else f"Exit code: {process.returncode}",
253
+ metadata={
254
+ "exit_code": process.returncode,
255
+ "command": command,
256
+ "cwd": str(cwd),
257
+ "streamed": True,
258
+ },
259
+ )
@@ -0,0 +1,121 @@
1
+ """
2
+ TODO Management Tools - Task planning and tracking.
3
+
4
+ Helps models plan and track multi-step tasks with status updates.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, Dict, List
9
+
10
+ from .base import Tool, ToolResult, ToolContext
11
+
12
+ # Session-based in-memory storage: session_id -> list of todo items
13
+ _todo_store: Dict[str, List[Dict[str, Any]]] = {}
14
+
15
+
16
+ class TodoWriteTool(Tool):
17
+ """Create or update the TODO list for the current session."""
18
+
19
+ @property
20
+ def name(self) -> str:
21
+ return "todo_write"
22
+
23
+ @property
24
+ def description(self) -> str:
25
+ return (
26
+ "Create and manage a structured task list for the current coding session. "
27
+ "Use for complex multi-step tasks (3+ steps), non-trivial work, or when the user "
28
+ "provides multiple tasks. Track progress with status: pending, in_progress, completed, cancelled. "
29
+ "Mark tasks in_progress when starting and completed when done. Keep only ONE in_progress at a time."
30
+ )
31
+
32
+ @property
33
+ def parameters(self) -> Dict[str, Any]:
34
+ return {
35
+ "type": "object",
36
+ "properties": {
37
+ "todos": {
38
+ "type": "array",
39
+ "description": "The full list of todo items (replaces existing list)",
40
+ "items": {
41
+ "type": "object",
42
+ "properties": {
43
+ "id": {
44
+ "type": "string",
45
+ "description": "Unique identifier for the todo item",
46
+ },
47
+ "content": {
48
+ "type": "string",
49
+ "description": "Brief description of the task",
50
+ },
51
+ "status": {
52
+ "type": "string",
53
+ "description": "Current status: pending, in_progress, completed, cancelled",
54
+ "enum": ["pending", "in_progress", "completed", "cancelled"],
55
+ },
56
+ "priority": {
57
+ "type": "string",
58
+ "description": "Priority: high, medium, low (default: medium)",
59
+ "enum": ["high", "medium", "low"],
60
+ },
61
+ },
62
+ "required": ["id", "content", "status"],
63
+ },
64
+ }
65
+ },
66
+ "required": ["todos"],
67
+ }
68
+
69
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
70
+ todos = args.get("todos", [])
71
+ session_id = getattr(ctx, "session_id", None) or ""
72
+ # Normalize: ensure each item has id, content, status; optional priority
73
+ normalized = []
74
+ for t in todos:
75
+ item = {
76
+ "id": str(t.get("id", "")),
77
+ "content": str(t.get("content", "")),
78
+ "status": str(t.get("status", "pending")).lower(),
79
+ "priority": str(t.get("priority", "medium")).lower(),
80
+ }
81
+ if item["status"] not in ("pending", "in_progress", "completed", "cancelled"):
82
+ item["status"] = "pending"
83
+ if item["priority"] not in ("high", "medium", "low"):
84
+ item["priority"] = "medium"
85
+ normalized.append(item)
86
+ _todo_store[session_id] = normalized
87
+ pending = sum(1 for t in normalized if t["status"] not in ("completed", "cancelled"))
88
+ return ToolResult(
89
+ success=True,
90
+ output=f"Todo list updated. {len(normalized)} items, {pending} pending.",
91
+ metadata={"todos": normalized, "count": len(normalized)},
92
+ )
93
+
94
+
95
+ class TodoReadTool(Tool):
96
+ """Read the current TODO list for the session."""
97
+
98
+ @property
99
+ def name(self) -> str:
100
+ return "todo_read"
101
+
102
+ @property
103
+ def description(self) -> str:
104
+ return (
105
+ "Read the current todo list for the session. Use at the start of work, before starting "
106
+ "new tasks, or when uncertain about next steps. Returns items with id, content, status, priority. "
107
+ "If no todos exist, returns an empty list. Leave parameters empty."
108
+ )
109
+
110
+ @property
111
+ def parameters(self) -> Dict[str, Any]:
112
+ return {"type": "object", "properties": {}, "required": []}
113
+
114
+ async def execute(self, args: Dict[str, Any], ctx: ToolContext) -> ToolResult:
115
+ session_id = getattr(ctx, "session_id", None) or ""
116
+ todos = _todo_store.get(session_id, [])
117
+ return ToolResult(
118
+ success=True,
119
+ output=json.dumps(todos, indent=2),
120
+ metadata={"todos": todos, "count": len(todos)},
121
+ )
@@ -0,0 +1,80 @@
1
+ """
2
+ Path Validation Utilities.
3
+
4
+ Provides functions to validate that file paths stay within the working directory,
5
+ preventing directory traversal attacks and unauthorized file access.
6
+ """
7
+
8
+ from pathlib import Path
9
+ import os
10
+ from typing import Optional
11
+
12
+
13
+ def validate_path_in_working_directory(path: str, working_directory: Path) -> Path:
14
+ """Validate that a path stays within working_directory.
15
+
16
+ This function prevents directory traversal attacks by ensuring that:
17
+ - Relative paths with `../` cannot escape the working directory
18
+ - Absolute paths outside the working directory are rejected
19
+ - All paths are resolved and normalized before validation
20
+
21
+ Args:
22
+ path: Path to validate (can be relative or absolute)
23
+ working_directory: The allowed working directory (must be absolute)
24
+
25
+ Returns:
26
+ Resolved absolute path within working_directory
27
+
28
+ Raises:
29
+ ValueError: If path escapes working_directory or working_directory is not absolute
30
+ """
31
+ # Ensure working_directory is absolute without resolving symlinks.
32
+ working_dir = Path(working_directory).absolute()
33
+ working_dir_real = Path(os.path.realpath(working_dir))
34
+
35
+ # Resolve the input path without collapsing symlinks so /var stays /var on macOS.
36
+ input_path = Path(path)
37
+ if input_path.is_absolute():
38
+ resolved = Path(os.path.abspath(input_path))
39
+ else:
40
+ resolved = Path(os.path.abspath(working_dir / input_path))
41
+
42
+ resolved_real = Path(os.path.realpath(resolved))
43
+
44
+ try:
45
+ resolved.relative_to(working_dir)
46
+ return resolved
47
+ except ValueError:
48
+ pass
49
+
50
+ try:
51
+ resolved_real.relative_to(working_dir_real)
52
+ except ValueError:
53
+ raise ValueError(
54
+ f"Path '{path}' resolves to '{resolved_real}' which is outside "
55
+ f"working directory '{working_dir_real}'. Access denied for security."
56
+ )
57
+
58
+ # Rebase to the non-symlink working directory for consistent relative paths.
59
+ return working_dir / resolved_real.relative_to(working_dir_real)
60
+
61
+
62
+ def validate_working_dir_parameter(working_dir: Optional[str], ctx_working_directory: Path) -> Path:
63
+ """Validate a working_dir parameter for shell commands.
64
+
65
+ Ensures that the working_dir parameter stays within the context's working directory.
66
+
67
+ Args:
68
+ working_dir: Optional working directory parameter from tool call
69
+ ctx_working_directory: The context's working directory (base for validation)
70
+
71
+ Returns:
72
+ Validated absolute path within ctx_working_directory
73
+
74
+ Raises:
75
+ ValueError: If working_dir escapes ctx_working_directory
76
+ """
77
+ if working_dir is None:
78
+ return ctx_working_directory.resolve()
79
+
80
+ return validate_path_in_working_directory(working_dir, ctx_working_directory)