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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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)
|