illusion-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
文件写入工具
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
本模块提供写入完整文件内容到本地文件系统的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- FileWriteTool: 写入完整文件内容的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import FileWriteTool
|
|
12
|
+
>>> tool = FileWriteTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from difflib import unified_diff
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, Field, model_validator
|
|
21
|
+
|
|
22
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FileWriteToolInput(BaseModel):
|
|
26
|
+
"""文件写入参数。
|
|
27
|
+
|
|
28
|
+
属性:
|
|
29
|
+
file_path: 要写入的文件路径
|
|
30
|
+
content: 完整的文件内容
|
|
31
|
+
create_directories: 是否创建父目录
|
|
32
|
+
|
|
33
|
+
兼容旧参数名:path 也可传入,会自动映射。
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
file_path: str = Field(description="Path of the file to write")
|
|
37
|
+
content: str = Field(description="Full file contents")
|
|
38
|
+
create_directories: bool = Field(default=True)
|
|
39
|
+
|
|
40
|
+
@model_validator(mode="before")
|
|
41
|
+
@classmethod
|
|
42
|
+
def _normalize_fields(cls, values: dict) -> dict:
|
|
43
|
+
"""将旧参数名映射到新参数名,确保向后兼容。"""
|
|
44
|
+
if "path" in values and "file_path" not in values:
|
|
45
|
+
values["file_path"] = values.pop("path")
|
|
46
|
+
return values
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FileWriteTool(BaseTool):
|
|
50
|
+
"""写入完整的文件内容。
|
|
51
|
+
|
|
52
|
+
用于创建新文件或完全重写现有文件。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name = "write_file"
|
|
56
|
+
description = """Writes a file to the local filesystem.
|
|
57
|
+
|
|
58
|
+
Usage:
|
|
59
|
+
- This tool will overwrite the existing file if there is one at the provided path.
|
|
60
|
+
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
|
|
61
|
+
- Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.
|
|
62
|
+
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.
|
|
63
|
+
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
|
|
64
|
+
"""
|
|
65
|
+
input_model = FileWriteToolInput
|
|
66
|
+
|
|
67
|
+
async def execute(
|
|
68
|
+
self,
|
|
69
|
+
arguments: FileWriteToolInput,
|
|
70
|
+
context: ToolExecutionContext,
|
|
71
|
+
) -> ToolResult:
|
|
72
|
+
"""执行文件写入操作,对新文件返回内容预览,对已有文件返回差异信息
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
arguments: 文件写入参数
|
|
76
|
+
context: 工具执行上下文
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ToolResult: 包含写入结果和差异/预览文本的执行结果
|
|
80
|
+
"""
|
|
81
|
+
# 解析文件路径
|
|
82
|
+
path = _resolve_path(context.cwd, arguments.file_path)
|
|
83
|
+
|
|
84
|
+
# 对于已有文件,执行读后写强制检查
|
|
85
|
+
if path.exists():
|
|
86
|
+
from illusion.tools.file_edit_tool import has_file_been_read, mark_file_read
|
|
87
|
+
if not has_file_been_read(str(path)):
|
|
88
|
+
return ToolResult(
|
|
89
|
+
output=(
|
|
90
|
+
f"You must read the file at {path} using the Read tool "
|
|
91
|
+
"before you can write to it. This tool will fail if you attempt "
|
|
92
|
+
"a write without reading the file first."
|
|
93
|
+
),
|
|
94
|
+
is_error=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 如果需要,创建父目录
|
|
98
|
+
if arguments.create_directories:
|
|
99
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
# 判断是创建还是更新
|
|
102
|
+
is_update = path.exists()
|
|
103
|
+
|
|
104
|
+
# 对于已有文件,读取原始内容以生成diff
|
|
105
|
+
original = ""
|
|
106
|
+
if is_update:
|
|
107
|
+
original = path.read_text(encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
# 写入文件内容
|
|
110
|
+
path.write_text(arguments.content, encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
# 写入后将文件标记为已读,以便后续编辑
|
|
113
|
+
from illusion.tools.file_edit_tool import mark_file_read
|
|
114
|
+
mark_file_read(str(path))
|
|
115
|
+
|
|
116
|
+
# 生成差异或预览
|
|
117
|
+
if is_update:
|
|
118
|
+
diff_text = _generate_diff(str(path), original, arguments.content)
|
|
119
|
+
return ToolResult(output=f"Updated {path}\n{diff_text}")
|
|
120
|
+
else:
|
|
121
|
+
preview = _generate_create_preview(str(path), arguments.content)
|
|
122
|
+
return ToolResult(output=f"Created {path}\n{preview}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _generate_diff(file_path: str, original: str, updated: str, context_lines: int = 3) -> str:
|
|
126
|
+
"""生成统一差异格式的文本
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
file_path: 文件路径
|
|
130
|
+
original: 原始内容
|
|
131
|
+
updated: 更新后内容
|
|
132
|
+
context_lines: 上下文行数
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
str: 差异文本
|
|
136
|
+
"""
|
|
137
|
+
original_lines = original.splitlines(keepends=True)
|
|
138
|
+
updated_lines = updated.splitlines(keepends=True)
|
|
139
|
+
diff_lines = list(unified_diff(
|
|
140
|
+
original_lines,
|
|
141
|
+
updated_lines,
|
|
142
|
+
fromfile=f"a/{file_path}",
|
|
143
|
+
tofile=f"b/{file_path}",
|
|
144
|
+
n=context_lines,
|
|
145
|
+
))
|
|
146
|
+
if not diff_lines:
|
|
147
|
+
return ""
|
|
148
|
+
return "".join(diff_lines).rstrip()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _generate_create_preview(file_path: str, content: str, max_lines: int = 10) -> str:
|
|
152
|
+
"""生成新文件创建的内容预览
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
file_path: 文件路径
|
|
156
|
+
content: 文件内容
|
|
157
|
+
max_lines: 最大预览行数
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
str: 预览文本
|
|
161
|
+
"""
|
|
162
|
+
lines = content.splitlines()
|
|
163
|
+
total = len(lines)
|
|
164
|
+
if total <= max_lines:
|
|
165
|
+
return content
|
|
166
|
+
preview_lines = lines[:max_lines]
|
|
167
|
+
remaining = total - max_lines
|
|
168
|
+
return "\n".join(preview_lines) + f"\n... +{remaining} lines"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _resolve_path(base: Path, candidate: str) -> Path:
|
|
172
|
+
"""解析相对路径为绝对路径。
|
|
173
|
+
|
|
174
|
+
参数:
|
|
175
|
+
base: 基础目录
|
|
176
|
+
candidate: 候选路径(可能是相对路径)
|
|
177
|
+
|
|
178
|
+
返回:
|
|
179
|
+
解析后的绝对路径
|
|
180
|
+
"""
|
|
181
|
+
path = Path(candidate).expanduser()
|
|
182
|
+
if not path.is_absolute():
|
|
183
|
+
path = base / path
|
|
184
|
+
return path.resolve()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Glob 工具 - 使用 ripgrep 列出匹配的文件。
|
|
3
|
+
|
|
4
|
+
核心原则:让 rg 去碰文件系统,Python 只处理结果。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
12
|
+
from illusion.utils.ripgrep import run_rg, RipgrepError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GlobToolInput(BaseModel):
|
|
18
|
+
"""Glob工具的参数模型
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
pattern: 相对于工作目录的glob模式
|
|
22
|
+
root: 可选的搜索根目录
|
|
23
|
+
limit: 返回结果数量限制
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
pattern: str = Field(description="Glob pattern relative to the working directory")
|
|
27
|
+
root: str | None = Field(default=None, description="Optional search root")
|
|
28
|
+
limit: int = Field(default=200, ge=1, le=5000)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def split_absolute_glob_pattern(pattern: str) -> tuple[str, str]:
|
|
32
|
+
"""
|
|
33
|
+
拆分绝对路径 glob 模式为根路径和相对模式。
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
pattern: 绝对路径 glob 模式,如 "E:/repo/**/*.py"
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
(根路径, 相对模式) 元组
|
|
40
|
+
"""
|
|
41
|
+
# 查找第一个通配符
|
|
42
|
+
wildcard_chars = ["*", "?", "[", "{"]
|
|
43
|
+
min_idx = len(pattern)
|
|
44
|
+
for char in wildcard_chars:
|
|
45
|
+
idx = pattern.find(char)
|
|
46
|
+
if idx != -1 and idx < min_idx:
|
|
47
|
+
min_idx = idx
|
|
48
|
+
|
|
49
|
+
# 查找最后一个路径分隔符(在通配符之前)
|
|
50
|
+
last_sep = -1
|
|
51
|
+
for i in range(min_idx - 1, -1, -1):
|
|
52
|
+
if pattern[i] in ("/", "\\"):
|
|
53
|
+
last_sep = i
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
if last_sep == -1:
|
|
57
|
+
return ".", pattern
|
|
58
|
+
|
|
59
|
+
root = pattern[:last_sep]
|
|
60
|
+
relative = pattern[last_sep + 1:]
|
|
61
|
+
|
|
62
|
+
# 处理 Windows 驱动器号
|
|
63
|
+
if len(root) == 2 and root[1] == ":":
|
|
64
|
+
root = root + "\\"
|
|
65
|
+
|
|
66
|
+
return root, relative
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GlobTool(BaseTool):
|
|
70
|
+
"""列出匹配glob模式的文件
|
|
71
|
+
|
|
72
|
+
使用说明:
|
|
73
|
+
- 快速的文件模式匹配工具,适用于任何规模的代码库
|
|
74
|
+
- 支持glob模式如 "**/*.js" 或 "src/**/*.ts"
|
|
75
|
+
- 返回按修改时间排序的匹配文件路径
|
|
76
|
+
- 当需要按名称模式查找文件时使用此工具
|
|
77
|
+
- 当进行开放性搜索可能需要多轮glob和grep时,使用Agent工具
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
name = "glob"
|
|
81
|
+
description = """- Fast file pattern matching tool that works with any codebase size
|
|
82
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
83
|
+
- Returns matching file paths sorted by modification time
|
|
84
|
+
- Use this tool when you need to find files by name patterns
|
|
85
|
+
- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead"""
|
|
86
|
+
input_model = GlobToolInput
|
|
87
|
+
|
|
88
|
+
def is_read_only(self, arguments: GlobToolInput) -> bool:
|
|
89
|
+
"""返回工具是否为只读操作
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
arguments: 工具输入参数
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
bool: 始终返回True,glob是只读操作
|
|
96
|
+
"""
|
|
97
|
+
del arguments
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
async def execute(self, arguments: GlobToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
101
|
+
"""执行glob搜索
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
arguments: 工具输入参数
|
|
105
|
+
context: 工具执行上下文
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
ToolResult: 搜索结果
|
|
109
|
+
"""
|
|
110
|
+
pattern = arguments.pattern
|
|
111
|
+
path = arguments.root
|
|
112
|
+
|
|
113
|
+
# 处理绝对路径模式
|
|
114
|
+
if not path and (":\\" in pattern or pattern.startswith("/")):
|
|
115
|
+
path, pattern = split_absolute_glob_pattern(pattern)
|
|
116
|
+
elif not path:
|
|
117
|
+
path = "."
|
|
118
|
+
|
|
119
|
+
# 构建 rg 参数
|
|
120
|
+
args = ["--files"]
|
|
121
|
+
|
|
122
|
+
# 默认包含隐藏文件
|
|
123
|
+
args.append("--hidden")
|
|
124
|
+
|
|
125
|
+
# 默认不尊重 .gitignore
|
|
126
|
+
args.append("--no-ignore")
|
|
127
|
+
|
|
128
|
+
# Glob 模式
|
|
129
|
+
args.extend(["--glob", pattern])
|
|
130
|
+
|
|
131
|
+
# 排除 VCS 目录
|
|
132
|
+
for vcs in [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"]:
|
|
133
|
+
args.extend(["--glob", f"!{vcs}"])
|
|
134
|
+
|
|
135
|
+
# 搜索路径
|
|
136
|
+
args.append(path)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
stdout, stderr, returncode = await run_rg(args)
|
|
140
|
+
|
|
141
|
+
# 退出码 1 表示无匹配
|
|
142
|
+
if returncode == 1:
|
|
143
|
+
return ToolResult(output="(no matches)")
|
|
144
|
+
|
|
145
|
+
# 其他非零退出码表示错误
|
|
146
|
+
if returncode != 0:
|
|
147
|
+
raise RipgrepError(f"rg 执行失败(退出码 {returncode}): {stderr}")
|
|
148
|
+
|
|
149
|
+
# 解析结果
|
|
150
|
+
lines = stdout.strip().split("\n")
|
|
151
|
+
if not lines or (len(lines) == 1 and not lines[0]):
|
|
152
|
+
return ToolResult(output="(no matches)")
|
|
153
|
+
|
|
154
|
+
# 限制结果数量
|
|
155
|
+
limit = arguments.limit
|
|
156
|
+
if limit and len(lines) > limit:
|
|
157
|
+
lines = lines[:limit]
|
|
158
|
+
|
|
159
|
+
return ToolResult(output="\n".join(lines))
|
|
160
|
+
|
|
161
|
+
except RipgrepError:
|
|
162
|
+
raise
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"glob 执行失败: {e}")
|
|
165
|
+
raise RipgrepError(f"glob 执行失败: {e}")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Grep 工具 - 使用 ripgrep 搜索文件内容。
|
|
3
|
+
|
|
4
|
+
核心原则:让 rg 去碰文件系统,Python 只处理结果。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
13
|
+
from illusion.utils.ripgrep import run_rg, RipgrepError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GrepToolInput(BaseModel):
|
|
19
|
+
"""Grep工具的参数模型
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
pattern: 要搜索的正则表达式
|
|
23
|
+
path: 要搜索的文件或目录
|
|
24
|
+
glob: 文件过滤glob(如 "*.js", "**/*.tsx")
|
|
25
|
+
output_mode: 输出模式:content显示匹配行,files_with_matches只显示文件路径(默认),count显示匹配计数
|
|
26
|
+
context_before: 匹配前的行数
|
|
27
|
+
context_after: 匹配后的行数
|
|
28
|
+
context: 匹配周围的行数(覆盖-B/-A)
|
|
29
|
+
case_sensitive: 是否区分大小写
|
|
30
|
+
type: rg --type 过滤器(如 "js", "py", "rust")
|
|
31
|
+
multiline: 启用多行匹配(rg -U --multiline-dotall)
|
|
32
|
+
head_limit: 最大结果数(0=无限制)
|
|
33
|
+
offset: 跳过前N个结果
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
pattern: str = Field(description="Regular expression to search for")
|
|
37
|
+
path: str | None = Field(default=None, description="File or directory to search")
|
|
38
|
+
glob: str | None = Field(default=None, description='File filter glob (e.g., "*.js", "**/*.tsx")')
|
|
39
|
+
output_mode: Literal["content", "files_with_matches", "count"] = Field(
|
|
40
|
+
default="files_with_matches",
|
|
41
|
+
description="Output mode: content shows matching lines, files_with_matches shows only file paths (default), count shows match counts",
|
|
42
|
+
)
|
|
43
|
+
context_before: int | None = Field(default=None, description="Lines before match")
|
|
44
|
+
context_after: int | None = Field(default=None, description="Lines after match")
|
|
45
|
+
context: int | None = Field(default=None, description="Lines around match (overrides -B/-A)")
|
|
46
|
+
case_sensitive: bool = Field(default=True, description="Case sensitive search")
|
|
47
|
+
type: str | None = Field(default=None, description='rg --type filter (e.g., "js", "py", "rust")')
|
|
48
|
+
multiline: bool = Field(default=False, description="Enable multiline matching (rg -U --multiline-dotall)")
|
|
49
|
+
head_limit: int = Field(default=250, ge=0, description="Max results (0 = unlimited)")
|
|
50
|
+
offset: int = Field(default=0, ge=0, description="Skip first N results")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GrepTool(BaseTool):
|
|
54
|
+
"""搜索文本文件的正则表达式模式
|
|
55
|
+
|
|
56
|
+
使用说明:
|
|
57
|
+
- 始终使用Grep进行搜索任务。永远不要调用Bash命令中的grep或rg。Grep工具已针对正确的权限和访问进行优化
|
|
58
|
+
- 支持完整正则表达式语法(如 "log.*Error", "function\\s+\\w+")
|
|
59
|
+
- 使用glob参数过滤文件(如 "*.js", "**/*.tsx")或type参数(如 "js", "py", "rust")
|
|
60
|
+
- 输出模式:"content"显示匹配行,"files_with_matches"只显示文件路径(默认),"count"显示匹配计数
|
|
61
|
+
- 对于需要多轮搜索的开放性搜索使用Agent工具
|
|
62
|
+
- 模式语法:使用ripgrep(不是grep)- 字面大括号需要转义(使用 `interface\\{\\}` 在Go代码中查找 `interface{}`)
|
|
63
|
+
- 多行匹配:默认模式仅在单行内匹配。对于跨行模式如 `struct \\{[\\s\\S]*?field`,使用 `multiline: true`
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name = "grep"
|
|
67
|
+
description = """A powerful search tool built on ripgrep
|
|
68
|
+
|
|
69
|
+
Usage:
|
|
70
|
+
- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.
|
|
71
|
+
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
72
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
73
|
+
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
74
|
+
- Use Agent tool for open-ended searches requiring multiple rounds
|
|
75
|
+
- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\\{\\}` to find `interface{}` in Go code)
|
|
76
|
+
- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \\{[\\s\\S]*?field`, use `multiline: true`"""
|
|
77
|
+
input_model = GrepToolInput
|
|
78
|
+
|
|
79
|
+
def is_read_only(self, arguments: GrepToolInput) -> bool:
|
|
80
|
+
"""返回工具是否为只读操作
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
arguments: 工具输入参数
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
bool: 始终返回True,grep是只读操作
|
|
87
|
+
"""
|
|
88
|
+
del arguments
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
async def execute(self, arguments: GrepToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
92
|
+
"""执行grep搜索
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
arguments: 工具输入参数
|
|
96
|
+
context: 工具执行上下文
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ToolResult: 搜索结果
|
|
100
|
+
"""
|
|
101
|
+
path = arguments.path or "."
|
|
102
|
+
|
|
103
|
+
# 构建 rg 参数
|
|
104
|
+
args = ["--hidden"]
|
|
105
|
+
|
|
106
|
+
# 排除 VCS 目录
|
|
107
|
+
for vcs in [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"]:
|
|
108
|
+
args.extend(["--glob", f"!{vcs}"])
|
|
109
|
+
|
|
110
|
+
# 行长限制
|
|
111
|
+
args.extend(["--max-columns", "500"])
|
|
112
|
+
|
|
113
|
+
# 大小写
|
|
114
|
+
if not arguments.case_sensitive:
|
|
115
|
+
args.append("-i")
|
|
116
|
+
|
|
117
|
+
# 多行模式
|
|
118
|
+
if arguments.multiline:
|
|
119
|
+
args.extend(["-U", "--multiline-dotall"])
|
|
120
|
+
|
|
121
|
+
# 输出模式
|
|
122
|
+
if arguments.output_mode == "files_with_matches":
|
|
123
|
+
args.append("-l")
|
|
124
|
+
elif arguments.output_mode == "count":
|
|
125
|
+
args.append("-c")
|
|
126
|
+
else:
|
|
127
|
+
args.append("-n") # 行号
|
|
128
|
+
|
|
129
|
+
# 上下文行
|
|
130
|
+
context_lines = arguments.context or 0
|
|
131
|
+
if context_lines:
|
|
132
|
+
args.extend(["-C", str(context_lines)])
|
|
133
|
+
else:
|
|
134
|
+
if arguments.context_before is not None:
|
|
135
|
+
args.extend(["-B", str(arguments.context_before)])
|
|
136
|
+
if arguments.context_after is not None:
|
|
137
|
+
args.extend(["-A", str(arguments.context_after)])
|
|
138
|
+
|
|
139
|
+
# 文件类型
|
|
140
|
+
if arguments.type:
|
|
141
|
+
args.extend(["--type", arguments.type])
|
|
142
|
+
|
|
143
|
+
# Glob 过滤
|
|
144
|
+
if arguments.glob:
|
|
145
|
+
# 保持大括号模式如 *.{ts,tsx} 完整
|
|
146
|
+
parts = arguments.glob.split()
|
|
147
|
+
for part in parts:
|
|
148
|
+
args.extend(["--glob", part])
|
|
149
|
+
|
|
150
|
+
# 搜索模式(以 - 开头的模式用 -e 前缀)
|
|
151
|
+
pattern = arguments.pattern
|
|
152
|
+
if pattern.startswith("-"):
|
|
153
|
+
args.extend(["-e", pattern])
|
|
154
|
+
else:
|
|
155
|
+
args.append(pattern)
|
|
156
|
+
|
|
157
|
+
# 搜索路径
|
|
158
|
+
args.append(path)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
stdout, stderr, returncode = await run_rg(args)
|
|
162
|
+
|
|
163
|
+
# 退出码 1 表示无匹配
|
|
164
|
+
if returncode == 1:
|
|
165
|
+
return ToolResult(output="(no matches)")
|
|
166
|
+
|
|
167
|
+
# 其他非零退出码表示错误
|
|
168
|
+
if returncode != 0:
|
|
169
|
+
raise RipgrepError(f"rg 执行失败(退出码 {returncode}): {stderr}")
|
|
170
|
+
|
|
171
|
+
# 解析结果
|
|
172
|
+
lines = stdout.strip().split("\n")
|
|
173
|
+
if not lines or (len(lines) == 1 and not lines[0]):
|
|
174
|
+
return ToolResult(output="(no matches)")
|
|
175
|
+
|
|
176
|
+
# 应用分页
|
|
177
|
+
offset = arguments.offset
|
|
178
|
+
head_limit = arguments.head_limit
|
|
179
|
+
if offset > 0:
|
|
180
|
+
lines = lines[offset:]
|
|
181
|
+
if head_limit > 0 and len(lines) > head_limit:
|
|
182
|
+
lines = lines[:head_limit]
|
|
183
|
+
|
|
184
|
+
return ToolResult(output="\n".join(lines))
|
|
185
|
+
|
|
186
|
+
except RipgrepError:
|
|
187
|
+
raise
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"grep 执行失败: {e}")
|
|
190
|
+
raise RipgrepError(f"grep 执行失败: {e}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP 资源列表工具
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供列出 MCP 资源的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- ListMcpResourcesTool: 列出 MCP 服务器上的资源
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import ListMcpResourcesTool
|
|
12
|
+
>>> tool = ListMcpResourcesTool(manager)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from illusion.mcp.client import McpClientManager
|
|
20
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ListMcpResourcesToolInput(BaseModel):
|
|
24
|
+
"""MCP 资源列表参数。"""
|
|
25
|
+
|
|
26
|
+
server: str | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
description="Optional MCP server name to filter resources",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ListMcpResourcesTool(BaseTool):
|
|
33
|
+
"""列出从已连接服务器发现的 MCP 资源。
|
|
34
|
+
|
|
35
|
+
用于查看可用的 MCP 服务器资源。
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name = "list_mcp_resources"
|
|
39
|
+
description = """List available resources from configured MCP servers.
|
|
40
|
+
Each result line is formatted as "server_name:uri description".
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
- server (optional): The name of a specific MCP server to get resources from. If not provided,
|
|
44
|
+
resources from all servers will be returned. If the specified server is unknown or has no
|
|
45
|
+
resources, a diagnostic message is returned."""
|
|
46
|
+
input_model = ListMcpResourcesToolInput
|
|
47
|
+
|
|
48
|
+
def __init__(self, manager: McpClientManager) -> None:
|
|
49
|
+
self._manager = manager
|
|
50
|
+
|
|
51
|
+
def is_read_only(self, arguments: ListMcpResourcesToolInput) -> bool:
|
|
52
|
+
del arguments
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
async def execute(self, arguments: ListMcpResourcesToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
56
|
+
del context
|
|
57
|
+
server = (arguments.server or "").strip() or None
|
|
58
|
+
# 获取资源(支持按 server 过滤)
|
|
59
|
+
resources = self._manager.list_resources(server_name=server)
|
|
60
|
+
if not resources:
|
|
61
|
+
statuses_getter = getattr(self._manager, "list_statuses", None)
|
|
62
|
+
if callable(statuses_getter):
|
|
63
|
+
statuses = statuses_getter()
|
|
64
|
+
if server is not None:
|
|
65
|
+
status = next((item for item in statuses if item.name == server), None)
|
|
66
|
+
if status is None:
|
|
67
|
+
return ToolResult(output=f"Unknown MCP server: {server}", is_error=True)
|
|
68
|
+
detail = f" ({status.detail})" if status.detail else ""
|
|
69
|
+
return ToolResult(
|
|
70
|
+
output=f"(no MCP resources on server '{server}', state={status.state}{detail})"
|
|
71
|
+
)
|
|
72
|
+
connected = [item.name for item in statuses if item.state == "connected"]
|
|
73
|
+
if connected:
|
|
74
|
+
return ToolResult(
|
|
75
|
+
output=f"(no MCP resources from connected servers: {', '.join(connected)})"
|
|
76
|
+
)
|
|
77
|
+
return ToolResult(output="(no MCP resources)")
|
|
78
|
+
return ToolResult(
|
|
79
|
+
output="\n".join(f"{item.server_name}:{item.uri} {item.description}".strip() for item in resources)
|
|
80
|
+
)
|