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,1934 @@
|
|
|
1
|
+
"""
|
|
2
|
+
斜杠命令注册模块
|
|
3
|
+
==============
|
|
4
|
+
|
|
5
|
+
本模块提供 IllusionCode 斜杠命令的注册和管理功能。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 注册和管理斜杠命令 (/xxx)
|
|
9
|
+
- 解析命令参数
|
|
10
|
+
- 提供内置命令处理器
|
|
11
|
+
|
|
12
|
+
类说明:
|
|
13
|
+
- CommandResult: 命令执行结果
|
|
14
|
+
- CommandContext: 命令执行上下文
|
|
15
|
+
- SlashCommand: 斜杠命令定义
|
|
16
|
+
- CommandRegistry: 命令注册表
|
|
17
|
+
|
|
18
|
+
函数说明:
|
|
19
|
+
- create_default_command_registry: 创建默认命令注册表
|
|
20
|
+
|
|
21
|
+
内置命令列表:
|
|
22
|
+
- /exit, /clear, /version, /status, /context, /summary
|
|
23
|
+
- /compact, /memory, /hooks, /resume
|
|
24
|
+
- /export, /share, /copy, /rewind, /files
|
|
25
|
+
- /init, /bridge, /login, /logout, /feedback
|
|
26
|
+
- /skills, /config, /mcp, /plugin, /reload-plugins
|
|
27
|
+
- /permissions, /plan, /thinking, /fast, /effort, /passes, /turns
|
|
28
|
+
- /continue, /model, /language, /output-style
|
|
29
|
+
- /doctor, /diff, /branch, /commit
|
|
30
|
+
- /issue, /pr_comments, /privacy-settings
|
|
31
|
+
- /delete, /rules
|
|
32
|
+
|
|
33
|
+
使用示例:
|
|
34
|
+
>>> from illusion.commands import create_default_command_registry
|
|
35
|
+
>>> registry = create_default_command_registry()
|
|
36
|
+
>>> result = registry.lookup("/version")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import importlib.metadata
|
|
42
|
+
import json
|
|
43
|
+
import subprocess
|
|
44
|
+
import sys
|
|
45
|
+
|
|
46
|
+
from illusion.config.i18n import (
|
|
47
|
+
COMMAND_DESCRIPTIONS_ZH,
|
|
48
|
+
_is_zh,
|
|
49
|
+
translate_command_message,
|
|
50
|
+
)
|
|
51
|
+
from datetime import datetime, timezone
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any, TYPE_CHECKING, Awaitable, Callable, Literal, get_args
|
|
55
|
+
|
|
56
|
+
import pyperclip
|
|
57
|
+
|
|
58
|
+
from illusion.config.paths import (
|
|
59
|
+
get_config_dir,
|
|
60
|
+
get_data_dir,
|
|
61
|
+
get_feedback_log_path,
|
|
62
|
+
get_project_config_dir,
|
|
63
|
+
get_project_issue_file,
|
|
64
|
+
get_project_pr_comments_file,
|
|
65
|
+
)
|
|
66
|
+
from illusion.bridge import get_bridge_manager
|
|
67
|
+
from illusion.bridge.types import WorkSecret
|
|
68
|
+
from illusion.bridge.work_secret import build_sdk_url, decode_work_secret, encode_work_secret
|
|
69
|
+
from illusion.api.provider import auth_status, detect_provider
|
|
70
|
+
from illusion.config.settings import Settings, load_settings, save_settings
|
|
71
|
+
from illusion.engine.messages import ConversationMessage
|
|
72
|
+
from illusion.engine.query_engine import QueryEngine
|
|
73
|
+
from illusion.memory import (
|
|
74
|
+
add_memory_entry,
|
|
75
|
+
get_memory_entrypoint,
|
|
76
|
+
get_project_memory_dir,
|
|
77
|
+
list_memory_files,
|
|
78
|
+
remove_memory_entry,
|
|
79
|
+
)
|
|
80
|
+
from illusion.output_styles import load_output_styles
|
|
81
|
+
from illusion.permissions import PermissionChecker, PermissionMode
|
|
82
|
+
from illusion.plugins import load_plugins
|
|
83
|
+
from illusion.prompts import build_runtime_system_prompt
|
|
84
|
+
from illusion.plugins.installer import install_plugin_from_path, uninstall_plugin
|
|
85
|
+
from illusion.services import (
|
|
86
|
+
estimate_conversation_tokens,
|
|
87
|
+
export_session_markdown,
|
|
88
|
+
save_session_snapshot,
|
|
89
|
+
summarize_messages,
|
|
90
|
+
)
|
|
91
|
+
from illusion.services.session_storage import get_project_session_dir, load_session_snapshot
|
|
92
|
+
from illusion.services.file_history import rewind_to
|
|
93
|
+
from illusion.skills import load_skill_registry
|
|
94
|
+
|
|
95
|
+
if TYPE_CHECKING:
|
|
96
|
+
from illusion.state import AppStateStore
|
|
97
|
+
from illusion.tools.base import ToolRegistry
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class CommandResult:
|
|
102
|
+
"""斜杠命令执行结果
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
message: 返回给用户的消息
|
|
106
|
+
should_exit: 是否应该退出程序
|
|
107
|
+
clear_screen: 是否应该清除屏幕
|
|
108
|
+
replay_messages: 要在TUI中重放的消息列表
|
|
109
|
+
continue_pending: 是否继续待处理的工具循环
|
|
110
|
+
continue_turns: 继续的回合数
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
message: str | None = None # 返回消息
|
|
114
|
+
should_exit: bool = False # 退出标志
|
|
115
|
+
clear_screen: bool = False # 清屏标志
|
|
116
|
+
replay_messages: list | None = None # ConversationMessage列表用于TUI重放
|
|
117
|
+
needs_api_rebuild: bool = False # 需要重建 API 客户端(跨 env 切换模型时)
|
|
118
|
+
continue_pending: bool = False # 继续待处理标志
|
|
119
|
+
continue_turns: int | None = None # 继续回合数
|
|
120
|
+
reset_session: bool = False # 是否重置会话ID
|
|
121
|
+
restored_session_id: str | None = None # 恢复的会话ID
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _resolve_ui_language(context: "CommandContext | None") -> str:
|
|
125
|
+
if context is not None and context.app_state is not None:
|
|
126
|
+
value = str(context.app_state.get().ui_language or "")
|
|
127
|
+
if value:
|
|
128
|
+
return value
|
|
129
|
+
return str(load_settings().ui_language)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _translate_command_message(message: str, *, locale: str) -> str:
|
|
135
|
+
"""翻译命令消息(委托给 i18n 模块)"""
|
|
136
|
+
return translate_command_message(message, locale=locale)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class CommandContext:
|
|
141
|
+
"""命令处理器可用的上下文
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
engine: 查询引擎实例
|
|
145
|
+
hooks_summary: hooks摘要
|
|
146
|
+
mcp_summary: MCP摘要
|
|
147
|
+
plugin_summary: 插件摘要
|
|
148
|
+
cwd: 当前工作目录
|
|
149
|
+
tool_registry: 工具注册表
|
|
150
|
+
app_state: 应用状态存储
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
engine: QueryEngine # 查询引擎
|
|
154
|
+
hooks_summary: str = "" # hooks摘要
|
|
155
|
+
mcp_summary: str = "" # MCP摘要
|
|
156
|
+
plugin_summary: str = "" # 插件摘要
|
|
157
|
+
cwd: str = "." # 当前工作目录
|
|
158
|
+
tool_registry: ToolRegistry | None = None # 工具注册表
|
|
159
|
+
app_state: AppStateStore | None = None # 应用状态
|
|
160
|
+
session_id: str = "" # 当前会话ID
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# 命令处理器类型别名
|
|
164
|
+
CommandHandler = Callable[[str, CommandContext], Awaitable[CommandResult]]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class SlashCommand:
|
|
169
|
+
"""斜杠命令定义
|
|
170
|
+
|
|
171
|
+
Attributes:
|
|
172
|
+
name: 命令名称 (不含前导/)
|
|
173
|
+
description: 命令描述
|
|
174
|
+
handler: 命令处理器函数
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
name: str # 命令名称
|
|
178
|
+
description: str # 命令描述
|
|
179
|
+
handler: CommandHandler # 处理器函数
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CommandRegistry:
|
|
183
|
+
"""斜杠命令到处理器的映射容器
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
_commands: 命令名到SlashCommand的映射
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(self) -> None:
|
|
190
|
+
self._commands: dict[str, SlashCommand] = {} # 命令映射初始化
|
|
191
|
+
|
|
192
|
+
def register(self, command: SlashCommand) -> None:
|
|
193
|
+
"""注册命令
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
command: 要注册的SlashCommand
|
|
197
|
+
"""
|
|
198
|
+
original_handler = command.handler
|
|
199
|
+
|
|
200
|
+
async def _localized_handler(args: str, context: CommandContext) -> CommandResult:
|
|
201
|
+
result = await original_handler(args, context)
|
|
202
|
+
if result.message:
|
|
203
|
+
result.message = _translate_command_message(
|
|
204
|
+
result.message,
|
|
205
|
+
locale=_resolve_ui_language(context),
|
|
206
|
+
)
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
self._commands[command.name] = SlashCommand(
|
|
210
|
+
name=command.name,
|
|
211
|
+
description=command.description,
|
|
212
|
+
handler=_localized_handler,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def lookup(self, raw_input: str) -> tuple[SlashCommand, str] | None:
|
|
216
|
+
"""解析斜杠命令并返回其处理器和原始参数
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
raw_input: 原始输入字符串
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
tuple[SlashCommand, str] | None: (命令对象, 参数) 或 None
|
|
223
|
+
"""
|
|
224
|
+
if not raw_input.startswith("/"): # 不是斜杠命令
|
|
225
|
+
return None
|
|
226
|
+
name, _, args = raw_input[1:].partition(" ") # 分割名称和参数
|
|
227
|
+
command = self._commands.get(name) # 查找命令
|
|
228
|
+
if command is None: # 未找到
|
|
229
|
+
return None
|
|
230
|
+
return command, args.strip() # 返回命令和参数
|
|
231
|
+
|
|
232
|
+
def help_text(self) -> str:
|
|
233
|
+
"""返回所有已注册命令的格式化摘要
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
str: 格式化的命令帮助文本
|
|
237
|
+
"""
|
|
238
|
+
locale = _resolve_ui_language(None)
|
|
239
|
+
lines = ["可用命令:" if _is_zh(locale) else "Available commands:"] # 标题
|
|
240
|
+
for command in sorted(self._commands.values(), key=lambda item: item.name): # 遍历命令
|
|
241
|
+
description = command.description
|
|
242
|
+
if _is_zh(locale):
|
|
243
|
+
description = COMMAND_DESCRIPTIONS_ZH.get(command.name, description)
|
|
244
|
+
lines.append(f"/{command.name:<12} {description}") # 格式化输出
|
|
245
|
+
return "\n".join(lines)
|
|
246
|
+
|
|
247
|
+
def list_commands(self) -> list[SlashCommand]:
|
|
248
|
+
"""按照注册顺序返回命令列表
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
list[SlashCommand]: 命令列表
|
|
252
|
+
"""
|
|
253
|
+
return list(self._commands.values())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _run_git_command(cwd: str, *args: str) -> tuple[bool, str]:
|
|
257
|
+
"""执行git命令并返回结果
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
cwd: 工作目录
|
|
261
|
+
args: git子命令和参数
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
tuple[bool, str]: (是否成功, 输出内容)
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
run_kwargs: dict = {}
|
|
268
|
+
if sys.platform == "win32":
|
|
269
|
+
run_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
270
|
+
completed = subprocess.run(
|
|
271
|
+
["git", *args],
|
|
272
|
+
cwd=cwd,
|
|
273
|
+
capture_output=True,
|
|
274
|
+
text=True,
|
|
275
|
+
check=False,
|
|
276
|
+
stdin=subprocess.DEVNULL,
|
|
277
|
+
**run_kwargs,
|
|
278
|
+
)
|
|
279
|
+
except FileNotFoundError: # git未安装
|
|
280
|
+
return False, "git is not installed."
|
|
281
|
+
output = (completed.stdout or completed.stderr).strip() # 合并输出
|
|
282
|
+
if completed.returncode != 0: # 失败
|
|
283
|
+
return False, output or f"git {' '.join(args)} failed"
|
|
284
|
+
return True, output # 成功
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _copy_to_clipboard(text: str) -> tuple[bool, str]:
|
|
288
|
+
"""复制文本到剪贴板
|
|
289
|
+
|
|
290
|
+
尝试多种复制方式: pyperclip, pbcopy, wl-copy, xclip, xsel
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
text: 要复制的文本
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
tuple[bool, str]: (是否成功, 目标位置)
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
pyperclip.copy(text)
|
|
300
|
+
return True, "clipboard"
|
|
301
|
+
except Exception:
|
|
302
|
+
clip_kwargs: dict = {}
|
|
303
|
+
if sys.platform == "win32":
|
|
304
|
+
clip_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
|
305
|
+
for command in (["pbcopy"], ["wl-copy"], ["xclip", "-selection", "clipboard"], ["xsel", "--clipboard"]):
|
|
306
|
+
try:
|
|
307
|
+
subprocess.run(command, input=text, text=True, check=True, capture_output=True, **clip_kwargs)
|
|
308
|
+
return True, "clipboard"
|
|
309
|
+
except Exception:
|
|
310
|
+
continue
|
|
311
|
+
fallback = get_data_dir() / "last_copy.txt" # 后备方案:文件
|
|
312
|
+
fallback.write_text(text, encoding="utf-8")
|
|
313
|
+
return False, str(fallback)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _last_message_text(messages: list[ConversationMessage]) -> str:
|
|
317
|
+
"""获取最后一条有内容的用户消息
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
messages: 消息列表
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
str: 消息文本,空字符串若无
|
|
324
|
+
"""
|
|
325
|
+
for message in reversed(messages): # 反向遍历
|
|
326
|
+
if message.text.strip(): # 有内容
|
|
327
|
+
return message.text.strip()
|
|
328
|
+
return ""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _rewind_turns(messages: list[ConversationMessage], turns: int) -> list[ConversationMessage]:
|
|
332
|
+
"""回退指定数量的对话回合
|
|
333
|
+
|
|
334
|
+
回退到上一个非空的、非斜杠命令的 user 消息
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
messages: 消息列表
|
|
338
|
+
turns: 回退回合数
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
list[ConversationMessage]: 回退后的消息列表
|
|
342
|
+
"""
|
|
343
|
+
updated = list(messages)
|
|
344
|
+
for _ in range(max(0, turns)):
|
|
345
|
+
if not updated:
|
|
346
|
+
break
|
|
347
|
+
while updated:
|
|
348
|
+
popped = updated.pop()
|
|
349
|
+
if popped.role == "user" and popped.text.strip() and not popped.text.strip().startswith("/"):
|
|
350
|
+
break
|
|
351
|
+
return updated
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _coerce_setting_value(settings: Settings, key: str, raw: str):
|
|
355
|
+
"""将字符串值强制转换为设置字段的正确类型
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
settings: 设置对象
|
|
359
|
+
key: 字段名
|
|
360
|
+
raw: 原始字符串值
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
转换后的值
|
|
364
|
+
|
|
365
|
+
Raises:
|
|
366
|
+
KeyError: 字段不存在
|
|
367
|
+
ValueError: 值无效
|
|
368
|
+
"""
|
|
369
|
+
field = Settings.model_fields.get(key) # 获取字段定义
|
|
370
|
+
if field is None: # 不存在
|
|
371
|
+
raise KeyError(key)
|
|
372
|
+
annotation = field.annotation # 类型注解
|
|
373
|
+
if annotation is bool: # 布尔类型
|
|
374
|
+
lowered = raw.lower()
|
|
375
|
+
if lowered in {"1", "true", "yes", "on"}: # 真值
|
|
376
|
+
return True
|
|
377
|
+
if lowered in {"0", "false", "no", "off"}: # 假值
|
|
378
|
+
return False
|
|
379
|
+
raise ValueError(f"Invalid boolean value for {key}: {raw}")
|
|
380
|
+
if annotation is int: # 整数类型
|
|
381
|
+
return int(raw)
|
|
382
|
+
if annotation is str: # 字符串类型
|
|
383
|
+
return raw
|
|
384
|
+
if annotation is Literal or getattr(annotation, "__origin__", None) is Literal: # 字面量类型
|
|
385
|
+
allowed = get_args(annotation)
|
|
386
|
+
if raw not in allowed: # 不在允许值中
|
|
387
|
+
raise ValueError(f"Invalid value for {key}: {raw}")
|
|
388
|
+
return raw
|
|
389
|
+
return raw
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _explore_codebase(root: Path) -> dict[str, Any]:
|
|
393
|
+
"""探索代码库结构,识别项目类型和工具链
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
root: 项目根目录
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
包含项目信息的字典
|
|
400
|
+
"""
|
|
401
|
+
findings: dict[str, Any] = {
|
|
402
|
+
"languages": [],
|
|
403
|
+
"frameworks": [],
|
|
404
|
+
"package_manager": None,
|
|
405
|
+
"build_commands": [],
|
|
406
|
+
"test_commands": [],
|
|
407
|
+
"lint_commands": [],
|
|
408
|
+
"format_commands": [],
|
|
409
|
+
"existing_configs": [],
|
|
410
|
+
"ci_config": None,
|
|
411
|
+
"has_gitignore": False,
|
|
412
|
+
"readme_summary": None,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# 扫描文件结构
|
|
416
|
+
try:
|
|
417
|
+
all_files = [
|
|
418
|
+
p for p in root.rglob("*")
|
|
419
|
+
if p.is_file()
|
|
420
|
+
and ".git" not in p.parts
|
|
421
|
+
and ".venv" not in p.parts
|
|
422
|
+
and "node_modules" not in p.parts
|
|
423
|
+
and "__pycache__" not in p.parts
|
|
424
|
+
]
|
|
425
|
+
except Exception:
|
|
426
|
+
all_files = []
|
|
427
|
+
|
|
428
|
+
# 检测语言
|
|
429
|
+
lang_indicators = {
|
|
430
|
+
".py": "Python",
|
|
431
|
+
".js": "JavaScript",
|
|
432
|
+
".ts": "TypeScript",
|
|
433
|
+
".jsx": "React",
|
|
434
|
+
".tsx": "React",
|
|
435
|
+
".java": "Java",
|
|
436
|
+
".go": "Go",
|
|
437
|
+
".rs": "Rust",
|
|
438
|
+
".rb": "Ruby",
|
|
439
|
+
".php": "PHP",
|
|
440
|
+
".cs": "C#",
|
|
441
|
+
".cpp": "C++",
|
|
442
|
+
".c": "C",
|
|
443
|
+
".swift": "Swift",
|
|
444
|
+
".kt": "Kotlin",
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
detected_langs = set()
|
|
448
|
+
for f in all_files:
|
|
449
|
+
if f.suffix in lang_indicators:
|
|
450
|
+
detected_langs.add(lang_indicators[f.suffix])
|
|
451
|
+
findings["languages"] = sorted(detected_langs)
|
|
452
|
+
|
|
453
|
+
# 检测框架和语言指示文件
|
|
454
|
+
framework_indicators = {
|
|
455
|
+
"package.json": None,
|
|
456
|
+
"requirements.txt": "Python",
|
|
457
|
+
"pyproject.toml": "Python",
|
|
458
|
+
"setup.py": "Python",
|
|
459
|
+
"Cargo.toml": "Rust",
|
|
460
|
+
"go.mod": "Go",
|
|
461
|
+
"pom.xml": "Java",
|
|
462
|
+
"build.gradle": "Java",
|
|
463
|
+
"Gemfile": "Ruby",
|
|
464
|
+
"composer.json": "PHP",
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for indicator, lang in framework_indicators.items():
|
|
468
|
+
if (root / indicator).exists():
|
|
469
|
+
if lang and lang not in findings["languages"]:
|
|
470
|
+
findings["languages"].append(lang)
|
|
471
|
+
|
|
472
|
+
# 检测配置文件关联的语言
|
|
473
|
+
config_lang_indicators = {
|
|
474
|
+
"tsconfig.json": "TypeScript",
|
|
475
|
+
"jsconfig.json": "JavaScript",
|
|
476
|
+
"webpack.config.js": "JavaScript",
|
|
477
|
+
"vite.config.ts": "TypeScript",
|
|
478
|
+
"vite.config.js": "JavaScript",
|
|
479
|
+
"next.config.js": "Next.js",
|
|
480
|
+
"nuxt.config.js": "Nuxt",
|
|
481
|
+
"angular.json": "Angular",
|
|
482
|
+
"vue.config.js": "Vue",
|
|
483
|
+
"svelte.config.js": "Svelte",
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for config_file, lang in config_lang_indicators.items():
|
|
487
|
+
if (root / config_file).exists():
|
|
488
|
+
if lang not in findings["languages"] and lang not in ["Next.js", "Nuxt", "Angular", "Vue", "Svelte"]:
|
|
489
|
+
findings["languages"].append(lang)
|
|
490
|
+
elif lang in ["Next.js", "Nuxt", "Angular", "Vue", "Svelte"] and lang not in findings["frameworks"]:
|
|
491
|
+
findings["frameworks"].append(lang)
|
|
492
|
+
|
|
493
|
+
# 检测包管理器
|
|
494
|
+
if (root / "package.json").exists():
|
|
495
|
+
if (root / "yarn.lock").exists():
|
|
496
|
+
findings["package_manager"] = "yarn"
|
|
497
|
+
elif (root / "pnpm-lock.yaml").exists():
|
|
498
|
+
findings["package_manager"] = "pnpm"
|
|
499
|
+
else:
|
|
500
|
+
findings["package_manager"] = "npm"
|
|
501
|
+
elif (root / "requirements.txt").exists() or (root / "pyproject.toml").exists():
|
|
502
|
+
findings["package_manager"] = "pip"
|
|
503
|
+
elif (root / "Cargo.toml").exists():
|
|
504
|
+
findings["package_manager"] = "cargo"
|
|
505
|
+
elif (root / "go.mod").exists():
|
|
506
|
+
findings["package_manager"] = "go"
|
|
507
|
+
|
|
508
|
+
# 读取 package.json 获取脚本
|
|
509
|
+
package_json = root / "package.json"
|
|
510
|
+
if package_json.exists():
|
|
511
|
+
try:
|
|
512
|
+
with open(package_json, encoding="utf-8") as f:
|
|
513
|
+
data = json.load(f)
|
|
514
|
+
scripts = data.get("scripts", {})
|
|
515
|
+
if "build" in scripts:
|
|
516
|
+
findings["build_commands"].append("npm run build")
|
|
517
|
+
if "test" in scripts:
|
|
518
|
+
findings["test_commands"].append("npm test")
|
|
519
|
+
if "lint" in scripts:
|
|
520
|
+
findings["lint_commands"].append("npm run lint")
|
|
521
|
+
if "dev" in scripts:
|
|
522
|
+
findings["build_commands"].append("npm run dev")
|
|
523
|
+
if "format" in scripts:
|
|
524
|
+
findings["format_commands"].append("npm run format")
|
|
525
|
+
except Exception:
|
|
526
|
+
pass
|
|
527
|
+
|
|
528
|
+
# 读取 pyproject.toml 获取命令
|
|
529
|
+
pyproject = root / "pyproject.toml"
|
|
530
|
+
if pyproject.exists():
|
|
531
|
+
try:
|
|
532
|
+
import tomllib
|
|
533
|
+
with open(pyproject, "rb") as f:
|
|
534
|
+
data = tomllib.load(f)
|
|
535
|
+
scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
|
|
536
|
+
if scripts:
|
|
537
|
+
findings["build_commands"].append("poetry run <script>")
|
|
538
|
+
# 检测 ruff/black 格式化配置
|
|
539
|
+
if "tool" in data:
|
|
540
|
+
if "ruff" in data["tool"]:
|
|
541
|
+
findings["format_commands"].append("ruff format")
|
|
542
|
+
if "lint" not in findings["lint_commands"]:
|
|
543
|
+
findings["lint_commands"].append("ruff check")
|
|
544
|
+
if "black" in data["tool"]:
|
|
545
|
+
findings["format_commands"].append("black")
|
|
546
|
+
except Exception:
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
# 检测 Makefile
|
|
550
|
+
makefile = root / "Makefile"
|
|
551
|
+
if makefile.exists():
|
|
552
|
+
try:
|
|
553
|
+
content = makefile.read_text(encoding="utf-8")
|
|
554
|
+
if "build:" in content:
|
|
555
|
+
findings["build_commands"].append("make build")
|
|
556
|
+
if "test:" in content:
|
|
557
|
+
findings["test_commands"].append("make test")
|
|
558
|
+
if "lint:" in content:
|
|
559
|
+
findings["lint_commands"].append("make lint")
|
|
560
|
+
if "fmt:" in content or "format:" in content:
|
|
561
|
+
findings["format_commands"].append("make fmt")
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
565
|
+
# 检测格式化工具配置
|
|
566
|
+
format_configs = {
|
|
567
|
+
".prettierrc": "prettier",
|
|
568
|
+
".prettierrc.json": "prettier",
|
|
569
|
+
".prettierrc.js": "prettier",
|
|
570
|
+
"prettier.config.js": "prettier",
|
|
571
|
+
"biome.json": "biome",
|
|
572
|
+
".eslintrc": "eslint",
|
|
573
|
+
".eslintrc.json": "eslint",
|
|
574
|
+
".eslintrc.js": "eslint",
|
|
575
|
+
"eslint.config.js": "eslint",
|
|
576
|
+
".golangci.yml": "golangci-lint",
|
|
577
|
+
".golangci.yaml": "golangci-lint",
|
|
578
|
+
"rustfmt.toml": "rustfmt",
|
|
579
|
+
".rustfmt.toml": "rustfmt",
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for config_file, tool_name in format_configs.items():
|
|
583
|
+
if (root / config_file).exists():
|
|
584
|
+
if tool_name not in findings["format_commands"]:
|
|
585
|
+
findings["format_commands"].append(tool_name)
|
|
586
|
+
|
|
587
|
+
# 检测 CI 配置
|
|
588
|
+
ci_configs = {
|
|
589
|
+
".github/workflows": "GitHub Actions",
|
|
590
|
+
".gitlab-ci.yml": "GitLab CI",
|
|
591
|
+
"Jenkinsfile": "Jenkins",
|
|
592
|
+
".circleci/config.yml": "CircleCI",
|
|
593
|
+
".travis.yml": "Travis CI",
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
for ci_path, ci_name in ci_configs.items():
|
|
597
|
+
if (root / ci_path).exists():
|
|
598
|
+
findings["ci_config"] = ci_name
|
|
599
|
+
break
|
|
600
|
+
|
|
601
|
+
# 检测 .gitignore
|
|
602
|
+
findings["has_gitignore"] = (root / ".gitignore").exists()
|
|
603
|
+
|
|
604
|
+
# 检测现有 AI 配置
|
|
605
|
+
ai_configs = [
|
|
606
|
+
".cursor/rules",
|
|
607
|
+
".cursorrules",
|
|
608
|
+
".github/copilot-instructions.md",
|
|
609
|
+
".windsurfrules",
|
|
610
|
+
".clinerules",
|
|
611
|
+
"AGENTS.md",
|
|
612
|
+
"CLAUDE.md",
|
|
613
|
+
"ILLUSION.md",
|
|
614
|
+
]
|
|
615
|
+
for config in ai_configs:
|
|
616
|
+
if (root / config).exists():
|
|
617
|
+
findings["existing_configs"].append(config)
|
|
618
|
+
|
|
619
|
+
# 检测常用框架
|
|
620
|
+
if (root / "package.json").exists():
|
|
621
|
+
try:
|
|
622
|
+
with open(root / "package.json", encoding="utf-8") as f:
|
|
623
|
+
data = json.load(f)
|
|
624
|
+
deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
625
|
+
if "react" in deps:
|
|
626
|
+
findings["frameworks"].append("React")
|
|
627
|
+
if "vue" in deps:
|
|
628
|
+
findings["frameworks"].append("Vue")
|
|
629
|
+
if "svelte" in deps:
|
|
630
|
+
findings["frameworks"].append("Svelte")
|
|
631
|
+
if "next" in deps:
|
|
632
|
+
findings["frameworks"].append("Next.js")
|
|
633
|
+
if "nuxt" in deps:
|
|
634
|
+
findings["frameworks"].append("Nuxt")
|
|
635
|
+
if "express" in deps:
|
|
636
|
+
findings["frameworks"].append("Express")
|
|
637
|
+
if "fastapi" in deps:
|
|
638
|
+
findings["frameworks"].append("FastAPI")
|
|
639
|
+
if "django" in deps:
|
|
640
|
+
findings["frameworks"].append("Django")
|
|
641
|
+
if "flask" in deps:
|
|
642
|
+
findings["frameworks"].append("Flask")
|
|
643
|
+
if "angular" in deps or "@angular/core" in deps:
|
|
644
|
+
findings["frameworks"].append("Angular")
|
|
645
|
+
except Exception:
|
|
646
|
+
pass
|
|
647
|
+
|
|
648
|
+
# 提取 README 摘要
|
|
649
|
+
readme = root / "README.md"
|
|
650
|
+
if readme.exists():
|
|
651
|
+
try:
|
|
652
|
+
content = readme.read_text(encoding="utf-8")
|
|
653
|
+
# 提取前几行作为项目描述,跳过 HTML 和图片
|
|
654
|
+
desc_lines = []
|
|
655
|
+
in_code_block = False
|
|
656
|
+
in_html_block = False
|
|
657
|
+
for line in content.split("\n")[:50]:
|
|
658
|
+
stripped = line.strip()
|
|
659
|
+
# 跳过代码块
|
|
660
|
+
if stripped.startswith("```"):
|
|
661
|
+
in_code_block = not in_code_block
|
|
662
|
+
continue
|
|
663
|
+
if in_code_block:
|
|
664
|
+
continue
|
|
665
|
+
# 跳过 HTML 块
|
|
666
|
+
if stripped.startswith("<") and not stripped.startswith("</"):
|
|
667
|
+
if stripped.endswith(">") and "/" not in stripped:
|
|
668
|
+
in_html_block = True
|
|
669
|
+
continue
|
|
670
|
+
if in_html_block:
|
|
671
|
+
if stripped.startswith("</") or stripped.endswith("/>"):
|
|
672
|
+
in_html_block = False
|
|
673
|
+
continue
|
|
674
|
+
# 跳过图片和空行
|
|
675
|
+
if stripped.startswith("![") or not stripped:
|
|
676
|
+
continue
|
|
677
|
+
# 跳过标题行
|
|
678
|
+
if stripped.startswith("#"):
|
|
679
|
+
continue
|
|
680
|
+
# 跳过 badge 和链接
|
|
681
|
+
if stripped.startswith("[") and "badges" in stripped.lower():
|
|
682
|
+
continue
|
|
683
|
+
# 保留有意义的文本
|
|
684
|
+
if len(stripped) > 10: # 过短的行通常是装饰
|
|
685
|
+
desc_lines.append(stripped)
|
|
686
|
+
if len(desc_lines) >= 3:
|
|
687
|
+
break
|
|
688
|
+
if desc_lines:
|
|
689
|
+
findings["readme_summary"] = " ".join(desc_lines)
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
|
|
693
|
+
return findings
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _generate_claudemd(findings: dict[str, Any], root: Path) -> str:
|
|
697
|
+
"""基于项目发现生成 CLAUDE.md 内容
|
|
698
|
+
|
|
699
|
+
Args:
|
|
700
|
+
findings: 项目探索结果
|
|
701
|
+
root: 项目根目录
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
CLAUDE.md 内容
|
|
705
|
+
"""
|
|
706
|
+
lines = [
|
|
707
|
+
"# CLAUDE.md",
|
|
708
|
+
"",
|
|
709
|
+
"This file provides guidance to Illusion Code when working with code in this repository.",
|
|
710
|
+
"",
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
# 项目概述
|
|
714
|
+
if findings.get("readme_summary"):
|
|
715
|
+
lines.append("## 项目概述")
|
|
716
|
+
lines.append(findings["readme_summary"])
|
|
717
|
+
lines.append("")
|
|
718
|
+
|
|
719
|
+
# 技术栈
|
|
720
|
+
if findings["languages"] or findings["frameworks"] or findings["package_manager"]:
|
|
721
|
+
lines.append("## 技术栈")
|
|
722
|
+
if findings["languages"]:
|
|
723
|
+
lines.append(f"- 主要语言: {', '.join(findings['languages'])}")
|
|
724
|
+
if findings["frameworks"]:
|
|
725
|
+
lines.append(f"- 框架: {', '.join(findings['frameworks'])}")
|
|
726
|
+
if findings["package_manager"]:
|
|
727
|
+
lines.append(f"- 包管理器: {findings['package_manager']}")
|
|
728
|
+
if findings.get("ci_config"):
|
|
729
|
+
lines.append(f"- CI/CD: {findings['ci_config']}")
|
|
730
|
+
lines.append("")
|
|
731
|
+
|
|
732
|
+
# 常用命令
|
|
733
|
+
has_commands = any([
|
|
734
|
+
findings["build_commands"],
|
|
735
|
+
findings["test_commands"],
|
|
736
|
+
findings["lint_commands"],
|
|
737
|
+
findings.get("format_commands")
|
|
738
|
+
])
|
|
739
|
+
if has_commands:
|
|
740
|
+
lines.append("## 常用命令")
|
|
741
|
+
if findings["build_commands"]:
|
|
742
|
+
lines.append(f"- 构建: `{findings['build_commands'][0]}`")
|
|
743
|
+
if findings["test_commands"]:
|
|
744
|
+
lines.append(f"- 测试: `{findings['test_commands'][0]}`")
|
|
745
|
+
if findings["lint_commands"]:
|
|
746
|
+
lines.append(f"- 代码检查: `{findings['lint_commands'][0]}`")
|
|
747
|
+
if findings.get("format_commands"):
|
|
748
|
+
lines.append(f"- 格式化: `{findings['format_commands'][0]}`")
|
|
749
|
+
lines.append("")
|
|
750
|
+
|
|
751
|
+
# 开发规范
|
|
752
|
+
lines.extend([
|
|
753
|
+
"## 开发规范",
|
|
754
|
+
"- 修改后请验证测试是否通过",
|
|
755
|
+
"- 保持代码变更最小化",
|
|
756
|
+
])
|
|
757
|
+
|
|
758
|
+
# 如果有格式化工具,添加格式化规范
|
|
759
|
+
if findings.get("format_commands"):
|
|
760
|
+
lines.append(f"- 代码格式化: 使用 `{findings['format_commands'][0]}`")
|
|
761
|
+
|
|
762
|
+
lines.append("")
|
|
763
|
+
|
|
764
|
+
# 如果有现有配置,提示用户
|
|
765
|
+
if findings["existing_configs"]:
|
|
766
|
+
lines.append("## 现有 AI 配置")
|
|
767
|
+
lines.append("检测到以下 AI 配置文件,内容可能需要合并:")
|
|
768
|
+
for config in findings["existing_configs"]:
|
|
769
|
+
lines.append(f"- `{config}`")
|
|
770
|
+
lines.append("")
|
|
771
|
+
|
|
772
|
+
return "\n".join(lines)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def create_default_command_registry() -> CommandRegistry:
|
|
776
|
+
"""Create the built-in command registry."""
|
|
777
|
+
registry = CommandRegistry()
|
|
778
|
+
|
|
779
|
+
async def _exit_handler(_: str, context: CommandContext) -> CommandResult:
|
|
780
|
+
del context
|
|
781
|
+
return CommandResult(should_exit=True)
|
|
782
|
+
|
|
783
|
+
async def _new_handler(_: str, context: CommandContext) -> CommandResult:
|
|
784
|
+
if context.session_id and context.engine.messages:
|
|
785
|
+
settings = load_settings()
|
|
786
|
+
system_prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
|
|
787
|
+
save_session_snapshot(
|
|
788
|
+
cwd=context.cwd,
|
|
789
|
+
model=settings.active_model_name,
|
|
790
|
+
system_prompt=system_prompt,
|
|
791
|
+
messages=context.engine.messages,
|
|
792
|
+
usage=context.engine.total_usage,
|
|
793
|
+
session_id=context.session_id,
|
|
794
|
+
)
|
|
795
|
+
context.engine.clear()
|
|
796
|
+
return CommandResult(
|
|
797
|
+
message="Started a new conversation session.",
|
|
798
|
+
clear_screen=True,
|
|
799
|
+
reset_session=True,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
async def _status_handler(_: str, context: CommandContext) -> CommandResult:
|
|
803
|
+
usage = context.engine.total_usage
|
|
804
|
+
state = context.app_state.get() if context.app_state is not None else None
|
|
805
|
+
return CommandResult(
|
|
806
|
+
message=(
|
|
807
|
+
f"Messages: {len(context.engine.messages)}\n"
|
|
808
|
+
f"Usage: input={usage.input_tokens} output={usage.output_tokens}\n"
|
|
809
|
+
f"Effort: {state.effort if state is not None else load_settings().effort}\n"
|
|
810
|
+
f"Passes: {state.passes if state is not None else load_settings().passes}"
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
async def _version_handler(_: str, context: CommandContext) -> CommandResult:
|
|
815
|
+
del context
|
|
816
|
+
try:
|
|
817
|
+
version = importlib.metadata.version("illusion")
|
|
818
|
+
except importlib.metadata.PackageNotFoundError:
|
|
819
|
+
version = "0.1.0"
|
|
820
|
+
return CommandResult(message=f"IllusionCode {version}")
|
|
821
|
+
|
|
822
|
+
async def _context_handler(args: str, context: CommandContext) -> CommandResult:
|
|
823
|
+
settings = load_settings()
|
|
824
|
+
tokens = args.split(maxsplit=1)
|
|
825
|
+
subcommand = tokens[0] if tokens else "prompt"
|
|
826
|
+
|
|
827
|
+
if subcommand == "prompt":
|
|
828
|
+
prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
|
|
829
|
+
return CommandResult(message=prompt)
|
|
830
|
+
if subcommand == "window" or subcommand == "show":
|
|
831
|
+
return CommandResult(message=f"Context window: {settings.context_window:,} tokens")
|
|
832
|
+
if subcommand == "__usage__":
|
|
833
|
+
from illusion.services.compact import estimate_conversation_tokens, get_context_window
|
|
834
|
+
estimated = estimate_conversation_tokens(context.engine.messages)
|
|
835
|
+
usage = context.engine.total_usage
|
|
836
|
+
context_window = get_context_window(settings.active_model_name)
|
|
837
|
+
percentage = int(estimated * 100 / context_window) if context_window > 0 else 0
|
|
838
|
+
remaining = max(0, context_window - estimated)
|
|
839
|
+
return CommandResult(
|
|
840
|
+
message=(
|
|
841
|
+
f"Context Window: {context_window:,} tokens\n"
|
|
842
|
+
f"Estimated Used: ~{estimated:,} tokens ({percentage}%)\n"
|
|
843
|
+
f"Remaining: ~{remaining:,} tokens\n"
|
|
844
|
+
f"Actual API Usage: input={usage.input_tokens:,} output={usage.output_tokens:,}\n"
|
|
845
|
+
f"Messages: {len(context.engine.messages)}"
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
if subcommand == "set" and len(tokens) == 2:
|
|
849
|
+
try:
|
|
850
|
+
value = int(tokens[1])
|
|
851
|
+
if value <= 0:
|
|
852
|
+
return CommandResult(message="Error: context window must be positive")
|
|
853
|
+
settings.context_window = value
|
|
854
|
+
save_settings(settings)
|
|
855
|
+
return CommandResult(message=f"Context window set to {value:,} tokens")
|
|
856
|
+
except ValueError:
|
|
857
|
+
return CommandResult(message="Error: invalid number")
|
|
858
|
+
return CommandResult(message="Usage: /context [prompt|window|set N]")
|
|
859
|
+
|
|
860
|
+
async def _summary_handler(args: str, context: CommandContext) -> CommandResult:
|
|
861
|
+
max_messages = 8
|
|
862
|
+
if args:
|
|
863
|
+
try:
|
|
864
|
+
max_messages = max(1, int(args))
|
|
865
|
+
except ValueError:
|
|
866
|
+
return CommandResult(message="Usage: /summary [MAX_MESSAGES]")
|
|
867
|
+
summary = summarize_messages(context.engine.messages, max_messages=max_messages)
|
|
868
|
+
return CommandResult(message=summary or "No conversation content to summarize.")
|
|
869
|
+
|
|
870
|
+
async def _compact_handler(args: str, context: CommandContext) -> CommandResult:
|
|
871
|
+
from illusion.services.compact import compact_conversation, compact_messages
|
|
872
|
+
|
|
873
|
+
# 解析参数:/compact [PRESERVE_RECENT] 或 /compact [custom instructions text]
|
|
874
|
+
preserve_recent = 6
|
|
875
|
+
custom_instructions: str | None = None
|
|
876
|
+
|
|
877
|
+
if args:
|
|
878
|
+
stripped = args.strip()
|
|
879
|
+
# 尝试解析为数字(preserve_recent)
|
|
880
|
+
try:
|
|
881
|
+
preserve_recent = max(1, int(stripped))
|
|
882
|
+
except ValueError:
|
|
883
|
+
# 非数字则视为自定义指令
|
|
884
|
+
custom_instructions = stripped
|
|
885
|
+
|
|
886
|
+
before = len(context.engine.messages)
|
|
887
|
+
before_tokens = estimate_conversation_tokens(context.engine.messages)
|
|
888
|
+
|
|
889
|
+
# 优先尝试 LLM 摘要;如果 API 客户端不可用则回退到传统方法
|
|
890
|
+
try:
|
|
891
|
+
settings = load_settings()
|
|
892
|
+
system_prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
|
|
893
|
+
compacted = await compact_conversation(
|
|
894
|
+
context.engine.messages,
|
|
895
|
+
api_client=context.engine._api_client,
|
|
896
|
+
model=context.engine._model,
|
|
897
|
+
system_prompt=system_prompt,
|
|
898
|
+
preserve_recent=preserve_recent,
|
|
899
|
+
custom_instructions=custom_instructions,
|
|
900
|
+
suppress_follow_up=False,
|
|
901
|
+
)
|
|
902
|
+
except Exception as exc:
|
|
903
|
+
# LLM 摘要失败,回退到传统方法
|
|
904
|
+
import logging
|
|
905
|
+
logging.getLogger(__name__).warning("LLM compact failed, falling back to simple compact: %s", exc)
|
|
906
|
+
compacted = compact_messages(context.engine.messages, preserve_recent=preserve_recent)
|
|
907
|
+
|
|
908
|
+
context.engine.load_messages(compacted)
|
|
909
|
+
after_tokens = estimate_conversation_tokens(compacted)
|
|
910
|
+
saved = max(0, before_tokens - after_tokens)
|
|
911
|
+
from illusion.config.i18n import t
|
|
912
|
+
return CommandResult(
|
|
913
|
+
message=t("compact_result", before=before, after=len(compacted), saved=f"{saved:,}")
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
async def _memory_handler(args: str, context: CommandContext) -> CommandResult:
|
|
917
|
+
tokens = args.split(maxsplit=1)
|
|
918
|
+
if not tokens:
|
|
919
|
+
memory_dir = get_project_memory_dir(context.cwd)
|
|
920
|
+
entrypoint = get_memory_entrypoint(context.cwd)
|
|
921
|
+
return CommandResult(
|
|
922
|
+
message=f"Memory directory: {memory_dir}\nEntrypoint: {entrypoint}"
|
|
923
|
+
)
|
|
924
|
+
action = tokens[0]
|
|
925
|
+
rest = tokens[1] if len(tokens) == 2 else ""
|
|
926
|
+
if action == "list":
|
|
927
|
+
memory_files = list_memory_files(context.cwd)
|
|
928
|
+
if not memory_files:
|
|
929
|
+
return CommandResult(message="No memory files.")
|
|
930
|
+
return CommandResult(message="\n".join(path.name for path in memory_files))
|
|
931
|
+
if action == "show" and rest:
|
|
932
|
+
memory_dir = get_project_memory_dir(context.cwd)
|
|
933
|
+
path = memory_dir / rest
|
|
934
|
+
if not path.exists():
|
|
935
|
+
path = memory_dir / f"{rest}.md"
|
|
936
|
+
if not path.exists():
|
|
937
|
+
return CommandResult(message=f"Memory entry not found: {rest}")
|
|
938
|
+
return CommandResult(message=path.read_text(encoding="utf-8"))
|
|
939
|
+
if action == "add" and rest:
|
|
940
|
+
title, separator, content = rest.partition("::")
|
|
941
|
+
if not separator or not title.strip() or not content.strip():
|
|
942
|
+
return CommandResult(message="Usage: /memory add TITLE :: CONTENT")
|
|
943
|
+
path = add_memory_entry(context.cwd, title.strip(), content.strip())
|
|
944
|
+
return CommandResult(message=f"Added memory entry {path.name}")
|
|
945
|
+
if action == "remove" and rest:
|
|
946
|
+
if remove_memory_entry(context.cwd, rest.strip()):
|
|
947
|
+
return CommandResult(message=f"Removed memory entry {rest.strip()}")
|
|
948
|
+
return CommandResult(message=f"Memory entry not found: {rest.strip()}")
|
|
949
|
+
return CommandResult(message="Usage: /memory [list|show NAME|add TITLE :: CONTENT|remove NAME]")
|
|
950
|
+
|
|
951
|
+
async def _hooks_handler(_: str, context: CommandContext) -> CommandResult:
|
|
952
|
+
return CommandResult(message=context.hooks_summary or "No hooks configured.")
|
|
953
|
+
|
|
954
|
+
async def _resume_handler(args: str, context: CommandContext) -> CommandResult:
|
|
955
|
+
from illusion.services.session_storage import list_session_snapshots, load_session_by_id
|
|
956
|
+
|
|
957
|
+
tokens = args.strip().split()
|
|
958
|
+
|
|
959
|
+
# /resume <session_id> — load a specific session
|
|
960
|
+
if tokens:
|
|
961
|
+
sid = tokens[0]
|
|
962
|
+
snapshot = load_session_by_id(context.cwd, sid)
|
|
963
|
+
if snapshot is None:
|
|
964
|
+
return CommandResult(message=f"Session not found: {sid}")
|
|
965
|
+
messages = [
|
|
966
|
+
ConversationMessage.model_validate(item)
|
|
967
|
+
for item in snapshot.get("messages", [])
|
|
968
|
+
]
|
|
969
|
+
context.engine.load_messages(messages)
|
|
970
|
+
summary = snapshot.get("summary", "")[:60]
|
|
971
|
+
return CommandResult(
|
|
972
|
+
message=f"Restored {len(messages)} messages from session {sid}"
|
|
973
|
+
+ (f" ({summary})" if summary else ""),
|
|
974
|
+
replay_messages=messages,
|
|
975
|
+
restored_session_id=str(snapshot.get("session_id") or sid),
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
# /resume — list sessions (for the TUI to show a picker)
|
|
979
|
+
sessions = list_session_snapshots(context.cwd, limit=10)
|
|
980
|
+
if not sessions:
|
|
981
|
+
# Fall back to latest.json
|
|
982
|
+
snapshot = load_session_snapshot(context.cwd)
|
|
983
|
+
if snapshot is None:
|
|
984
|
+
return CommandResult(message="No saved sessions found for this project.")
|
|
985
|
+
messages = [
|
|
986
|
+
ConversationMessage.model_validate(item)
|
|
987
|
+
for item in snapshot.get("messages", [])
|
|
988
|
+
]
|
|
989
|
+
context.engine.load_messages(messages)
|
|
990
|
+
return CommandResult(
|
|
991
|
+
message=f"Restored {len(messages)} messages from the latest session.",
|
|
992
|
+
replay_messages=messages,
|
|
993
|
+
restored_session_id=str(snapshot.get("session_id", "")),
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
# Format session list for display / picker
|
|
997
|
+
import time
|
|
998
|
+
lines = ["Saved sessions:"]
|
|
999
|
+
for s in sessions:
|
|
1000
|
+
ts = time.strftime("%m/%d %H:%M", time.localtime(s["created_at"]))
|
|
1001
|
+
summary = s["summary"][:50] or "(no summary)"
|
|
1002
|
+
lines.append(f" {s['session_id']} {ts} {s['message_count']}msg {summary}")
|
|
1003
|
+
lines.append("")
|
|
1004
|
+
lines.append("Use /resume <session_id> to restore a specific session.")
|
|
1005
|
+
return CommandResult(message="\n".join(lines))
|
|
1006
|
+
|
|
1007
|
+
async def _export_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1008
|
+
path = export_session_markdown(cwd=context.cwd, messages=context.engine.messages)
|
|
1009
|
+
return CommandResult(message=f"Exported transcript to {path}")
|
|
1010
|
+
|
|
1011
|
+
async def _share_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1012
|
+
path = export_session_markdown(cwd=context.cwd, messages=context.engine.messages)
|
|
1013
|
+
return CommandResult(message=f"Created shareable transcript snapshot at {path}")
|
|
1014
|
+
|
|
1015
|
+
async def _copy_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1016
|
+
text = args.strip() or _last_message_text(context.engine.messages)
|
|
1017
|
+
if not text:
|
|
1018
|
+
return CommandResult(message="Nothing to copy.")
|
|
1019
|
+
copied, target = _copy_to_clipboard(text)
|
|
1020
|
+
if copied:
|
|
1021
|
+
return CommandResult(message=f"Copied {len(text)} characters to the clipboard.")
|
|
1022
|
+
return CommandResult(message=f"Clipboard unavailable. Saved copied text to {target}")
|
|
1023
|
+
|
|
1024
|
+
async def _rewind_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1025
|
+
turns = 1
|
|
1026
|
+
if args.strip():
|
|
1027
|
+
try:
|
|
1028
|
+
turns = max(1, int(args.strip()))
|
|
1029
|
+
except ValueError:
|
|
1030
|
+
return CommandResult(message="Usage: /rewind [TURNS]")
|
|
1031
|
+
before = len(context.engine.messages)
|
|
1032
|
+
updated = _rewind_turns(context.engine.messages, turns)
|
|
1033
|
+
removed = before - len(updated)
|
|
1034
|
+
|
|
1035
|
+
# 文件回退:找到目标快照并恢复文件
|
|
1036
|
+
reverted_count = 0
|
|
1037
|
+
fh = context.engine.file_history
|
|
1038
|
+
if fh is not None and fh.snapshots:
|
|
1039
|
+
# 目标轮次索引 = 当前快照数 - 回退轮次数
|
|
1040
|
+
target_turn = max(0, len(fh.snapshots) - turns)
|
|
1041
|
+
reverted_files = rewind_to(fh, target_turn)
|
|
1042
|
+
reverted_count = len(reverted_files)
|
|
1043
|
+
|
|
1044
|
+
# 硬删除:直接截断消息列表
|
|
1045
|
+
context.engine.load_messages(updated)
|
|
1046
|
+
|
|
1047
|
+
# 构建反馈消息
|
|
1048
|
+
lines = [f"Rewound {turns} turn(s); removed {removed} message(s)."]
|
|
1049
|
+
if reverted_count > 0:
|
|
1050
|
+
lines.append(f"Reverted {reverted_count} file(s).")
|
|
1051
|
+
|
|
1052
|
+
return CommandResult(
|
|
1053
|
+
clear_screen=True,
|
|
1054
|
+
replay_messages=list(updated),
|
|
1055
|
+
message="\n".join(lines),
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
async def _files_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1059
|
+
raw = args.strip()
|
|
1060
|
+
root = Path(context.cwd)
|
|
1061
|
+
max_items = 30
|
|
1062
|
+
tokens = raw.split(maxsplit=1)
|
|
1063
|
+
if tokens and tokens[0] == "dirs":
|
|
1064
|
+
dirs = [
|
|
1065
|
+
path
|
|
1066
|
+
for path in sorted(root.rglob("*"))
|
|
1067
|
+
if path.is_dir() and ".git" not in path.parts and ".venv" not in path.parts
|
|
1068
|
+
]
|
|
1069
|
+
lines = [str(path.relative_to(root)) for path in dirs[:max_items]]
|
|
1070
|
+
if len(dirs) > max_items:
|
|
1071
|
+
lines.append(f"... {len(dirs) - max_items} more")
|
|
1072
|
+
return CommandResult(message="\n".join(lines) if lines else "(no directories)")
|
|
1073
|
+
if tokens and tokens[0].isdigit():
|
|
1074
|
+
max_items = max(1, min(int(tokens[0]), 200))
|
|
1075
|
+
raw = tokens[1] if len(tokens) == 2 else ""
|
|
1076
|
+
needle = raw.lower()
|
|
1077
|
+
files = [
|
|
1078
|
+
path
|
|
1079
|
+
for path in sorted(root.rglob("*"))
|
|
1080
|
+
if path.is_file() and ".git" not in path.parts and ".venv" not in path.parts
|
|
1081
|
+
]
|
|
1082
|
+
if needle:
|
|
1083
|
+
files = [path for path in files if needle in str(path.relative_to(root)).lower()]
|
|
1084
|
+
lines = [str(path.relative_to(root)) for path in files[:max_items]]
|
|
1085
|
+
if len(files) > max_items:
|
|
1086
|
+
lines.append(f"... {len(files) - max_items} more")
|
|
1087
|
+
return CommandResult(
|
|
1088
|
+
message="\n".join(lines) if lines else "(no matching files)"
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
async def _init_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1092
|
+
"""智能初始化项目配置
|
|
1093
|
+
|
|
1094
|
+
流程:
|
|
1095
|
+
1. 探索代码库结构
|
|
1096
|
+
2. 识别项目类型和工具链
|
|
1097
|
+
3. 生成有针对性的配置
|
|
1098
|
+
"""
|
|
1099
|
+
del args
|
|
1100
|
+
root = Path(context.cwd)
|
|
1101
|
+
project_dir = get_project_config_dir(context.cwd)
|
|
1102
|
+
created: list[str] = []
|
|
1103
|
+
findings: dict[str, Any] = {}
|
|
1104
|
+
|
|
1105
|
+
# Phase 1: 探索代码库
|
|
1106
|
+
findings = _explore_codebase(root)
|
|
1107
|
+
|
|
1108
|
+
# Phase 2: 生成 CLAUDE.md
|
|
1109
|
+
claudemd = root / "CLAUDE.md"
|
|
1110
|
+
if not claudemd.exists():
|
|
1111
|
+
content = _generate_claudemd(findings, root)
|
|
1112
|
+
claudemd.write_text(content, encoding="utf-8")
|
|
1113
|
+
created.append("CLAUDE.md")
|
|
1114
|
+
|
|
1115
|
+
# Phase 3: 创建项目配置目录
|
|
1116
|
+
for relative, content in (
|
|
1117
|
+
(
|
|
1118
|
+
project_dir / "README.md",
|
|
1119
|
+
"# Project IllusionCode Config\n\nThis directory stores project-specific IllusionCode state.\n",
|
|
1120
|
+
),
|
|
1121
|
+
(
|
|
1122
|
+
project_dir / "memory" / "MEMORY.md",
|
|
1123
|
+
"# Project Memory\n\nAdd reusable project knowledge here.\n",
|
|
1124
|
+
),
|
|
1125
|
+
(
|
|
1126
|
+
project_dir / "plugins" / ".gitkeep",
|
|
1127
|
+
"",
|
|
1128
|
+
),
|
|
1129
|
+
(
|
|
1130
|
+
project_dir / "skills" / ".gitkeep",
|
|
1131
|
+
"",
|
|
1132
|
+
),
|
|
1133
|
+
):
|
|
1134
|
+
relative.parent.mkdir(parents=True, exist_ok=True)
|
|
1135
|
+
if not relative.exists():
|
|
1136
|
+
relative.write_text(content, encoding="utf-8")
|
|
1137
|
+
created.append(str(relative.relative_to(root)))
|
|
1138
|
+
|
|
1139
|
+
# Phase 4: 生成报告
|
|
1140
|
+
if not created:
|
|
1141
|
+
return CommandResult(message="Project already initialized for IllusionCode.")
|
|
1142
|
+
|
|
1143
|
+
report_lines = [
|
|
1144
|
+
"✨ **Illusion Code 项目初始化完成**\n",
|
|
1145
|
+
"## 已创建文件",
|
|
1146
|
+
*[f"- {item}" for item in created],
|
|
1147
|
+
"",
|
|
1148
|
+
"## 项目分析",
|
|
1149
|
+
]
|
|
1150
|
+
|
|
1151
|
+
if findings.get("languages"):
|
|
1152
|
+
report_lines.append(f"- **检测到语言**: {', '.join(findings['languages'])}")
|
|
1153
|
+
if findings.get("frameworks"):
|
|
1154
|
+
report_lines.append(f"- **检测到框架**: {', '.join(findings['frameworks'])}")
|
|
1155
|
+
if findings.get("package_manager"):
|
|
1156
|
+
report_lines.append(f"- **包管理器**: {findings['package_manager']}")
|
|
1157
|
+
if findings.get("build_commands"):
|
|
1158
|
+
report_lines.append(f"- **构建命令**: {', '.join(findings['build_commands'])}")
|
|
1159
|
+
if findings.get("test_commands"):
|
|
1160
|
+
report_lines.append(f"- **测试命令**: {', '.join(findings['test_commands'])}")
|
|
1161
|
+
if findings.get("lint_commands"):
|
|
1162
|
+
report_lines.append(f"- **代码检查**: {', '.join(findings['lint_commands'])}")
|
|
1163
|
+
if findings.get("format_commands"):
|
|
1164
|
+
report_lines.append(f"- **格式化工具**: {', '.join(findings['format_commands'])}")
|
|
1165
|
+
if findings.get("ci_config"):
|
|
1166
|
+
report_lines.append(f"- **CI/CD**: {findings['ci_config']}")
|
|
1167
|
+
|
|
1168
|
+
report_lines.extend([
|
|
1169
|
+
"",
|
|
1170
|
+
"## 下一步建议",
|
|
1171
|
+
"- 查看 `CLAUDE.md` 了解项目配置",
|
|
1172
|
+
"- 运行 `/memory` 管理项目记忆",
|
|
1173
|
+
"- 运行 `/skills` 查看可用技能",
|
|
1174
|
+
"- 根据需要调整 CLAUDE.md 中的配置",
|
|
1175
|
+
])
|
|
1176
|
+
|
|
1177
|
+
return CommandResult(message="\n".join(report_lines))
|
|
1178
|
+
|
|
1179
|
+
async def _bridge_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1180
|
+
tokens = args.split()
|
|
1181
|
+
if not tokens or tokens[0] == "show":
|
|
1182
|
+
sessions = get_bridge_manager().list_sessions()
|
|
1183
|
+
lines = [
|
|
1184
|
+
"Bridge summary:",
|
|
1185
|
+
"- backend host: available",
|
|
1186
|
+
f"- cwd: {context.cwd}",
|
|
1187
|
+
f"- sessions: {len(sessions)}",
|
|
1188
|
+
"- utilities: encode, decode, sdk, spawn, list, output, stop",
|
|
1189
|
+
]
|
|
1190
|
+
return CommandResult(message="\n".join(lines))
|
|
1191
|
+
if tokens[0] == "encode" and len(tokens) == 3:
|
|
1192
|
+
encoded = encode_work_secret(
|
|
1193
|
+
WorkSecret(version=1, session_ingress_token=tokens[2], api_base_url=tokens[1])
|
|
1194
|
+
)
|
|
1195
|
+
return CommandResult(message=encoded)
|
|
1196
|
+
if tokens[0] == "decode" and len(tokens) == 2:
|
|
1197
|
+
secret = decode_work_secret(tokens[1])
|
|
1198
|
+
return CommandResult(message=json.dumps(secret.__dict__, indent=2))
|
|
1199
|
+
if tokens[0] == "sdk" and len(tokens) == 3:
|
|
1200
|
+
return CommandResult(message=build_sdk_url(tokens[1], tokens[2]))
|
|
1201
|
+
if tokens[0] == "spawn" and len(tokens) >= 2:
|
|
1202
|
+
command = args[len("spawn ") :]
|
|
1203
|
+
handle = await get_bridge_manager().spawn(
|
|
1204
|
+
session_id=f"bridge-{datetime.now(timezone.utc).strftime('%H%M%S')}",
|
|
1205
|
+
command=command,
|
|
1206
|
+
cwd=context.cwd,
|
|
1207
|
+
)
|
|
1208
|
+
return CommandResult(
|
|
1209
|
+
message=f"Spawned bridge session {handle.session_id} pid={handle.process.pid}"
|
|
1210
|
+
)
|
|
1211
|
+
if tokens[0] == "list":
|
|
1212
|
+
sessions = get_bridge_manager().list_sessions()
|
|
1213
|
+
if not sessions:
|
|
1214
|
+
return CommandResult(message="No bridge sessions.")
|
|
1215
|
+
return CommandResult(
|
|
1216
|
+
message="\n".join(
|
|
1217
|
+
f"{item.session_id} [{item.status}] pid={item.pid} {item.command}"
|
|
1218
|
+
for item in sessions
|
|
1219
|
+
)
|
|
1220
|
+
)
|
|
1221
|
+
if tokens[0] == "output" and len(tokens) == 2:
|
|
1222
|
+
return CommandResult(message=get_bridge_manager().read_output(tokens[1]) or "(no output)")
|
|
1223
|
+
if tokens[0] == "stop" and len(tokens) == 2:
|
|
1224
|
+
try:
|
|
1225
|
+
await get_bridge_manager().stop(tokens[1])
|
|
1226
|
+
except ValueError as exc:
|
|
1227
|
+
return CommandResult(message=str(exc))
|
|
1228
|
+
return CommandResult(message=f"Stopped bridge session {tokens[1]}")
|
|
1229
|
+
return CommandResult(
|
|
1230
|
+
message="Usage: /bridge [show|encode API_BASE_URL TOKEN|decode SECRET|sdk API_BASE_URL SESSION_ID|spawn CMD|list|output SESSION_ID|stop SESSION_ID]"
|
|
1231
|
+
)
|
|
1232
|
+
|
|
1233
|
+
async def _reload_plugins_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1234
|
+
settings = load_settings()
|
|
1235
|
+
plugins = load_plugins(settings, context.cwd)
|
|
1236
|
+
if not plugins:
|
|
1237
|
+
return CommandResult(message="No plugins discovered.")
|
|
1238
|
+
lines = ["Reloaded plugins:"]
|
|
1239
|
+
for plugin in plugins:
|
|
1240
|
+
state = "enabled" if plugin.enabled else "disabled"
|
|
1241
|
+
lines.append(f"- {plugin.manifest.name} [{state}]")
|
|
1242
|
+
return CommandResult(message="\n".join(lines))
|
|
1243
|
+
|
|
1244
|
+
async def _skills_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1245
|
+
skill_registry = load_skill_registry(context.cwd)
|
|
1246
|
+
if args:
|
|
1247
|
+
skill = skill_registry.get(args)
|
|
1248
|
+
if skill is None:
|
|
1249
|
+
return CommandResult(message=f"Skill not found: {args}")
|
|
1250
|
+
return CommandResult(message=skill.content)
|
|
1251
|
+
skills = skill_registry.list_skills()
|
|
1252
|
+
if not skills:
|
|
1253
|
+
return CommandResult(message="No skills available.")
|
|
1254
|
+
lines = ["Available skills:"]
|
|
1255
|
+
for skill in skills:
|
|
1256
|
+
source = f" [{skill.source}]"
|
|
1257
|
+
lines.append(f"- {skill.name}{source}: {skill.description}")
|
|
1258
|
+
return CommandResult(message="\n".join(lines))
|
|
1259
|
+
|
|
1260
|
+
async def _config_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1261
|
+
del context
|
|
1262
|
+
settings = load_settings()
|
|
1263
|
+
tokens = args.split(maxsplit=2)
|
|
1264
|
+
if not tokens or tokens[0] == "show":
|
|
1265
|
+
return CommandResult(message=settings.model_dump_json(indent=2))
|
|
1266
|
+
if tokens[0] == "set" and len(tokens) == 3:
|
|
1267
|
+
key, value = tokens[1], tokens[2]
|
|
1268
|
+
if key not in Settings.model_fields:
|
|
1269
|
+
return CommandResult(message=f"Unknown config key: {key}")
|
|
1270
|
+
try:
|
|
1271
|
+
coerced = _coerce_setting_value(settings, key, value)
|
|
1272
|
+
except ValueError as exc:
|
|
1273
|
+
return CommandResult(message=str(exc))
|
|
1274
|
+
setattr(settings, key, coerced)
|
|
1275
|
+
save_settings(settings)
|
|
1276
|
+
return CommandResult(message=f"Updated {key}")
|
|
1277
|
+
return CommandResult(message="Usage: /config [show|set KEY VALUE]")
|
|
1278
|
+
|
|
1279
|
+
async def _login_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1280
|
+
del context
|
|
1281
|
+
settings = load_settings()
|
|
1282
|
+
provider = detect_provider(settings)
|
|
1283
|
+
api_key = args.strip()
|
|
1284
|
+
if not api_key:
|
|
1285
|
+
masked = (
|
|
1286
|
+
f"{settings.api_key[:6]}...{settings.api_key[-4:]}"
|
|
1287
|
+
if settings.api_key
|
|
1288
|
+
else "(not configured)"
|
|
1289
|
+
)
|
|
1290
|
+
return CommandResult(
|
|
1291
|
+
message=(
|
|
1292
|
+
f"Auth status:\n"
|
|
1293
|
+
f"- provider: {provider.name}\n"
|
|
1294
|
+
f"- auth_status: {auth_status(settings)}\n"
|
|
1295
|
+
f"- base_url: {settings.base_url or '(default)'}\n"
|
|
1296
|
+
f"- model: {settings.model}\n"
|
|
1297
|
+
f"- api_key: {masked}\n"
|
|
1298
|
+
"Usage: /login API_KEY"
|
|
1299
|
+
)
|
|
1300
|
+
)
|
|
1301
|
+
env_key = settings._active_env_key
|
|
1302
|
+
env = settings._active_env
|
|
1303
|
+
env.api_key = api_key
|
|
1304
|
+
settings.model_extra[env_key] = env.model_dump()
|
|
1305
|
+
save_settings(settings)
|
|
1306
|
+
return CommandResult(message="Stored API key in ~/.illusion/settings.json")
|
|
1307
|
+
|
|
1308
|
+
async def _logout_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1309
|
+
del context
|
|
1310
|
+
settings = load_settings()
|
|
1311
|
+
env_key = settings._active_env_key
|
|
1312
|
+
env = settings._active_env
|
|
1313
|
+
env.api_key = ""
|
|
1314
|
+
settings.model_extra[env_key] = env.model_dump()
|
|
1315
|
+
save_settings(settings)
|
|
1316
|
+
return CommandResult(message="Cleared stored API key.")
|
|
1317
|
+
|
|
1318
|
+
async def _feedback_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1319
|
+
del context
|
|
1320
|
+
path = get_feedback_log_path()
|
|
1321
|
+
if not args.strip():
|
|
1322
|
+
return CommandResult(message=f"Feedback log: {path}\nUsage: /feedback TEXT")
|
|
1323
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
1324
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
1325
|
+
handle.write(f"[{timestamp}] {args.strip()}\n")
|
|
1326
|
+
return CommandResult(message=f"Saved feedback to {path}")
|
|
1327
|
+
|
|
1328
|
+
async def _fast_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1329
|
+
settings = load_settings()
|
|
1330
|
+
current = (
|
|
1331
|
+
context.app_state.get().fast_mode
|
|
1332
|
+
if context.app_state is not None
|
|
1333
|
+
else settings.fast_mode
|
|
1334
|
+
)
|
|
1335
|
+
action = args.strip() or "toggle"
|
|
1336
|
+
if action == "show":
|
|
1337
|
+
return CommandResult(message=f"Fast mode: {'on' if current else 'off'}")
|
|
1338
|
+
enabled = {"on": True, "off": False, "toggle": not current}.get(action)
|
|
1339
|
+
if enabled is None:
|
|
1340
|
+
return CommandResult(message="Usage: /fast [show|on|off|toggle]")
|
|
1341
|
+
settings.fast_mode = enabled
|
|
1342
|
+
save_settings(settings)
|
|
1343
|
+
if context.app_state is not None:
|
|
1344
|
+
context.app_state.set(fast_mode=enabled)
|
|
1345
|
+
return CommandResult(message=f"Fast mode {'enabled' if enabled else 'disabled'}.")
|
|
1346
|
+
|
|
1347
|
+
async def _thinking_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1348
|
+
settings = load_settings()
|
|
1349
|
+
current = (
|
|
1350
|
+
context.app_state.get().show_thinking
|
|
1351
|
+
if context.app_state is not None
|
|
1352
|
+
else settings.show_thinking
|
|
1353
|
+
)
|
|
1354
|
+
action = args.strip() or "toggle"
|
|
1355
|
+
if action == "show":
|
|
1356
|
+
return CommandResult(message=f"Thinking mode: {'on' if current else 'off'}")
|
|
1357
|
+
enabled = {"on": True, "off": False, "toggle": not current}.get(action)
|
|
1358
|
+
if enabled is None:
|
|
1359
|
+
return CommandResult(message="Usage: /thinking [show|on|off|toggle]")
|
|
1360
|
+
settings.show_thinking = enabled
|
|
1361
|
+
save_settings(settings)
|
|
1362
|
+
if context.app_state is not None:
|
|
1363
|
+
context.app_state.set(show_thinking=enabled)
|
|
1364
|
+
return CommandResult(message=f"Thinking mode {'enabled' if enabled else 'disabled'}.")
|
|
1365
|
+
|
|
1366
|
+
async def _help_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1367
|
+
return CommandResult(message=registry.help_text())
|
|
1368
|
+
|
|
1369
|
+
async def _effort_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1370
|
+
settings = load_settings()
|
|
1371
|
+
current = context.app_state.get().effort if context.app_state is not None else settings.effort
|
|
1372
|
+
value = args.strip() or "show"
|
|
1373
|
+
if value == "show":
|
|
1374
|
+
return CommandResult(message=f"Reasoning effort: {current}")
|
|
1375
|
+
if value not in {"low", "medium", "high", "xhigh", "max"}:
|
|
1376
|
+
return CommandResult(message="Usage: /effort [show|low|medium|high|xhigh|max]")
|
|
1377
|
+
# 验证 effort 级别
|
|
1378
|
+
try:
|
|
1379
|
+
from illusion.api.effort import EffortMapper
|
|
1380
|
+
effort_level = EffortMapper.normalize(value)
|
|
1381
|
+
except ValueError:
|
|
1382
|
+
return CommandResult(message="Usage: /effort [show|low|medium|high|xhigh|max]")
|
|
1383
|
+
settings.effort = value
|
|
1384
|
+
save_settings(settings)
|
|
1385
|
+
# 更新 QueryEngine 的 effort 值
|
|
1386
|
+
context.engine.effort = effort_level
|
|
1387
|
+
context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
|
|
1388
|
+
if context.app_state is not None:
|
|
1389
|
+
context.app_state.set(effort=value)
|
|
1390
|
+
return CommandResult(message=f"Reasoning effort set to {value}.")
|
|
1391
|
+
|
|
1392
|
+
async def _passes_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1393
|
+
settings = load_settings()
|
|
1394
|
+
current = context.app_state.get().passes if context.app_state is not None else settings.passes
|
|
1395
|
+
value = args.strip() or "show"
|
|
1396
|
+
if value == "show":
|
|
1397
|
+
return CommandResult(message=f"Passes: {current}")
|
|
1398
|
+
try:
|
|
1399
|
+
passes = max(1, min(int(value), 8))
|
|
1400
|
+
except ValueError:
|
|
1401
|
+
return CommandResult(message="Usage: /passes [show|COUNT]")
|
|
1402
|
+
settings.passes = passes
|
|
1403
|
+
save_settings(settings)
|
|
1404
|
+
context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
|
|
1405
|
+
if context.app_state is not None:
|
|
1406
|
+
context.app_state.set(passes=passes)
|
|
1407
|
+
return CommandResult(message=f"Pass count set to {passes}.")
|
|
1408
|
+
|
|
1409
|
+
async def _turns_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1410
|
+
settings = load_settings()
|
|
1411
|
+
tokens = args.split()
|
|
1412
|
+
if not tokens or tokens[0] == "show":
|
|
1413
|
+
return CommandResult(
|
|
1414
|
+
message=(
|
|
1415
|
+
f"Max turns (engine): {context.engine.max_turns}\n"
|
|
1416
|
+
f"Max turns (config): {settings.max_turns}\n"
|
|
1417
|
+
"Usage: /turns [show|COUNT]"
|
|
1418
|
+
)
|
|
1419
|
+
)
|
|
1420
|
+
if tokens[0] == "set" and len(tokens) == 2:
|
|
1421
|
+
raw = tokens[1]
|
|
1422
|
+
elif len(tokens) == 1:
|
|
1423
|
+
raw = tokens[0]
|
|
1424
|
+
else:
|
|
1425
|
+
return CommandResult(message="Usage: /turns [show|COUNT]")
|
|
1426
|
+
try:
|
|
1427
|
+
turns = int(raw)
|
|
1428
|
+
except ValueError:
|
|
1429
|
+
return CommandResult(message="Usage: /turns [show|COUNT]")
|
|
1430
|
+
turns = max(1, min(turns, 512))
|
|
1431
|
+
settings.max_turns = turns
|
|
1432
|
+
save_settings(settings)
|
|
1433
|
+
context.engine.set_max_turns(turns)
|
|
1434
|
+
return CommandResult(message=f"Max turns set to {turns}.")
|
|
1435
|
+
|
|
1436
|
+
async def _continue_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1437
|
+
raw = args.strip()
|
|
1438
|
+
if not context.engine.has_pending_continuation():
|
|
1439
|
+
return CommandResult(message="Nothing to continue (no pending tool results).")
|
|
1440
|
+
|
|
1441
|
+
turns: int | None = None
|
|
1442
|
+
if raw:
|
|
1443
|
+
tokens = raw.split()
|
|
1444
|
+
if tokens[0] == "set" and len(tokens) == 2:
|
|
1445
|
+
raw = tokens[1]
|
|
1446
|
+
try:
|
|
1447
|
+
turns = int(raw)
|
|
1448
|
+
except ValueError:
|
|
1449
|
+
return CommandResult(message="Usage: /continue [COUNT]")
|
|
1450
|
+
turns = max(1, min(turns, 512))
|
|
1451
|
+
|
|
1452
|
+
return CommandResult(
|
|
1453
|
+
message="Continuing pending tool loop...",
|
|
1454
|
+
continue_pending=True,
|
|
1455
|
+
continue_turns=turns,
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
async def _issue_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1459
|
+
path = get_project_issue_file(context.cwd)
|
|
1460
|
+
tokens = args.split(maxsplit=1)
|
|
1461
|
+
action = tokens[0] if tokens else "show"
|
|
1462
|
+
rest = tokens[1] if len(tokens) == 2 else ""
|
|
1463
|
+
if action == "show":
|
|
1464
|
+
if not path.exists():
|
|
1465
|
+
return CommandResult(message=f"No issue context. File path: {path}")
|
|
1466
|
+
return CommandResult(message=path.read_text(encoding="utf-8"))
|
|
1467
|
+
if action == "set" and rest:
|
|
1468
|
+
title, separator, body = rest.partition("::")
|
|
1469
|
+
if not separator or not title.strip() or not body.strip():
|
|
1470
|
+
return CommandResult(message="Usage: /issue set TITLE :: BODY")
|
|
1471
|
+
content = f"# {title.strip()}\n\n{body.strip()}\n"
|
|
1472
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1473
|
+
path.write_text(content, encoding="utf-8")
|
|
1474
|
+
return CommandResult(message=f"Saved issue context to {path}")
|
|
1475
|
+
if action == "clear":
|
|
1476
|
+
if path.exists():
|
|
1477
|
+
path.unlink()
|
|
1478
|
+
return CommandResult(message="Cleared issue context.")
|
|
1479
|
+
return CommandResult(message="No issue context to clear.")
|
|
1480
|
+
return CommandResult(message="Usage: /issue [show|set TITLE :: BODY|clear]")
|
|
1481
|
+
|
|
1482
|
+
async def _pr_comments_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1483
|
+
path = get_project_pr_comments_file(context.cwd)
|
|
1484
|
+
tokens = args.split(maxsplit=1)
|
|
1485
|
+
action = tokens[0] if tokens else "show"
|
|
1486
|
+
rest = tokens[1] if len(tokens) == 2 else ""
|
|
1487
|
+
if action == "show":
|
|
1488
|
+
if not path.exists():
|
|
1489
|
+
return CommandResult(message=f"No PR comments context. File path: {path}")
|
|
1490
|
+
return CommandResult(message=path.read_text(encoding="utf-8"))
|
|
1491
|
+
if action == "add" and rest:
|
|
1492
|
+
location, separator, comment = rest.partition("::")
|
|
1493
|
+
if not separator or not location.strip() or not comment.strip():
|
|
1494
|
+
return CommandResult(message="Usage: /pr_comments add FILE[:LINE] :: COMMENT")
|
|
1495
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else "# PR Comments\n"
|
|
1496
|
+
if not existing.endswith("\n"):
|
|
1497
|
+
existing += "\n"
|
|
1498
|
+
existing += f"- {location.strip()}: {comment.strip()}\n"
|
|
1499
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1500
|
+
path.write_text(existing, encoding="utf-8")
|
|
1501
|
+
return CommandResult(message=f"Added PR comment to {path}")
|
|
1502
|
+
if action == "clear":
|
|
1503
|
+
if path.exists():
|
|
1504
|
+
path.unlink()
|
|
1505
|
+
return CommandResult(message="Cleared PR comments context.")
|
|
1506
|
+
return CommandResult(message="No PR comments context to clear.")
|
|
1507
|
+
return CommandResult(message="Usage: /pr_comments [show|add FILE[:LINE] :: COMMENT|clear]")
|
|
1508
|
+
|
|
1509
|
+
async def _mcp_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1510
|
+
settings = load_settings()
|
|
1511
|
+
tokens = args.split()
|
|
1512
|
+
if tokens and tokens[0] == "auth" and len(tokens) >= 3:
|
|
1513
|
+
server_name = tokens[1]
|
|
1514
|
+
config = settings.mcp_servers.get(server_name)
|
|
1515
|
+
if config is None:
|
|
1516
|
+
return CommandResult(message=f"Unknown MCP server: {server_name}")
|
|
1517
|
+
|
|
1518
|
+
if len(tokens) == 3:
|
|
1519
|
+
mode = "bearer"
|
|
1520
|
+
key = None
|
|
1521
|
+
value = tokens[2]
|
|
1522
|
+
elif len(tokens) == 4:
|
|
1523
|
+
mode = tokens[2]
|
|
1524
|
+
key = None
|
|
1525
|
+
value = tokens[3]
|
|
1526
|
+
elif len(tokens) == 5:
|
|
1527
|
+
mode = tokens[2]
|
|
1528
|
+
key = tokens[3]
|
|
1529
|
+
value = tokens[4]
|
|
1530
|
+
else:
|
|
1531
|
+
return CommandResult(
|
|
1532
|
+
message="Usage: /mcp auth SERVER TOKEN | /mcp auth SERVER [bearer|env] VALUE | /mcp auth SERVER header KEY VALUE"
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
if hasattr(config, "headers"):
|
|
1536
|
+
if mode not in {"bearer", "header"}:
|
|
1537
|
+
return CommandResult(message="HTTP/WS MCP auth supports bearer or header modes.")
|
|
1538
|
+
header_key = key or "Authorization"
|
|
1539
|
+
header_value = (
|
|
1540
|
+
f"Bearer {value}" if mode == "bearer" and header_key == "Authorization" else value
|
|
1541
|
+
)
|
|
1542
|
+
headers = dict(getattr(config, "headers", {}) or {})
|
|
1543
|
+
headers[header_key] = header_value
|
|
1544
|
+
settings.mcp_servers[server_name] = config.model_copy(update={"headers": headers})
|
|
1545
|
+
elif hasattr(config, "env"):
|
|
1546
|
+
if mode not in {"bearer", "env"}:
|
|
1547
|
+
return CommandResult(message="stdio MCP auth supports bearer or env modes.")
|
|
1548
|
+
env_key = key or "MCP_AUTH_TOKEN"
|
|
1549
|
+
env_value = f"Bearer {value}" if mode == "bearer" else value
|
|
1550
|
+
env = dict(getattr(config, "env", {}) or {})
|
|
1551
|
+
env[env_key] = env_value
|
|
1552
|
+
settings.mcp_servers[server_name] = config.model_copy(update={"env": env})
|
|
1553
|
+
else:
|
|
1554
|
+
return CommandResult(message=f"Server {server_name} does not support auth updates")
|
|
1555
|
+
save_settings(settings)
|
|
1556
|
+
return CommandResult(message=f"Saved MCP auth for {server_name}. Restart session to reconnect.")
|
|
1557
|
+
return CommandResult(message=context.mcp_summary or "No MCP servers configured.")
|
|
1558
|
+
|
|
1559
|
+
async def _plugin_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1560
|
+
settings = load_settings()
|
|
1561
|
+
tokens = args.split()
|
|
1562
|
+
if not tokens or tokens[0] == "list":
|
|
1563
|
+
return CommandResult(message=context.plugin_summary or "No plugins discovered.")
|
|
1564
|
+
if tokens[0] == "enable" and len(tokens) == 2:
|
|
1565
|
+
settings.enabled_plugins[tokens[1]] = True
|
|
1566
|
+
save_settings(settings)
|
|
1567
|
+
return CommandResult(message=f"Enabled plugin '{tokens[1]}'. Restart session to reload.")
|
|
1568
|
+
if tokens[0] == "disable" and len(tokens) == 2:
|
|
1569
|
+
settings.enabled_plugins[tokens[1]] = False
|
|
1570
|
+
save_settings(settings)
|
|
1571
|
+
return CommandResult(message=f"Disabled plugin '{tokens[1]}'. Restart session to reload.")
|
|
1572
|
+
if tokens[0] == "install" and len(tokens) == 2:
|
|
1573
|
+
path = install_plugin_from_path(tokens[1])
|
|
1574
|
+
return CommandResult(message=f"Installed plugin to {path}")
|
|
1575
|
+
if tokens[0] == "uninstall" and len(tokens) == 2:
|
|
1576
|
+
if uninstall_plugin(tokens[1]):
|
|
1577
|
+
return CommandResult(message=f"Uninstalled plugin '{tokens[1]}'")
|
|
1578
|
+
return CommandResult(message=f"Plugin '{tokens[1]}' not found")
|
|
1579
|
+
plugins = load_plugins(settings, context.cwd)
|
|
1580
|
+
if plugins:
|
|
1581
|
+
return CommandResult(message=context.plugin_summary)
|
|
1582
|
+
return CommandResult(message="Usage: /plugin [list|enable NAME|disable NAME|install PATH|uninstall NAME]")
|
|
1583
|
+
|
|
1584
|
+
_MODE_LABELS = {"default": "Default", "plan": "Plan Mode", "full_auto": "Auto"}
|
|
1585
|
+
|
|
1586
|
+
async def _permissions_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1587
|
+
settings = load_settings()
|
|
1588
|
+
tokens = args.split()
|
|
1589
|
+
if not tokens or tokens[0] == "show":
|
|
1590
|
+
permission = settings.permission
|
|
1591
|
+
label = _MODE_LABELS.get(permission.mode.value, permission.mode.value)
|
|
1592
|
+
return CommandResult(
|
|
1593
|
+
message=(
|
|
1594
|
+
f"Mode: {label}\n"
|
|
1595
|
+
f"Allowed tools: {permission.allowed_tools}\n"
|
|
1596
|
+
f"Denied tools: {permission.denied_tools}"
|
|
1597
|
+
)
|
|
1598
|
+
)
|
|
1599
|
+
if tokens[0] == "set" and len(tokens) == 2:
|
|
1600
|
+
settings.permission.mode = PermissionMode(tokens[1])
|
|
1601
|
+
save_settings(settings)
|
|
1602
|
+
context.engine.set_permission_checker(PermissionChecker(settings.permission))
|
|
1603
|
+
if context.app_state is not None:
|
|
1604
|
+
context.app_state.set(permission_mode=settings.permission.mode.value)
|
|
1605
|
+
label = _MODE_LABELS.get(tokens[1], tokens[1])
|
|
1606
|
+
return CommandResult(message=f"Permission mode set to {label}")
|
|
1607
|
+
return CommandResult(message="Usage: /permissions [show|set MODE]")
|
|
1608
|
+
|
|
1609
|
+
async def _plan_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1610
|
+
settings = load_settings()
|
|
1611
|
+
mode = args.strip() or "on"
|
|
1612
|
+
if mode in {"on", "enter"}:
|
|
1613
|
+
settings.permission.mode = PermissionMode.PLAN
|
|
1614
|
+
save_settings(settings)
|
|
1615
|
+
context.engine.set_permission_checker(PermissionChecker(settings.permission))
|
|
1616
|
+
if context.app_state is not None:
|
|
1617
|
+
context.app_state.set(permission_mode=settings.permission.mode.value)
|
|
1618
|
+
return CommandResult(message="Plan mode enabled.")
|
|
1619
|
+
if mode in {"off", "exit"}:
|
|
1620
|
+
settings.permission.mode = PermissionMode.DEFAULT
|
|
1621
|
+
save_settings(settings)
|
|
1622
|
+
context.engine.set_permission_checker(PermissionChecker(settings.permission))
|
|
1623
|
+
if context.app_state is not None:
|
|
1624
|
+
context.app_state.set(permission_mode=settings.permission.mode.value)
|
|
1625
|
+
return CommandResult(message="Plan mode disabled.")
|
|
1626
|
+
return CommandResult(message="Usage: /plan [on|off]")
|
|
1627
|
+
|
|
1628
|
+
async def _model_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1629
|
+
from illusion.config.i18n import t as i18n_t
|
|
1630
|
+
settings = load_settings()
|
|
1631
|
+
tokens = args.split(maxsplit=1)
|
|
1632
|
+
if not tokens or tokens[0] == "show":
|
|
1633
|
+
env = settings._active_env
|
|
1634
|
+
return CommandResult(
|
|
1635
|
+
message=i18n_t("model_active", model=settings.model) + "\n" +
|
|
1636
|
+
i18n_t("model_env_model", name=settings.active_model_name) + "\n" +
|
|
1637
|
+
i18n_t("model_api_format", fmt=env.api_format) + "\n" +
|
|
1638
|
+
i18n_t("model_base_url", url=env.base_url or i18n_t("model_default_url"))
|
|
1639
|
+
)
|
|
1640
|
+
if tokens[0] == "list":
|
|
1641
|
+
lines = []
|
|
1642
|
+
for env_key, env in settings.list_envs().items():
|
|
1643
|
+
for model_key, model_name in env.list_models().items():
|
|
1644
|
+
ref = f"{env_key}.{model_key}"
|
|
1645
|
+
active = " (active)" if ref == settings.model else ""
|
|
1646
|
+
lines.append(f" {ref}{active}: {model_name} ({env.api_format})")
|
|
1647
|
+
return CommandResult(message=i18n_t("model_list_title") + "\n" + "\n".join(lines))
|
|
1648
|
+
# 切换模型
|
|
1649
|
+
model_ref = tokens[0] if tokens[0] != "set" else (tokens[1] if len(tokens) > 1 else "")
|
|
1650
|
+
if "." in model_ref:
|
|
1651
|
+
env_key, model_key = model_ref.split(".", 1)
|
|
1652
|
+
env = settings.get_env(env_key)
|
|
1653
|
+
if env and env.get_model(model_key):
|
|
1654
|
+
old_env_key = settings._active_env_key
|
|
1655
|
+
settings.model = model_ref
|
|
1656
|
+
save_settings(settings)
|
|
1657
|
+
context.engine.set_model(env.get_model(model_key))
|
|
1658
|
+
if context.app_state is not None:
|
|
1659
|
+
context.app_state.set(model=env.get_model(model_key))
|
|
1660
|
+
# 跨 env 切换时告知调用方需要重建 API 客户端
|
|
1661
|
+
needs_rebuild = env_key != old_env_key
|
|
1662
|
+
return CommandResult(
|
|
1663
|
+
message=i18n_t("model_set_to", ref=model_ref, name=env.get_model(model_key)),
|
|
1664
|
+
needs_api_rebuild=needs_rebuild,
|
|
1665
|
+
)
|
|
1666
|
+
return CommandResult(message=i18n_t("model_unknown", ref=model_ref))
|
|
1667
|
+
|
|
1668
|
+
async def _language_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1669
|
+
settings = load_settings()
|
|
1670
|
+
current = (
|
|
1671
|
+
str(context.app_state.get().ui_language)
|
|
1672
|
+
if context.app_state is not None
|
|
1673
|
+
else settings.ui_language
|
|
1674
|
+
)
|
|
1675
|
+
tokens = args.split()
|
|
1676
|
+
if not tokens or tokens[0] == "show":
|
|
1677
|
+
return CommandResult(message=f"UI language: {current}")
|
|
1678
|
+
if tokens[0] == "list":
|
|
1679
|
+
return CommandResult(message="Available UI languages: zh-CN, en")
|
|
1680
|
+
if tokens[0] == "set" and len(tokens) == 2:
|
|
1681
|
+
value = tokens[1]
|
|
1682
|
+
if value not in {"zh-CN", "en"}:
|
|
1683
|
+
return CommandResult(message="Usage: /language [show|list|set zh-CN|set en]")
|
|
1684
|
+
settings.ui_language = value
|
|
1685
|
+
save_settings(settings)
|
|
1686
|
+
if context.app_state is not None:
|
|
1687
|
+
context.app_state.set(ui_language=value)
|
|
1688
|
+
return CommandResult(message=f"UI language set to {value}")
|
|
1689
|
+
return CommandResult(message="Usage: /language [show|list|set zh-CN|set en]")
|
|
1690
|
+
|
|
1691
|
+
async def _output_style_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1692
|
+
settings = load_settings()
|
|
1693
|
+
tokens = args.split(maxsplit=1)
|
|
1694
|
+
styles = load_output_styles()
|
|
1695
|
+
available = {style.name: style for style in styles}
|
|
1696
|
+
current = (
|
|
1697
|
+
context.app_state.get().output_style
|
|
1698
|
+
if context.app_state is not None
|
|
1699
|
+
else settings.output_style
|
|
1700
|
+
)
|
|
1701
|
+
if not tokens or tokens[0] == "show":
|
|
1702
|
+
return CommandResult(message=f"Output style: {current}")
|
|
1703
|
+
if tokens[0] == "list":
|
|
1704
|
+
return CommandResult(
|
|
1705
|
+
message="\n".join(f"{style.name} [{style.source}]" for style in styles)
|
|
1706
|
+
)
|
|
1707
|
+
if tokens[0] == "set" and len(tokens) == 2:
|
|
1708
|
+
if tokens[1] not in available:
|
|
1709
|
+
return CommandResult(message=f"Unknown output style: {tokens[1]}")
|
|
1710
|
+
settings.output_style = tokens[1]
|
|
1711
|
+
save_settings(settings)
|
|
1712
|
+
if context.app_state is not None:
|
|
1713
|
+
context.app_state.set(output_style=tokens[1])
|
|
1714
|
+
return CommandResult(message=f"Output style set to {tokens[1]}")
|
|
1715
|
+
return CommandResult(message="Usage: /output-style [show|list|set NAME]")
|
|
1716
|
+
|
|
1717
|
+
async def _doctor_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1718
|
+
settings = load_settings()
|
|
1719
|
+
memory_dir = get_project_memory_dir(context.cwd)
|
|
1720
|
+
state = context.app_state.get() if context.app_state is not None else None
|
|
1721
|
+
lines = [
|
|
1722
|
+
"Doctor summary:",
|
|
1723
|
+
f"- cwd: {context.cwd}",
|
|
1724
|
+
f"- model: {settings.model}",
|
|
1725
|
+
f"- permission_mode: {state.permission_mode if state is not None else settings.permission.mode}",
|
|
1726
|
+
f"- output_style: {state.output_style if state is not None else settings.output_style}",
|
|
1727
|
+
f"- ui_language: {state.ui_language if state is not None else settings.ui_language}",
|
|
1728
|
+
f"- effort: {state.effort if state is not None else settings.effort}",
|
|
1729
|
+
f"- passes: {state.passes if state is not None else settings.passes}",
|
|
1730
|
+
f"- memory_dir: {memory_dir}",
|
|
1731
|
+
f"- plugin_count: {max(len(context.plugin_summary.splitlines()) - 1, 0) if context.plugin_summary else 0}",
|
|
1732
|
+
f"- mcp_configured: {'yes' if context.mcp_summary and 'No MCP' not in context.mcp_summary else 'no'}",
|
|
1733
|
+
]
|
|
1734
|
+
return CommandResult(message="\n".join(lines))
|
|
1735
|
+
|
|
1736
|
+
async def _privacy_settings_handler(_: str, context: CommandContext) -> CommandResult:
|
|
1737
|
+
settings = load_settings()
|
|
1738
|
+
session_dir = get_project_session_dir(context.cwd)
|
|
1739
|
+
lines = [
|
|
1740
|
+
"Privacy settings:",
|
|
1741
|
+
f"- user_config_dir: {get_config_dir()}",
|
|
1742
|
+
f"- project_config_dir: {get_project_config_dir(context.cwd)}",
|
|
1743
|
+
f"- session_dir: {session_dir}",
|
|
1744
|
+
f"- feedback_log: {get_feedback_log_path()}",
|
|
1745
|
+
f"- api_base_url: {settings.base_url or '(default Anthropic-compatible endpoint)'}",
|
|
1746
|
+
"- network: enabled only for provider and explicit web/MCP calls",
|
|
1747
|
+
"- storage: local files under ~/.illusion and project .illusion",
|
|
1748
|
+
]
|
|
1749
|
+
return CommandResult(message="\n".join(lines))
|
|
1750
|
+
|
|
1751
|
+
async def _diff_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1752
|
+
if args.strip() == "full":
|
|
1753
|
+
ok, output = _run_git_command(context.cwd, "diff", "HEAD")
|
|
1754
|
+
return CommandResult(message=output or "(no diff)")
|
|
1755
|
+
ok, output = _run_git_command(context.cwd, "diff", "--stat")
|
|
1756
|
+
if not ok:
|
|
1757
|
+
return CommandResult(message=output)
|
|
1758
|
+
return CommandResult(message=output or "(no diff)")
|
|
1759
|
+
|
|
1760
|
+
async def _branch_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1761
|
+
action = args.strip() or "show"
|
|
1762
|
+
if action == "show":
|
|
1763
|
+
ok, current = _run_git_command(context.cwd, "branch", "--show-current")
|
|
1764
|
+
if not ok:
|
|
1765
|
+
return CommandResult(message=current)
|
|
1766
|
+
return CommandResult(message=f"Current branch: {current or '(detached HEAD)'}")
|
|
1767
|
+
if action == "list":
|
|
1768
|
+
ok, branches = _run_git_command(context.cwd, "branch", "--format", "%(refname:short)")
|
|
1769
|
+
return CommandResult(message=branches if ok else branches)
|
|
1770
|
+
return CommandResult(message="Usage: /branch [show|list]")
|
|
1771
|
+
|
|
1772
|
+
async def _commit_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1773
|
+
message = args.strip()
|
|
1774
|
+
if not message:
|
|
1775
|
+
ok, status = _run_git_command(context.cwd, "status", "--short")
|
|
1776
|
+
return CommandResult(message=status if ok and status else "(working tree clean)")
|
|
1777
|
+
ok, status = _run_git_command(context.cwd, "status", "--short")
|
|
1778
|
+
if not ok:
|
|
1779
|
+
return CommandResult(message=status)
|
|
1780
|
+
if not status.strip():
|
|
1781
|
+
return CommandResult(message="Nothing to commit.")
|
|
1782
|
+
ok, output = _run_git_command(context.cwd, "add", "-A")
|
|
1783
|
+
if not ok:
|
|
1784
|
+
return CommandResult(message=output)
|
|
1785
|
+
ok, output = _run_git_command(context.cwd, "commit", "-m", message)
|
|
1786
|
+
return CommandResult(message=output if ok else output)
|
|
1787
|
+
|
|
1788
|
+
async def _delete_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1789
|
+
from illusion.services.session_storage import (
|
|
1790
|
+
delete_all_sessions,
|
|
1791
|
+
delete_session_by_id,
|
|
1792
|
+
list_session_snapshots,
|
|
1793
|
+
)
|
|
1794
|
+
from illusion.services.file_history import cleanup_file_history, cleanup_all_file_histories
|
|
1795
|
+
|
|
1796
|
+
tokens = args.strip().split()
|
|
1797
|
+
|
|
1798
|
+
# /delete — 列出会话供选择
|
|
1799
|
+
if not tokens:
|
|
1800
|
+
sessions = list_session_snapshots(context.cwd, limit=10)
|
|
1801
|
+
if not sessions:
|
|
1802
|
+
return CommandResult(message="No saved sessions found for this project.")
|
|
1803
|
+
import time
|
|
1804
|
+
lines = ["Saved sessions:"]
|
|
1805
|
+
for s in sessions:
|
|
1806
|
+
ts = time.strftime("%m/%d %H:%M", time.localtime(s["created_at"]))
|
|
1807
|
+
summary = s["summary"][:50] or "(no summary)"
|
|
1808
|
+
lines.append(f" {s['session_id']} {ts} {s['message_count']}msg {summary}")
|
|
1809
|
+
lines.append("")
|
|
1810
|
+
lines.append("Usage: /delete <session_id> — delete a specific session")
|
|
1811
|
+
lines.append(" /delete all — delete all sessions")
|
|
1812
|
+
return CommandResult(message="\n".join(lines))
|
|
1813
|
+
|
|
1814
|
+
# /delete all / /delete __all__ — 清除所有会话
|
|
1815
|
+
if tokens[0] in ("all", "__all__"):
|
|
1816
|
+
count = delete_all_sessions(context.cwd)
|
|
1817
|
+
cleanup_all_file_histories()
|
|
1818
|
+
context.engine.clear()
|
|
1819
|
+
return CommandResult(
|
|
1820
|
+
message=f"Deleted {count} session file(s).",
|
|
1821
|
+
clear_screen=True,
|
|
1822
|
+
reset_session=True,
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
# /delete <session_id> — 删除指定会话
|
|
1826
|
+
sid = tokens[0]
|
|
1827
|
+
if delete_session_by_id(context.cwd, sid):
|
|
1828
|
+
cleanup_file_history(sid)
|
|
1829
|
+
if sid == context.session_id:
|
|
1830
|
+
context.engine.clear()
|
|
1831
|
+
return CommandResult(
|
|
1832
|
+
message=f"Deleted current session: {sid}",
|
|
1833
|
+
clear_screen=True,
|
|
1834
|
+
reset_session=True,
|
|
1835
|
+
)
|
|
1836
|
+
return CommandResult(message=f"Deleted session: {sid}")
|
|
1837
|
+
return CommandResult(message=f"Session not found: {sid}")
|
|
1838
|
+
|
|
1839
|
+
async def _rules_handler(args: str, context: CommandContext) -> CommandResult:
|
|
1840
|
+
from illusion.skills.loader import get_project_rules_dir
|
|
1841
|
+
|
|
1842
|
+
rules_dir = get_project_rules_dir(context.cwd)
|
|
1843
|
+
rule_files = sorted(rules_dir.glob("*.md"))
|
|
1844
|
+
|
|
1845
|
+
if not rule_files:
|
|
1846
|
+
return CommandResult(message=f"No rules found in {rules_dir}")
|
|
1847
|
+
|
|
1848
|
+
tokens = args.strip().split()
|
|
1849
|
+
|
|
1850
|
+
# /rules — 列出所有规则
|
|
1851
|
+
if not tokens:
|
|
1852
|
+
lines = [f"Rules directory: {rules_dir}", ""]
|
|
1853
|
+
for i, path in enumerate(rule_files, 1):
|
|
1854
|
+
# 读取第一行作为预览
|
|
1855
|
+
content = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
1856
|
+
first_line = content.split("\n", 1)[0][:60] if content else "(empty)"
|
|
1857
|
+
lines.append(f" {i}. {path.stem} — {first_line}")
|
|
1858
|
+
lines.append("")
|
|
1859
|
+
lines.append("Usage: /rules <name|number> — view a specific rule")
|
|
1860
|
+
return CommandResult(message="\n".join(lines))
|
|
1861
|
+
|
|
1862
|
+
# /rules <name|number> — 显示指定规则内容
|
|
1863
|
+
target = tokens[0]
|
|
1864
|
+
selected = None
|
|
1865
|
+
|
|
1866
|
+
# 按编号选择
|
|
1867
|
+
try:
|
|
1868
|
+
idx = int(target) - 1
|
|
1869
|
+
if 0 <= idx < len(rule_files):
|
|
1870
|
+
selected = rule_files[idx]
|
|
1871
|
+
except ValueError:
|
|
1872
|
+
pass
|
|
1873
|
+
|
|
1874
|
+
# 按名称选择
|
|
1875
|
+
if selected is None:
|
|
1876
|
+
for path in rule_files:
|
|
1877
|
+
if path.stem.lower() == target.lower():
|
|
1878
|
+
selected = path
|
|
1879
|
+
break
|
|
1880
|
+
|
|
1881
|
+
if selected is None:
|
|
1882
|
+
return CommandResult(message=f"Rule not found: {target}. Use /rules to list available rules.")
|
|
1883
|
+
|
|
1884
|
+
content = selected.read_text(encoding="utf-8", errors="replace").strip()
|
|
1885
|
+
return CommandResult(message=f"# {selected.stem}\n\n{content}")
|
|
1886
|
+
|
|
1887
|
+
registry.register(SlashCommand("exit", "Exit IllusionCode", _exit_handler))
|
|
1888
|
+
registry.register(SlashCommand("clear", "Clear conversation and start a new session", _new_handler))
|
|
1889
|
+
registry.register(SlashCommand("new", "Start a new conversation session", _new_handler))
|
|
1890
|
+
registry.register(SlashCommand("version", "Show the installed IllusionCode version", _version_handler))
|
|
1891
|
+
registry.register(SlashCommand("status", "Show session status", _status_handler))
|
|
1892
|
+
registry.register(SlashCommand("context", "Show active system prompt or manage context window", _context_handler))
|
|
1893
|
+
registry.register(SlashCommand("summary", "Summarize conversation history", _summary_handler))
|
|
1894
|
+
registry.register(SlashCommand("compact", "Compact older conversation history", _compact_handler))
|
|
1895
|
+
registry.register(SlashCommand("memory", "Inspect and manage project memory", _memory_handler))
|
|
1896
|
+
registry.register(SlashCommand("hooks", "Show configured hooks", _hooks_handler))
|
|
1897
|
+
registry.register(SlashCommand("resume", "Restore the latest saved session", _resume_handler))
|
|
1898
|
+
registry.register(SlashCommand("export", "Export the current transcript", _export_handler))
|
|
1899
|
+
registry.register(SlashCommand("share", "Create a shareable transcript snapshot", _share_handler))
|
|
1900
|
+
registry.register(SlashCommand("copy", "Copy the latest response or provided text", _copy_handler))
|
|
1901
|
+
registry.register(SlashCommand("rewind", "Remove the latest conversation turn(s)", _rewind_handler))
|
|
1902
|
+
registry.register(SlashCommand("files", "List files in the current workspace", _files_handler))
|
|
1903
|
+
registry.register(SlashCommand("init", "Initialize project IllusionCode files", _init_handler))
|
|
1904
|
+
registry.register(SlashCommand("bridge", "Inspect bridge helpers and spawn bridge sessions", _bridge_handler))
|
|
1905
|
+
registry.register(SlashCommand("login", "Show auth status or store an API key", _login_handler))
|
|
1906
|
+
registry.register(SlashCommand("logout", "Clear the stored API key", _logout_handler))
|
|
1907
|
+
registry.register(SlashCommand("feedback", "Save CLI feedback to the local feedback log", _feedback_handler))
|
|
1908
|
+
registry.register(SlashCommand("skills", "List or show available skills", _skills_handler))
|
|
1909
|
+
registry.register(SlashCommand("config", "Show or update configuration", _config_handler))
|
|
1910
|
+
registry.register(SlashCommand("mcp", "Show MCP status", _mcp_handler))
|
|
1911
|
+
registry.register(SlashCommand("plugin", "Manage plugins", _plugin_handler))
|
|
1912
|
+
registry.register(SlashCommand("reload-plugins", "Reload plugin discovery for this workspace", _reload_plugins_handler))
|
|
1913
|
+
registry.register(SlashCommand("permissions", "Show or update permission mode", _permissions_handler))
|
|
1914
|
+
registry.register(SlashCommand("plan", "Toggle plan permission mode", _plan_handler))
|
|
1915
|
+
registry.register(SlashCommand("thinking", "Show or update thinking mode", _thinking_handler))
|
|
1916
|
+
registry.register(SlashCommand("help", "Show available commands and their usage", _help_handler))
|
|
1917
|
+
registry.register(SlashCommand("fast", "Show or update fast mode", _fast_handler))
|
|
1918
|
+
registry.register(SlashCommand("effort", "Show or update reasoning effort", _effort_handler))
|
|
1919
|
+
registry.register(SlashCommand("passes", "Show or update reasoning pass count", _passes_handler))
|
|
1920
|
+
registry.register(SlashCommand("turns", "Show or update maximum agentic turn count", _turns_handler))
|
|
1921
|
+
registry.register(SlashCommand("continue", "Continue the previous tool loop if it was interrupted", _continue_handler))
|
|
1922
|
+
registry.register(SlashCommand("model", "Show or update the default model", _model_handler))
|
|
1923
|
+
registry.register(SlashCommand("language", "Show or update UI language", _language_handler))
|
|
1924
|
+
registry.register(SlashCommand("output-style", "Show or update output style", _output_style_handler))
|
|
1925
|
+
registry.register(SlashCommand("doctor", "Show environment diagnostics", _doctor_handler))
|
|
1926
|
+
registry.register(SlashCommand("diff", "Show git diff output", _diff_handler))
|
|
1927
|
+
registry.register(SlashCommand("branch", "Show git branch information", _branch_handler))
|
|
1928
|
+
registry.register(SlashCommand("commit", "Show status or create a git commit", _commit_handler))
|
|
1929
|
+
registry.register(SlashCommand("issue", "Show or update project issue context", _issue_handler))
|
|
1930
|
+
registry.register(SlashCommand("pr_comments", "Show or update project PR comments context", _pr_comments_handler))
|
|
1931
|
+
registry.register(SlashCommand("privacy-settings", "Show local privacy and storage settings", _privacy_settings_handler))
|
|
1932
|
+
registry.register(SlashCommand("delete", "Delete saved sessions", _delete_handler))
|
|
1933
|
+
registry.register(SlashCommand("rules", "View project rules", _rules_handler))
|
|
1934
|
+
return registry
|