适配器系统解决一个问题: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.spawn xterm-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 前置防护:checkResumeTargetExists stat 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)

  1. 建文件实现 CliAdapter;2. CliId 加成员;3. registry.ts 三处(import/case/export);4. worker.ts CLI_DISPLAY_NAMES;5. card-builder cliDisplayNames;6. CLI_ID_CHOICES 只能追加到尾部(历史序号是脚本化 setup 的稳定接口);7. README。若要支持文件沙盒,还有三项必查(SQLite/会话态目录级 bind、transcript bridge 真实路径可见、authPaths 目录级优先),当前沙盒模型是 fresh tmpfs 根 + 按 policy 逐条 bwrap bind。

CLI 选择与配置是用户视角,见 adapters。