适配器系统解决一个问题:botmux 的会话管线(spawn → 注入输入 → 解析输出 → 卡片流式回传 → resume)是 CLI 无关的,而每种 CLI 的参数、TUI 行为、会话存储各不相同。src/adapters/ 把差异收敛为两张接口面:CliAdapter(每种 CLI 一个文件,src/adapters/cli/)与 SessionBackend(承载 CLI 进程的终端后端,src/adapters/backend/)。worker 只依赖这两张接口(进程模型见 daemon-worker,整体架构见 architecture-overview)。
目录结构
src/adapters/
├── cli/ # CLI 适配器(~3.8 万行中的大头,57 文件中的 ~40 个)
│ ├── types.ts # CliAdapter 接口 + CliId 联合类型
│ ├── registry.ts # 注册表:可执行名表、路径解析、工厂 switch + 缓存
│ ├── runtime.ts # cliRuntime:Codex 兼容发行版声明与校验
│ ├── claude-code.ts … # 每个 CLI 一个文件(25 个适配器)
│ ├── runner-input.ts # runner 型适配器共享的 stdin 控制行注入
│ └── runner-control-channel.ts # runner 输出侧 OSC 控制帧编解码
├── backend/ # 会话后端:pty / tmux / zellij / herdr / zmx / riff
├── adopt-route.ts # /adopt 接管的外部 CLI 的 hook 路由(祖先 PID 反查会话)
├── hook-command.ts # `botmux hook <cliId>` 命令构造
├── hook-installer.ts # 把 askUserQuestion hook 幂等写入各 CLI 配置
└── coco-ask-plugin.ts # CoCo 的 ask 插件安装
CliAdapter 接口面(src/adapters/cli/types.ts)
接口约 40 个成员,按职责分组(全量注释见源文件,设计意图写得非常细):
spawn 与参数构造
resolvedBin:CLI 二进制绝对路径。一律惰性解析(首次读取才 shell out),保证botmux setup枚举选项时不触发探测。buildArgs(opts):构造启动参数。opts 携带sessionId / resume / resumeSessionId / forkSession / initialPrompt / workingDir / model / reasoningEffort / disableCliBypass / skillPluginDir / readIsolation / remoteWsUrl等——这是接口里最大的一个”参数袋”,各适配器各取所需。prepareInitialPromptArg/passesInitialPromptViaArgs/maxInitialPromptArgBytes:首条 prompt 是烤进 argv 还是走启动后 stdin 队列。tmux 的命令行长度限制远低于 ARG_MAX,所以有按字节上限延迟投递的机制。
输入注入
writeInput(pty, content, context):把用户消息写进终端。返回值可选但关键:{ submitted, submissionDisposition, recheck }。submissionDisposition(submitted / untouched / flushed_invalid / dirty_unknown)区分”可安全重试”与”状态未知不得乱动”,是整套提交确认协议的核心。writeStructuredInput:codex-app 专用的结构化旁路(应用上下文与可见消息分离)。PtyHandle是后端投递面:write / sendText / sendSpecialKeys / pasteText,另挂claudeJsonlPath / cliPid / cliCwd供 claude 系做提交确认与 session id 跟踪。
输出解析与状态标记(worker 的空闲检测消费这些 regex)
completionPattern / busyPattern / idleToBusyPattern / readyPattern:完成、忙碌、idle→busy 边沿、输入框就绪四类标记。readyPattern会门控每一轮空闲判定,不只是启动期。altScreen、injectsReadyHook(SessionStart hook 发botmux session-ready真就绪信号)、deferFirstPromptTimeoutUntilReady。
resume 与接管
buildResumeCommand:给用户可粘贴的本地续会命令(“session closed” 卡片用)。checkResumeTargetExists:spawn 前同步探测 resume 目标是否存在,避免 CLI 报错退出被 daemon 放大成崩溃重启循环;探测不了就返回 undefined 走 worker 的二次兜底。listResumableSessions:扫描磁盘 transcript 供/adopt导入(ResumableSession)。
skill / hook / MCP 注入:skillsDir、pluginDir(Claude 走 --plugin-dir 的会话级插件)、skillDelivery、hookInstall(声明式写配置,三种格式 claude-settings / opencode-plugin / grok-hooks,见 hook-installer.ts)、ensureAskHook(命令式,CoCo 用)、asksViaHook、mcpGateway(进程级 MCP Gateway 的配置落点)。
沙盒与隔离:authPaths(沙盒内保持真实可写的登录态)、sandboxExtraExecPaths / sandboxReadonlyPaths、supportsReadIsolation、spawnEnv、claudeDataDir / claudeStateJsonPath。详见 sandbox。
能力位:supportsTypeAhead(忙时可收输入)、reliableTurnTerminal(transcript 级回合边界,durable 投递的前提)、mergeQueuedInput、injectsSessionContext(系统提示走 CLI flag 而非消息内联)、systemHints、modelChoices、defaultPassthroughCommands、buildSessionRenameCommand。
token 统计不在接口面上:TUI 适配器靠 transcript 桥,runner 型适配器把用量随 OSC final 标记上报(codex-app 从 thread/tokenUsage/updated 累加,见 cloud-agents)。
registry.ts 注册表机制
RAW_CLI_EXECUTABLES:CliId → 裸命令名(mira/riff为 undefined,API 型无本地可执行文件)。刻意与工厂 switch 放在一起,让 setup/dashboard 的可用性检查不构造适配器、不触发惰性 shell 探测。resolveCommand:经 login shell(-lc)→ 交互 shell(-ic)逐级command -v,覆盖 nvm/fnm 与只写 rc 的安装器;用setsid+ stdio 隔离防止探测过程抢控制终端把 setup 挂起;macOS 上兜底查/Applications/Codex.app内置 codex。createCliAdapterSync:25 分支的工厂 switch;createCliAdapter外面包一层Map<id:pathOverride, CliAdapter>缓存。CliId联合类型是权威清单(types.ts 末尾),当前 25 个:claude-code / seed / relay / aiden / coco / codex / codex-app / cursor / gemini / genius / opencode / antigravity / mtr / hermes / mira / mir / traex / pi / copilot / oh-my-pi / kimi / grok / kiro-cli / riff / reasonix。
cliRuntime:Codex 兼容发行版(runtime.ts)
严格兼容 Codex 协议的第三方发行版不需要新适配器:bots.json 里 cliId: codex + cliRuntime: { id, displayName, executable, update }。resolveCliRuntime 产出可冻结的 CliRuntimeSnapshot(source: official | legacy-path | configured),钉在 session 上;update 策略为 auto / self / none / npm(packageName)。目前只有 codex 接受结构化 runtime;id: codex 与 basename codex 保留给官方内置。校验严格(未知字段报错、executable 只接受裸名或绝对路径、legacy cliPathOverride 必须与 executable 逐字节一致),新写入者同时影子到 cliPathOverride 以便旧版 botmux 回滚兼容。
会话后端(backend/)
SessionBackend 接口(backend/types.ts):spawn / write / resize / onData / onScreenResync / onExit / kill + 屏幕快照族(captureCurrentScreen / captureViewport / settleCurrentScreen)+ 提交歧义围栏(captureAmbiguousSubmissionFence / confirm / cancel,防止一次不确定写入后被重复恢复);远端型扩展(riff)另有 onAccessUrl / onTaskDone / destroySession / prepareShutdownDetach 等异步拆除协议。BackendType = pty | tmux | herdr | zellij | zmx | riff。
selectSessionBackend(session-backend-selector.ts)按类型分流,默认 tmux:先查 TmuxBackend.sessionName(sessionId) 是否存在,存在则 reattach。decideBackendGate 是硬门:请求的持久化后端本机不可用时拒绝启动并发卡片,不再静默降级 PTY——静默降级正是”以为在 tmux、实际 daemon 重启全丢”的 bug 根源;PTY 只能显式 opt-in。沙盒/读隔离只在 botmux 自己掌控启动包装的后端(pty/tmux)上执行,herdr/zellij/zmx 会 fail-closed(backendSandboxCompatibilityError)。
本地进程适配的两种通用模式:
- node-pty 直驱(
PtyBackend):pty.spawnxterm-256color,per-bot env 直接并进子进程 env(没有共享 server,无泄漏问题)。不跨 daemon 重启存活,仅应急。 - tmux 常驻(
TmuxBackend/TmuxPipeBackend):CLI 跑在bmx-*命名 tmux session,天然跨 daemon 重启。托管模式用 send-keys/paste-buffer 注入、capture-pane 取屏;TmuxPipeBackend是零接触观察模式(/adopt):mkfifo+tmux pipe-pane -O 'cat > fifo'复制 pane 全部字节流,写入仍走 send-keys,pane 用真实地址而非合成会话名。per-bot env 用 per-pane/usr/bin/env KEY=VAL前缀注入,绝不进共享 server 全局 env。tmux 后端的更多细节见 tmux。
本地进程适配器通用模式(以 claude-code.ts 为例)
createClaudeFamilyAdapter(variant)工厂:Claude Code 与其 fork(seed)共享全部机制,只差dataDir / stateJsonPath / spawnEnv等变体参数(claudeDataDir字段让 worker 的 JSONL 确认、bridge 回退都对着同一数据根)。- buildArgs:新会话
--session-id、续会--resume <nativeId>(可--fork-session分叉)、--model、--dangerously-skip-permissions+ 进程级--settings(仅 bypass 键,disableCliBypass时全不传)、--disallowed-tools(EnterPlanMode/ExitPlanMode,v3 goal 再加 AskUserQuestion)、--plugin-dir(会话级 skill 注入)、--append-system-prompt(botmux 路由上下文,对应injectsSessionContext)。 - writeInput 模拟人手打字:96 字节分块 + 节流的
send-keys -l,换行用\+ Enter 软换行规避 bracketed-paste 被 CLI 中途关掉的老坑;提交确认不靠 sleep,而是盯~/.claude/projects/<cwd-hash>/<sid>.jsonl增长,session id 轮换经sessions/<pid>.json跟踪。 - resume 前置防护:
checkResumeTargetExistsstat JSONL;/cd换目录后syncClaudeResumeTargetToCwd把 transcript 原子拷贝到新 project dir(全程 symlink 对抗)。
codex.ts 同构但细节不同:--no-alt-screen、busy/ready regex(› 提示符)、hybrid --remote <ws> 模式接入 RPC engine(见 cloud-agents)。
runner 型适配器
mira / mir / codex-app 三个适配器的 resolvedBin 是 process.execPath——spawn 的不是 TUI 而是一个 Node runner,stdin 收 ::botmux-<id>:<base64>\n 控制行(分块节流注入,runner-input.ts),stdout 以 OSC 777 控制帧回传 final/thread 事件(runner-control-channel.ts)。协议与三个 runner 实现见 cloud-agents。
新增适配器的清单(cli/CLAUDE.md)
- 建文件实现
CliAdapter;2.CliId加成员;3. registry.ts 三处(import/case/export);4. worker.tsCLI_DISPLAY_NAMES;5. card-buildercliDisplayNames;6.CLI_ID_CHOICES只能追加到尾部(历史序号是脚本化 setup 的稳定接口);7. README。若要支持文件沙盒,还有三项必查(SQLite/会话态目录级 bind、transcript bridge 真实路径可见、authPaths 目录级优先),当前沙盒模型是 fresh tmpfs 根 + 按 policy 逐条 bwrap bind。
CLI 选择与配置是用户视角,见 adapters。