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,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
最小 Jupyter notebook 编辑工具
|
|
3
|
+
==============================
|
|
4
|
+
|
|
5
|
+
本模块提供编辑 Jupyter notebook 单元格的功能,无需使用 nbformat。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- NotebookEditTool: 编辑 notebook 单元格的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import NotebookEditTool
|
|
12
|
+
>>> tool = NotebookEditTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import secrets
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotebookEditToolInput(BaseModel):
|
|
28
|
+
"""Notebook 编辑参数。
|
|
29
|
+
|
|
30
|
+
属性:
|
|
31
|
+
notebook_path: Jupyter notebook 文件的绝对路径
|
|
32
|
+
cell_id: 要编辑的单元格 ID
|
|
33
|
+
new_source: 单元格的新源代码
|
|
34
|
+
cell_type: 单元格类型(code 或 markdown)
|
|
35
|
+
edit_mode: 编辑类型:replace、insert 或 delete
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
notebook_path: str = Field(description="The absolute path to the Jupyter notebook file")
|
|
39
|
+
cell_id: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="The ID of the cell to edit. Use edit_mode=insert to add a new cell at this index, edit_mode=delete to delete.",
|
|
42
|
+
)
|
|
43
|
+
new_source: str = Field(default="", description="The new source for the cell")
|
|
44
|
+
cell_type: Literal["code", "markdown"] | None = Field(
|
|
45
|
+
default=None,
|
|
46
|
+
description="The type of the cell (code or markdown). Required for insert mode. Defaults to the current cell type for replace.",
|
|
47
|
+
)
|
|
48
|
+
edit_mode: Literal["replace", "insert", "delete"] = Field(
|
|
49
|
+
default="replace",
|
|
50
|
+
description="The type of edit to make. replace: replace cell content, insert: add new cell at index, delete: remove the cell.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NotebookEditTool(BaseTool):
|
|
55
|
+
"""编辑 notebook 单元格而不需要 nbformat。
|
|
56
|
+
|
|
57
|
+
用于修改 Jupyter notebook (.ipynb 文件) 中的单元格内容。
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name = "notebook_edit"
|
|
61
|
+
description = """Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_id is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_id. Use edit_mode=delete to delete the cell at the index specified by cell_id. Defaults to edit_mode=replace. When using edit_mode=insert, cell_type is required. When using edit_mode=replace, cell_type defaults to the current cell type."""
|
|
62
|
+
input_model = NotebookEditToolInput
|
|
63
|
+
|
|
64
|
+
async def execute(
|
|
65
|
+
self,
|
|
66
|
+
arguments: NotebookEditToolInput,
|
|
67
|
+
context: ToolExecutionContext,
|
|
68
|
+
) -> ToolResult:
|
|
69
|
+
# 解析文件路径
|
|
70
|
+
path = _resolve_path(context.cwd, arguments.notebook_path)
|
|
71
|
+
|
|
72
|
+
# 验证 .ipynb 扩展名
|
|
73
|
+
if path.suffix.lower() != ".ipynb":
|
|
74
|
+
return ToolResult(
|
|
75
|
+
output=f"File must have .ipynb extension: {path}",
|
|
76
|
+
is_error=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# 对现有文件进行读后编辑检查
|
|
80
|
+
if path.exists():
|
|
81
|
+
from illusion.tools.file_edit_tool import has_file_been_read
|
|
82
|
+
if not has_file_been_read(str(path)):
|
|
83
|
+
return ToolResult(
|
|
84
|
+
output=f"You must read the file at {path} using the Read tool before you can edit it.",
|
|
85
|
+
is_error=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 加载 notebook
|
|
89
|
+
notebook = _load_notebook(path)
|
|
90
|
+
if notebook is None:
|
|
91
|
+
return ToolResult(output=f"Notebook not found: {path}", is_error=True)
|
|
92
|
+
|
|
93
|
+
# 获取单元格列表
|
|
94
|
+
cells = notebook.setdefault("cells", [])
|
|
95
|
+
|
|
96
|
+
# 从 cell_id 解析单元格索引
|
|
97
|
+
cell_index = _resolve_cell_index(cells, arguments.cell_id)
|
|
98
|
+
if cell_index is None:
|
|
99
|
+
return ToolResult(
|
|
100
|
+
output=f"Cell ID '{arguments.cell_id}' not found in notebook {path}",
|
|
101
|
+
is_error=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# 确定单元格类型
|
|
105
|
+
effective_cell_type = arguments.cell_type
|
|
106
|
+
if effective_cell_type is None:
|
|
107
|
+
if arguments.edit_mode == "insert":
|
|
108
|
+
return ToolResult(
|
|
109
|
+
output="cell_type is required for insert mode",
|
|
110
|
+
is_error=True,
|
|
111
|
+
)
|
|
112
|
+
# 对于 replace/delete,使用现有单元格类型
|
|
113
|
+
if 0 <= cell_index < len(cells):
|
|
114
|
+
effective_cell_type = cells[cell_index].get("cell_type", "code")
|
|
115
|
+
else:
|
|
116
|
+
effective_cell_type = "code"
|
|
117
|
+
|
|
118
|
+
# 执行编辑操作
|
|
119
|
+
if arguments.edit_mode == "delete":
|
|
120
|
+
if cell_index >= len(cells):
|
|
121
|
+
return ToolResult(
|
|
122
|
+
output=f"Cell index {cell_index} out of range (notebook has {len(cells)} cells)",
|
|
123
|
+
is_error=True,
|
|
124
|
+
)
|
|
125
|
+
cells.pop(cell_index)
|
|
126
|
+
_save_notebook(path, notebook)
|
|
127
|
+
return ToolResult(
|
|
128
|
+
output=f"Deleted cell {cell_index} from {path}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if arguments.edit_mode == "insert":
|
|
132
|
+
new_cell = _empty_cell(effective_cell_type)
|
|
133
|
+
new_cell["id"] = _generate_cell_id()
|
|
134
|
+
new_cell["source"] = arguments.new_source
|
|
135
|
+
# 在指定索引处插入
|
|
136
|
+
insert_at = min(cell_index, len(cells))
|
|
137
|
+
cells.insert(insert_at, new_cell)
|
|
138
|
+
_save_notebook(path, notebook)
|
|
139
|
+
return ToolResult(
|
|
140
|
+
output=f"Inserted cell at index {insert_at} in {path}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Replace 模式
|
|
144
|
+
if cell_index >= len(cells):
|
|
145
|
+
return ToolResult(
|
|
146
|
+
output=f"Cell index {cell_index} out of range (notebook has {len(cells)} cells)",
|
|
147
|
+
is_error=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
cell = cells[cell_index]
|
|
151
|
+
cell["cell_type"] = effective_cell_type
|
|
152
|
+
cell.setdefault("metadata", {})
|
|
153
|
+
if effective_cell_type == "code":
|
|
154
|
+
cell.setdefault("outputs", [])
|
|
155
|
+
cell.setdefault("execution_count", None)
|
|
156
|
+
# 替换时重置执行状态
|
|
157
|
+
cell["execution_count"] = None
|
|
158
|
+
cell["outputs"] = []
|
|
159
|
+
|
|
160
|
+
cell["source"] = arguments.new_source
|
|
161
|
+
|
|
162
|
+
_save_notebook(path, notebook)
|
|
163
|
+
return ToolResult(output=f"Updated notebook cell {cell_index} in {path}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _resolve_path(base: Path, candidate: str) -> Path:
|
|
167
|
+
"""解析相对路径为绝对路径。"""
|
|
168
|
+
path = Path(candidate).expanduser()
|
|
169
|
+
if not path.is_absolute():
|
|
170
|
+
path = base / path
|
|
171
|
+
return path.resolve()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _resolve_cell_index(cells: list[dict], cell_id: str | None) -> int | None:
|
|
175
|
+
"""将 cell_id 解析为数字索引。
|
|
176
|
+
|
|
177
|
+
支持:
|
|
178
|
+
- None → 默认为 0
|
|
179
|
+
- 数字索引如 "3" → 3
|
|
180
|
+
- "cell-N" 格式 → N
|
|
181
|
+
- 实际单元格 ID 字符串 → 按 id 字段匹配
|
|
182
|
+
"""
|
|
183
|
+
if cell_id is None:
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
# 尝试直接解析为数字索引
|
|
187
|
+
try:
|
|
188
|
+
return int(cell_id)
|
|
189
|
+
except ValueError:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
# 尝试 "cell-N" 格式
|
|
193
|
+
if cell_id.startswith("cell-"):
|
|
194
|
+
try:
|
|
195
|
+
return int(cell_id[5:])
|
|
196
|
+
except ValueError:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
# 尝试匹配实际单元格 ID
|
|
200
|
+
for i, cell in enumerate(cells):
|
|
201
|
+
if cell.get("id") == cell_id:
|
|
202
|
+
return i
|
|
203
|
+
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _load_notebook(path: Path) -> dict | None:
|
|
208
|
+
"""从磁盘加载 notebook。如果文件不存在返回 None。"""
|
|
209
|
+
if not path.exists():
|
|
210
|
+
return None
|
|
211
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _save_notebook(path: Path, notebook: dict) -> None:
|
|
215
|
+
"""将 notebook 保存到磁盘。"""
|
|
216
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
path.write_text(json.dumps(notebook, indent=1) + "\n", encoding="utf-8")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _generate_cell_id() -> str:
|
|
221
|
+
"""为 nbformat >= 4.5 生成唯一的单元格 ID。"""
|
|
222
|
+
return secrets.token_hex(8)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _empty_cell(cell_type: str) -> dict:
|
|
226
|
+
"""创建空单元格。"""
|
|
227
|
+
if cell_type == "markdown":
|
|
228
|
+
return {"cell_type": "markdown", "metadata": {}, "source": ""}
|
|
229
|
+
return {
|
|
230
|
+
"cell_type": "code",
|
|
231
|
+
"metadata": {},
|
|
232
|
+
"source": "",
|
|
233
|
+
"outputs": [],
|
|
234
|
+
"execution_count": None,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _normalize_source(source: str | list[str]) -> str:
|
|
239
|
+
"""规范化源代码(支持字符串或字符串列表)。"""
|
|
240
|
+
if isinstance(source, list):
|
|
241
|
+
return "".join(source)
|
|
242
|
+
return str(source)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PowerShell 命令执行工具
|
|
3
|
+
======================
|
|
4
|
+
|
|
5
|
+
本模块提供执行 PowerShell 命令并捕获标准输出/错误的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- PowerShellTool: 执行 PowerShell 命令的工具
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import PowerShellTool
|
|
12
|
+
>>> tool = PowerShellTool()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
from illusion.tools.base import BaseTool, ToolExecutionContext, ToolResult
|
|
28
|
+
from illusion.tools.shell_common import MAX_OUTPUT_LENGTH, CommandExecutor
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# PowerShell 版本类型
|
|
32
|
+
PowerShellEdition = Literal["core", "desktop"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# PowerShell 检测
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
def _find_powershell() -> str | None:
|
|
40
|
+
"""在系统上查找 PowerShell。优先 pwsh (Core 7+) 而非 powershell (5.1)。"""
|
|
41
|
+
pwsh = shutil.which("pwsh")
|
|
42
|
+
if pwsh:
|
|
43
|
+
return pwsh
|
|
44
|
+
ps = shutil.which("powershell")
|
|
45
|
+
if ps:
|
|
46
|
+
return ps
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_powershell_edition(powershell_path: str | None) -> PowerShellEdition | None:
|
|
51
|
+
"""根据可执行文件名确定 PowerShell 版本。
|
|
52
|
+
|
|
53
|
+
'pwsh' → Core (7+), 'powershell' → Desktop (5.1)。
|
|
54
|
+
"""
|
|
55
|
+
if not powershell_path:
|
|
56
|
+
return None
|
|
57
|
+
base = powershell_path.replace("/", "\\").split("\\")[-1].lower()
|
|
58
|
+
base = base.replace(".exe", "")
|
|
59
|
+
if base == "pwsh":
|
|
60
|
+
return "core"
|
|
61
|
+
return "desktop"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# 提示词生成
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
_DEFAULT_TIMEOUT_MS = 120_000 # 默认超时 2 分钟
|
|
69
|
+
_MAX_TIMEOUT_MS = 600_000 # 最大超时 10 分钟
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_background_usage_note() -> str | None:
|
|
73
|
+
if os.environ.get("ILLUSION_DISABLE_BACKGROUND_TASKS", "").lower() in ("1", "true"):
|
|
74
|
+
return None
|
|
75
|
+
return (
|
|
76
|
+
" - You can use the `run_in_background` parameter to run the command in the background. "
|
|
77
|
+
"Only use this if you don't need the result immediately and are OK being notified when "
|
|
78
|
+
"the command completes later. You do not need to check the output right away - you'll be "
|
|
79
|
+
"notified when it finishes."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_sleep_guidance() -> str | None:
|
|
84
|
+
if os.environ.get("ILLUSION_DISABLE_BACKGROUND_TASKS", "").lower() in ("1", "true"):
|
|
85
|
+
return None
|
|
86
|
+
return (
|
|
87
|
+
" - Avoid unnecessary `Start-Sleep` commands:\n"
|
|
88
|
+
" - Do not sleep between commands that can run immediately — just run them.\n"
|
|
89
|
+
" - If your command is long running and you would like to be notified when it "
|
|
90
|
+
"finishes — simply run your command using `run_in_background`. There is no need to "
|
|
91
|
+
"sleep in this case.\n"
|
|
92
|
+
" - Do not retry failing commands in a sleep loop — diagnose the root cause or "
|
|
93
|
+
"consider an alternative approach.\n"
|
|
94
|
+
" - If waiting for a background task you started with `run_in_background`, you will "
|
|
95
|
+
"be notified when it completes — do not poll.\n"
|
|
96
|
+
" - If you must poll an external process, use a check command rather than sleeping first.\n"
|
|
97
|
+
" - If you must sleep, keep the duration short (1-5 seconds) to avoid blocking the user."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_edition_section(edition: PowerShellEdition | None) -> str:
|
|
102
|
+
"""Version-specific syntax guidance.
|
|
103
|
+
|
|
104
|
+
The model's training data covers both editions but it can't tell which one
|
|
105
|
+
it's targeting, so it either emits pwsh-7 syntax on 5.1 (parser error → exit 1)
|
|
106
|
+
or needlessly avoids && on 7.
|
|
107
|
+
"""
|
|
108
|
+
if edition == "desktop":
|
|
109
|
+
return (
|
|
110
|
+
"PowerShell edition: Windows PowerShell 5.1 (powershell.exe)\n"
|
|
111
|
+
" - Pipeline chain operators `&&` and `||` are NOT available — they cause a parser "
|
|
112
|
+
"error. To run B only if A succeeds: `A; if ($?) { B }`. To chain unconditionally: `A; B`.\n"
|
|
113
|
+
" - Ternary (`?:`), null-coalescing (`??`), and null-conditional (`?.`) operators are "
|
|
114
|
+
"NOT available. Use `if/else` and explicit `$null -eq` checks instead.\n"
|
|
115
|
+
" - Avoid `2>&1` on native executables. In 5.1, redirecting a native command's stderr "
|
|
116
|
+
"inside PowerShell wraps each line in an ErrorRecord (NativeCommandError) and sets `$?` to "
|
|
117
|
+
"`$false` even when the exe returned exit code 0. stderr is already captured for you — "
|
|
118
|
+
"don't redirect it.\n"
|
|
119
|
+
" - Default file encoding is UTF-16 LE (with BOM). When writing files other tools will "
|
|
120
|
+
"read, pass `-Encoding utf8` to `Out-File`/`Set-Content`.\n"
|
|
121
|
+
" - `ConvertFrom-Json` returns a PSCustomObject, not a hashtable. `-AsHashtable` is not available."
|
|
122
|
+
)
|
|
123
|
+
if edition == "core":
|
|
124
|
+
return (
|
|
125
|
+
"PowerShell edition: PowerShell 7+ (pwsh)\n"
|
|
126
|
+
" - Pipeline chain operators `&&` and `||` ARE available and work like bash. Prefer "
|
|
127
|
+
"`cmd1 && cmd2` over `cmd1; cmd2` when cmd2 should only run if cmd1 succeeds.\n"
|
|
128
|
+
" - Ternary (`$cond ? $a : $b`), null-coalescing (`??`), and null-conditional (`?.`) "
|
|
129
|
+
"operators are available.\n"
|
|
130
|
+
" - Default file encoding is UTF-8 without BOM."
|
|
131
|
+
)
|
|
132
|
+
# Detection not yet resolved or PS not installed — give conservative 5.1-safe guidance.
|
|
133
|
+
return (
|
|
134
|
+
"PowerShell edition: unknown — assume Windows PowerShell 5.1 for compatibility\n"
|
|
135
|
+
" - Do NOT use `&&`, `||`, ternary `?:`, null-coalescing `??`, or null-conditional `?:`. "
|
|
136
|
+
"These are PowerShell 7+ only and parser-error on 5.1.\n"
|
|
137
|
+
" - To chain commands conditionally: `A; if ($?) { B }`. Unconditionally: `A; B`."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _build_powershell_description() -> str:
|
|
142
|
+
ps_path = _find_powershell()
|
|
143
|
+
edition = _get_powershell_edition(ps_path)
|
|
144
|
+
background_note = _get_background_usage_note()
|
|
145
|
+
sleep_guidance = _get_sleep_guidance()
|
|
146
|
+
|
|
147
|
+
sections = [
|
|
148
|
+
"Executes a given PowerShell command with optional timeout. Each invocation starts a "
|
|
149
|
+
"fresh PowerShell process; shell state (variables, functions) does not persist between calls.",
|
|
150
|
+
"",
|
|
151
|
+
"IMPORTANT: This tool is for terminal operations via PowerShell: git, npm, docker, and PS "
|
|
152
|
+
"cmdlets. DO NOT use it for file operations (reading, writing, editing, searching, finding files) "
|
|
153
|
+
"- use the specialized tools for this instead.",
|
|
154
|
+
"",
|
|
155
|
+
_get_edition_section(edition),
|
|
156
|
+
"",
|
|
157
|
+
"Before executing the command, please follow these steps:",
|
|
158
|
+
"",
|
|
159
|
+
"1. Directory Verification:",
|
|
160
|
+
" - If the command will create new directories or files, first use `Get-ChildItem` (or `ls`) "
|
|
161
|
+
"to verify the parent directory exists and is the correct location",
|
|
162
|
+
"",
|
|
163
|
+
"2. Command Execution:",
|
|
164
|
+
" - Always quote file paths that contain spaces with double quotes",
|
|
165
|
+
" - Capture the output of the command.",
|
|
166
|
+
"",
|
|
167
|
+
"PowerShell Syntax Notes:",
|
|
168
|
+
' - Variables use $ prefix: $myVar = "value"',
|
|
169
|
+
" - Escape character is backtick (`), not backslash",
|
|
170
|
+
" - Use Verb-Noun cmdlet naming: Get-ChildItem, Set-Location, New-Item, Remove-Item",
|
|
171
|
+
" - Common aliases: ls (Get-ChildItem), cd (Set-Location), cat (Get-Content), rm (Remove-Item)",
|
|
172
|
+
" - Pipe operator | works similarly to bash but passes objects, not text",
|
|
173
|
+
" - Use Select-Object, Where-Object, ForEach-Object for filtering and transformation",
|
|
174
|
+
' - String interpolation: "Hello $name" or "Hello $($obj.Property)"',
|
|
175
|
+
" - Registry access uses PSDrive prefixes: `HKLM:\\SOFTWARE\\...`, `HKCU:\\...` — NOT raw "
|
|
176
|
+
"`HKEY_LOCAL_MACHINE\\...`",
|
|
177
|
+
' - Environment variables: read with `$env:NAME`, set with `$env:NAME = "value"` '
|
|
178
|
+
"(NOT `Set-Variable` or bash `export`)",
|
|
179
|
+
' - Call native exe with spaces in path via call operator: `& "C:\\Program Files\\App\\app.exe" arg1 arg2`',
|
|
180
|
+
"",
|
|
181
|
+
"Interactive and blocking commands (will hang — this tool runs with -NonInteractive):",
|
|
182
|
+
" - NEVER use `Read-Host`, `Get-Credential`, `Out-GridView`, `$Host.UI.PromptForChoice`, or `pause`",
|
|
183
|
+
" - Destructive cmdlets (`Remove-Item`, `Stop-Process`, `Clear-Content`, etc.) may prompt for "
|
|
184
|
+
"confirmation. Add `-Confirm:$false` when you intend the action to proceed. Use `-Force` for "
|
|
185
|
+
"read-only/hidden items.",
|
|
186
|
+
" - Never use `git rebase -i`, `git add -i`, or other commands that open an interactive editor",
|
|
187
|
+
"",
|
|
188
|
+
"Passing multiline strings (commit messages, file content) to native executables:",
|
|
189
|
+
" - Use a single-quoted here-string so PowerShell does not expand `$` or backticks inside. "
|
|
190
|
+
"The closing `'@` MUST be at column 0 (no leading whitespace) on its own line — indenting it "
|
|
191
|
+
"is a parse error:",
|
|
192
|
+
"<example>",
|
|
193
|
+
"git commit -m @'",
|
|
194
|
+
"Commit message here.",
|
|
195
|
+
"Second line with $literal dollar signs.",
|
|
196
|
+
"'@",
|
|
197
|
+
"</example>",
|
|
198
|
+
" - Use `@'...'@` (single-quoted, literal) not `@\"...\"@` (double-quoted, interpolated) "
|
|
199
|
+
"unless you need variable expansion",
|
|
200
|
+
" - For arguments containing `-`, `@`, or other characters PowerShell parses as operators, "
|
|
201
|
+
"use the stop-parsing token: `git log --% --format=%H`",
|
|
202
|
+
"",
|
|
203
|
+
"Usage notes:",
|
|
204
|
+
" - The command argument is required.",
|
|
205
|
+
f" - You can specify an optional timeout in milliseconds (up to {_MAX_TIMEOUT_MS}ms / "
|
|
206
|
+
f"{_MAX_TIMEOUT_MS // 60000} minutes). If not specified, commands will timeout after "
|
|
207
|
+
f"{_DEFAULT_TIMEOUT_MS}ms ({_DEFAULT_TIMEOUT_MS // 60000} minutes).",
|
|
208
|
+
" - It is very helpful if you write a clear, concise description of what this command does.",
|
|
209
|
+
f" - If the output exceeds {MAX_OUTPUT_LENGTH} characters, output will be truncated before "
|
|
210
|
+
"being returned to you.",
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
if background_note is not None:
|
|
214
|
+
sections.append(background_note)
|
|
215
|
+
|
|
216
|
+
sections.extend([
|
|
217
|
+
" - Avoid using PowerShell to run commands that have dedicated tools, unless explicitly instructed:",
|
|
218
|
+
" - File search: Use Glob (NOT Get-ChildItem -Recurse)",
|
|
219
|
+
" - Content search: Use Grep (NOT Select-String)",
|
|
220
|
+
" - Read files: Use Read (NOT Get-Content)",
|
|
221
|
+
" - Edit files: Use Edit",
|
|
222
|
+
" - Write files: Use Write (NOT Set-Content/Out-File)",
|
|
223
|
+
" - Communication: Output text directly (NOT Write-Output/Write-Host)",
|
|
224
|
+
" - When issuing multiple commands:",
|
|
225
|
+
" - If the commands are independent and can run in parallel, make multiple PowerShell tool "
|
|
226
|
+
"calls in a single message.",
|
|
227
|
+
" - If the commands depend on each other and must run sequentially, chain them in a single "
|
|
228
|
+
"PowerShell call (see edition-specific chaining syntax above).",
|
|
229
|
+
" - Use `;` only when you need to run commands sequentially but don't care if earlier commands fail.",
|
|
230
|
+
" - DO NOT use newlines to separate commands (newlines are ok in quoted strings and here-strings)",
|
|
231
|
+
" - Do NOT prefix commands with `cd` or `Set-Location` -- the working directory is already set "
|
|
232
|
+
"to the correct project directory automatically.",
|
|
233
|
+
])
|
|
234
|
+
|
|
235
|
+
if sleep_guidance is not None:
|
|
236
|
+
sections.append(sleep_guidance)
|
|
237
|
+
|
|
238
|
+
sections.extend([
|
|
239
|
+
" - For git commands:",
|
|
240
|
+
" - Prefer to create a new commit rather than amending an existing commit.",
|
|
241
|
+
" - Before running destructive operations (e.g., git reset --hard, git push --force, "
|
|
242
|
+
"git checkout --), consider whether there is a safer alternative that achieves the same goal. "
|
|
243
|
+
"Only use destructive operations when they are truly the best approach.",
|
|
244
|
+
" - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) "
|
|
245
|
+
"unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.",
|
|
246
|
+
])
|
|
247
|
+
|
|
248
|
+
return "\n".join(sections)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class PowerShellToolInput(BaseModel):
|
|
252
|
+
"""Arguments for the powershell tool."""
|
|
253
|
+
|
|
254
|
+
command: str = Field(description="PowerShell command to execute")
|
|
255
|
+
cwd: str | None = Field(default=None, description="Working directory override")
|
|
256
|
+
timeout_ms: int = Field(default=120000, ge=1000, le=600000)
|
|
257
|
+
run_in_background: bool = Field(
|
|
258
|
+
default=False,
|
|
259
|
+
description="Set to true to run this command in the background",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class PowerShellTool(BaseTool):
|
|
264
|
+
"""执行 PowerShell 命令并捕获标准输出/错误。
|
|
265
|
+
|
|
266
|
+
用于在 Windows 平台上执行 PowerShell 命令。
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
name = "powershell"
|
|
270
|
+
description = _build_powershell_description()
|
|
271
|
+
input_model = PowerShellToolInput
|
|
272
|
+
|
|
273
|
+
async def execute(self, arguments: PowerShellToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
274
|
+
# 查找 PowerShell
|
|
275
|
+
powershell = _find_powershell()
|
|
276
|
+
if powershell is None:
|
|
277
|
+
return ToolResult(output="PowerShell is not available on this machine", is_error=True)
|
|
278
|
+
|
|
279
|
+
# 解析工作目录
|
|
280
|
+
cwd = Path(arguments.cwd).expanduser() if arguments.cwd else context.cwd
|
|
281
|
+
|
|
282
|
+
# 确定版本特定的标志
|
|
283
|
+
edition = _get_powershell_edition(powershell)
|
|
284
|
+
if edition == "core":
|
|
285
|
+
# pwsh 7+ 支持 -NoProfile -NonInteractive -Command
|
|
286
|
+
args = ["-NoProfile", "-NonInteractive", "-Command", arguments.command]
|
|
287
|
+
else:
|
|
288
|
+
# Windows PowerShell 5.1
|
|
289
|
+
args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", arguments.command]
|
|
290
|
+
|
|
291
|
+
# 创建子进程
|
|
292
|
+
kwargs: dict = {}
|
|
293
|
+
if sys.platform == "win32":
|
|
294
|
+
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
295
|
+
process = await asyncio.create_subprocess_exec(
|
|
296
|
+
powershell,
|
|
297
|
+
*args,
|
|
298
|
+
cwd=str(cwd.resolve()),
|
|
299
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
300
|
+
stdout=asyncio.subprocess.PIPE,
|
|
301
|
+
stderr=asyncio.subprocess.PIPE,
|
|
302
|
+
**kwargs,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# 后台运行模式
|
|
306
|
+
if arguments.run_in_background:
|
|
307
|
+
async def _background_wait():
|
|
308
|
+
try:
|
|
309
|
+
# 必须消费 stdout/stderr,避免管道缓冲区满导致进程挂起
|
|
310
|
+
stdout_task = asyncio.create_task(process.stdout.read())
|
|
311
|
+
stderr_task = asyncio.create_task(process.stderr.read())
|
|
312
|
+
await process.wait()
|
|
313
|
+
stdout_task.cancel()
|
|
314
|
+
stderr_task.cancel()
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
asyncio.create_task(_background_wait(), name=f"ps-bg-{process.pid}")
|
|
319
|
+
return ToolResult(
|
|
320
|
+
output=f"Command launched in background (pid={process.pid})",
|
|
321
|
+
is_error=False,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# 执行命令并归一化结果
|
|
325
|
+
timeout_seconds = arguments.timeout_ms // 1000
|
|
326
|
+
result = await CommandExecutor.run_and_normalize(
|
|
327
|
+
process,
|
|
328
|
+
timeout=timeout_seconds,
|
|
329
|
+
)
|
|
330
|
+
return ToolResult(
|
|
331
|
+
output=result.output,
|
|
332
|
+
is_error=result.is_error,
|
|
333
|
+
metadata=dict(result.metadata),
|
|
334
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP 资源读取工具
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
本模块提供读取 MCP 资源的功能。
|
|
6
|
+
|
|
7
|
+
主要组件:
|
|
8
|
+
- ReadMcpResourceTool: 从 MCP 服务器读取资源
|
|
9
|
+
|
|
10
|
+
使用示例:
|
|
11
|
+
>>> from illusion.tools import ReadMcpResourceTool
|
|
12
|
+
>>> tool = ReadMcpResourceTool(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 ReadMcpResourceToolInput(BaseModel):
|
|
24
|
+
"""MCP 资源读取参数。
|
|
25
|
+
|
|
26
|
+
属性:
|
|
27
|
+
server: MCP 服务器名称
|
|
28
|
+
uri: 资源 URI
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
server: str = Field(description="MCP server name")
|
|
32
|
+
uri: str = Field(description="Resource URI")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReadMcpResourceTool(BaseTool):
|
|
36
|
+
"""从 MCP 服务器读取一个资源。
|
|
37
|
+
|
|
38
|
+
用于访问 MCP 服务器提供的资源内容。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
name = "read_mcp_resource"
|
|
42
|
+
description = """Reads a specific resource from an MCP server, identified by server name and resource URI.
|
|
43
|
+
|
|
44
|
+
Parameters:
|
|
45
|
+
- server (required): The name of the MCP server from which to read the resource
|
|
46
|
+
- uri (required): The URI of the resource to read"""
|
|
47
|
+
input_model = ReadMcpResourceToolInput
|
|
48
|
+
|
|
49
|
+
def __init__(self, manager: McpClientManager) -> None:
|
|
50
|
+
self._manager = manager
|
|
51
|
+
|
|
52
|
+
def is_read_only(self, arguments: ReadMcpResourceToolInput) -> bool:
|
|
53
|
+
del arguments
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
async def execute(self, arguments: ReadMcpResourceToolInput, context: ToolExecutionContext) -> ToolResult:
|
|
57
|
+
del context
|
|
58
|
+
# 读取资源
|
|
59
|
+
try:
|
|
60
|
+
output = await self._manager.read_resource(arguments.server, arguments.uri)
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
return ToolResult(output=str(exc), is_error=True)
|
|
63
|
+
return ToolResult(output=output)
|