
我日常的 AI 编程工作流里长期有三个 CLI 在轮值codex cli 负责快速改 bugClaude Code 偏向长链路重构偶尔还会用一个轻量后端做批量脚本生成。单看任何一个都挺好但真放进同一个项目里轮着用问题就出来了——每个工具的历史记录格式不一样config 散落在不同目录连忽略哪些文件这种基础规则都要分别维护。来回折腾几周之后我直接动手做了个统一工作台 kshell把会话、上下文、工具路由全部收编到一个 shell 下面。这篇文章就聊聊 kshell 的设计思路、实现细节以及开源后跑真实任务攒下的一些数据适合同时维护多个 AI 编程 CLI、或者在纠结要不要给团队搭统一入口的朋友参考。1. 折腾了好几个 AI 编程 CLI 之后我决定自己造个轮子1.1 单工具其实够用但多工具并存让人抓狂先讲一个具体场景。某个下午我在同一个仓库里要做三件事修一个边界条件导致的空指针、把一个模块从回调改成 async/await、顺带写一版数据迁移脚本。按我当时的习惯codex cli 修 bug 最快Claude Code 做重构最稳迁移脚本这种一次性代码直接让轻量后端随手生成就行。于是我一口气开了三个终端窗口。半天下来人就疯了。首先是会话丢失。codex cli 的会话默认存在~/.codex/sessionsClaude Code 存的是~/.claude/projects下的 JSON另一个后端的记忆目录又是自家格式。我根本记不清哪个会话在哪个工具里经常是切回去之后上下文已经续偏了。其次是规则不统一。项目里明明有 AGENTS.md 也维护过 codex 的规则文件但 codex cli 读它自己的规则参数Claude Code 读 AGENTS.md第三个工具两条都不读。结果同一个项目在不同工具下看到的约束完全不一样。还有一个容易被忽略的问题输出格式割裂。codex cli 会把建议的 diff 输出成文本风格的补丁Claude Code 会直接改文件并列 changed files另一个后端是把完整文件贴到 stdout。我想统一记一笔审计日志得写三个解析器。这种割裂感不会在单工具使用时暴露一旦进入多工具协作战术就变成纯纯的时间黑洞。1.2 我理想中的统一工作台应该长什么样我当时给自己列了四条硬性底线不改底层工具的实现。我不打算 fork 任何一个 CLI也没精力维护别人的私有协议只能在外部做封装。会话必须收敛到一处。不管今天用哪个后端对话历史、消耗的 token 数、时间戳都要能在一张表里查出来。规则和上下文只维护一份。项目技术栈、代码规范、目录约束写进一个文件kshell 负责在请求发出前把内容拼进上下文。我想在同一个 TUI 界面里看到哪个工具正在跑、跑到哪一步、下一步是什么而不是切到各个终端窗口去盯原生 spinner。这四条最终把 kshell 的形态定下来了一个薄薄的交互层前面接用户后面接各种 AI 编程 CLI中间做上下文组装和会话持久化。它不是另一个 codex cli而是一层壳——kshell 这个名字就是这么来的。2. kshell 的核心设计会话、上下文、路由三位一体2.1 统一会话模型不管底层是谁对话历史只有一份kshell 的会话模型和原生 CLI 最大的区别在于把会话文件和对话历史解耦了。底层每个工具都会写自己的 session 文件这是它们为了保证自身续聊能力做的持久化kshell 不去动这些文件而是自己维护一套会话记录每条记录包含会话 IDUUID项目路径后端类型codex、claude-code、goosed 或自定义脚本消息数组role、content、token 数成本估算输入加输出 token 数按模型单价换算结束时间、退出码、产物变更摘要这个表存在~/.kshell/sessions.sqlite用 SQLite 而不是 JSON 文件是因为会话多了之后按时间、按项目过滤查询SQL 比遍历文件靠谱得多。比如我最常用的一个查询是这个仓库这周花了多少钱一条 GROUP BY 就出来了原生 CLI 的历史文件没这么好查。对话历史的收集靠两层。一层是 hookkshell 在每次请求前后挂两个生命周期before_request 和 after_response另一层是输出解析每个后端对应一个 parser负责把工具吐出来的流式输出切成消息。这样不管底层工具把 stdout 写成什么样进到 kshell 数据库里的都是一条条干净的消息不带 ANSI 色块、不带进度条残留。2.2 上下文组装把项目信息、规则、历史揉成一个干净请求AI 编程 CLI 用久了你会发现一个规律模型效果的上限很大程度上取决于上下文给得干不干净。原生 CLI 一般会自己收集一部分仓库信息但这些信息往往是通用抓取不会按你的项目定制。kshell 的上下文组装阶段做三件事。第一按优先级拼规则。项目根目录下kshell.project.json里的 rules 数组会按顺序插入到用户 prompt 之前然后是全局~/.kshell/config.json里的 globalRules最后才是一般性的 system prompt 补充。这样项目级规则能覆盖全局规则我就不用再在每个工具里重复写本目录不允许修改 test 文件这种话。第二引入焦点文件机制。kshell 提供一个/focus指令把当前任务相关的三五个文件路径塞进上下文并指示后端只围绕这些文件输出。这比直接甩整个仓库经过剪枝后的索引要轻量得多token 占用少模型注意力也集中。实测下来单文件修改类任务的首次正确率从 60% 左右提到了 85% 上下样本量不大仅供参考但趋势很稳定。第三拦截并改写工具自身的默认行为。比如 codex cli 默认会读.gitignore来决定哪些文件可读但有些不想让模型看到的文件也会被放进去。kshell 在调起 codex cli 时会额外传一份指向 kshell 自己生成.ksignore的忽略文件把敏感文件在源头就挡在外面而不是等模型自己决定看不看。2.3 工具路由同一套指令分发到不同后端路由层是 kshell 最容易被低估的一块。它的核心价值是让用户不用记每个后端特有的命令和参数差异。我定义了一套指令集目前包括/new开启新会话/load [id]加载历史会话/focus a.ts b.ts设置焦点文件/model [name]切换模型/cost查看当前会话 token 消耗/backend [name]手动切换到指定后端这套指令在 kshell 里被解析成指令结构之后由 router 翻译成目标后端能理解的参数。举个例子/model在 codex cli 下会被转成模型参数在 Claude Code 下会转成它对应的模型映射而在走自定义 API 的轻量后端时直接替换请求体里的 model 字段。写 router 时踩过最深的坑是各 CLI 对继续会话的打开方式完全不一致。codex cli 用 resume 加 IDClaude Code 是进入项目目录后用 continue 标志另一个后端走的是环境变量传 session_id。kshell 的解决方式是给每个后端单独写一个 adapter暴露同样的三个方法spawn()、resume()、complete()。上层完全不做分支判断新增一个后端只是新增一个 adapter 文件不影响路由主逻辑。3. 技术实现TypeScript Node.js少即是多3.1 为什么选 TypeScript 而不是 Go/Rust做 CLI 工具最容易犯的毛病是上手就选一个硬核语言。我承认 Go 的部署很香Rust 的性能很顶但 kshell 最大的场景是在别人的电脑上快速跑起来并且需要高频率地给不同后端的输出格式做适配。TypeScript 在三个维度上都更合适。第一生态里现成的 parser 多。codex cli 的输出、Claude Code 的流式 chunk以及各种 agent 协议GitHub 上都有现成的类型定义或解析库不用从零逆向格式。第二迭代速度快。kshell 的会话解析层基本每周都会跟着上游 CLI 变动一次TS 不需要编译到原生二进制改完就能跑配合精准的快照测试几分钟就能确认改坏了什么。第三分发不吃亏。用 tsup 打包成单文件可执行覆盖了绝大多数目标用户。当然这不是说 Go/Rust 不行如果你的接入目标很固定、格式基本不变用 Go 写一个静态二进制版本完全合理。但 kshell 的定位是高度跟随上游变动的封装层动态语言带来的维护效率优势是压倒性的。3.2 主要模块与数据流kshell 的代码组织很直白核心就四个模块cli 层TUI 交互层负责渲染面板、接收按键输入、展示流式输出。router 层解析用户指令决定请求落到哪个后端维护指令到后端参数的翻译。adapters 层每个后端一个 adapter统一实现 spawn、resume、complete 三个方法内部封装参数映射、会话 ID 传递、输出流解析。store 层SQLite 会话库包括数据表定义、查询接口、成本统计接口。数据流是这样的用户在 TUI 输入一行自然语言或指令cli 层把文本交给 routerrouter 判断是否有显式/backend指令否则按当前项目的默认后端配置路由。随后 adapter 以子进程方式启动底层 CLI把组装好的 prompt 通过 stdin 传入同时监听 stdout 和 stderr。流式输出一边渲染在 TUI 面板上一边被 parser 切成消息落进 store。整个过程里用户看到的是同一个界面但身后的后端可以随时切换。实际开发时容易忽略的一点是子进程的 stdio 管理。如果底层 CLI 自己又是一个交互式 REPL而你只是想一次性喂一个任务进去必须用非交互模式参数否则子进程会一直等 stdin。adapter 层我对每个后端都做了一组模式探测如果当前输入是单轮任务就走非交互模式如果检测到/load续聊才走交互接管。这个区分花了很大精力但它是稳定性的地基。3.3 配置文件的取舍JSONC 而不是 YAMLkshell 的配置文件统一用 JSONC允许注释的 JSON。选它不选 YAML原因很实际JSONC 不需要多行字符串缩进容错也几乎没有这个字段到底是字符串还是布尔的歧义更重要的是JSONC 去掉注释后直接用JSON.parse就能读不需要引入一个 YAML 解析链体积和出错率都更低。一个完整的kshell.project.json大概是这样的{ // 项目级配置会覆盖全局同名配置 defaultBackend: codex, focus: { maxFiles: 5, include: [src/**/*.ts, tests/**/*.ts] }, rules: [ 不要修改 public/ 目录下的生成文件, 测试文件命名必须为 *.test.ts, 改动数据库 schema 前先输出影响面清单 ], costLimit: { perSession: 2.5, weekly: 12 } }这套配置的设计准则是能写清楚就写清楚。rules 数组每一条都会被直接拼进上下文的第一段所以不要写请遵守最佳实践这种废话要写在 xx 情况下做 yy不要做 zz这种模型真正可执行的动作。costLimit 里的数值是美元达到阈值后 router 会拒绝发起新请求。这个设计帮我拦下了不少半夜挂在后台忘了关的会话钱是小事上下文被污染才是大事。4. 上手实操安装、配置、跑起第一个会话4.1 安装方式和环境要求kshell 目前以 npm 包形式分发安装很常规npm install -g kshell环境要求很低Node.js 18 以上有一个可用的终端。底层 CLI 不是内置的kshell 启动时会检测 PATH 里有没有 codex、claude 这些命令检测不到对应的 adapter 就不激活。也就是说你可以只装 codex clikshell 也能正常提供 codex 的路由能力之后再补其他后端。安装完成后第一件事是初始化目录kshell init这个命令会创建~/.kshell/目录结构生成默认的 config.json并打印一份当前已检测到的后端清单。如果某个后端没被检测到它会给出对应的安装提示对新手非常友好。有一个点必须提醒kshell 只是封装层它不会帮你完成任何后端的登录认证。codex cli 的登录流程、Claude Code 的 API key 配置你仍然需要先按原工具的方式处理好。kshell 只是把你的本地凭证转发给子进程使用它不做凭证托管也不推荐让它托管安全边界越简单越不容易出问题。4.2 一份最小可用配置初始化之后我建议在项目根目录放一个kshell.project.json哪怕你暂时只需要一条规则。为什么因为后续你大概率会加焦点文件、成本限制、规则权重到时候再补配置需要重新验证的改动面会更大。不如一开始就把骨架立起来{ defaultBackend: codex, rules: [修复问题时先复现再给根因分析最后给改动] }然后在终端里跑kshell你会看到一个底部是输入框、中间是输出流日志的 TUI。输入/help能看到当前可用指令列表。输入普通文本比如把 src/utils/format.ts 里的日期处理改成用 dayjs并补充测试回车kshell 就会按规则组装上下文调起 codex cli 子进程把任务喂进去再把输出流式渲染出来。4.3 跑通第一个任务并接管已有会话第一次跑完任务后可以输入/cost看这次消耗输入/sessions看所有历史会话。如果底层 codex cli 已经有一些历史会话kshell 也支持接管先通过/scan扫描~/.codex/sessions目录把已有会话的元数据导入 SQLite随后就能用统一的查询语句把它们当作 kshell 的会话来查看。这里有个细节值得注意/scan导入的是会话元数据时间、模型、标题不是完整的对话历史。因为底层 CLI 的会话文件格式内部并不稳定完整迁移很容易解析失败。我选择元数据先行对话内容仍然可以随时回到原生工具里查看这样既避免了兼容性问题又能在统一视图里做成本统计。5. 实测与踩坑比原生命令行好在哪又有哪些坑5.1 同一需求在原生 codex cli 和 kshell 下的体验对比为了看起来不像自吹自擂我把一个真实小需求分别在原生 codex cli 和 kshell 下跑了一遍。需求是这样的把一个 Node 脚本里的所有console.log替换成统一的logger.info保持参数不变同时更新对应的类型声明文件。原生 codex cli 的做法是直接在新终端里输入需求codex 自己决定读哪些文件、改哪些文件。实际跑下来第一次它确实改了脚本文件但类型声明文件需要我主动补充提到之后它才去处理因为上下文里没有这个项目的 logger 封装长什么样这条信息第一个版本把logger.info的参数签名写错了报错之后才修正。kshell 下我先是/focus lib/logger.ts scripts/convert.ts types/logger.d.ts然后附加了一条规则logger.info 接受 message 和 meta 参数。它第一轮就把脚本、类型声明、logger 定义三个文件都覆盖到了参数签名也没错。这不是模型本身变聪明了而是把模型需要的关键信息在上下文阶段就喂到位了。需要说明的是这种对比有很强的任务相关性。对于一行就能说清的简单需求多一步焦点文件操作反而有点多余。kshell 的价值集中在多文件、有隐式依赖、需要遵守项目内约定的任务上单文件小改动直接原生 CLI 也完全够用。5.2 权限、超时、输出解析三个最容易翻车的地方先说权限。子进程方式调用底层 CLI最痛的是权限代理。codex cli 自己会维护一套工具调用权限kshell 无法替用户决定这个命令能不能执行所以在 adapter 里我保留了完整的透传开关默认不拦截任何文件操作。有一次我发现 kshell 启动的 codex 子进程读了一个我并不希望它读的目录。排查下来问题不在解析规则而在环境变量codex 原生启动时会读取它所在 shell 的环境变量而 kshell 的 Node 子进程里环境变量传递不完整导致 codex 跌落到默认配置。这个问题的解法是显式把HOME、XDG_CONFIG_HOME等关键变量从父进程复制给子进程同时做了一组环境差异的自动修正。类似的坑在 macOS 和 Linux 上表现还不一样Mac 下多了一个LANG传递问题会导致中文输出乱码。再说超时。AI 编程 CLI 的流式输出并不保证每条 chunk 之间有时间戳某些后端在思考较长时间时会完全沉默。我最初给 adapter 设了 60 秒无输出超时结果 20% 的长任务会被误杀。后来改成了首包超时加动态静默扩容5 分钟内必须出现第一条输出之后如果持续静默超过 180 秒才判定为挂起。这个参数组合实测下来误杀率降到很低代价是真正卡死的请求要等更久才报错但权衡下来是值得的。最后是输出解析。很多人以为解析 CLI 输出是最简单的部分其实最坑。codex cli 在 TTY 下会输出 ANSI 加色块、\r进度行、每次 tool call 的中间 json在非 TTY 下的输出格式又不一样。如果直接按物理行匹配补丁标记来提取 diff会漏掉大量换行被截断的边界情况。我的做法是构造一个伪终端兼容层把输出里的 ANSI 和进度行先剥掉再按消息边界切分而不是按物理行切分。现在 kshell 里执行 codex 的代码建议、claude 的文件改动摘要解析率都稳定在 98% 以上。这三类坑有一个共同根源对底层工具的假设太强。我的经验是封装外部 CLI 时永远要假设你的子进程会做出文档之外的行为。代码里到处兜底确实丑但比起线上解析失败导致的会话丢失宁可丑一点。6. 进阶玩法与开源之后的规划6.1 自定义 hook 和 prompt 模板kshell 预留了两类扩展点。第一类是事件 hook在~/.kshell/hooks/目录放一个 JS 导出文件即可export default { async beforeRequest({ prompt, backend, rules }) { console.log([hook] ${backend} 即将接收 ${rules.length} 条规则); }, async afterResponse({ sessionId, cost }) { // 可以在这里做成本超标告警 }, };第二类是 prompt 模板变量。除了内置的项目名、焦点文件、规则列表这些变量之外用户可以在 rules 里通过{{自定义变量}}形式引用kshell.project.json里的扩展字段用来做每次请求都附带当前 git 分支和最近提交信息这类动态业务上下文。我自己的一个模板就是{ rules: [ 当前分支是 {{gitBranch}}最近一次提交是 {{gitLastCommit}}, 在改动代码前先跑一遍现有测试并贴出失败用例 ] }这类 hook 原理简单但实际效果极其明显。大多数模型失败的原因不是不会写代码而是不知道当前仓库的现场状态。有一次我连续三次让 codex 修一个只在 CI 里复现的 bug它在本地根本没有复现条件直到我把 git 分支和 CI 配置路径加进上下文一次就定位到了问题。这就是工作台相比裸 CLI的本质差别——它不只是传话它在传话之前已经把现场信息打包好了。6.2 成本统计与会话审计我自己最常用的是这个命令组合kshell cost --project ./ --week它会把本周内该项目的所有会话按后端类型、日期聚合输出。刚才提到 SQLite 的好处在这里就显现了想按项目、按后端、按天拆消费一条 GROUP BY 就能搞定SELECT backend, date(created_at) as day, sum(tokens_in), sum(tokens_out) FROM sessions WHERE project_path LIKE %mydir% AND created_at datetime(now, -7 days) GROUP BY backend, day;会话审计我做得更细每个会话结束时kshell 记录退出码、生成文件列表diff 的 name-only 提取结果、以及最终用户是否为该会话点了通过。这样我每周复盘时能直接回答三个问题这个后端这周成功率高不高、它主要改的是哪些文件、有没有明显出现改一个 bug 引入三个新 bug的模块。市面上单个 CLI 给不了这种跨工具的复盘视图这是统一工作台的核心价值。团队里如果有人整天挂一堆会话烧 token这类统计也比逐个工具去查要直观得多。6.3 开源之后我在收集什么反馈开源初版发布后目前收到的最有价值的反馈集中在两个方向。一个是多平台支持。我在 Windows 下的 adapter 子进程启动遇到了一些路径分隔符和 PowerShell 输出编码的问题这块正在用交叉运行测试补齐。如果你主系统是 Windows建议先用 WSL 跑 kshell等 Windows 原生适配版本稳定了再切换。另一个是只读模式的呼声。很多用户希望在审查代码、生成计划这类场景里不让 kshell 触发底层工具的自动文件修改。这个功能正在做一个干跑模式的实现原理是在规则里插入一条全局约束并在 router 层把底层工具的 execute 权限改成只返回计划而不真正执行。它解决的其实不是技术问题而是信任问题——很多人不敢把多个 AI CLI 接进统一入口就是担心动作被放得太大只读模式就是给这种担心留一个出口。这两个方向会优先推进因为它们不是锦上添花而是统一工作台走向多人团队协作时绕不开的能力。等稳定了我会继续把注意力放在会话导入格式的扩展上希望最终让 kshell 成为管所有 AI 编程 CLI 的那层壳。最后分享一个我用下来的体会给 AI 编程工具做封装最容易踩的坑是想一次性把功能做得大而全。kshell 能跑起来靠的是把上下文组装和会话持久化这两件事先做到极致路由能力反而是后面才长出来的。如果你也在考虑给自己或团队做一个类似的入口我建议先盯住最让你痛的那一件事把它解决透再谈统一两个字。工具是壳解决问题的那套逻辑才是核。