super-code-assistant 3.3.6__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 (61) hide show
  1. commands/__init__.py +859 -0
  2. core/__init__.py +0 -0
  3. core/config.py +263 -0
  4. core/config_template.json +7 -0
  5. core/context.py +271 -0
  6. core/engine.py +635 -0
  7. core/file_state.py +279 -0
  8. core/llm.py +309 -0
  9. core/model_capabilities.py +45 -0
  10. core/permissions.py +204 -0
  11. core/sandbox/__init__.py +15 -0
  12. core/sandbox/blacklist.py +176 -0
  13. core/sandbox/config.py +38 -0
  14. core/sandbox/network.py +136 -0
  15. core/sandbox/path_protection.py +126 -0
  16. core/session.py +295 -0
  17. core/tool.py +45 -0
  18. features/__init__.py +0 -0
  19. features/compact.py +945 -0
  20. features/coordinator.py +105 -0
  21. features/cost_tracker.py +184 -0
  22. features/extract_memories.py +326 -0
  23. features/find_relevant_memories.py +376 -0
  24. features/git_ai.py +256 -0
  25. features/memory.py +531 -0
  26. features/memory_age.py +66 -0
  27. features/memory_scan.py +153 -0
  28. features/memory_types.py +34 -0
  29. features/plan.py +327 -0
  30. features/skills.py +300 -0
  31. features/worker_manager.py +232 -0
  32. mcp/__init__.py +0 -0
  33. mcp/client.py +112 -0
  34. mcp/loader.py +80 -0
  35. mcp/tool_proxy.py +59 -0
  36. super_code_assistant-3.3.6.dist-info/METADATA +45 -0
  37. super_code_assistant-3.3.6.dist-info/RECORD +61 -0
  38. super_code_assistant-3.3.6.dist-info/WHEEL +5 -0
  39. super_code_assistant-3.3.6.dist-info/entry_points.txt +2 -0
  40. super_code_assistant-3.3.6.dist-info/top_level.txt +7 -0
  41. tools/__init__.py +21 -0
  42. tools/agent.py +132 -0
  43. tools/ask_user.py +111 -0
  44. tools/bash.py +77 -0
  45. tools/file_edit.py +269 -0
  46. tools/file_read.py +206 -0
  47. tools/file_write.py +78 -0
  48. tools/glob_tool.py +81 -0
  49. tools/grep_tool.py +134 -0
  50. tools/plan_tools.py +75 -0
  51. tools/skill.py +108 -0
  52. tools/tool.py +44 -0
  53. tools/web_fetch.py +129 -0
  54. tools/web_search.py +220 -0
  55. tui/__init__.py +0 -0
  56. tui/app.py +726 -0
  57. tui/clipboard_image.py +42 -0
  58. tui/keylistener.py +140 -0
  59. tui/prompt.py +752 -0
  60. tui/query.py +200 -0
  61. tui/rendering.py +135 -0
