codex-to-im 1.0.20 → 1.0.22

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  它的主路径不是“改造 Codex 本体”,而是:
8
8
 
9
9
  1. 在本机启动一个 Web 工作台和 bridge
10
- 2. 配置飞书或微信
10
+ 2. 在工作台中新增并配置一个或多个通道实例
11
11
  3. 把桌面里的 Codex 会话接到 IM
12
12
  4. 在 IM 中继续对话、切线程、查看状态
13
13
 
@@ -22,8 +22,16 @@
22
22
 
23
23
  ## 支持的通道
24
24
 
25
- - 飞书:支持 bot 配置、连通性测试、共享线程、流式卡片、图片和文件发送。
26
- - 微信:支持扫码登录、共享线程和文本反馈。
25
+ - 飞书:支持创建多个机器人实例、连通性测试、共享线程、流式卡片、图片和文件发送。
26
+ - 微信:支持创建多个实例、扫码登录、共享线程和文本反馈。
27
+
28
+ 每个通道实例都可以配置自己的别名,例如:
29
+
30
+ - `飞书主号`
31
+ - `飞书备份号`
32
+ - `微信工作号`
33
+
34
+ 这些别名只用于区分不同聊天入口,不会改变 Codex 会话本身的语义。
27
35
 
28
36
  ## 快速开始
29
37
 
@@ -50,6 +58,31 @@ npm install -g codex-to-im
50
58
  codex-to-im
51
59
  ```
52
60
 
61
+ 如果只想启动后台 bridge,不打开 UI:
62
+
63
+ ```bash
64
+ codex-to-im start
65
+ ```
66
+
67
+ ### 开机自启动(Windows)
68
+
69
+ 当前支持把 **bridge** 注册为 Windows 开机启动任务,UI 仍然按需通过 `codex-to-im` 打开。
70
+
71
+ ```powershell
72
+ codex-to-im autostart status
73
+ codex-to-im autostart install
74
+ codex-to-im autostart uninstall
75
+ ```
76
+
77
+ 说明:
78
+
79
+ - `codex-to-im autostart install` 和 `codex-to-im autostart uninstall` 需要在**管理员 PowerShell / 终端**中执行。
80
+ - 安装时会要求输入当前 Windows 登录密码,用于创建开机任务。
81
+ - 自动启动只拉起 bridge,不会自动打开 Web UI。
82
+ - 手动再次执行 `codex-to-im` 只会补启动 UI,不会重复启动 bridge。
83
+ - 当前实现基于 Windows 自带任务计划程序,不依赖 WinSW、NSSM 等第三方组件。
84
+ - Web 工作台现在只展示自动启动状态;真正的启用和关闭请使用上面的管理员命令。
85
+
53
86
  默认会打开本地工作台:
54
87
 
55
88
  ```text
@@ -73,7 +106,7 @@ codex-to-im stop
73
106
 
74
107
  ### 1. 接管桌面线程
75
108
 
76
- 在 Web 工作台里配置好飞书或微信后,启动 bridge。
109
+ 在 Web 工作台里新增好飞书或微信通道实例后,启动 bridge。
77
110
  然后在 IM 中发送:
78
111
 
