2026/10/7 20:16:48

我给 AI 编程工具们造了个统一入口:kshell 开源了

我给 AI 编程工具们造了个统一入口:kshell 开源了 一个轻量级 agent 工作台自动发现你本机装的所有 AI 编程 CLI把它们的散落会话、工作区、终端和远程服务器收进一个界面。 Go 1.23 / Wails v2 / React 19 / BubbleteaWindows · macOS · LinuxMIT 协议。一、先说说我为什么想造这个工具不知道你有没有这种感觉——过去一年我装了太多 AI 编程工具。Claude Code、Codex CLI、Gemini CLI、Cursor、OpenCode还有些公司内部封装的。它们每一个都有自己的脾气会话记录散落在完全不同的目录用完全不同的格式存着有的存 JSONL有的干脆存进 SQLite想恢复一个三天前的会话先猜猜它写到哪个文件里去了我有 12 个项目在跑不同的工具切一次要开三个终端窗口。某天我算了一下我花在找会话上的时间比花在写代码上的还多。市面上的工具要么是再实现一个 AI 对话协议要么是绑定死一个生态。而我想要的其实特别朴素别再让我记路径了。扫我的磁盘把所有工具的会话列出来让我一键回去。于是有了 kshell。二、核心设计发现 → 选择 → 交付kshell 的定位只有三个动词发现Discovery → 扫磁盘找出已装工具 工作区 历史会话 选择Selection → 统一的列表 / 网格视图跨工具横向比较 交付Delivery → 把会话交还给原生 CLI 继续跑第三步是刻意的设计选择值得展开讲。为什么不自己实现对话协议Claude Code 有自己的 ACP、Codex 有自己的 resume 参数、Cursor 又是一套。每家都在长出自己的方言。kshell 的立场是不重复实现对话能力把会话交还给工具自己的 CLI。终端路径点击会话 → 拼出claude --resume id或codex resume id扔进 ConPTY/PTY 跑原生交互界面退出后回到 kshell聊天路径ACP对已接入 ACP 的工具Claude Code、Cursor、CodeBuddy走 Agent Client Protocol在窗口内直接对话工具不支持 ACP 时自动回退到终端。这样做的收益是显而易见的kshell 不追着各家协议跑工具升级了它大概率还活着。而且工具本身的新特性新的 diff 算法、新的工具调用能力你第一时间就能用上。三、架构一条单向的数据流providers → discovery → ui / desktop 识别工具、解析会话 扫描与缓存 呈现整个后端只有 217 个 Go 文件、约 3.4 万行代码模块划分很克制模块职责代码量internal/desktopWails 绑定层58 个文件桌面端最大头~10000 行internal/providers各工具适配 Provider 接口~5300 行internal/discovery扫描与缓存~2400 行internal/terminalConPTY/PTY 终端后端~2000 行internal/remoteSSH 连接存储与候选扫描~1900 行internal/uiBubbletea TUI 三视图~2500 行internal/acp/internal/chatAgent Client Protocol 客户端~2600 行3.1 Provider 接口加新工具不用改代码这是整个项目最想拿出来讲的设计。type Provider interface { ID() string DisplayName() string DetectSpec(home string) DetectSpec SessionRoots(home string) []string SessionFilePattern() string ParseSession(path string, head []byte) (*Session, error) NewSessionCmd(ws string, bin string) Launch ResumeCmd(s Session, bin string) Launch }实现这个接口你的工具就被 kshell 纳管了。但内置四工具是硬编码的——因为每家的检测与 resume 都有历史包袱配置表达不了。真正有意思的是那些可选接口它们解决了文件遍历这套抽象不够用的现实// 工具的 TUI 要跟随 kshell 的浅/深主题 type Themer interface { ThemeArgs(...) ([]string, map[string]string, string) } // 「会话不在文件里」——比如 opencode 把会话存进了 SQLite type SessionEnumerator interface { EnumerateSessions(home string, bin string) ([]Session, error) } // glob 盖不住的深层文件比如 CodeBuddy 的 subagents/*.jsonl type PathMatcher interface { MatchSessionRel(rel string) bool }SessionEnumerator是我最满意的一个。opencode 新版把 JSON 会话迁进了~/.local/share/opencode/opencode.db文件遍历这套假设直接失效。kshell 的处理不是加特例而是让 provider 自己声明我能枚举opencodeSessionsSQL select id, directory as cwd, title, time_created as created, time_updated as updated from session where parent_id is null and time_archived is null order by time_updated desc⚠️ 这里有个坑我自己也踩了这条 SQL 必须写成单行。Windows 上 opencode 是 npm 的.cmd包装脚本命令行最终交给cmd.exe重解析多行 SQL 会在换行处被截断——症状是一条会话都查不出来而且日志里看不出任何异常。代码注释里已经写死了这条规矩// 必须写成**单行**... 换行只影响可读性绝不要为了排版拆行。3.2 YAML 声明新工具零代码扩展不想写 Go直接在~/.kshell/providers.yaml里声明providers: - id: mytool name: MyTool detect: command: mytool dirs: [~/.mytool] sessions: glob: ~/.mytool/projects/*/*.jsonl format: jsonl fields: cwd: cwd id: sessionId timestamp: timestamp title: message.content resume: args: [--resume, {id}] verified: false # 声明式配置一律标未实测MergeProviders合并时内置同 ID 优先——用户配置可以扩展但不能篡改内置工具的行为。verified: false这个字段是刻意的声明式配置没经过实测UI 上会明确标出避免用户误以为和内置工具一样可靠。3.3 只读文件头部性能上的关键决策Claude Code / Codex 的 JSONL 会话动辄好几 MB。kshell 全程只读文件头部字节各家上限不同codex256 KBgemini2 MB解析出的元数据ID、cwd、时间、标题对这个量级完全够用。整读多 MB 的 JSONL 是纯粹的浪费。3.4 缓存mtime size 双重门控每次刷新都全盘解析 JSONL 是不可接受的。kshell 的两级缓存都用mtime size 双因子判定是否复用// internal/discovery/index.go:79 return ok entry.MTime mtime entry.Size size为什么两个因子都要只比 mtime 会漏掉同一时间戳内被等量改写的情况只比 size 会漏掉内容变了但长度没变。两者都相同才认为文件未变。再往上还有一层snapshot.json支撑秒开 后台刷新——用户看到的是上次的完整结果同时后台在重扫扫完再原子替换。感知延迟基本为零。3.5 终端退出drain 机制与那 40% 的输出这是踩过坑才有的代码。桌面端关闭终端时直接Close()ConPTY 会丢掉约 40% 的尾部输出——agent 刚吐完一整段回复你一关窗口最后几句就没了。正确做法是 drain延迟 80–500ms → 持续收取剩余输出 → 输出排空后再关闭延迟时长的逻辑是既要给子进程留出写完缓冲区的时间又不能让关终端这个操作感觉卡顿。顺便区分两个容易混淆的东西drain是关闭终端时排空缓冲区~/.kshell/exit.signal是桌面版轮询该文件优雅退出。后者是给构建脚本用的而且构建过程不会退出正在运行的实例——你可以一边用着旧版本一边重新构建。四、SSH安全上的几条硬规矩远程管理这块我定了很明确的底线始终走系统ssh二进制加-o BatchModeyes复用~/.ssh/config、ssh-agent、ProxyJump、known_hosts不传密码不绕过主机密钥校验私钥只存路径绝不把密钥内容落盘到connections.yaml。候选扫描还做了置信度分级来源置信度~/.ssh/config高.env*/ Springapplication*.yml中docker-compose/Makefile/deploy*.sh/ ansible低README 等文档低低置信度候选默认不勾选必须人工确认后才写入connections.yaml。自动猜出来的服务器直接给你连上等于把连错机器执行 rm -rf的风险前置到扫描阶段了。五、两个入口无模式开关入口技术产物桌面端Wails v2 React 19 xtermkshell-desktopTUIBubbleteakshellTUI 的定位不是降级方案而是键盘流的老手会真的喜欢的形态┌ kshell ●claude ●codex ○gemini ws: ~/projects/demo ─┐ │ [Sessions] Files Remote (Tab 切换) │ ├──────────────────────┬──────────────────────────────────────┤ │ WORKSPACES │ PREVIEW │ │ ▸ demo 12 │ 会话摘要 / 文件内容 / ssh 输出 │ ├──────────────────────┴──────────────────────────────────────┤ │ ↑↓ 移动 ⏎ 进入 / 搜索 n 新建 r 重扫 ? 帮助 q 退出 │ └─────────────────────────────────────────────────────────────┘n新建会话、r重新扫描、/搜索过滤、?帮助——全键盘可达。六、目前支持的工具工具会话存储终端 resumeACP 聊天Claude Code~/.claude/projects/slug/*.jsonl已验证claude-agent-acpCodex CLI~/.codex/sessions/**/*.jsonl已验证走终端Cursor~/.cursor/projects/*/agent-transcripts/内置cursor-acpCodeBuddy~/.codebuddy/projects/*/*.jsonl已验证CLI--acpGemini CLI~/.gemini/tmp/推断未实测走终端OpenCodeSQLiteopencode db … --format json内置--session走终端⚠️表格里的已验证和推断未实测是有意区分的。工具升级会改会话格式我不敢替厂商打包票。凡是我本机没真实跑过的都老实标出来。检测上的一个坑Cursor 的 shim 会被删光Cursor 官方更新器会周期性清空安装目录里的入口 shim只留下node.exeindex.js本体。这时候在 PATH 上Detect必然落空。kshell 的兜底策略是在各个InstallDirs里找node.exe 与主脚本都存活的组合// shim 全部落空时Detect 会在 InstallDirs 各目录里找 // node.exe 与主脚本存活的组合作为最后兜底Sourcenode-entry。 NodeEntryScript string于是Detection里多了一个BinArgs来承载入口前缀参数// BinPath 指向 node.exe 时为 [主脚本名]普通可执行文件为 nil。 // 启动与版本探测都须先拼上这组参数。 BinArgs []string这里又踩过一次坑最初版本没拼BinArgs子进程继承了别的cwd找不到主脚本。所以启动和版本探测都必须先拼上这组参数——现在有回归测试盯着TestDetectAllCarriesBinArgs还特意隔离了 PATH 以免依赖本机环境。七、前端一个 Store 常驻 xterm前端架构刻意做得很薄单一 Zustand storestate/store.ts页签状态持久化到 localStorage所有后端调用经lib/api.ts→ 生成的window.go.desktop.App后端推送经EventsOnterminal:data/scan:done/chat:update。有一个性能细节值得单独说xterm 实例常驻挂载terminalRegistry切页签只隐藏不销毁。早期版本切走终端页签就销毁 xterm切回来时缓冲区、滚动位置、ANSI 状态全丢重建还有闪烁。改成常驻挂载后切页签是零成本操作。前端 106 个 TS/TSX 文件106 个测试配套——npm test走 vitest jsdom 环境。八、工程实践测试206 个_test.goGo 侧206 个测试文件覆盖重点在容易回归的地方会话解析、路径匹配、缓存门控、忽略规则继承。go build ./... go vet ./... go test ./... -count1前端另跑npm test/npm run build。顺手修掉的两个真 bug继承忽略inherited ignoregitignore 命中的是目录时其下的子项也要继承忽略状态。第一版只判断直接命中导致文件树里显示了本该隐藏的文件。嵌套 git 的中间层徽章仓库套仓库时中间那层.git目录应该显式标出来——只标最外层会让人误以为整棵子树归属同一个仓库。这两个 bug 都不是逻辑复杂而是测试没覆盖到组合场景。修完之后补了回归用例。构建# TUI → dist\kshell.exe .\build.ps1 # 桌面端 → dist\kshell-desktop.exe自动构建 frontend .\build.ps1 -Desktopbuild.ps1 -Desktop不会请求正在运行的旧实例退出构建期间你可以继续用旧版本。dist 拷贝若因kshell-desktop.exe被占用失败脚本会提示你从托盘退出后重试。版本目前v0.1.3。232 次提交MIT 协议。九、明确不做的事先说清楚不做什么比列功能更有诚意❌SFTP / 文件传输——系统scp就够❌端口转发——ssh -L是成熟方案❌云同步——会话文件本机就有同步等于制造第二份真相❌再实现一遍各家对话协议——这是最大的技术债来源绕开十、怎么用下载GitHub Releases 或 GitCode Releases桌面端还可以在「设置 → 通用 → 关于」里检查更新升级优先走 GitCode 国内源。从源码构建git clone https://github.com/kaiys202212/kshell.git cd kshell go install github.com/wailsapp/wails/v2/cmd/wailslatest # 桌面端需要 .\build.ps1 # TUI .\build.ps1 -Desktop # 桌面端写在最后这个项目从一个具体的痛点长出来AI 编程工具越多找回自己的会话就越难。我没有试图造一个更强的 AI 编程工具而是造了一个中立的入口。谁家工具好用就用谁kshell 只负责让你能看见它们、方便地回到它们。如果你也在同时用好几个 AI CLI欢迎来试也欢迎提 issue——尤其是新的工具适配那是最容易一起把事情做好的地方。仓库地址https://github.com/kaiys202212/kshell