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.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. 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
+ )