79
112
  ```text
@@ -144,6 +177,13 @@ codex-to-im stop
144
177
  - 默认模型:从本机可用模型中选择。
145
178
  - 反馈使用 markdown:控制 bridge 发到通道里的文本反馈是否走 markdown。
146
179
  - 允许局域网访问 Web 控制台:便于手机或局域网设备访问。
180
+ - 通道实例:可以为飞书或微信新增多个机器人/账号入口,并给每个实例设置别名。
181
+
182
+ 当前主配置保存在:
183
+
184
+ - `~/.codex-to-im/config.v2.json`
185
+
186
+ 兼容保留的 `config.env` 只作为快照和旧工具兜底,不再完整表示多实例通道配置。
147
187
 
148
188
  ## 当前边界
149
189
 
package/README_EN.md CHANGED
@@ -8,7 +8,7 @@ The product is no longer centered around a Codex skill. The main path is:
8
8
 
9
9
  1. Install `codex-to-im`
10
10
  2. Open the local web workbench
11
- 3. Configure IM channels
11
+ 3. Create one or more channel instances in the workbench
12
12
  4. Start the bridge in the background
13
13
  5. Bind real desktop Codex threads to Feishu or Weixin chats
14
14
 
@@ -29,8 +29,8 @@ Windows host installation guide: [docs/install-windows.md](docs/install-windows.
29
29
 
30
30
  - Local background bridge service
31
31
  - Local web workbench for configuration, testing, logs, and bindings
32
- - Feishu credential setup and connectivity testing
33
- - Weixin QR login flow
32
+ - Multi-instance Feishu bot setup and connectivity testing
33
+ - Multi-instance Weixin login flow
34
34
  - Desktop session discovery from `~/.codex/sessions`
35
35
  - Web-side binding updates for IM chats
36
36
 
@@ -85,6 +85,31 @@ codex-to-im
85
85
 
86
86
  This launches the local workbench and opens it in your browser.
87
87
 
88
+ If you only want the background bridge without opening the UI:
89
+
90
+ ```bash
91
+ codex-to-im start
92
+ ```
93
+
94
+ ### Boot Autostart on Windows
95
+
96
+ The bridge can be registered as a Windows boot task. The Web UI remains on-demand and is still opened manually with `codex-to-im`.
97
+
98
+ ```powershell
99
+ codex-to-im autostart status
100
+ codex-to-im autostart install
101
+ codex-to-im autostart uninstall
102
+ ```
103
+
104
+ Notes:
105
+
106
+ - `codex-to-im autostart install` and `codex-to-im autostart uninstall` must be run from an **elevated Administrator PowerShell / terminal**.
107
+ - Installation prompts for the current Windows account password so the startup task can be created.
108
+ - Autostart only launches the bridge; it does not open the Web UI.
109
+ - Running `codex-to-im` manually later only starts the UI if needed and will not duplicate the bridge.
110
+ - The current implementation uses Windows Task Scheduler and does not require WinSW, NSSM, or PM2.
111
+ - The Web UI is read-only for autostart status; enable/disable it from the administrator terminal commands above.
112
+
88
113
  By default the workbench runs at:
89
114
 
90
115
  ```text
@@ -123,12 +148,13 @@ codex-to-im stop
123
148
  ## Main Workflow
124
149
 
125
150
  1. Open the workbench
126
- 2. Fill in Feishu credentials or trigger Weixin QR login
127
- 3. Save config and test connectivity
128
- 4. Start the bridge
129
- 5. Open the desktop sessions section
130
- 6. Bind a Feishu or Weixin chat to the target thread
131
- 7. Continue the same Codex thread from IM
151
+ 2. Create a Feishu or Weixin channel instance in the workbench
152
+ 3. Give the instance an alias such as `Feishu Main` or `Weixin Work`
153
+ 4. Save config and test connectivity
154
+ 5. Start the bridge
155
+ 6. Open the desktop sessions section
156
+ 7. Bind a Feishu or Weixin chat to the target thread
157
+ 8. Continue the same Codex thread from IM
132
158
 
133
159
  If LAN access is enabled, the easiest path is to copy the LAN login link from the local workbench and open it on your phone or another device on the same network.
134
160
 
@@ -179,7 +205,13 @@ The configuration page also includes Codex runtime controls:
179
205
  - global default reasoning level
180
206
  - can be overridden per IM session with `/reasoning`
181
207
  - official runtime levels are `minimal`, `low`, `medium`, `high`, `xhigh`
182
- - IM numeric aliases are `1=minimal`, `2=low`, `3=medium`, `4=high`, `5=xhigh`
208
+ - IM numeric aliases are `1=minimal`, `2=low`, `3=medium`, `4=high`, `5=xhigh`
209
+
210
+ The primary persisted configuration now lives in:
211
+
212
+ - `~/.codex-to-im/config.v2.json`
213
+
214
+ The legacy `config.env` file is still written as a compatibility snapshot, but it no longer fully represents multi-instance channel setup.
183
215
 
184
216
  If you are using `codex-to-im` on your own development machine for real coding work, the more aggressive recommended setup is:
185
217
 
@@ -193,6 +225,8 @@ The channel pages also expose a “Use Markdown for bridge feedback” switch:
193
225
  - disabled by default for WeChat
194
226
  - affects text sent through the bridge, including normal replies, shared-thread mirror messages, and system feedback such as `/h`, `/status`, and `/threads`
