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.
- commands/__init__.py +859 -0
- core/__init__.py +0 -0
- core/config.py +263 -0
- core/config_template.json +7 -0
- core/context.py +271 -0
- core/engine.py +635 -0
- core/file_state.py +279 -0
- core/llm.py +309 -0
- core/model_capabilities.py +45 -0
- core/permissions.py +204 -0
- core/sandbox/__init__.py +15 -0
- core/sandbox/blacklist.py +176 -0
- core/sandbox/config.py +38 -0
- core/sandbox/network.py +136 -0
- core/sandbox/path_protection.py +126 -0
- core/session.py +295 -0
- core/tool.py +45 -0
- features/__init__.py +0 -0
- features/compact.py +945 -0
- features/coordinator.py +105 -0
- features/cost_tracker.py +184 -0
- features/extract_memories.py +326 -0
- features/find_relevant_memories.py +376 -0
- features/git_ai.py +256 -0
- features/memory.py +531 -0
- features/memory_age.py +66 -0
- features/memory_scan.py +153 -0
- features/memory_types.py +34 -0
- features/plan.py +327 -0
- features/skills.py +300 -0
- features/worker_manager.py +232 -0
- mcp/__init__.py +0 -0
- mcp/client.py +112 -0
- mcp/loader.py +80 -0
- mcp/tool_proxy.py +59 -0
- super_code_assistant-3.3.6.dist-info/METADATA +45 -0
- super_code_assistant-3.3.6.dist-info/RECORD +61 -0
- super_code_assistant-3.3.6.dist-info/WHEEL +5 -0
- super_code_assistant-3.3.6.dist-info/entry_points.txt +2 -0
- super_code_assistant-3.3.6.dist-info/top_level.txt +7 -0
- tools/__init__.py +21 -0
- tools/agent.py +132 -0
- tools/ask_user.py +111 -0
- tools/bash.py +77 -0
- tools/file_edit.py +269 -0
- tools/file_read.py +206 -0
- tools/file_write.py +78 -0
- tools/glob_tool.py +81 -0
- tools/grep_tool.py +134 -0
- tools/plan_tools.py +75 -0
- tools/skill.py +108 -0
- tools/tool.py +44 -0
- tools/web_fetch.py +129 -0
- tools/web_search.py +220 -0
- tui/__init__.py +0 -0
- tui/app.py +726 -0
- tui/clipboard_image.py +42 -0
- tui/keylistener.py +140 -0
- tui/prompt.py +752 -0
- tui/query.py +200 -0
- 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,,
|
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)
|