illusion-code 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
illusion/cli.py ADDED
@@ -0,0 +1,1228 @@
1
+ """
2
+ IllusionCode CLI 入口模块
3
+ ========================
4
+
5
+ 本模块提供 IllusionCode 命令行界面,使用 typer 构建。
6
+
7
+ 主要功能:
8
+ - 交互式会话模式
9
+ - 非交互式打印模式
10
+ - MCP 服务器管理
11
+ - 插件管理
12
+ - 认证管理
13
+ - Cron 任务调度管理
14
+
15
+ 子命令说明:
16
+ - mcp: MCP 服务器管理(list、add、remove)
17
+ - plugin: 插件管理(list、install、uninstall)
18
+ - auth: 认证管理(login、status、logout、switch)
19
+ - cron: Cron 调度管理(start、stop、status、list、toggle、history、logs)
20
+
21
+ 使用示例:
22
+ >>> illusion # 启动交互式会话
23
+ >>> illusion -p "你的提示词" # 非交互式打印模式
24
+ >>> illusion auth login # 认证登录
25
+ >>> illusion mcp list # 列出 MCP 服务器
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json # JSON 解析和序列化
31
+ import sys # 系统相关功能
32
+ from pathlib import Path # 路径操作
33
+ from typing import Any, Optional # 类型注解
34
+
35
+ import typer # CLI 框架
36
+
37
+ # 确保 Windows 上 stdout/stderr 使用 UTF-8,防止通过 tsx 继承 stdio 管道时的 UnicodeEncodeError
38
+ if hasattr(sys.stdout, "reconfigure"):
39
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
40
+ if hasattr(sys.stderr, "reconfigure"):
41
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
42
+
43
+ # 应用程序版本
44
+ __version__ = "0.1.0"
45
+
46
+
47
+ def _version_callback(value: bool) -> None:
48
+ """版本回调函数
49
+
50
+ 当用户使用 --version 选项时调用,打印版本号并退出程序。
51
+
52
+ Args:
53
+ value: 标志位,当前始终为 True
54
+ """
55
+ if value:
56
+ print(f"illusion {__version__}") # 打印版本信息
57
+ raise typer.Exit() # 退出程序
58
+
59
+
60
+ # 创建主应用程序
61
+ app = typer.Typer(
62
+ name="illusion",
63
+ help=(
64
+ "Illusion Code - AI 驱动的编程助手\n"
65
+ "默认启动交互式会话,使用 -p/--print 进入非交互模式"
66
+ ),
67
+ add_completion=False,
68
+ rich_markup_mode="rich",
69
+ invoke_without_command=True,
70
+ )
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # 子命令
75
+ # ---------------------------------------------------------------------------
76
+
77
+ # 创建子命令应用(mcp、plugin、auth、cron)
78
+ mcp_app = typer.Typer(name="mcp", help="MCP 服务器管理 / Manage MCP servers")
79
+ plugin_app = typer.Typer(name="plugin", help="插件管理 / Manage plugins")
80
+ auth_app = typer.Typer(name="auth", help="认证管理 / Manage authentication")
81
+ cron_app = typer.Typer(name="cron", help="定时任务管理 / Manage cron scheduler and jobs")
82
+ web_app = typer.Typer(name="web", help="启动 Web 界面 / Launch Web UI")
83
+
84
+ # 注册子命令到主应用
85
+ app.add_typer(mcp_app)
86
+ app.add_typer(plugin_app)
87
+ app.add_typer(auth_app)
88
+ app.add_typer(cron_app)
89
+ app.add_typer(web_app)
90
+
91
+
92
+ # ---- mcp 子命令 ----
93
+
94
+ @mcp_app.command("list")
95
+ def mcp_list() -> None:
96
+ """列出已配置的 MCP 服务器
97
+
98
+ 加载当前设置和插件,列出所有已配置的 MCP 服务器及其传输类型。
99
+ """
100
+ from illusion.config import load_settings
101
+ from illusion.mcp.config import load_mcp_server_configs
102
+ from illusion.plugins import load_plugins
103
+
104
+ settings = load_settings()
105
+ cwd = str(Path.cwd())
106
+ plugins = load_plugins(settings, cwd)
107
+ configs = load_mcp_server_configs(settings, plugins, cwd)
108
+ if not configs:
109
+ print(_t("mcp_none"))
110
+ return
111
+ for name, cfg in configs.items():
112
+ if hasattr(cfg, "type"):
113
+ transport = cfg.type
114
+ if transport == "stdio":
115
+ cmd = getattr(cfg, "command", "")
116
+ detail = f" ({cmd})" if cmd else ""
117
+ elif transport in ("http", "ws"):
118
+ url = getattr(cfg, "url", "")
119
+ detail = f" ({url})" if url else ""
120
+ else:
121
+ detail = ""
122
+ else:
123
+ transport = "unknown"
124
+ detail = ""
125
+ print(f" {name}: {transport}{detail}")
126
+
127
+
128
+ @mcp_app.command("add")
129
+ def mcp_add(
130
+ name: str = typer.Argument(..., help="Server name"),
131
+ config_json: str = typer.Argument(..., help="Server config as JSON string"),
132
+ ) -> None:
133
+ """添加 MCP 服务器配置
134
+
135
+ Args:
136
+ name: 服务器名称
137
+ config_json: 服务器配置的 JSON 字符串
138
+ """
139
+ from illusion.config import load_settings, save_settings
140
+ from illusion.mcp.types import McpServerConfig
141
+
142
+ settings = load_settings()
143
+ try:
144
+ raw = json.loads(config_json)
145
+ except json.JSONDecodeError as exc:
146
+ print(_t("mcp_invalid_json", exc=exc), file=sys.stderr)
147
+ raise typer.Exit(1)
148
+ try:
149
+ cfg = McpServerConfig.model_validate(raw)
150
+ except Exception as exc:
151
+ print(_t("mcp_invalid_config", exc=exc), file=sys.stderr)
152
+ raise typer.Exit(1)
153
+ if not isinstance(settings.mcp_servers, dict):
154
+ settings.mcp_servers = {}
155
+ settings.mcp_servers[name] = cfg
156
+ save_settings(settings)
157
+ print(_t("mcp_added", name=name))
158
+
159
+
160
+ @mcp_app.command("remove")
161
+ def mcp_remove(
162
+ name: str = typer.Argument(..., help="Server name to remove"),
163
+ ) -> None:
164
+ """移除 MCP 服务器配置
165
+
166
+ Args:
167
+ name: 要移除的服务器名称
168
+ """
169
+ from illusion.config import load_settings, save_settings
170
+
171
+ settings = load_settings()
172
+ if not isinstance(settings.mcp_servers, dict) or name not in settings.mcp_servers:
173
+ print(_t("mcp_not_found", name=name), file=sys.stderr)
174
+ raise typer.Exit(1)
175
+ del settings.mcp_servers[name]
176
+ save_settings(settings)
177
+ print(_t("mcp_removed", name=name))
178
+
179
+
180
+ # ---- plugin 子命令 ----
181
+
182
+ @plugin_app.command("list")
183
+ def plugin_list() -> None:
184
+ """列出已安装的插件"""
185
+ from illusion.config import load_settings
186
+ from illusion.plugins import load_plugins
187
+
188
+ settings = load_settings()
189
+ plugins = load_plugins(settings, str(Path.cwd()))
190
+ if not plugins:
191
+ print(_t("plugin_none"))
192
+ return
193
+ for plugin in plugins:
194
+ status = _t("plugin_enabled") if plugin.enabled else _t("plugin_disabled")
195
+ print(f" {plugin.name} [{status}] - {plugin.description or ''}")
196
+
197
+
198
+ @plugin_app.command("install")
199
+ def plugin_install(
200
+ source: str = typer.Argument(..., help="Plugin source (path or URL)"),
201
+ ) -> None:
202
+ """从源路径安装插件"""
203
+ from illusion.plugins.installer import install_plugin_from_path
204
+
205
+ result = install_plugin_from_path(source)
206
+ print(_t("plugin_installed", name=result))
207
+
208
+
209
+ @plugin_app.command("uninstall")
210
+ def plugin_uninstall(
211
+ name: str = typer.Argument(..., help="Plugin name to uninstall"),
212
+ ) -> None:
213
+ """卸载插件"""
214
+ from illusion.plugins.installer import uninstall_plugin
215
+
216
+ uninstall_plugin(name)
217
+ print(_t("plugin_uninstalled", name=name))
218
+
219
+
220
+ # ---- cron 子命令(对齐 openclaw cron CLI) ----
221
+
222
+ @cron_app.command("start")
223
+ def cron_start() -> None:
224
+ """启动 cron 调度器"""
225
+ from illusion.services.cron_scheduler import is_scheduler_running, start_daemon
226
+
227
+ if is_scheduler_running():
228
+ print(_t("cron_already_running"))
229
+ return
230
+ pid = start_daemon()
231
+ print(_t("cron_started", pid=pid))
232
+
233
+
234
+ @cron_app.command("stop")
235
+ def cron_stop() -> None:
236
+ """停止 cron 调度器"""
237
+ from illusion.services.cron_scheduler import stop_scheduler
238
+
239
+ if stop_scheduler():
240
+ print(_t("cron_stopped"))
241
+ else:
242
+ print(_t("cron_not_running"))
243
+
244
+
245
+ @cron_app.command("status")
246
+ def cron_status_cmd() -> None:
247
+ """显示 cron 调度器状态和任务统计"""
248
+ from illusion.services.cron_scheduler import scheduler_status
249
+
250
+ status = scheduler_status()
251
+ state = _t("cron_state_running") if status["running"] else _t("cron_state_stopped")
252
+ print(f"Scheduler: {state}" + (f" (pid={status['pid']})" if status["pid"] else ""))
253
+ print(f"Jobs: {status['enabled_jobs']} {_t('cron_enabled')} / {status['total_jobs']} total")
254
+ print(f"Log: {status['log_file']}")
255
+
256
+
257
+ @cron_app.command("list")
258
+ def cron_list_cmd() -> None:
259
+ """列出所有 cron 任务"""
260
+ from illusion.services.cron import load_cron_jobs
261
+
262
+ jobs = load_cron_jobs()
263
+ if not jobs:
264
+ print(_t("cron_jobs_none"))
265
+ return
266
+ never = _t("cron_never")
267
+ na = _t("cron_na")
268
+ for job in jobs:
269
+ enabled = "+" if job.get("enabled", True) else "-"
270
+ name = job.get("name", job.get("id", "?"))
271
+ schedule = job.get("schedule", "?")
272
+ recurring = _t("cron_recurring") if job.get("recurring", True) else _t("cron_oneshot")
273
+
274
+ last = job.get("last_run", never)
275
+ if last != never:
276
+ last = last[:19]
277
+ last_status = job.get("last_status", "")
278
+ status_indicator = f" [{last_status}]" if last_status else ""
279
+
280
+ next_run = job.get("next_run", na)
281
+ if next_run != na:
282
+ next_run = next_run[:19]
283
+
284
+ errors = job.get("consecutive_errors", 0)
285
+ error_str = f" [{_t('cron_errors', n=errors)}]" if errors > 0 else ""
286
+
287
+ print(f" [{enabled}] {name} {schedule} ({recurring})")
288
+ print(f" {_t('cron_prompt_label')}: {job.get('prompt', '?')[:60]}")
289
+ print(f" {_t('cron_last_label')}: {last}{status_indicator} {_t('cron_next_label')}: {next_run}{error_str}")
290
+
291
+
292
+ @cron_app.command("toggle")
293
+ def cron_toggle_cmd(
294
+ name: str = typer.Argument(..., help="Job name or ID"),
295
+ enabled: bool = typer.Argument(..., help="true to enable, false to disable"),
296
+ ) -> None:
297
+ """启用或禁用 cron 任务"""
298
+ from illusion.services.cron import set_job_enabled
299
+
300
+ if not set_job_enabled(name, enabled):
301
+ print(_t("cron_job_not_found", name=name))
302
+ raise typer.Exit(1)
303
+ state = _t("cron_enabled") if enabled else _t("cron_disabled")
304
+ print(_t("cron_job_state", name=name, state=state))
305
+
306
+
307
+ @cron_app.command("run")
308
+ def cron_run_cmd(
309
+ name: str = typer.Argument(..., help="Job name or ID"),
310
+ ) -> None:
311
+ """手动触发执行 cron 任务"""
312
+ import asyncio
313
+
314
+ from illusion.services.cron import get_cron_job
315
+ from illusion.services.cron_scheduler import execute_job
316
+
317
+ job = get_cron_job(name)
318
+ if job is None:
319
+ print(_t("cron_job_not_found", name=name))
320
+ raise typer.Exit(1)
321
+
322
+ prompt = job.get("prompt", "")
323
+ if not prompt:
324
+ print(_t("cron_no_prompt", name=name))
325
+ raise typer.Exit(1)
326
+
327
+ print(_t("cron_running_job", name=name))
328
+ entry = asyncio.run(execute_job(job))
329
+ status = entry.get("status", "unknown")
330
+ rc = entry.get("returncode", "?")
331
+ print(_t("cron_finished", status=status, rc=rc))
332
+
333
+ stdout = entry.get("stdout", "").strip()
334
+ stderr = entry.get("stderr", "").strip()
335
+ if stdout:
336
+ print(f"{_t('cron_output')}\n{stdout}")
337
+ if stderr and status != "success":
338
+ print(f"{_t('cron_error')}\n{stderr}")
339
+
340
+
341
+ @cron_app.command("history")
342
+ def cron_history_cmd(
343
+ name: str | None = typer.Argument(None, help="Filter by job name"),
344
+ limit: int = typer.Option(20, "--limit", "-n", help="Number of entries"),
345
+ ) -> None:
346
+ """显示 cron 执行历史记录"""
347
+ from illusion.services.cron_scheduler import load_history
348
+
349
+ entries = load_history(limit=limit, job_name=name)
350
+ if not entries:
351
+ print(_t("cron_no_history"))
352
+ return
353
+ for entry in entries:
354
+ ts = entry.get("started_at", "?")[:19]
355
+ status = entry.get("status", "?")
356
+ rc = entry.get("returncode", "?")
357
+ job_name = entry.get("name", "?")
358
+ prompt_preview = entry.get("prompt", "")[:40]
359
+ print(f" {ts} {job_name} {status} (rc={rc})")
360
+ if prompt_preview:
361
+ print(f" {_t('cron_prompt_label')}: {prompt_preview}")
362
+ stderr = entry.get("stderr", "").strip()
363
+ if stderr and status != "success":
364
+ for line in stderr.splitlines()[:3]:
365
+ print(f" {_t('cron_error')} {line}")
366
+
367
+
368
+ @cron_app.command("logs")
369
+ def cron_logs_cmd(
370
+ lines: int = typer.Option(30, "--lines", "-n", help="Number of lines"),
371
+ ) -> None:
372
+ """显示 cron 调度器日志"""
373
+ from illusion.config.paths import get_logs_dir
374
+
375
+ log_path = get_logs_dir() / "cron_scheduler.log"
376
+ if not log_path.exists():
377
+ print(_t("cron_no_log"))
378
+ return
379
+ content = log_path.read_text(encoding="utf-8", errors="replace")
380
+ tail = content.splitlines()[-lines:]
381
+ for line in tail:
382
+ print(line)
383
+
384
+
385
+ # ---- auth 子命令 ----
386
+
387
+ # i18n 从共享模块导入
388
+ from illusion.config.i18n import MESSAGES as _I18N, t as _t # noqa: E402
389
+
390
+
391
+ def _ensure_language() -> str:
392
+ """确保 ui_language 已设置,未设置时让用户选择
393
+
394
+ Returns:
395
+ str: 当前 ui_language 值
396
+ """
397
+ from illusion.config import load_settings, save_settings
398
+ settings = load_settings()
399
+ if settings.ui_language:
400
+ return settings.ui_language
401
+
402
+ print(_t("select_language"))
403
+ print(" 1. 中文 (zh-CN)")
404
+ print(" 2. English (en-US)")
405
+ raw = typer.prompt("1/2", default="1")
406
+ lang = "zh-CN" if raw.strip() == "1" else "en-US"
407
+ settings.ui_language = lang
408
+ save_settings(settings)
409
+ return lang
410
+
411
+
412
+ _PROVIDER_OPTIONS: list[tuple[str, dict[str, str]]] = [
413
+ ("custom", _I18N["custom_provider"]),
414
+ ("anthropic", _I18N["anthropic_label"]),
415
+ ("openai", _I18N["openai_label"]),
416
+ ("copilot", _I18N["copilot_label"]),
417
+ ("codex", _I18N["codex_label"]),
418
+ ]
419
+
420
+ _API_FORMAT_OPTIONS: list[tuple[str, str]] = [
421
+ ("openai", "OpenAI"),
422
+ ("anthropic", "Anthropic"),
423
+ ]
424
+
425
+ _DEFAULT_ENDPOINTS: dict[str, str] = {
426
+ "anthropic": "https://api.anthropic.com",
427
+ "openai": "https://api.openai.com/v1",
428
+ "copilot": "https://api.githubcopilot.com",
429
+ "codex": "https://chatgpt.com/backend-api",
430
+ }
431
+
432
+ _DEFAULT_MODELS: dict[str, str] = {
433
+ "anthropic": "claude-sonnet-4-6",
434
+ "openai": "gpt-5.4",
435
+ "copilot": "gpt-5.5",
436
+ "codex": "codex-mini",
437
+ }
438
+
439
+
440
+ @auth_app.command("login")
441
+ def auth_login() -> None:
442
+ """交互式配置提供商认证
443
+
444
+ 流程:选择提供商 → 认证 → 保存
445
+ Copilot 使用 GitHub OAuth 设备码流程,其他提供商使用 API 密钥。
446
+ """
447
+ from illusion.auth.flows import ApiKeyFlow
448
+ from illusion.auth.manager import AuthManager
449
+ from illusion.auth.storage import store_env_credential
450
+
451
+ _ensure_language()
452
+ manager = AuthManager()
453
+
454
+ # 1. 选择提供商
455
+ print(_t("select_provider"))
456
+ for i, (key, labels) in enumerate(_PROVIDER_OPTIONS, 1):
457
+ lang = manager.settings.ui_language or "en-US"
458
+ label = labels.get(lang, labels.get("en-US", key))
459
+ print(f" {i}. {label}")
460
+ raw = typer.prompt(_t("enter_number"), default="1")
461
+ try:
462
+ idx = int(raw.strip()) - 1
463
+ if 0 <= idx < len(_PROVIDER_OPTIONS):
464
+ provider_choice = _PROVIDER_OPTIONS[idx][0]
465
+ else:
466
+ print(_t("invalid_selection"), file=sys.stderr)
467
+ raise typer.Exit(1)
468
+ except ValueError:
469
+ print(_t("invalid_selection"), file=sys.stderr)
470
+ raise typer.Exit(1)
471
+
472
+ # --- Copilot 走设备码 OAuth 流程 ---
473
+ if provider_choice == "copilot":
474
+ _copilot_login(manager)
475
+ return
476
+
477
+ # --- Codex 走外部 CLI 凭据读取流程 ---
478
+ if provider_choice == "codex":
479
+ _codex_login(manager)
480
+ return
481
+
482
+ # --- 其他提供商走 API 密钥流程 ---
483
+
484
+ # 2. 确定 API 格式
485
+ if provider_choice == "anthropic":
486
+ api_format = "anthropic"
487
+ elif provider_choice == "openai":
488
+ api_format = "openai"
489
+ else:
490
+ # 自定义提供商:让用户选择 API 格式
491
+ print(_t("select_api_format"))
492
+ for i, (fmt, label) in enumerate(_API_FORMAT_OPTIONS, 1):
493
+ print(f" {i}. {label}")
494
+ raw = typer.prompt(_t("enter_number"), default="1")
495
+ try:
496
+ idx = int(raw.strip()) - 1
497
+ if 0 <= idx < len(_API_FORMAT_OPTIONS):
498
+ api_format = _API_FORMAT_OPTIONS[idx][0]
499
+ else:
500
+ print(_t("invalid_selection"), file=sys.stderr)
501
+ raise typer.Exit(1)
502
+ except ValueError:
503
+ print(_t("invalid_selection"), file=sys.stderr)
504
+ raise typer.Exit(1)
505
+
506
+ # 3. 输入端点
507
+ default_ep = _DEFAULT_ENDPOINTS.get(provider_choice, "")
508
+ if default_ep:
509
+ prompt_text = f"{_t('enter_endpoint')} ({_t('default_endpoint')}: {default_ep}): "
510
+ endpoint = input(prompt_text).strip()
511
+ if not endpoint:
512
+ endpoint = default_ep
513
+ else:
514
+ endpoint = input(f"{_t('enter_endpoint')}: ").strip()
515
+ if not endpoint:
516
+ print(_t("endpoint_required"), file=sys.stderr)
517
+ raise typer.Exit(1)
518
+
519
+ # 4. 输入 API 密钥
520
+ flow = ApiKeyFlow(prompt_text=_t("enter_api_key"))
521
+ try:
522
+ api_key = flow.run()
523
+ except ValueError:
524
+ print(_t("api_key_required"), file=sys.stderr)
525
+ raise typer.Exit(1)
526
+
527
+ # 5. 输入模型名称
528
+ default_model = _DEFAULT_MODELS.get(provider_choice, "")
529
+ if default_model:
530
+ prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
531
+ model_name = input(prompt_text).strip()
532
+ if not model_name:
533
+ model_name = default_model
534
+ else:
535
+ model_name = input(f"{_t('enter_model')}: ").strip()
536
+ if not model_name:
537
+ print(_t("model_required"), file=sys.stderr)
538
+ raise typer.Exit(1)
539
+
540
+ # 6. 分配 env_N 并保存
541
+ envs = manager.list_envs()
542
+ if envs:
543
+ # 找到下一个可用的 env_N
544
+ existing_nums = []
545
+ for k in envs:
546
+ try:
547
+ existing_nums.append(int(k.split("_")[1]))
548
+ except (ValueError, IndexError):
549
+ pass
550
+ next_num = max(existing_nums, default=0) + 1
551
+ else:
552
+ next_num = 1
553
+ env_key = f"env_{next_num}"
554
+
555
+ # 保存到 settings.json
556
+ env_config = {
557
+ "api_format": api_format,
558
+ "base_url": endpoint,
559
+ "api_key": "", # 不在 settings.json 中存储实际密钥
560
+ "model_1": model_name,
561
+ }
562
+ setattr(manager.settings, env_key, env_config)
563
+ manager.settings.model = f"{env_key}:model_1"
564
+ manager.save_settings()
565
+
566
+ # 保存密钥到 credentials.json
567
+ store_env_credential(env_key, "api_key", api_key)
568
+
569
+ print(_t("env_saved", env_key=env_key))
570
+
571
+
572
+ def _copilot_login(manager: Any) -> None:
573
+ """Copilot 设备码 OAuth 认证流程
574
+
575
+ Args:
576
+ manager: AuthManager 实例
577
+ """
578
+
579
+ from illusion.auth.copilot import CopilotAuth
580
+
581
+ copilot = CopilotAuth()
582
+
583
+ # 1. 启动设备码流程
584
+ try:
585
+ flow = copilot.start_device_flow()
586
+ except Exception as exc:
587
+ print(f"Error: {exc}", file=sys.stderr)
588
+ raise typer.Exit(1)
589
+
590
+ # 2. 显示用户码和验证 URL
591
+ print(_t("copilot_open_url"))
592
+ print(f" {flow['verification_uri']}")
593
+ print(_t("copilot_enter_code", code=flow["user_code"]))
594
+ print()
595
+ print(_t("copilot_waiting"))
596
+
597
+ # 3. 轮询等待授权
598
+ try:
599
+ success = copilot.poll_for_token(flow["device_code"])
600
+ except RuntimeError as exc:
601
+ msg = str(exc)
602
+ if "过期" in msg or "expired" in msg.lower():
603
+ print(_t("copilot_device_expired"), file=sys.stderr)
604
+ elif "拒绝" in msg or "denied" in msg.lower():
605
+ print(_t("copilot_auth_denied"), file=sys.stderr)
606
+ elif "订阅" in msg or "subscription" in msg.lower():
607
+ print(_t("copilot_no_subscription"), file=sys.stderr)
608
+ else:
609
+ print(f"Error: {exc}", file=sys.stderr)
610
+ raise typer.Exit(1)
611
+
612
+ if not success:
613
+ print(_t("copilot_device_expired"), file=sys.stderr)
614
+ raise typer.Exit(1)
615
+
616
+ status = copilot.get_status()
617
+ username = status.get("username") or ""
618
+ print(_t("copilot_auth_success", user=username))
619
+
620
+ # 4. 输入模型名称
621
+ default_model = _DEFAULT_MODELS.get("copilot", "gpt-5.5")
622
+ prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
623
+ model_name = input(prompt_text).strip()
624
+ if not model_name:
625
+ model_name = default_model
626
+
627
+ # 5. 分配 env_N 并保存
628
+ envs = manager.list_envs()
629
+ if envs:
630
+ existing_nums = []
631
+ for k in envs:
632
+ try:
633
+ existing_nums.append(int(k.split("_")[1]))
634
+ except (ValueError, IndexError):
635
+ pass
636
+ next_num = max(existing_nums, default=0) + 1
637
+ else:
638
+ next_num = 1
639
+ env_key = f"env_{next_num}"
640
+
641
+ env_config = {
642
+ "api_format": "openai",
643
+ "base_url": _DEFAULT_ENDPOINTS["copilot"],
644
+ "api_key": "",
645
+ "model_1": model_name,
646
+ "provider": "copilot",
647
+ }
648
+ setattr(manager.settings, env_key, env_config)
649
+ manager.settings.model = f"{env_key}:model_1"
650
+ manager.save_settings()
651
+
652
+ print(_t("env_saved", env_key=env_key))
653
+
654
+
655
+ def _codex_login(manager: Any) -> None:
656
+ """Codex 外部 CLI 凭据读取流程
657
+
658
+ 从 ~/.codex/auth.json 读取已由 Codex CLI 管理的认证信息。
659
+
660
+ Args:
661
+ manager: AuthManager 实例
662
+ """
663
+ from illusion.auth.external import default_binding_for_provider, load_external_credential
664
+
665
+ # 1. 检查 Codex CLI 认证是否存在
666
+ binding = default_binding_for_provider("openai_codex")
667
+ try:
668
+ cred = load_external_credential(binding)
669
+ except (ValueError, FileNotFoundError):
670
+ print(_t("codex_not_found"), file=sys.stderr)
671
+ raise typer.Exit(1)
672
+
673
+ username = cred.profile_label or cred.value[:8] + "..."
674
+ print(_t("codex_auth_success", user=username))
675
+
676
+ # 2. 输入模型名称
677
+ default_model = _DEFAULT_MODELS.get("codex", "codex-mini")
678
+ prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
679
+ model_name = input(prompt_text).strip()
680
+ if not model_name:
681
+ model_name = default_model
682
+
683
+ # 3. 分配 env_N 并保存
684
+ envs = manager.list_envs()
685
+ if envs:
686
+ existing_nums = []
687
+ for k in envs:
688
+ try:
689
+ existing_nums.append(int(k.split("_")[1]))
690
+ except (ValueError, IndexError):
691
+ pass
692
+ next_num = max(existing_nums, default=0) + 1
693
+ else:
694
+ next_num = 1
695
+ env_key = f"env_{next_num}"
696
+
697
+ env_config = {
698
+ "api_format": "openai",
699
+ "base_url": _DEFAULT_ENDPOINTS["codex"],
700
+ "api_key": "",
701
+ "model_1": model_name,
702
+ "provider": "codex",
703
+ }
704
+ setattr(manager.settings, env_key, env_config)
705
+ manager.settings.model = f"{env_key}:model_1"
706
+ manager.save_settings()
707
+
708
+ print(_t("env_saved", env_key=env_key))
709
+
710
+
711
+ @auth_app.command("status")
712
+ def auth_status_cmd() -> None:
713
+ """显示所有环境的认证状态"""
714
+ from illusion.auth.manager import AuthManager
715
+
716
+ _ensure_language()
717
+ manager = AuthManager()
718
+ statuses = manager.get_env_credential_statuses()
719
+
720
+ if not statuses:
721
+ print(_t("no_envs"))
722
+ return
723
+
724
+ print(_t("env_status_title"))
725
+
726
+ # 列宽
727
+ col_env = 10
728
+ col_format = 12
729
+ col_model = 28
730
+ col_endpoint = 36
731
+ col_cred = 10
732
+
733
+ header = (
734
+ f"{_t('col_env'):<{col_env}} "
735
+ f"{_t('col_format'):<{col_format}} "
736
+ f"{_t('col_model'):<{col_model}} "
737
+ f"{_t('col_endpoint'):<{col_endpoint}} "
738
+ f"{_t('col_credential'):<{col_cred}} "
739
+ )
740
+ print(header)
741
+ print("-" * len(header))
742
+
743
+ for name, info in statuses.items():
744
+ cred_str = _t("configured") if info["has_credential"] else _t("missing")
745
+ active_str = f" {_t('active_mark')}" if info["active"] else ""
746
+ ep = info["base_url"] or "-"
747
+ print(
748
+ f"{name:<{col_env}} "
749
+ f"{info['api_format']:<{col_format}} "
750
+ f"{info['model']:<{col_model}} "
751
+ f"{ep:<{col_endpoint}} "
752
+ f"{cred_str:<{col_cred}} "
753
+ f"{active_str}"
754
+ )
755
+
756
+
757
+ @auth_app.command("logout")
758
+ def auth_logout(
759
+ env_key: Optional[str] = typer.Argument(None, help="Environment to clear (e.g. env_1)"),
760
+ ) -> None:
761
+ """清除环境的已存储凭据
762
+
763
+ Args:
764
+ env_key: 要清除的环境,默认交互式选择
765
+ """
766
+ from illusion.auth.manager import AuthManager
767
+
768
+ _ensure_language()
769
+ manager = AuthManager()
770
+
771
+ if env_key is None:
772
+ envs = manager.list_envs()
773
+ if not envs:
774
+ print(_t("no_envs"))
775
+ raise typer.Exit(1)
776
+ print(_t("select_env_to_logout"))
777
+ env_keys = list(envs.keys())
778
+ for i, k in enumerate(env_keys, 1):
779
+ print(f" {i}. {k}")
780
+ raw = typer.prompt(_t("enter_number"), default="1")
781
+ try:
782
+ idx = int(raw.strip()) - 1
783
+ if 0 <= idx < len(env_keys):
784
+ env_key = env_keys[idx]
785
+ else:
786
+ print(_t("invalid_selection"), file=sys.stderr)
787
+ raise typer.Exit(1)
788
+ except ValueError:
789
+ print(_t("invalid_selection"), file=sys.stderr)
790
+ raise typer.Exit(1)
791
+
792
+ manager.clear_env_api_key(env_key)
793
+ print(_t("credential_cleared", env_key=env_key))
794
+
795
+
796
+ @auth_app.command("switch")
797
+ def auth_switch(
798
+ env_key: Optional[str] = typer.Argument(None, help="Environment to switch to (e.g. env_1)"),
799
+ ) -> None:
800
+ """切换活动环境
801
+
802
+ Args:
803
+ env_key: 要切换的环境,无参数时交互式选择
804
+ """
805
+ from illusion.auth.manager import AuthManager
806
+
807
+ _ensure_language()
808
+ manager = AuthManager()
809
+
810
+ if env_key is None:
811
+ envs = manager.list_envs()
812
+ if not envs:
813
+ print(_t("no_envs"))
814
+ raise typer.Exit(1)
815
+ print(_t("select_env_to_switch"))
816
+ env_keys = list(envs.keys())
817
+ for i, k in enumerate(env_keys, 1):
818
+ print(f" {i}. {k}")
819
+ raw = typer.prompt(_t("enter_number"), default="1")
820
+ try:
821
+ idx = int(raw.strip()) - 1
822
+ if 0 <= idx < len(env_keys):
823
+ env_key = env_keys[idx]
824
+ else:
825
+ print(_t("invalid_selection"), file=sys.stderr)
826
+ raise typer.Exit(1)
827
+ except ValueError:
828
+ print(_t("invalid_selection"), file=sys.stderr)
829
+ raise typer.Exit(1)
830
+
831
+ try:
832
+ manager.use_env(env_key)
833
+ except ValueError:
834
+ print(_t("env_not_found", env_key=env_key), file=sys.stderr)
835
+ raise typer.Exit(1)
836
+ print(_t("switched_to", env_key=env_key))
837
+
838
+
839
+ @auth_app.command("add-model")
840
+ def auth_add_model(
841
+ env_key: str = typer.Argument(..., help="Environment key (e.g. env_1)"),
842
+ model_name: str = typer.Argument(..., help="Model name to add"),
843
+ ) -> None:
844
+ """在已有的 env_N 中增加模型(model_N)
845
+
846
+ Args:
847
+ env_key: 环境键名,如 env_1
848
+ model_name: 要添加的模型名称
849
+ """
850
+ from illusion.auth.manager import AuthManager
851
+
852
+ _ensure_language()
853
+ manager = AuthManager()
854
+
855
+ env = manager.settings.get_env(env_key)
856
+ if env is None:
857
+ print(_t("env_not_found", env_key=env_key), file=sys.stderr)
858
+ raise typer.Exit(1)
859
+
860
+ # 找到下一个可用的 model_N 编号
861
+ existing = []
862
+ for k in env.list_models():
863
+ try:
864
+ existing.append(int(k.split("_")[1]))
865
+ except (ValueError, IndexError):
866
+ pass
867
+ next_num = max(existing, default=0) + 1
868
+ model_key = f"model_{next_num}"
869
+
870
+ # 写入配置
871
+ env_config = env.model_dump()
872
+ env_config[model_key] = model_name
873
+ setattr(manager.settings, env_key, env_config)
874
+ manager.save_settings()
875
+
876
+ print(_t("model_added", env_key=env_key, model_key=model_key, model_name=model_name))
877
+
878
+
879
+ # ---- web 子命令 ----
880
+
881
+
882
+ @web_app.callback(invoke_without_command=True)
883
+ def web_start(
884
+ port: int = typer.Option(3000, "--port", "-p", help="Web 服务端口"),
885
+ host: str = typer.Option("127.0.0.1", "--host", help="监听地址"),
886
+ dev: bool = typer.Option(False, "--dev", help="开发模式(启用 CORS,不 serve 静态文件)"),
887
+ model: Optional[str] = typer.Option(None, "--model", "-m", help="指定模型"),
888
+ prompt: Optional[str] = typer.Option(None, "--prompt", help="初始提示词"),
889
+ ) -> None:
890
+ """启动 Illusion Code Web 界面 / Launch Illusion Code Web UI"""
891
+ import threading
892
+ import uvicorn
893
+ from illusion.ui.web.server import create_app
894
+ from illusion.ui.web.ws_host import WebHostConfig
895
+
896
+ config = WebHostConfig(
897
+ model=model,
898
+ )
899
+
900
+ app = create_app(dev=dev, host_config=config)
901
+
902
+ url = f"http://{host}:{port}"
903
+ typer.echo(f"Illusion Code Web UI: {url}")
904
+ if not dev:
905
+ import webbrowser
906
+ threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
907
+
908
+ uvicorn.run(app, host=host, port=port, log_level="warning")
909
+
910
+
911
+ # ---------------------------------------------------------------------------
912
+ # 主命令
913
+ # ---------------------------------------------------------------------------
914
+
915
+ @app.callback(invoke_without_command=True)
916
+ def main(
917
+ ctx: typer.Context,
918
+ version: bool = typer.Option(
919
+ False,
920
+ "--version",
921
+ "-v",
922
+ help="Show version and exit",
923
+ callback=_version_callback,
924
+ is_eager=True,
925
+ ),
926
+ # --- Session ---
927
+ continue_session: bool = typer.Option(
928
+ False,
929
+ "--continue",
930
+ "-c",
931
+ help="Continue the most recent conversation in the current directory",
932
+ rich_help_panel="Session",
933
+ ),
934
+ resume: str | None = typer.Option(
935
+ None,
936
+ "--resume",
937
+ "-r",
938
+ help="Resume a conversation by session ID, or open picker",
939
+ rich_help_panel="Session",
940
+ ),
941
+ name: str | None = typer.Option(
942
+ None,
943
+ "--name",
944
+ "-n",
945
+ help="Set a display name for this session",
946
+ rich_help_panel="Session",
947
+ ),
948
+ # --- Model & Effort ---
949
+ model: str | None = typer.Option(
950
+ None,
951
+ "--model",
952
+ "-m",
953
+ help="Model alias (e.g. 'sonnet', 'opus') or full model ID",
954
+ rich_help_panel="Model & Effort",
955
+ ),
956
+ effort: str | None = typer.Option(
957
+ None,
958
+ "--effort",
959
+ help="Effort level for the session (low, medium, high, max)",
960
+ rich_help_panel="Model & Effort",
961
+ ),
962
+ verbose: bool = typer.Option(
963
+ False,
964
+ "--verbose",
965
+ help="Override verbose mode setting from config",
966
+ rich_help_panel="Model & Effort",
967
+ ),
968
+ max_turns: int | None = typer.Option(
969
+ None,
970
+ "--max-turns",
971
+ help="Maximum number of agentic turns (useful with --print)",
972
+ rich_help_panel="Model & Effort",
973
+ ),
974
+ # --- Output ---
975
+ print_mode: str | None = typer.Option(
976
+ None,
977
+ "--print",
978
+ "-p",
979
+ help="Print response and exit. Pass your prompt as the value: -p 'your prompt'",
980
+ rich_help_panel="Output",
981
+ ),
982
+ output_format: str | None = typer.Option(
983
+ None,
984
+ "--output-format",
985
+ help="Output format with --print: text (default), json, or stream-json",
986
+ rich_help_panel="Output",
987
+ ),
988
+ # --- Permissions ---
989
+ permission_mode: str | None = typer.Option(
990
+ None,
991
+ "--permission-mode",
992
+ help="Permission mode: default, plan, or full_auto",
993
+ rich_help_panel="Permissions",
994
+ ),
995
+ dangerously_skip_permissions: bool = typer.Option(
996
+ False,
997
+ "--dangerously-skip-permissions",
998
+ help="Bypass all permission checks (only for sandboxed environments)",
999
+ rich_help_panel="Permissions",
1000
+ ),
1001
+ allowed_tools: Optional[list[str]] = typer.Option(
1002
+ None,
1003
+ "--allowed-tools",
1004
+ help="Comma or space-separated list of tool names to allow",
1005
+ rich_help_panel="Permissions",
1006
+ ),
1007
+ disallowed_tools: Optional[list[str]] = typer.Option(
1008
+ None,
1009
+ "--disallowed-tools",
1010
+ help="Comma or space-separated list of tool names to deny",
1011
+ rich_help_panel="Permissions",
1012
+ ),
1013
+ # --- System & Context ---
1014
+ system_prompt: str | None = typer.Option(
1015
+ None,
1016
+ "--system-prompt",
1017
+ "-s",
1018
+ help="Override the default system prompt",
1019
+ rich_help_panel="System & Context",
1020
+ ),
1021
+ append_system_prompt: str | None = typer.Option(
1022
+ None,
1023
+ "--append-system-prompt",
1024
+ help="Append text to the default system prompt",
1025
+ rich_help_panel="System & Context",
1026
+ ),
1027
+ settings_file: str | None = typer.Option(
1028
+ None,
1029
+ "--settings",
1030
+ help="Path to a JSON settings file or inline JSON string",
1031
+ rich_help_panel="System & Context",
1032
+ ),
1033
+ base_url: str | None = typer.Option(
1034
+ None,
1035
+ "--base-url",
1036
+ help="Anthropic-compatible API base URL",
1037
+ rich_help_panel="System & Context",
1038
+ ),
1039
+ api_key: str | None = typer.Option(
1040
+ None,
1041
+ "--api-key",
1042
+ "-k",
1043
+ help="API key (overrides config and environment)",
1044
+ rich_help_panel="System & Context",
1045
+ ),
1046
+ bare: bool = typer.Option(
1047
+ False,
1048
+ "--bare",
1049
+ help="Minimal mode: skip hooks, plugins, MCP, and auto-discovery",
1050
+ rich_help_panel="System & Context",
1051
+ ),
1052
+ api_format: str | None = typer.Option(
1053
+ None,
1054
+ "--api-format",
1055
+ help="API format: 'anthropic' (default) or 'openai' (DashScope, GitHub Models, etc.)",
1056
+ rich_help_panel="System & Context",
1057
+ ),
1058
+ # --- Advanced ---
1059
+ debug: bool = typer.Option(
1060
+ False,
1061
+ "--debug",
1062
+ "-d",
1063
+ help="Enable debug logging",
1064
+ rich_help_panel="Advanced",
1065
+ ),
1066
+ mcp_config: Optional[list[str]] = typer.Option(
1067
+ None,
1068
+ "--mcp-config",
1069
+ help="Load MCP servers from JSON files or strings",
1070
+ rich_help_panel="Advanced",
1071
+ ),
1072
+ cwd: str = typer.Option(
1073
+ str(Path.cwd()),
1074
+ "--cwd",
1075
+ help="Working directory for the session",
1076
+ hidden=True,
1077
+ ),
1078
+ backend_only: bool = typer.Option(
1079
+ False,
1080
+ "--backend-only",
1081
+ help="Run the structured backend host for the React terminal UI",
1082
+ hidden=True,
1083
+ ),
1084
+ ) -> None:
1085
+ """主入口函数:启动交互式会话或运行单个提示词
1086
+
1087
+ 支持多种运行模式:
1088
+ - 交互式会话模式(默认)
1089
+ - 非交互式打印模式(使用 -p/--print)
1090
+ - 继续会话(使用 --continue 或 --resume)
1091
+
1092
+ Args:
1093
+ ctx: Typer 上下文对象
1094
+ version: 显示版本号选项
1095
+ continue_session: 继续最近会话选项
1096
+ resume: 通过会话 ID 恢复会话选项
1097
+ name: 会话显示名称
1098
+ model: 模型别名或完整模型 ID
1099
+ effort: 会话努力级别
1100
+ verbose: 覆盖详细输出模式设置
1101
+ max_turns: 最大代理轮次数
1102
+ print_mode: 打印模式提示词
1103
+ output_format: 输出格式
1104
+ permission_mode: 权限模式
1105
+ dangerously_skip_permissions: 跳过权限检查
1106
+ allowed_tools: 允许的工具列表
1107
+ disallowed_tools: 禁止的工具列表
1108
+ system_prompt: 覆盖默认系统提示词
1109
+ append_system_prompt: 追加到默认系统提示词
1110
+ settings_file: 设置文件路径
1111
+ base_url: Anthropic 兼容 API 基础 URL
1112
+ api_key: API 密钥
1113
+ bare: 最小模式
1114
+ api_format: API 格式
1115
+ debug: 启用调试日志
1116
+ mcp_config: 从 JSON 文件或字符串加载 MCP 服务器
1117
+ cwd: 会话工作目录
1118
+ backend_only: 运行结构化后端主机
1119
+ """
1120
+ if ctx.invoked_subcommand is not None: # 如果调用了子命令,直接返回
1121
+ return
1122
+
1123
+ import asyncio # 异步编程模块
1124
+
1125
+ if dangerously_skip_permissions: # 如果跳过权限检查
1126
+ permission_mode = "full_auto" # 设置为完全自动模式
1127
+
1128
+ from illusion.ui.app import run_print_mode, run_repl # 导入 UI 模块
1129
+
1130
+ # 处理 --continue 和 --resume 标志
1131
+ if continue_session or resume is not None:
1132
+ from illusion.services.session_storage import ( # 导入会话存储模块
1133
+ list_session_snapshots, # 列出会话快照
1134
+ load_session_by_id, # 按 ID 加载会话
1135
+ load_session_snapshot, # 加载会话快照
1136
+ )
1137
+
1138
+ session_data = None # 会话数据
1139
+ if continue_session:
1140
+ session_data = load_session_snapshot(cwd)
1141
+ if session_data is None:
1142
+ print(_t("session_not_found_prev"), file=sys.stderr)
1143
+ raise typer.Exit(1)
1144
+ print(_t("session_continuing", summary=session_data.get('summary', '(?)')[:60]))
1145
+ elif resume == "" or resume is None:
1146
+ sessions = list_session_snapshots(cwd, limit=10)
1147
+ if not sessions:
1148
+ print(_t("session_no_saved"), file=sys.stderr)
1149
+ raise typer.Exit(1)
1150
+ print(_t("session_saved_list"))
1151
+ for i, s in enumerate(sessions, 1):
1152
+ print(f" {i}. [{s['session_id']}] {s.get('summary', '?')[:50]} ({_t('session_msg_count', n=s['message_count'])})")
1153
+ choice = typer.prompt(_t("session_enter_id"))
1154
+ try:
1155
+ idx = int(choice) - 1
1156
+ if 0 <= idx < len(sessions):
1157
+ session_data = load_session_by_id(cwd, sessions[idx]["session_id"])
1158
+ else:
1159
+ print(_t("invalid_selection"), file=sys.stderr)
1160
+ raise typer.Exit(1)
1161
+ except ValueError:
1162
+ session_data = load_session_by_id(cwd, choice)
1163
+ if session_data is None:
1164
+ print(_t("session_not_found", id=choice), file=sys.stderr)
1165
+ raise typer.Exit(1)
1166
+ else:
1167
+ session_data = load_session_by_id(cwd, resume)
1168
+ if session_data is None:
1169
+ print(_t("session_not_found", id=resume), file=sys.stderr)
1170
+ raise typer.Exit(1)
1171
+
1172
+ # 将会话传递给 REPL
1173
+ asyncio.run(
1174
+ run_repl(
1175
+ prompt=None, # 无提示词,使用恢复的会话
1176
+ cwd=cwd, # 工作目录
1177
+ model=session_data.get("model") or model, # 模型
1178
+ backend_only=backend_only, # 仅后端模式
1179
+ base_url=base_url, # 基础 URL
1180
+ system_prompt=session_data.get("system_prompt") or system_prompt, # 系统提示词
1181
+ api_key=api_key, # API 密钥
1182
+ restore_messages=session_data.get("messages"), # 恢复的消息
1183
+ restore_session_id=session_data.get("session_id"),
1184
+ effort=effort, # 推理强度级别
1185
+ )
1186
+ )
1187
+ return
1188
+
1189
+ # 打印模式处理
1190
+ if print_mode is not None:
1191
+ prompt = print_mode.strip()
1192
+ if not prompt:
1193
+ print(_t("print_requires_prompt"), file=sys.stderr)
1194
+ raise typer.Exit(1)
1195
+ # 运行打印模式
1196
+ asyncio.run(
1197
+ run_print_mode(
1198
+ prompt=prompt, # 提示词
1199
+ output_format=output_format or "text", # 输出格式
1200
+ cwd=cwd, # 工作目录
1201
+ model=model, # 模型
1202
+ base_url=base_url, # 基础 URL
1203
+ system_prompt=system_prompt, # 系统提示词
1204
+ append_system_prompt=append_system_prompt, # 追加系统提示词
1205
+ api_key=api_key, # API 密钥
1206
+ api_format=api_format, # API 格式
1207
+ permission_mode=permission_mode, # 权限模式
1208
+ max_turns=max_turns, # 最大轮次
1209
+ effort=effort, # 推理强度级别
1210
+ )
1211
+ )
1212
+ return
1213
+
1214
+ # 启动交互式 REPL 会话
1215
+ asyncio.run(
1216
+ run_repl(
1217
+ prompt=None, # 无初始提示词
1218
+ cwd=cwd, # 工作目录
1219
+ model=model, # 模型
1220
+ max_turns=max_turns, # 最大轮次
1221
+ backend_only=backend_only, # 仅后端模式
1222
+ base_url=base_url, # 基础 URL
1223
+ system_prompt=system_prompt, # 系统提示词
1224
+ api_key=api_key, # API 密钥
1225
+ api_format=api_format, # API 格式
1226
+ effort=effort, # 推理强度级别
1227
+ )
1228
+ )