195
227
 
228
+ Each channel instance can have its own alias. The alias only identifies which IM entry point handled the chat; it does not change Codex session semantics or model behavior.
229
+
196
230
  ## Update
197
231
 
198
232
  On Windows, `npm update -g codex-to-im` can fail with `EBUSY` if the background UI or bridge is still running from the global install directory.
@@ -1,5 +1,10 @@
1
- # Codex-to-IM Configuration
2
- # Copy to ~/.codex-to-im/config.env and edit
1
+ # Codex-to-IM legacy env snapshot / migration reference
2
+ # The primary runtime config is now ~/.codex-to-im/config.v2.json and is
3
+ # normally managed from the Web workbench.
4
+ # This file is kept only for:
5
+ # 1. migrating old single-instance setups to v2
6
+ # 2. compatibility snapshots / old tooling fallback
7
+ # It no longer fully represents multi-instance channel configuration.
3
8
 
4
9
  # Runtime backend: claude | codex | auto
5
10
  # claude — uses Claude Code CLI + @anthropic-ai/claude-agent-sdk
@@ -7,7 +12,9 @@
7
12
  # auto — tries Claude first, falls back to Codex if CLI not found
8
13
  CTI_RUNTIME=codex
9
14
 
10
- # Enabled channels (comma-separated: telegram,discord,feishu,qq,weixin)
15
+ # Legacy provider enable list (still used by non-v2 providers such as
16
+ # telegram / discord / qq; Feishu and Weixin are now configured as channel
17
+ # instances in config.v2.json).
11
18
  CTI_ENABLED_CHANNELS=feishu
12
19
 
13
20
  # Default workspace root for `/new proj1` style project creation.
@@ -72,10 +79,13 @@ CTI_TG_CHAT_ID=your-chat-id
72
79
  # CTI_DISCORD_ALLOWED_CHANNELS=channel_id_1
73
80
  # CTI_DISCORD_ALLOWED_GUILDS=guild_id_1
74
81
 
75
- # ── Feishu / Lark ──
76
- # CTI_FEISHU_APP_ID=your-app-id
82
+ # ── Feishu / Lark (legacy single-instance migration fields) ──
83
+ # For new setups, add Feishu/Lark channel instances in the Web workbench and
84
+ # choose the site there. These env vars are only used when migrating an old
85
+ # single-instance setup into config.v2.json.
86
+ # CTI_FEISHU_APP_ID=your-app-id
77
87
  # CTI_FEISHU_APP_SECRET=your-app-secret
78
- # CTI_FEISHU_DOMAIN=https://open.feishu.cn
88
+ # CTI_FEISHU_SITE=feishu
79
89
  # CTI_FEISHU_ALLOWED_USERS=user_id_1,user_id_2
80
90
  # Enable streaming response cards in Feishu (default true).
81
91
  # Requires published Feishu permissions such as:
@@ -100,9 +110,12 @@ CTI_TG_CHAT_ID=your-chat-id
100
110
  # Max image size in MB (default 20)
101
111
  # CTI_QQ_MAX_IMAGE_SIZE=20
102
112
 
103
- # ── WeChat / 微信 ──
104
- # No static token is required here. Use the QR login helper to add accounts:
105
- # npm run weixin:login
113
+ # ── WeChat / 微信 (legacy single-instance migration fields) ──
114
+ # For new setups, add Weixin channel instances in the Web workbench. These env
115
+ # vars are only used when migrating an old single-instance setup into
116
+ # config.v2.json.
117
+ # No static token is required here. Use the QR login helper to add accounts:
118
+ # npm run weixin:login
106
119
  # Optional protocol overrides (normally leave unset)
107
120
  # CTI_WEIXIN_BASE_URL=https://ilinkai.weixin.qq.com
108
121
  # CTI_WEIXIN_CDN_BASE_URL=https://novac2c.cdn.weixin.qq.com/c2c
package/dist/cli.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'module'; const require = createRequire(import.meta.url);
3
3
 
4
+ // src/cli.ts
5
+ import { stdin as input, stdout as output } from "node:process";
6
+
4
7
  // src/service-manager.ts
5
8
  import fs2 from "node:fs";
6
9
  import os2 from "node:os";
