@openai-lite/codex-feishu 0.1.0

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 ADDED
@@ -0,0 +1,164 @@
1
+ # codex-feishu
2
+
3
+ Codex remote access, straight from Feishu. Use Codex from terminal or Feishu with shared conversation context.
4
+
5
+ ## Chinese docs
6
+
7
+ - 中文文档(安装、命令、架构、限制):[docs/README.zh-CN.md](docs/README.zh-CN.md)
8
+
9
+ ## What it does
10
+
11
+ - No Codex core patching.
12
+ - `codex-feishu init` writes MCP config + bridge config.
13
+ - `codex-feishu daemon` bridges:
14
+ - Feishu text/image -> Codex
15
+ - Codex streaming output -> Feishu cards/text
16
+ - approvals / request_user_input -> Feishu quick actions
17
+ - per-chat mapping for `thread_id` and `cwd`.
18
+ - private chat auto-bind, group chat supports bind code.
19
+
20
+ ## Preview
21
+
22
+ Feishu side experience snapshots:
23
+
24
+ <table>
25
+ <tr>
26
+ <td align="center">
27
+ <a href="docs/assets/feishu-preview-session.png">
28
+ <img src="docs/assets/feishu-preview-session.png" alt="Feishu session status card" width="240" />
29
+ </a>
30
+ <br />
31
+ Session status
32
+ </td>
33
+ <td align="center">
34
+ <a href="docs/assets/feishu-preview-image-reply.png">
35
+ <img src="docs/assets/feishu-preview-image-reply.png" alt="Feishu image and reply flow" width="240" />
36
+ </a>
37
+ <br />
38
+ Image + reply flow
39
+ </td>
40
+ <td align="center">
41
+ <a href="docs/assets/feishu-preview-image-reply1.jpg">
42
+ <img src="docs/assets/feishu-preview-image-reply1.jpg" alt="Feishu image and reply flow variant" width="240" />
43
+ </a>
44
+ <br />
45
+ Image + reply flow v2
46
+ </td>
47
+ </tr>
48
+ </table>
49
+
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ npm i -g @openai/codex #安装codex(如果已安装可以跳过)
55
+
56
+ npm i -g @openai-lite/codex-feishu
57
+ ```
58
+
59
+ ## Quick start (macOS / Linux)
60
+
61
+ ```bash
62
+ codex-feishu init --app-id <FEISHU_APP_ID> --app-secret <FEISHU_APP_SECRET> daemon
63
+ ```
64
+
65
+ Notes:
66
+ - `init ... daemon` restarts daemon in background and prints bind info automatically.
67
+ - Daemon is treated as singleton; `init ... daemon` will stop previous daemon instances first.
68
+ - If `bot_open_id` can be auto-detected, QR opens bot chat directly.
69
+ - If auto-detect fails, it degrades to `/bind CODE` mode (still usable).
70
+
71
+ Refresh bind payload anytime:
72
+
73
+ ```bash
74
+ codex-feishu qrcode
75
+ ```
76
+
77
+ ## Quick start (Windows)
78
+
79
+ ```powershell
80
+ npm i -g @openai-lite/codex-feishu
81
+ codex-feishu init --app-id <FEISHU_APP_ID> --app-secret <FEISHU_APP_SECRET> daemon
82
+ ```
83
+
84
+ Windows defaults:
85
+ - RPC endpoint: `tcp://127.0.0.1:9765`
86
+ - You can override with `CODEX_FEISHU_RPC_ENDPOINT`.
87
+
88
+ Optional checks:
89
+
90
+ ```bash
91
+ codex-feishu doctor
92
+ codex
93
+ ```
94
+
95
+ ## Feishu setup
96
+
97
+ - 设置清单(中文): [docs/FEISHU_SETUP.zh-CN.md](docs/FEISHU_SETUP.zh-CN.md)
98
+
99
+ ## CLI commands
100
+
101
+ - `codex-feishu init [flags] [daemon|--daemon]`
102
+ - `codex-feishu doctor`
103
+ - `codex-feishu daemon`
104
+ - `codex-feishu qrcode [--purpose <text>] [--ascii] [--json]`
105
+ - `codex-feishu inbound --chat-id <id> --text <msg> [--user-id <id>]`
106
+ - `codex-feishu mcp` (internal MCP entry, normally no manual use)
107
+
108
+ ## Feishu commands
109
+
110
+ Core:
111
+ - `/bind <CODE>`: bind current Feishu chat
112
+ - `/rebind`: reset binding and generate a new bind code
113
+ - `/status`: show current chat status
114
+ - `/help`: show command help
115
+ - `/group`: show group-chat usage guide
116
+ - `/new`: create and switch to a new thread
117
+ - `/stop`: stop current generation
118
+ - `/pending`: list pending approvals/input requests
119
+
120
+ Session / directory:
121
+ - `/resume [index|thread_id]`
122
+ - `/fork [index|thread_id]`
123
+ - `/threads`
124
+ - `/sw <index|thread_id>`
125
+ - `/cwd`
126
+ - `/cwd <PATH>`
127
+ - `/cwd <PATH> new`
128
+
129
+ Codex capability bridge:
130
+ - `/review [instructions|branch:<name>|commit:<sha>]`
131
+ - `/compact`
132
+ - `/model [list|clear|<model_id>]`
133
+ - `/approvals [untrusted|on-failure|on-request|never]`
134
+ - `/permissions [read-only|workspace-write|danger-full-access]`
135
+ - `/plan [on|off|toggle]` (compat mode)
136
+ - `/init` (create/complete `AGENTS.md` through Codex)
137
+ - `/skills`
138
+ - `/mcp [list|get|add|remove|login|logout] ...`
139
+
140
+ Approval responses:
141
+ - quick reply `1` / `2` / `3`
142
+ - `/approve [pending_id] [session]`
143
+ - `/deny [pending_id]`
144
+ - `/cancel [pending_id]`
145
+ - `/answer <pending_id> <text|json>`
146
+
147
+ ## Behavior notes
148
+
149
+ - `/mcp` output is intentionally raw passthrough from native `codex mcp ...` (often code-block/table style).
150
+ - If mapped thread is invalid, daemon auto-recovers by creating a new thread and retrying once.
151
+ - Feishu and terminal are state-synced, but rendering is not pixel-identical.
152
+
153
+ ## Runtime files
154
+
155
+ - `~/.codex/config.toml` (MCP server block)
156
+ - `~/.codex-feishu/config.json`
157
+ - `~/.codex-feishu/state.json`
158
+ - `~/.codex-feishu/run/daemon.pid`
159
+ - `~/.codex-feishu/run/daemon.log`
160
+
161
+ ## Architecture and plan
162
+
163
+ - Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
164
+ - Priority plan: [docs/PRIORITY_PLAN.md](docs/PRIORITY_PLAN.md)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli.js";
3
+
4
+ runCli(process.argv.slice(2)).catch((err) => {
5
+ // eslint-disable-next-line no-console
6
+ console.error(err instanceof Error ? err.message : String(err));
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,118 @@
1
+ # codex-feishu Architecture
2
+
3
+ ## Scope
4
+
5
+ - Keep Codex as the main runtime.
6
+ - Do not patch Codex core.
7
+ - Bridge Feishu <-> Codex via daemon and stable interfaces.
8
+
9
+ ## Topology
10
+
11
+ ```text
12
+ Feishu Client <-> Feishu OpenAPI (long connection)
13
+ |
14
+ v
15
+ codex-feishu daemon
16
+ | |
17
+ | +-- state.json (binding/thread/cwd/pending)
18
+ |
19
+ +-- AppServerClient <-> codex app-server / codex proto
20
+ |
21
+ +-- JSON-RPC endpoint (UDS or TCP on Windows)
22
+ ^
23
+ |
24
+ Codex MCP
25
+ ```
26
+
27
+ ## System diagram
28
+
29
+ ```mermaid
30
+ flowchart LR
31
+ U1[User in Feishu] --> F[Feishu App]
32
+ U2[User in Terminal] --> C[Codex CLI]
33
+ F <--> O[Feishu OpenAPI\nLong Connection]
34
+ O <--> D[codex-feishu daemon]
35
+ C <--> D
36
+ D <--> A[AppServerClient]
37
+ A <--> S[codex app-server / proto]
38
+ D <--> R[JSON-RPC Endpoint\nUDS or TCP on Windows]
39
+ C --> M[MCP tools]
40
+ M --> R
41
+ D --> ST[state.json]
42
+ ```
43
+
44
+ ## Request/response flow (one question)
45
+
46
+ ```mermaid
47
+ sequenceDiagram
48
+ participant User as Feishu User
49
+ participant Feishu as Feishu App/OpenAPI
50
+ participant Daemon as codex-feishu daemon
51
+ participant Codex as codex app-server/proto
52
+ participant Terminal as Codex CLI (optional)
53
+
54
+ User->>Feishu: Send text/image
55
+ Feishu->>Daemon: im.message.receive_v1
56
+ Daemon->>Daemon: Resolve binding (chat_id -> thread_id/cwd)
57
+ Daemon->>Codex: turn/start
58
+ Codex-->>Daemon: Streaming deltas + status events
59
+ Daemon-->>Feishu: Patch/send cards and text
60
+ Daemon-->>Terminal: Same thread state remains available
61
+ ```
62
+
63
+ ## Components
64
+
65
+ - `codex-feishu daemon`
66
+ - Handles binding, per-chat thread/cwd mapping, message relay, approval relay.
67
+ - Exposes RPC methods under `feishu/*` and `bridge/*`.
68
+ - `FeishuBridge`
69
+ - Long-connection event/message receiver and sender.
70
+ - `AppServerClient`
71
+ - Prefers `codex app-server`; degrades to `codex proto` when needed.
72
+ - MCP server (`codex-feishu mcp`)
73
+ - Exposes tools to Codex side (`feishu_qrcode`, `feishu_status`, `feishu_new_thread`).
74
+
75
+ ## Data model (persisted)
76
+
77
+ - `bindings[chat_id]`
78
+ - `active_thread_id`, `active_cwd`
79
+ - per-chat preferences: `preferred_model`, `approval_policy`, `sandbox_mode`, `plan_mode`
80
+ - `pending_bind_codes`
81
+ - `thread_titles`
82
+ - `active_thread_id`
83
+
84
+ `thread_buffers` and recent streaming caches are runtime-only and not fully persisted.
85
+
86
+ ## Main flows
87
+
88
+ ### 1) Bind
89
+
90
+ 1. Generate bind payload (`feishu/qrcode`).
91
+ 2. User sends `/bind CODE` (or private-chat auto-bind).
92
+ 3. Daemon stores `chat_id -> thread/cwd` mapping.
93
+
94
+ ### 2) User message
95
+
96
+ 1. Feishu message enters daemon (`feishu/inbound_text` / `feishu/inbound_image`).
97
+ 2. Daemon resolves mapped thread/cwd and per-chat overrides.
98
+ 3. Daemon starts turn via app-server/proto.
99
+ 4. Streaming events are relayed back to Feishu cards/messages.
100
+
101
+ ### 3) Approval
102
+
103
+ 1. Codex emits approval or request_user_input.
104
+ 2. Daemon stores pending item and sends action hints to Feishu.
105
+ 3. Feishu quick reply (`1/2/3` or explicit command) resolves pending item.
106
+ 4. First valid reply wins.
107
+
108
+ ## Compatibility strategy
109
+
110
+ - Treat Codex as evolving upstream.
111
+ - Prefer passthrough behavior when possible (`/mcp`, `/skills` style).
112
+ - Keep custom adapter logic minimal and isolated in daemon RPC handlers.
113
+ - For unsupported app-server methods, return explicit downgrade message or fallback path.
114
+
115
+ ## Known tradeoffs
116
+
117
+ - Terminal and Feishu are state-consistent, but rendering cannot be pixel-identical.
118
+ - Some TUI-only commands have no strict app-server equivalent; daemon uses compatibility mode where possible.
@@ -0,0 +1,46 @@
1
+ # 飞书准备与权限
2
+
3
+ 这份文档用于单独说明 `codex-feishu` 的飞书侧准备步骤与权限清单。
4
+
5
+ ## 必要配置
6
+
7
+ 1. 开启应用的机器人能力。
8
+ 2. 事件订阅方式选择“长连接(long connection)”。
9
+ 3. 订阅事件:
10
+ - `im.message.receive_v1`
11
+ 4. 开通接口权限(按当前代码实际调用):
12
+ - `im.v1.message.create`(发送文本/卡片/图片消息)
13
+ - `im.v1.message.patch`(更新卡片)
14
+ - `im.v1.image.create`(上传图片)
15
+ - `im.v1.messageResource.get`(下载会话内图片)
16
+ - `im.v1.image.get`(图片下载兜底)
17
+ - `bot.v3.info`(可选,仅用于自动获取 `bot_open_id`)
18
+ 5. 权限或事件变更后,发布应用版本。
19
+
20
+ ## 权限清单 JSON
21
+
22
+ ```json
23
+ {
24
+ "subscription_mode": "long_connection",
25
+ "events": [
26
+ "im.message.receive_v1"
27
+ ],
28
+ "scopes": {
29
+ "tenant": [
30
+ "aily:message:read",
31
+ "aily:message:write",
32
+ "im:message",
33
+ "im:message.group_at_msg:readonly",
34
+ "im:message.group_msg",
35
+ "im:message.p2p_msg:readonly",
36
+ "im:message:send_as_bot",
37
+ "im:resource"
38
+ ],
39
+ "user": [
40
+ "aily:message:read",
41
+ "aily:message:write",
42
+ "im:message"
43
+ ]
44
+ }
45
+ }
46
+ ```
@@ -0,0 +1,130 @@
1
+ # codex-feishu 中文文档
2
+
3
+ Codex 远程化:飞书直连。你可以在飞书或终端任意一端继续同一条对话,上下文自动同步。
4
+
5
+ ## 目标
6
+
7
+ 在不修改 Codex 本体的前提下,通过 sidecar/daemon + MCP,把飞书与终端接到同一条会话链路。
8
+
9
+ ## 快速架构图
10
+
11
+ ```text
12
+ 飞书用户 -> 飞书 -> codex-feishu daemon -> codex(app-server/proto) -> 结果回传飞书
13
+ ^
14
+ |
15
+ Codex CLI / MCP
16
+ ```
17
+
18
+ 完整架构与时序图: [ARCHITECTURE.md](./ARCHITECTURE.md)
19
+
20
+ ## 安装
21
+
22
+ ### Linux / macOS
23
+
24
+ ```bash
25
+ npm i -g @openai-lite/codex-feishu
26
+ codex-feishu init --app-id <FEISHU_APP_ID> --app-secret <FEISHU_APP_SECRET> daemon
27
+ ```
28
+
29
+ ### Windows
30
+
31
+ ```powershell
32
+ npm i -g @openai-lite/codex-feishu
33
+ codex-feishu init --app-id <FEISHU_APP_ID> --app-secret <FEISHU_APP_SECRET> daemon
34
+ ```
35
+
36
+ Windows 说明:
37
+ - 默认 RPC 端点:`tcp://127.0.0.1:9765`
38
+ - 可通过 `CODEX_FEISHU_RPC_ENDPOINT` 覆盖
39
+
40
+ ## init/daemon 说明
41
+
42
+ - `init ... daemon` 会自动重启后台 daemon,并自动打印绑定信息(含二维码/链接/绑定指令)。
43
+ - daemon 按单实例运行;`init ... daemon` 会先停止旧进程,再启动新进程。
44
+ - 需要重新获取绑定信息时,执行:
45
+
46
+ ```bash
47
+ codex-feishu qrcode
48
+ ```
49
+
50
+ - 日志文件:`~/.codex-feishu/run/daemon.log`
51
+ - Linux/macOS 实时看日志:`tail -f ~/.codex-feishu/run/daemon.log`
52
+ - Windows 实时看日志:
53
+
54
+ ```powershell
55
+ Get-Content -Path "$env:USERPROFILE\.codex-feishu\run\daemon.log" -Wait
56
+ ```
57
+
58
+ - 可选检查命令(不是安装必需):
59
+
60
+ ```bash
61
+ codex-feishu doctor
62
+ codex
63
+ ```
64
+
65
+ ## 飞书准备与权限
66
+
67
+ - [FEISHU_SETUP.zh-CN.md](./FEISHU_SETUP.zh-CN.md)
68
+
69
+ ## 飞书侧命令
70
+
71
+ ### 基础
72
+
73
+ - `/bind <CODE>`:绑定当前飞书会话到 Codex
74
+ - `/rebind`:解除当前绑定并生成新绑定码
75
+ - `/status`:查看当前会话状态(会话ID、目录、待审批等)
76
+ - `/help`:查看命令帮助
77
+ - `/group`:查看群聊触发与绑定说明
78
+ - `/new`:新建会话并切换到新会话
79
+ - `/stop`:中断当前生成
80
+ - `/pending`:查看当前待审批/待输入项
81
+
82
+ ### 会话与目录
83
+
84
+ - `/resume [序号|会话ID]`
85
+ - `/fork [序号|会话ID]`
86
+ - `/threads`
87
+ - `/sw <序号|会话ID>`
88
+ - `/cwd`
89
+ - `/cwd <PATH>`
90
+ - `/cwd <PATH> new`
91
+
92
+ ### Codex 能力桥接
93
+
94
+ - `/review [说明|branch:<分支>|commit:<sha>]`
95
+ - `/compact`
96
+ - `/model [list|clear|<model_id>]`
97
+ - `/approvals [untrusted|on-failure|on-request|never]`
98
+ - `/permissions [read-only|workspace-write|danger-full-access]`
99
+ - `/plan [on|off|toggle]`(兼容模式)
100
+ - `/init`(通过 Codex 生成/补全 `AGENTS.md`)
101
+ - `/skills`
102
+ - `/mcp [list|get|add|remove|login|logout] ...`
103
+
104
+ ### 审批快捷
105
+
106
+ - 直接回复 `1` / `2` / `3`
107
+ - `/approve [pending_id] [session]`
108
+ - `/deny [pending_id]`
109
+ - `/cancel [pending_id]`
110
+ - `/answer <pending_id> <text|json>`
111
+
112
+ ## 行为细节
113
+
114
+ - 私聊支持自动绑定:首次消息可自动建立映射并继续转发。
115
+ - 群聊建议 `@机器人` 触发;未绑定时需先 `/bind <CODE>`。
116
+ - `/mcp` 返回是对原生 `codex mcp ...` 的透传,通常是代码块/表格样式。
117
+ - 线程失效时,daemon 会自动新建线程并重试一次。
118
+
119
+ ## 运行文件
120
+
121
+ - `~/.codex/config.toml`(MCP 配置)
122
+ - `~/.codex-feishu/config.json`
123
+ - `~/.codex-feishu/state.json`
124
+ - `~/.codex-feishu/run/daemon.pid`
125
+ - `~/.codex-feishu/run/daemon.log`
126
+
127
+ ## 相关文档
128
+
129
+ - 架构说明:[ARCHITECTURE.md](./ARCHITECTURE.md)
130
+ - 迭代清单:[PRIORITY_PLAN.md](./PRIORITY_PLAN.md)
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@openai-lite/codex-feishu",
3
+ "version": "0.1.0",
4
+ "description": "Feishu bridge for Codex with dual-end synchronized conversations.",
5
+ "type": "module",
6
+ "bin": {
7
+ "codex-feishu": "bin/codex-feishu.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "dependencies": {
13
+ "@larksuiteoapi/node-sdk": "*",
14
+ "qrcode": "^1.5.4"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "src",
19
+ "docs",
20
+ "README.md"
21
+ ],
22
+ "license": "Apache-2.0"
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,158 @@
1
+ import { runDaemon } from "./commands/daemon.js";
2
+ import { runDoctor } from "./commands/doctor.js";
3
+ import { runInbound } from "./commands/inbound.js";
4
+ import { runInit } from "./commands/init.js";
5
+ import { runMcp } from "./commands/mcp.js";
6
+ import { fetchQrcode, formatQrcodeSummary, renderAsciiQr, runQrcode } from "./commands/qrcode.js";
7
+ import { restartDaemonDetached } from "./lib/daemon_control.js";
8
+
9
+ const HELP = `codex-feishu
10
+
11
+ Usage:
12
+ codex-feishu init [--app-id <id>] [--app-secret <secret>] [--bot-open-id <open_id>] [--encrypt-key <key>] [--verify-token <token>] [--codex-bin <path>] [daemon|--daemon]
13
+ codex-feishu doctor
14
+ codex-feishu daemon
15
+ codex-feishu qrcode [--purpose <text>] [--ascii] [--json]
16
+ codex-feishu inbound --chat-id <chat_id> --text <message> [--user-id <id>]
17
+ codex-feishu mcp (internal, for Codex MCP server)
18
+ codex-feishu help
19
+ `;
20
+
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+
25
+ async function fetchQrcodeWithRetry(options = {}) {
26
+ const maxWaitMs = Number.isFinite(options.maxWaitMs) ? Math.max(1000, options.maxWaitMs) : 20000;
27
+ const intervalMs = Number.isFinite(options.intervalMs) ? Math.max(100, options.intervalMs) : 500;
28
+ const deadline = Date.now() + maxWaitMs;
29
+ let lastErr = null;
30
+
31
+ while (Date.now() < deadline) {
32
+ try {
33
+ return await fetchQrcode({
34
+ purpose: options.purpose ?? "init_daemon_start",
35
+ autostart: false,
36
+ cwdHint: options.cwdHint ?? process.cwd(),
37
+ timeoutMs: options.timeoutMs ?? 1500,
38
+ });
39
+ } catch (err) {
40
+ lastErr = err;
41
+ // Wait daemon socket ready after background spawn.
42
+ // eslint-disable-next-line no-await-in-loop
43
+ await sleep(intervalMs);
44
+ }
45
+ }
46
+
47
+ throw lastErr ?? new Error(`waited ${maxWaitMs}ms but qrcode is still unavailable`);
48
+ }
49
+
50
+ function parseArgs(argv) {
51
+ const args = [];
52
+ const flags = {};
53
+ for (let i = 0; i < argv.length; i += 1) {
54
+ const token = argv[i];
55
+ if (token.startsWith("--")) {
56
+ const key = token.slice(2);
57
+ const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
58
+ flags[key] = value;
59
+ } else {
60
+ args.push(token);
61
+ }
62
+ }
63
+ return { args, flags };
64
+ }
65
+
66
+ function watchLogCommand(logPath) {
67
+ if (process.platform === "win32") {
68
+ return `powershell -NoProfile -Command "Get-Content -Path '${logPath}' -Wait"`;
69
+ }
70
+ return `tail -f ${logPath}`;
71
+ }
72
+
73
+ export async function runCli(argv) {
74
+ const { args, flags } = parseArgs(argv);
75
+ const cmd = args[0] ?? "help";
76
+
77
+ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
78
+ // eslint-disable-next-line no-console
79
+ console.log(HELP);
80
+ return;
81
+ }
82
+
83
+ if (cmd === "init") {
84
+ const startDaemon = args[1] === "daemon" || flags.daemon === "true";
85
+ await runInit(flags, { startDaemon });
86
+ if (startDaemon) {
87
+ const result = await restartDaemonDetached();
88
+ // eslint-disable-next-line no-console
89
+ console.log("\nDaemon:");
90
+ // eslint-disable-next-line no-console
91
+ console.log(
92
+ `- Previous: ${result.stopResult.action}${
93
+ result.stopResult.pid ? ` (pid=${result.stopResult.pid})` : ""
94
+ }`,
95
+ );
96
+ // eslint-disable-next-line no-console
97
+ console.log(`- Started: pid=${result.startResult.pid} (background)`);
98
+ // eslint-disable-next-line no-console
99
+ console.log(`- PID file: ${result.pidPath}`);
100
+ // eslint-disable-next-line no-console
101
+ console.log(`- Log file: ${result.logPath}`);
102
+ if (Array.isArray(result.stopResults) && result.stopResults.length > 1) {
103
+ // eslint-disable-next-line no-console
104
+ console.log(`- Cleaned stale daemons: ${result.stopResults.length}`);
105
+ }
106
+ // eslint-disable-next-line no-console
107
+ console.log(`- Watch log: ${watchLogCommand(result.logPath)}`);
108
+ // eslint-disable-next-line no-console
109
+ console.log("- Bind: waiting for daemon readiness (up to 20s)...");
110
+
111
+ try {
112
+ const qr = await fetchQrcodeWithRetry({
113
+ purpose: "init_daemon_start",
114
+ cwdHint: process.cwd(),
115
+ maxWaitMs: 20000,
116
+ intervalMs: 500,
117
+ timeoutMs: 1500,
118
+ });
119
+ const asciiQr = await renderAsciiQr(qr?.qr_text);
120
+ // eslint-disable-next-line no-console
121
+ console.log("\nBind:");
122
+ // eslint-disable-next-line no-console
123
+ console.log(formatQrcodeSummary(qr, { asciiQr }));
124
+ } catch (err) {
125
+ // eslint-disable-next-line no-console
126
+ console.log(`\nBind: unavailable (${err?.message ?? String(err)})`);
127
+ }
128
+ }
129
+ return;
130
+ }
131
+
132
+ if (cmd === "doctor") {
133
+ await runDoctor();
134
+ return;
135
+ }
136
+
137
+ if (cmd === "daemon") {
138
+ await runDaemon(flags);
139
+ return;
140
+ }
141
+
142
+ if (cmd === "qrcode" || cmd === "qr") {
143
+ await runQrcode(flags);
144
+ return;
145
+ }
146
+
147
+ if (cmd === "mcp") {
148
+ await runMcp();
149
+ return;
150
+ }
151
+
152
+ if (cmd === "inbound") {
153
+ await runInbound(flags);
154
+ return;
155
+ }
156
+
157
+ throw new Error(`unknown command: ${cmd}\n\n${HELP}`);
158
+ }