mcp/loader.py ADDED
@@ -0,0 +1,80 @@
1
+ """MCP Loader — 读取 MCP 配置,启动所有 MCP server 并返回工具列表。
2
+
3
+ 配置文件格式:
4
+ {
5
+ "mcpServers": {
6
+ "filesystem": {
7
+ "command": "npx",
8
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
9
+ "env": {} // 可选,追加到当前环境变量
10
+ }
11
+ }
12
+ }
13
+
14
+ 查找顺序:当前工作目录(.mcp.json)→ exe 同级目录(mcp.json)→ 全局配置目录(~/.config/super-code/mcp.json)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import shutil
21
+ from pathlib import Path
22
+
23
+ from core.tool import Tool
24
+ from mcp.client import MCPClient
25
+ from mcp.tool_proxy import build_mcp_tools
26
+
27
+ # 全局持有所有已启动的 client,程序退出时统一关闭
28
+ _active_clients: list[MCPClient] = []
29
+
30
+
31
+ def load_mcp_tools(cwd: str | Path = ".") -> list[Tool]:
32
+ """读取配置、启动 server、返回所有 MCP 工具代理。启动失败的 server 跳过并打印警告。"""
33
+ config = _find_config(Path(cwd))
34
+ if config is None:
35
+ return []
36
+
37
+ servers = config.get("mcpServers", {})
38
+ tools: list[Tool] = []
39
+
40
+ for name, spec in servers.items(): # 这里的servers是个字典,所以上述注释是没问题的
41
+ command = spec.get("command", "")
42
+ # Windows 下 Popen 不会自动补 .cmd/.bat 后缀,需用 shutil.which 解析为绝对路径
43
+ resolved = shutil.which(command)
44
+ if resolved:
45
+ command = resolved
46
+ args = spec.get("args", [])
47
+ # 合并当前环境变量 + server 自定义 env(server 可能需要 API key 等)
48
+ env = {**os.environ, **spec.get("env", {})} if spec.get("env") else None
49
+
50
+ try:
51
+ client = MCPClient(name=name, command=command, args=args, env=env)
52
+ _active_clients.append(client)
53
+ server_tools = build_mcp_tools(client)
54
+ tools.extend(server_tools)
55
+ print(f"[MCP] {name}: {len(server_tools)} tool(s) loaded")
56
+ except Exception as e:
57
+ # 单个 server 启动失败不影响整体,打印警告继续
58
+ print(f"[MCP] Warning: failed to start server '{name}': {e}")
59
+
60
+ return tools
61
+
62
+
63
+ def shutdown_mcp():
64
+ """关闭所有 MCP server 子进程,在程序退出时调用。"""
65
+ for client in _active_clients:
66
+ client.close()
67
+ _active_clients.clear()
68
+
69
+
70
+ def _find_config(cwd: Path) -> dict | None:
71
+ """按优先级查找 .mcp.json:项目目录 → 便携目录(exe同级) → 全局配置目录。"""
72
+ from core.config import get_portable_dir
73
+ portable_mcp = get_portable_dir() / "mcp.json"
74
+ for candidate in [cwd / ".mcp.json", portable_mcp, Path.home() / ".config" / "super-code" / "mcp.json"]:
75
+ if candidate.exists():
76
+ try:
77
+ return json.loads(candidate.read_text(encoding="utf-8"))
78
+ except Exception:
79
+ return None
80
+ return None
mcp/tool_proxy.py ADDED
@@ -0,0 +1,59 @@
1
+ """MCP Tool Proxy — 将 MCP server 的每个工具包装成项目通用的 Tool 实例。
2
+
3
+ 工具命名规则:mcp__{serverName}__{toolName}
4
+ - 双下划线分隔,避免与本地工具名冲突
5
+ - LLM 看到的就是这个名字,调用时也用这个名字
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from core.tool import Tool, ToolResult
10
+ from mcp.client import MCPClient
11
+
12
+
13
+ class MCPToolProxy(Tool):
14
+ """代理单个 MCP 工具,把调用转发给对应的 MCPClient。"""
15
+
16
+ def __init__(self, client: MCPClient, tool_def: dict):
17
+ self._client = client
18
+ # 原始工具名(server 内部使用)
19
+ self._tool_name = tool_def["name"]
20
+ # 对外暴露的名字加上 server 前缀,避免冲突
21
+ self._name = f"mcp__{client.name}__{self._tool_name}"
22
+ self._description = tool_def.get("description", "")
23
+ # inputSchema 直接透传 MCP server 返回的 JSON Schema
24
+ self._input_schema = tool_def.get("inputSchema", {"type": "object", "properties": {}})
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ return self._name
29
+
30
+ @property
31
+ def description(self) -> str:
32
+ return self._description
33
+
34
+ @property
35
+ def input_schema(self) -> dict:
36
+ return self._input_schema
37
+
38
+ def is_read_only(self) -> bool:
39
+ # MCP 工具默认需要权限确认,保守处理
40
+ return False
41
+
42
+ def get_activity_description(self, **kwargs) -> str | None:
43
+ return f"MCP {self._client.name}: {self._tool_name}"
44
+
45
+ def execute(self, **kwargs) -> ToolResult:
46
+ try:
47
+ result = self._client.call_tool(self._tool_name, kwargs)
48
+ return ToolResult(content=result)
49
+ except Exception as e:
50
+ return ToolResult(content=f"MCP error: {e}", is_error=True)
51
+
52
+
53
+ def build_mcp_tools(client: MCPClient) -> list[MCPToolProxy]:
54
+ """从一个 MCP server 获取所有工具并返回代理列表。"""
55
+ try:
56
+ tool_defs = client.list_tools()
57
+ except Exception as e:
58
+ raise RuntimeError(f"Failed to list tools from MCP server '{client.name}': {e}") from e
59
+ return [MCPToolProxy(client, td) for td in tool_defs]
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: super-code-assistant
3
+ Version: 3.3.6
4
+ Summary: Minimal AI coding assistant
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: openai>=1.0
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: python-dotenv>=1.0
10
+ Requires-Dist: prompt_toolkit>=3.0
11
+
12
+ # super-code
13
+
14
+ Minimal AI coding assistant —— 命令行交互式 AI 编码助手。
15
+
16
+ ## 安装
17
+
18
+ ```bash
19
+ pip install super-code-assistant
20
+ ```
21
+
22
+ 安装后**首次运行**自动初始化配置到 `~/.config/super-code/super-code.json`,
23
+ 编辑该文件填入 api_key 即可使用。
24
+
25
+ ## 配置
26
+
27
+ 配置按优先级加载:CLI 参数 > 项目 `./.super-code.json` > 便携目录
28
+ (exe 同级 `super-code.json`)> 全局 `~/.config/super-code/super-code.json` > 默认值。
29
+
30
+ 首次运行从内置模板生成全局配置,已有配置永不覆盖。
31
+
32
+ ## 使用
33
+
34
+ ```bash
35
+ super-code # 交互式 REPL
36
+ super-code -p "..." # 一次性提问
37
+ super-code --resume <id> # 恢复会话
38
+ super-code --coordinator # 协调者模式(多 worker 并行)
39
+ ```
40
+
41
+ ## 功能
42
+
43
+ - OpenAI 兼容 API;多模态图片输入(拖拽/粘贴截图)
44
+ - 工具调用:文件读写编辑、Bash、Web 搜索/抓取、多 worker 协调者
45
+ - 会话持久化(恢复/增量压缩/剪枝)、记忆系统、Skills、Plan 模式、沙箱
@@ -0,0 +1,61 @@
1
+ commands/__init__.py,sha256=yLQw9ruzKWtRRoxkIWXrD9CCUTIYbUHcuFg-CjG6RWY,36762
2
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ core/config.py,sha256=1Y278govTEd6tvYoYLkGZ-Kqp_l4zsaayGnQOLox31M,9369
4
+ core/config_template.json,sha256=03a1T7B3bCSX9cmtzuUzPAn3ZgoRqyIaz6WId2TGquk,163
5
+ core/context.py,sha256=fFNOJnLn-Zd7Q_r2hBLO09wURzWue_IsBoIkj7tu0KA,13814
6
+ core/engine.py,sha256=lS4Qe0N6rygi2cN7jmVfxJ4stsMpfmv-Pomd01_KGWY,34696
7
+ core/file_state.py,sha256=ybg91T4691uiCgzieVvimoR8VQAcg-Bumy3U45cpv04,9159
8
+ core/llm.py,sha256=wfwp3EWeLsGTMe0N0ZlqHLpPXCV36QJaL4nJygoGlCs,12857
9
+ core/model_capabilities.py,sha256=5Us_yR9lfCP9QAlRufVL8xbAk3dOaDCzXr01L5legmo,1606
10
+ core/permissions.py,sha256=e5aX9G_g4DRMLzIeEoTA_efhOGEp9H8xW9QZBvDfonk,8447
11
+ core/session.py,sha256=WCuU85-tGGCKBEWZNFUErEMLeei-gBhe1BMfTqO8Eio,13513
12
+ core/tool.py,sha256=pja5cvC_uRavxQT6y7p-3L2mTrXlAU5WSuMFlPiY-9E,1105
13
+ core/sandbox/__init__.py,sha256=2qd8Kz3xb2AIZajOcjSYZzW02VSZUVt4pZFiOFL6FuY,571
14
+ core/sandbox/blacklist.py,sha256=j6gofIX5g52xHnTRoz6wVp10B6zMrn3xm7xuuXq-h08,7282
15
+ core/sandbox/config.py,sha256=Rdf5Bqnls-g33htdZCQyOWw6QrMvi6nIVPowvN5z7-Q,1795
16
+ core/sandbox/network.py,sha256=Ko3WmgPY7RtcMw5LypvVokB4KCo3mWrjHNMqF-3kqlI,5295
17
+ core/sandbox/path_protection.py,sha256=gcGMc-DtgddGZZ1lukv2XP-TsSdDtjuwEB-BmtH2VMQ,4009
18
+ features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ features/compact.py,sha256=9hEcErCpYfC10QIAm3acz3zIRhFaB2UdjTIZj4EAB8I,47139
20
+ features/coordinator.py,sha256=coV2PNTOWZoa_uk5xBPFebKBOnHuRSJb0xC1_Nc9Wco,4681
21
+ features/cost_tracker.py,sha256=JV_sh6MMEQYej_KL21m0pFQc732EBbEk7HIEIcFiRuA,6930
22
+ features/extract_memories.py,sha256=Dv_AYKiiV-RwLHnZSaGj5tFD26MKUu4d9NTCiUOikpQ,14125
23
+ features/find_relevant_memories.py,sha256=haJ3EzvmE0MemDSdaMXwS7pwERnTtldkIsNaAyk3v0g,16118
24
+ features/git_ai.py,sha256=mH7OxBZcsRC3_fiOSbVHtruBdI6vWLtwamsUSUrNxqQ,9666
25
+ features/memory.py,sha256=L4baz6N6Sc0GN9x2UmJjVa7e-pmjDg1eQCSbbcqAQSM,22032
26
+ features/memory_age.py,sha256=NcOBffUehlfLEe6RS3DFgdSxH_I_uMB-7oCoP5VLfnE,2753
27
+ features/memory_scan.py,sha256=sESOkDxR47HJbZA4s_fzrP4eyr7PIvIiLV1D3fbs5wI,6606
28
+ features/memory_types.py,sha256=oq_Ff8rUx-15kL1JjmJlhSlHvixsis5QVtHNlnznO94,1451
29
+ features/plan.py,sha256=UqxWgG_c4K07s-ds0iRj3VoDfIbXwJ714koBGSOem2M,15594
30
+ features/skills.py,sha256=7eeLbWrVaxMb9zqWdWKOmhE_ejgpbOQRitB795oa908,12860
31
+ features/worker_manager.py,sha256=PtZb22n8Idlnw-PUTvpfu_aF_bpw6uAZf4HHIfvYK88,9502
32
+ mcp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ mcp/client.py,sha256=l4nlGB87s-nu0bMeXRXDba_IUjEMup3JlLKnF3fYhyk,4696
34
+ mcp/loader.py,sha256=5O_2e-3W4yxclXGjMJf-MX45g47G47EMoyYZPWRXZbY,2981
35
+ mcp/tool_proxy.py,sha256=m8nk7EHsB8_kcC1Ov_G6sGb3dz0et6MdhVdb5gpr47g,2204
36
+ tools/__init__.py,sha256=ERwR9XYgF67VGWs1FMXaWwWtPdj9n-VhHE_awJ6wlxw,530
37
+ tools/agent.py,sha256=d4OxW10o71xRs-Mn8soxihrxrBSynbjiul98tVM6fjI,5751
38
+ tools/ask_user.py,sha256=NJV3zdGswX_N8CzEc7e83FkrVXmPrxsHrwgF8byyD2M,4653
39
+ tools/bash.py,sha256=U91ZyGBJaWbftlw_DTk3w_7-lIpAMDMsihZ4oSE2BTI,3549
40
+ tools/file_edit.py,sha256=anJ-MMdvQRTTwlm_IPLKPbmGarDq8xztdVfuvDQ7KOI,13341
41
+ tools/file_read.py,sha256=j7Ekh9p6nqdeSD8FUBXeY7nfkpM1welE8tWuvjRQqkI,8292
42
+ tools/file_write.py,sha256=AjNzXHOS6B7sEMYJroAy8gu7Q_w0galHyugPVMk5g10,3052
43
+ tools/glob_tool.py,sha256=PKNzuoWmIAa_h0MvVnXXVv_kpYHuipFN-n_jpQX474g,3026
44
+ tools/grep_tool.py,sha256=Cjrt4SbuLqE4VK6F1QLWwjwgPBX8rle0_gv4fJlVpp4,5811
45
+ tools/plan_tools.py,sha256=pi_F7uNRvwhWuuMzx7yDvQRb9qLb5lP23QZZ1N8mC4E,3766
46
+ tools/skill.py,sha256=6VhKZR4me5GySn0Td3tSPaaOapCYqqlyTfLlPsvjbvc,4934
47
+ tools/tool.py,sha256=qVvRqsiWvOKdSAnbHDQbMnPpiwQYv5m4ZG6rmYhUJE8,909
48
+ tools/web_fetch.py,sha256=JuiUoX5u4PBLc1_uquWqXG_wVXs3XwBJIIY8wwb6218,4904
49
+ tools/web_search.py,sha256=yWeXIz4_kqf7jVBMJn14EcgWyohF1jPuNNZFvGxPZ4M,8197
50
+ tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
+ tui/app.py,sha256=qGDy1KosfuhEQb8IFNKQoTPyeYUOuualSxyvSz825fQ,37362
52
+ tui/clipboard_image.py,sha256=Uxdpu4ZOsKdHlmqPrmLDRCR8opD8UFa9Hg1sW0yMp38,1356
53
+ tui/keylistener.py,sha256=4KMIGq9Vmx1mZ2qzE-I1H8oJVkAqEzZr4NvQVlQW5H8,4801
54
+ tui/prompt.py,sha256=uyAOy0_9gN7Q-ZCQ6iAngvk_XA-XZsAkiCoElwZg_sg,34234
55
+ tui/query.py,sha256=XaUlKd7F1g8goJ8BG4m6cMQRzsZmOmmnP66LwtuvAHI,9848
56
+ tui/rendering.py,sha256=4p0uM6PkTNQvaF8XM2-Abo_XYveJj32JFM-T4pU3ZDw,6548
57
+ super_code_assistant-3.3.6.dist-info/METADATA,sha256=Dw8fFEoWbzhLAypgIXmWFkDMKn9xyXk1EPTMJ52Oe3Q,1372
58
+ super_code_assistant-3.3.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
59
+ super_code_assistant-3.3.6.dist-info/entry_points.txt,sha256=SlRMdhdtlJ60cF7XSVKSDqOayhP2ow0VbI7PcMRiWNQ,44
60
+ super_code_assistant-3.3.6.dist-info/top_level.txt,sha256=MKanz5WSNqivvcMNkZ3zMhWWaEOSIfVqahxmlA8pMX0,45
61
+ super_code_assistant-3.3.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ super-code = tui.app:main
@@ -0,0 +1,7 @@
1
+ commands
2
+ core
3
+ features
4
+ mcp
5
+ support
6
+ tools
7
+ tui
tools/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .ask_user import AskUserQuestionTool
2
+ from .bash import BashTool
3
+ from .file_edit import FileEditTool
4
+ from .file_read import FileReadTool
5
+ from .file_write import FileWriteTool
6
+ from .glob_tool import GlobTool
7
+ from .grep_tool import GrepTool
8
+ from .web_fetch import WebFetchTool
9
+ from .web_search import WebSearchTool
10
+
11
+ __all__ = [
12
+ "AskUserQuestionTool",
13
+ "BashTool",
14
+ "FileEditTool",
15
+ "FileReadTool",
16
+ "FileWriteTool",
17
+ "GlobTool",
18
+ "GrepTool",
19
+ "WebFetchTool",
20
+ "WebSearchTool",
21
+ ]
tools/agent.py ADDED
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from core.tool import Tool, ToolResult
6
+ from features.worker_manager import WorkerManager
7
+
8
+
9
+ class AgentTool(Tool):
10
+ name = "Agent"
11
+ description = (
12
+ "Spawn a background worker for research, implementation, or "
13
+ "verification. Returns immediately with a task_id. Final results "
14
+ "arrive later as a <task-notification> user message."
15
+ )
16
+ input_schema = {
17
+ "type": "object",
18
+ "properties": {
19
+ "description": {"type": "string", "description": "Short label for the worker task"},
20
+ "prompt": {"type": "string", "description": "Self-contained instructions for the worker"},
21
+ "subagent_type": {
22
+ "type": "string",
23
+ "enum": ["worker"],
24
+ "default": "worker",
25
+ "description": "Only 'worker' is currently supported",
26
+ },
27
+ },
28
+ "required": ["description", "prompt"],
29
+ }
30
+
31
+ def get_activity_description(self, **kwargs) -> str | None:
32
+ desc = kwargs.get("description", "")
33
+ return f"Running agent: {desc}" if desc else "Running agent…"
34
+
35
+ def __init__(self, manager: WorkerManager):
36
+ self._manager = manager
37
+
38
+ def execute(self, description: str = "", prompt: str = "",
39
+ subagent_type: str = "worker", **_) -> ToolResult:
40
+ # 长 prompt(多 worker 并行、单 prompt 数百字)走 OpenAI 兼容流式协议时,
41
+ # tool_call.arguments 的 JSON 字符串可能在传输中被截断,llm.py 解析失败
42
+ # 静默退化为 {} → 这里收到空 description/prompt。直接调用会让 Python 抛
43
+ # "missing required positional arguments",TUI 显示为 Tool error,
44
+ # 用户和模型都不知道是 JSON 截断导致的。改为返回明确 is_error tool_result,
45
+ # 模型读到提示后会重试一次(多数情况第二次能成)。
46
+ if not description or not prompt:
47
+ return ToolResult(
48
+ content=(
49
+ "Error: Agent tool requires both 'description' (non-empty) and "
50
+ "'prompt' (non-empty). The previous call had empty/missing input "
51
+ "— this usually means the tool_call arguments JSON was truncated "
52
+ "in transit. Retry with a complete JSON input. If the prompt is "
53
+ "very long, consider splitting the task into smaller workers."
54
+ ),
55
+ is_error=True,
56
+ )
57
+ try:
58
+ payload = self._manager.spawn(
59
+ description=description, prompt=prompt, subagent_type=subagent_type)
60
+ except ValueError as exc:
61
+ return ToolResult(content=f"Error: {exc}", is_error=True)
62
+ return ToolResult(content=json.dumps(payload, ensure_ascii=False))
63
+
64
+
65
+ class SendMessageTool(Tool):
66
+ name = "SendMessage"
67
+ description = (
68
+ "Continue an existing idle worker by task_id. Use this after a worker "
69
+ "has already reported back and you want it to take another step."
70
+ )
71
+ input_schema = {
72
+ "type": "object",
73
+ "properties": {
74
+ "to": {"type": "string", "description": "Worker task id to continue"},
75
+ "message": {"type": "string", "description": "Next self-contained instruction"},
76
+ },
77
+ "required": ["to", "message"],
78
+ }
79
+
80
+ def __init__(self, manager: WorkerManager):
81
+ self._manager = manager
82
+
83
+ def execute(self, to: str = "", message: str = "", **_) -> ToolResult:
84
+ # 同 AgentTool.execute:JSON 截断时 to/message 可能为空,返回明确错误让模型重试
85
+ # 而不是 Python 抛 missing positional arguments。
86
+ if not to or not message:
87
+ return ToolResult(
88
+ content=(
89
+ "Error: SendMessage requires both 'to' (worker task_id) and "
90
+ "'message' (non-empty). The previous call had empty/missing input "
91
+ "— likely a tool_call arguments JSON truncation. Retry with a "
92
+ "complete JSON input."
93
+ ),
94
+ is_error=True,
95
+ )
96
+ try:
97
+ payload = self._manager.continue_task(task_id=to, message=message)
98
+ except ValueError as exc:
99
+ return ToolResult(content=f"Error: {exc}", is_error=True)
100
+ return ToolResult(content=json.dumps(payload, ensure_ascii=False))
101
+
102
+
103
+ class TaskStopTool(Tool):
104
+ name = "TaskStop"
105
+ description = "Stop a running worker by task_id."
106
+ input_schema = {
107
+ "type": "object",
108
+ "properties": {
109
+ "task_id": {"type": "string", "description": "Worker task id"},
110
+ },
111
+ "required": ["task_id"],
112
+ }
113
+
114
+ def __init__(self, manager: WorkerManager):
115
+ self._manager = manager
116
+
117
+ def execute(self, task_id: str = "", **_) -> ToolResult:
118
+ # 同上:JSON 截断时 task_id 可能为空,返回明确错误而不是 Python 异常。
119
+ if not task_id:
120
+ return ToolResult(
121
+ content=(
122
+ "Error: TaskStop requires 'task_id'. The previous call had "
123
+ "empty/missing input — likely a tool_call arguments JSON "
124
+ "truncation. Retry with the worker's task_id."
125
+ ),
126
+ is_error=True,
127
+ )
128
+ try:
129
+ payload = self._manager.stop_task(task_id=task_id)
130
+ except ValueError as exc:
131
+ return ToolResult(content=f"Error: {exc}", is_error=True)
132
+ return ToolResult(content=json.dumps(payload, ensure_ascii=False))
tools/ask_user.py ADDED
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from core.tool import Tool, ToolResult
4
+
5
+
6
+ class AskUserQuestionTool(Tool):
7
+ @property
8
+ def name(self) -> str:
9
+ return "AskUserQuestion"
10
+
11
+ @property
12
+ def description(self) -> str:
13
+ return (
14
+ "Ask the user a question with predefined options. Use this to gather "
15
+ "preferences, clarify ambiguous instructions, or get decisions on "
16
+ "implementation choices. Each question has 2-4 options plus an automatic "
17
+ "'Other' option for free-form input."
18
+ )
19
+
20
+ @property
21
+ def input_schema(self) -> dict:
22
+ return {
23
+ "type": "object",
24
+ "properties": {
25
+ "questions": {
26
+ "type": "array",
27
+ "items": {
28
+ "type": "object",
29
+ "properties": {
30
+ "question": {"type": "string"},
31
+ "options": {
32
+ "type": "array",
33
+ "items": {
34
+ "type": "object",
35
+ "properties": {
36
+ "label": {"type": "string"},
37
+ "description": {"type": "string"},
38
+ },
39
+ "required": ["label", "description"],
40
+ },
41
+ "minItems": 2,
42
+ "maxItems": 4,
43
+ },
44
+ "multiSelect": {"type": "boolean", "default": False},
45
+ },
46
+ "required": ["question", "options"],
47
+ },
48
+ "minItems": 1,
49
+ "maxItems": 4,
50
+ }
51
+ },
52
+ "required": ["questions"],
53
+ }
54
+
55
+ def is_read_only(self) -> bool:
56
+ return True
57
+
58
+ def execute(self, **kwargs) -> ToolResult:
59
+ questions = kwargs.get("questions", [])
60
+ if not questions:
61
+ return ToolResult(content="No questions provided.", is_error=True)
62
+
63
+ # 用 prompt_toolkit 而不是裸 input():主 TUI 用 prompt_toolkit Application
64
+ # (bordered_prompt) 之后会调整 terminal 状态,加上 Rich Live spinner 的后台
65
+ # 重绘竞争,裸 input() 会出现按键被吞、必须按 Enter 才解锁的诡异现象。
66
+ # prompt_toolkit 的 prompt() 启动时会再次接管 terminal,与既有 TUI 链路兼容。
67
+ # 失败时(如非交互式环境)回退到 input() 保持原行为。
68
+ from prompt_toolkit import prompt as pt_prompt
69
+
70
+ def _ask(label: str) -> str:
71
+ try:
72
+ return pt_prompt(label).strip()
73
+ except Exception:
74
+ return input(label).strip()
75
+
76
+ answers: list[str] = []
77
+ for q in questions:
78
+ question_text = q.get("question", "")
79
+ options = q.get("options", [])
80
+ labels = [o["label"] for o in options]
81
+
82
+ from rich.console import Console
83
+ console = Console()
84
+ console.print(f"\n[bold]{question_text}[/bold]")
85
+ for i, o in enumerate(options, 1):
86
+ desc = o.get("description", "")
87
+ console.print(f" {i}) {o['label']}" + (f" — {desc}" if desc else ""))
88
+ console.print(f" {len(options)+1}) Other")
89
+
90
+ while True:
91
+ try:
92
+ raw = _ask(" Choice: ")
93
+ except (EOFError, KeyboardInterrupt):
94
+ return ToolResult(content="User cancelled the question.", is_error=True)
95
+ if raw.isdigit():
96
+ idx = int(raw) - 1
97
+ if 0 <= idx < len(options):
98
+ answers.append(f"{question_text} => {labels[idx]}")
99
+ break
100
+ if idx == len(options):
101
+ try:
102
+ other = _ask(" Enter your answer: ")
103
+ except (EOFError, KeyboardInterrupt):
104
+ return ToolResult(content="User cancelled the question.", is_error=True)
105
+ answers.append(f"{question_text} => {other}")
106
+ break
107
+ elif raw:
108
+ answers.append(f"{question_text} => {raw}")
109
+ break
110
+
111
+ return ToolResult(content="User answered:\n" + "\n".join(answers))
tools/bash.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import subprocess
5
+ from core.tool import Tool, ToolResult
6
+
7
+ _DEFAULT_TIMEOUT = 120
8
+
9
+ # 匹配 Bash 输出重定向的目标路径:> path, >> path, 1> path, 2> path 等
10
+ _REDIRECT_RE = re.compile(r'(?:>>|[12]?>>?)\s*([^\s|;&]+)')
11
+
12
+
13
+ class BashTool(Tool):
14
+ name = "Bash"
15
+ description = (
16
+ "Executes a given bash command and returns its output.\n\n"
17
+ "IMPORTANT: Avoid using this tool to run `find`, `grep`, `cat`, `head`, `tail`, "
18
+ "`sed`, `awk`, or `echo` commands — use dedicated tools instead.\n\n"
19
+ " - File search: use Glob\n - Content search: use Grep\n"
20
+ " - Read files: use Read\n - Edit files: use Edit\n - Write files: use Write"
21
+ )
22
+ input_schema = {
23
+ "type": "object",
24
+ "properties": {
25
+ "command": {"type": "string", "description": "The bash command to execute"},
26
+ "description": {"type": "string", "description": "What this command does"},
27
+ "timeout": {"type": "integer", "description": "Timeout in seconds", "default": 120},
28
+ },
29
+ "required": ["command"],
30
+ }
31
+
32
+ def __init__(self, sandbox_manager=None):
33
+ self._sandbox = sandbox_manager
34
+
35
+ def get_activity_description(self, **kwargs) -> str | None:
36
+ command = kwargs.get("command", "")
37
+ preview = command[:60] + "…" if len(command) > 60 else command
38
+ return f"Running {preview}" if command else None
39
+
40
+ def execute(self, command: str, description: str = "",
41
+ timeout: int = _DEFAULT_TIMEOUT, **kwargs) -> ToolResult:
42
+ # 沙箱启用时先过滤危险命令,通过后再执行
43
+ if self._sandbox is not None:
44
+ allowed, reason = self._sandbox.check(command)
45
+ if not allowed:
46
+ return ToolResult(content=f"Error: {reason}", is_error=True)
47
+ # 检查网络外发域名(curl/wget 等)
48
+ allowed, reason = self._sandbox.check_network(command)
49
+ if not allowed:
50
+ return ToolResult(content=f"Error: {reason}", is_error=True)
51
+ # 检查重定向目标是否指向受保护路径
52
+ for m in _REDIRECT_RE.finditer(command):
53
+ target = m.group(1).strip().strip("'\"")
54
+ if target:
55
+ allowed, reason = self._sandbox.check_path(target, "write")
56
+ if not allowed:
57
+ return ToolResult(content=f"Error: {reason}", is_error=True)
58
+ try:
59
+ result = subprocess.run(
60
+ command, shell=True, capture_output=True,
61
+ text=True, encoding="utf-8", errors="replace", timeout=timeout,
62
+ )
63
+ parts = []
64
+ if result.stdout:
65
+ stdout = result.stdout.rstrip()
66
+ if len(stdout) > 10_000:
67
+ stdout = stdout[:10_000] + f"\n\n... (output truncated)"
68
+ parts.append(stdout)
69
+ if result.stderr:
70
+ parts.append(f"[stderr]\n{result.stderr.rstrip()}")
71
+ if result.returncode != 0:
72
+ parts.append(f"[exit code: {result.returncode}]")
73
+ return ToolResult(content="\n".join(parts) if parts else "(no output)")
74
+ except subprocess.TimeoutExpired:
75
+ return ToolResult(content=f"Error: Command timed out after {timeout}s", is_error=True)
76
+ except Exception as e:
77
+ return ToolResult(content=f"Error: {e}", is_error=True)