@@ -22,6 +25,7 @@ function resolveDefaultCtiHome() {
22
25
  }
23
26
  var CTI_HOME = process.env.CTI_HOME || resolveDefaultCtiHome();
24
27
  var CONFIG_PATH = path.join(CTI_HOME, "config.env");
28
+ var CONFIG_V2_PATH = path.join(CTI_HOME, "config.v2.json");
25
29
  function parseEnvFile(content) {
26
30
  const entries = /* @__PURE__ */ new Map();
27
31
  for (const line of content.split("\n")) {
@@ -55,6 +59,9 @@ var bridgePidFile = path2.join(runtimeDir, "bridge.pid");
55
59
  var bridgeStatusFile = path2.join(runtimeDir, "status.json");
56
60
  var uiStatusFile = path2.join(runtimeDir, "ui-server.json");
57
61
  var uiPort = 4781;
62
+ var bridgeAutostartTaskName = "CodexToIMBridge";
63
+ var bridgeAutostartLauncherFile = path2.join(runtimeDir, "bridge-autostart.ps1");
64
+ var npmUninstallLogFile = path2.join(runtimeDir, "npm-uninstall.log");
58
65
  var WINDOWS_HIDE = process.platform === "win32" ? { windowsHide: true } : {};
59
66
  function ensureDirs() {
60
67
  fs2.mkdirSync(runtimeDir, { recursive: true });
@@ -88,6 +95,153 @@ function isProcessAlive(pid) {
88
95
  function sleep(ms) {
89
96
  return new Promise((resolve) => setTimeout(resolve, ms));
90
97
  }
98
+ function getCurrentWindowsUser() {
99
+ const user = process.env.USERNAME || os2.userInfo().username;
100
+ const domain = process.env.USERDOMAIN;
101
+ return domain ? `${domain}\\${user}` : user;
102
+ }
103
+ function escapePowerShellSingleQuoted(value) {
104
+ return value.replace(/'/g, "''");
105
+ }
106
+ function runCommand(command, args, options = {}) {
107
+ return new Promise((resolve, reject) => {
108
+ const child = spawn(command, args, {
109
+ cwd: options.cwd,
110
+ env: options.env,
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ ...WINDOWS_HIDE
113
+ });
114
+ let stdout = "";
115
+ let stderr = "";
116
+ child.stdout.on("data", (chunk) => {
117
+ stdout += chunk.toString();
118
+ });
119
+ child.stderr.on("data", (chunk) => {
120
+ stderr += chunk.toString();
121
+ });
122
+ child.on("error", reject);
123
+ child.on("close", (code) => {
124
+ resolve({ code: code ?? 0, stdout, stderr });
125
+ });
126
+ });
127
+ }
128
+ async function runPowerShell(script) {
129
+ const result = await runCommand(
130
+ "powershell.exe",
131
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script]
132
+ );
133
+ if (result.code !== 0) {
134
+ throw new Error((result.stderr || result.stdout || "PowerShell command failed.").trim());
135
+ }
136
+ return result.stdout.trim();
137
+ }
138
+ async function ensureWindowsAdminSession() {
139
+ if (process.platform !== "win32") {
140
+ return;
141
+ }
142
+ const raw = await runPowerShell("([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)");
143
+ if (raw.trim().toLowerCase() !== "true") {
144
+ throw new Error("\u8BF7\u5148\u4EE5\u7BA1\u7406\u5458\u8EAB\u4EFD\u6253\u5F00 PowerShell \u6216\u7EC8\u7AEF\uFF0C\u518D\u6267\u884C\u5F00\u673A\u81EA\u542F\u52A8\u5B89\u88C5/\u5378\u8F7D\u547D\u4EE4\u3002");
145
+ }
146
+ }
147
+ function ensureBridgeAutostartLauncher() {
148
+ ensureDirs();
149
+ const content = [
150
+ "$ErrorActionPreference = 'Stop'",
151
+ `$env:CTI_HOME = '${escapePowerShellSingleQuoted(CTI_HOME)}'`,
152
+ "$cmd = Get-Command 'codex-to-im.cmd' -ErrorAction SilentlyContinue",
153
+ "if (-not $cmd) { $cmd = Get-Command 'codex-to-im' -ErrorAction SilentlyContinue }",
154
+ "$node = (Get-Command 'node' -ErrorAction Stop).Source",
155
+ "if ($cmd) {",
156
+ " & $cmd.Source start",
157
+ " exit $LASTEXITCODE",
158
+ "}",
159
+ "$npm = Get-Command 'npm.cmd' -ErrorAction SilentlyContinue",
160
+ "if (-not $npm) { $npm = Get-Command 'npm' -ErrorAction SilentlyContinue }",
161
+ "if ($npm) {",
162
+ " try {",
163
+ " $globalRoot = (& $npm.Source root -g 2>$null).Trim()",
164
+ " if ($globalRoot) {",
165
+ " $cliPath = Join-Path (Join-Path $globalRoot 'codex-to-im') 'dist\\cli.mjs'",
166
+ " if (Test-Path $cliPath) {",
167
+ " & $node $cliPath start",
168
+ " exit $LASTEXITCODE",
169
+ " }",
170
+ " }",
171
+ " } catch { }",
172
+ "}",
173
+ `& $node '${escapePowerShellSingleQuoted(path2.join(packageRoot, "dist", "cli.mjs"))}' start`,
174
+ "exit $LASTEXITCODE",
175
+ ""
176
+ ].join("\r\n");
177
+ fs2.writeFileSync(bridgeAutostartLauncherFile, content, "utf-8");
178
+ return bridgeAutostartLauncherFile;
179
+ }
180
+ function parsePowerShellJson(raw) {
181
+ return JSON.parse(raw);
182
+ }
183
+ function buildDeferredGlobalNpmUninstallLaunch(options = {}) {
184
+ const packageName = options.packageName || "codex-to-im";
185
+ const logPath = options.logPath || npmUninstallLogFile;
186
+ const delayMs = options.delayMs ?? 1500;
187
+ const platform = options.platform || process.platform;
188
+ const npmCommand = options.npmCommand || (platform === "win32" ? "npm.cmd" : "npm");
189
+ const command = options.nodePath || process.execPath;
190
+ const cwd = options.cwd || os2.homedir();
191
+ const script = [
192
+ "const { spawn } = require('node:child_process');",
193
+ "const fs = require('node:fs');",
194
+ `const logPath = ${JSON.stringify(logPath)};`,
195
+ `const npmCommand = ${JSON.stringify(npmCommand)};`,
196
+ `const npmArgs = ['uninstall', '-g', ${JSON.stringify(packageName)}];`,
197
+ `const childCwd = ${JSON.stringify(cwd)};`,
198
+ `const delayMs = ${JSON.stringify(delayMs)};`,
199
+ "const writeLog = (message) => {",
200
+ " try { fs.appendFileSync(logPath, String(message).endsWith('\\n') ? String(message) : String(message) + '\\n'); } catch {}",
201
+ "};",
202
+ "setTimeout(() => {",
203
+ " let fd;",
204
+ " try { fd = fs.openSync(logPath, 'a'); } catch (error) { writeLog(error); process.exit(1); return; }",
205
+ " const child = spawn(npmCommand, npmArgs, { cwd: childCwd, detached: false, stdio: ['ignore', fd, fd], windowsHide: true });",
206
+ " child.on('error', (error) => { writeLog(error); process.exit(1); });",
207
+ " child.on('close', (code) => { process.exit(typeof code === 'number' ? code : 0); });",
208
+ "}, delayMs);"
209
+ ].join("\n");
210
+ return {
211
+ command,
212
+ args: ["-e", script],
213
+ npmCommand,
214
+ logPath,
215
+ delayMs
216
+ };
217
+ }
218
+ async function launchDeferredGlobalNpmUninstall() {
219
+ ensureDirs();
220
+ const launch = buildDeferredGlobalNpmUninstallLaunch();
221
+ fs2.writeFileSync(
222
+ launch.logPath,
223
+ [
224
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] Scheduling global uninstall.`,
225
+ `${launch.npmCommand} uninstall -g codex-to-im`,
226
+ ""
227
+ ].join("\n"),
228
+ "utf-8"
229
+ );
230
+ await new Promise((resolve, reject) => {
231
+ const child = spawn(launch.command, launch.args, {
232
+ cwd: os2.homedir(),
233
+ detached: true,
234
+ stdio: "ignore",
235
+ ...WINDOWS_HIDE
236
+ });
237
+ child.once("error", reject);
238
+ child.once("spawn", () => {
239
+ child.unref();
240
+ resolve();
241
+ });
242
+ });
243
+ return launch;
244
+ }
91
245
  function getUiServerUrl(port = uiPort) {
92
246
  return `http://127.0.0.1:${port}`;
93
247
  }
@@ -212,6 +366,120 @@ async function stopBridge() {
212
366
  }
213
367
  return getBridgeStatus();
214
368
  }
369
+ async function getBridgeAutostartStatus() {
370
+ const base = {
371
+ supported: process.platform === "win32",
372
+ installed: false,
373
+ enabled: false,
374
+ mode: "startup",
375
+ taskName: bridgeAutostartTaskName,
376
+ runAsUser: process.platform === "win32" ? getCurrentWindowsUser() : void 0,
377
+ launcherPath: bridgeAutostartLauncherFile
378
+ };
379
+ if (process.platform !== "win32") {
380
+ return {
381
+ ...base,
382
+ error: "\u5F53\u524D\u53EA\u652F\u6301 Windows \u81EA\u52A8\u542F\u52A8\u3002"
383
+ };
384
+ }
385
+ const script = [
386
+ `$task = Get-ScheduledTask -TaskName '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}' -ErrorAction SilentlyContinue`,
387
+ "if (-not $task) {",
388
+ " [pscustomobject]@{",
389
+ " supported = $true",
390
+ " installed = $false",
391
+ " enabled = $false",
392
+ ` mode = 'startup'`,
393
+ ` taskName = '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}'`,
394
+ ` launcherPath = '${escapePowerShellSingleQuoted(bridgeAutostartLauncherFile)}'`,
395
+ " } | ConvertTo-Json -Compress",
396
+ " exit 0",
397
+ "}",
398
+ `$info = Get-ScheduledTaskInfo -TaskName '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}' -ErrorAction SilentlyContinue`,
399
+ "[pscustomobject]@{",
400
+ " supported = $true",
401
+ " installed = $true",
402
+ " enabled = [bool]$task.Settings.Enabled",
403
+ ` mode = 'startup'`,
404
+ ` taskName = '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}'`,
405
+ ` launcherPath = '${escapePowerShellSingleQuoted(bridgeAutostartLauncherFile)}'`,
406
+ " runAsUser = $task.Principal.UserId",
407
+ " state = [string]$task.State",
408
+ "} | ConvertTo-Json -Compress"
409
+ ].join("\n");
410
+ try {
411
+ const raw = await runPowerShell(script);
412
+ return {
413
+ ...base,
414
+ ...parsePowerShellJson(raw)
415
+ };
416
+ } catch (error) {
417
+ return {
418
+ ...base,
419
+ error: error instanceof Error ? error.message : String(error)
420
+ };
421
+ }
422
+ }
423
+ async function installBridgeAutostart(password) {
424
+ if (process.platform !== "win32") {
425
+ throw new Error("\u5F53\u524D\u53EA\u652F\u6301 Windows \u81EA\u52A8\u542F\u52A8\u3002");
426
+ }
427
+ if (!password) {
428
+ throw new Error("\u5F53\u524D Windows \u767B\u5F55\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A\u3002");
429
+ }
430
+ await ensureWindowsAdminSession();
431
+ const launcherPath = ensureBridgeAutostartLauncher();
432
+ const user = getCurrentWindowsUser();
433
+ const script = [
434
+ `$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File "${escapePowerShellSingleQuoted(launcherPath)}"'`,
435
+ "$trigger = New-ScheduledTaskTrigger -AtStartup",
436
+ "$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew",
437
+ `Register-ScheduledTask -TaskName '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}' -Action $action -Trigger $trigger -Settings $settings -User '${escapePowerShellSingleQuoted(user)}' -Password '${escapePowerShellSingleQuoted(password)}' -RunLevel Limited -Force | Out-Null`
438
+ ].join("; ");
439
+ await runPowerShell(script);
440
+ return await getBridgeAutostartStatus();
441
+ }
442
+ async function uninstallBridgeAutostart() {
443
+ if (process.platform !== "win32") {
444
+ return await getBridgeAutostartStatus();
445
+ }
446
+ await ensureWindowsAdminSession();
447
+ const script = [
448
+ `$task = Get-ScheduledTask -TaskName '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}' -ErrorAction SilentlyContinue`,
449
+ "if ($task) {",
450
+ ` Unregister-ScheduledTask -TaskName '${escapePowerShellSingleQuoted(bridgeAutostartTaskName)}' -Confirm:$false`,
451
+ "}"
452
+ ].join("; ");
453
+ await runPowerShell(script);
454
+ try {
455
+ if (fs2.existsSync(bridgeAutostartLauncherFile)) {
456
+ fs2.unlinkSync(bridgeAutostartLauncherFile);
457
+ }
458
+ } catch {
459
+ }
460
+ return await getBridgeAutostartStatus();
461
+ }
462
+ async function uninstallCodexToImPackage() {
463
+ const autostartBefore = await getBridgeAutostartStatus();
464
+ if (process.platform === "win32" && autostartBefore.installed) {
465
+ await ensureWindowsAdminSession();
466
+ }
467
+ const ui = await stopUiServer();
468
+ const bridge = await stopBridge();
469
+ const autostart = autostartBefore.installed ? await uninstallBridgeAutostart() : autostartBefore;
470
+ if (autostart.installed) {
471
+ throw new Error(`\u672A\u80FD\u5220\u9664\u5F00\u673A\u81EA\u542F\u52A8\u4EFB\u52A1 ${autostart.taskName}\uFF0C\u5DF2\u53D6\u6D88 npm \u5168\u5C40\u5378\u8F7D\u3002`);
472
+ }
473
+ const launch = await launchDeferredGlobalNpmUninstall();
474
+ return {
475
+ ui,
476
+ bridge,
477
+ autostart,
478
+ npmCommand: launch.npmCommand,
479
+ logPath: launch.logPath,
480
+ scheduled: true
481
+ };
482
+ }
215
483
  async function ensureUiServerRunning() {
216
484
  ensureDirs();
217
485
  const current = getUiServerStatus();
@@ -296,9 +564,53 @@ function openBrowser(url) {
296
564
  }
297
565
 
298
566
  // src/cli.ts
567
+ async function promptHidden(question) {
568
+ if (!input.isTTY || !output.isTTY) {
569
+ throw new Error("\u5F53\u524D\u7EC8\u7AEF\u4E0D\u652F\u6301\u9690\u85CF\u8F93\u5165\uFF0C\u8BF7\u5728\u53EF\u4EA4\u4E92\u7EC8\u7AEF\u4E2D\u6267\u884C\u3002");
570
+ }
571
+ output.write(question);
572
+ input.resume();
573
+ input.setEncoding("utf8");
574
+ input.setRawMode?.(true);
575
+ return await new Promise((resolve, reject) => {
576
+ let value = "";
577
+ const onData = (chunk) => {
578
+ for (const ch of chunk) {
579
+ if (ch === "") {
580
+ cleanup();
581
+ reject(new Error("\u5DF2\u53D6\u6D88\u3002"));
582
+ return;
583
+ }
584
+ if (ch === "\r" || ch === "\n") {
585
+ cleanup();
586
+ output.write("\n");
587
+ resolve(value);
588
+ return;
589
+ }
590
+ if (ch === "\b" || ch === "\x7F") {
591
+ value = value.slice(0, -1);
592
+ continue;
593
+ }
594
+ value += ch;
595
+ }
596
+ };
597
+ const cleanup = () => {
598
+ input.off("data", onData);
599
+ input.setRawMode?.(false);
600
+ input.pause();
601
+ };
602
+ input.on("data", onData);
603
+ });
604
+ }
299
605
  async function main() {
300
606
  const command = process.argv[2] || "open";
301
607
  switch (command) {
608
+ case "start": {
609
+ const status = await startBridge();
610
+ process.stdout.write(`Bridge started. PID: ${status.pid || "-"}
611
+ `);
612
+ return;
613
+ }
302
614
  case "open": {
303
615
  const status = await ensureUiServerRunning();
304
616
  const url = getUiServerUrl(status.port);
@@ -337,28 +649,83 @@ async function main() {
337
649
  return;
338
650
  }
339
651
  case "stop": {
340
- const bridge = await stopBridge();
341
652
  const ui = await stopUiServer();
653
+ const bridge = await stopBridge();
342
654
  process.stdout.write(
343
655
  `Stopped services. UI running=${ui.running ? "yes" : "no"}, Bridge running=${bridge.running ? "yes" : "no"}
344
656
  `
345
657
  );
346
658
  return;
347
659
  }
660
+ case "uninstall": {
661
+ const result = await uninstallCodexToImPackage();
662
+ process.stdout.write(
663
+ [
664
+ `Stopped services. UI running=${result.ui.running ? "yes" : "no"}, Bridge running=${result.bridge.running ? "yes" : "no"}`,
665
+ result.autostart.installed ? `Bridge autostart still installed: ${result.autostart.taskName}` : "Bridge autostart removed.",
666
+ `Global npm uninstall scheduled via ${result.npmCommand}.`,
667
+ `Log: ${result.logPath}`,
668
+ "\u5F53\u524D\u547D\u4EE4\u9000\u51FA\u540E\uFF0C\u540E\u53F0\u4F1A\u7EE7\u7EED\u6267\u884C\u5168\u5C40\u5378\u8F7D\u3002"
669
+ ].join("\n") + "\n"
670
+ );
671
+ return;
672
+ }
348
673
  case "status": {
349
674
  const ui = getUiServerStatus();
350
675
  const bridge = getBridgeStatus();
351
676
  const url = getCurrentUiServerUrl();
677
+ const autostart = await getBridgeAutostartStatus();
352
678
  process.stdout.write(
353
679
  [
354
680
  `UI: ${ui.running ? "running" : "stopped"}${url ? ` (${url})` : ""}`,
355
- `Bridge: ${bridge.running ? "running" : "stopped"}`
681
+ `Bridge: ${bridge.running ? "running" : "stopped"}`,
682
+ `Bridge Autostart: ${autostart.installed ? autostart.enabled ? "enabled" : "disabled" : "not installed"}`
356
683
  ].join("\n") + "\n"
357
684
  );
358
685
  return;
359
686
  }
687
+ case "autostart": {
688
+ const subcommand = process.argv[3] || "status";
689
+ switch (subcommand) {
690
+ case "status": {
691
+ const status = await getBridgeAutostartStatus();
692
+ process.stdout.write(
693
+ [
694
+ `Supported: ${status.supported ? "yes" : "no"}`,
695
+ `Installed: ${status.installed ? "yes" : "no"}`,
696
+ `Enabled: ${status.enabled ? "yes" : "no"}`,
697
+ `Mode: ${status.mode}`,
698
+ `Task: ${status.taskName}`,
699
+ status.runAsUser ? `Run As: ${status.runAsUser}` : void 0,
700
+ status.state ? `State: ${status.state}` : void 0,
701
+ status.error ? `Error: ${status.error}` : void 0
702
+ ].filter(Boolean).join("\n") + "\n"
703
+ );
704
+ return;
705
+ }
706
+ case "install": {
707
+ await ensureWindowsAdminSession();
708
+ const password = await promptHidden("\u8BF7\u8F93\u5165\u5F53\u524D Windows \u767B\u5F55\u5BC6\u7801\uFF08\u7528\u4E8E\u521B\u5EFA\u5F00\u673A\u542F\u52A8\u4EFB\u52A1\uFF09: ");
709
+ const status = await installBridgeAutostart(password);
710
+ process.stdout.write(`Bridge autostart installed. Task: ${status.taskName}
711
+ `);
712
+ return;
713
+ }
714
+ case "uninstall": {
715
+ const status = await uninstallBridgeAutostart();
716
+ process.stdout.write(
717
+ status.installed ? `Bridge autostart task still exists: ${status.taskName}
718
+ ` : "Bridge autostart removed.\n"
719
+ );
720
+ return;
721
+ }
722
+ default:
723
+ process.stdout.write("Usage: codex-to-im autostart [status|install|uninstall]\n");
724
+ return;
725
+ }
726
+ }
360
727
  default:
361
- process.stdout.write("Usage: codex-to-im [open|url|stop|status]\n");
728
+ process.stdout.write("Usage: codex-to-im [start|open|url|stop|status|autostart|uninstall]\n");
362
729
  }
363
730
  }
364
731
  main().catch((error) => {