跳到正文

cocoindex-code

一个命令行工具兼 MCP 服务器,为代码库维护一份本地语义索引——tree-sitter 切块、向量存进 SQLite——于是编码 agent 可以用描述去找代码,而不必逐个读文件;后台常驻一个持有模型的守护进程,索引只重算变化过的部分。

Screenshot of cocoindex-code
编辑截图, 1 Oct 2026cocoindex-code ↗

这是什么

一个 Python 命令行工具,为代码库建一份语义索引,好让编码 agent 用描述去搜索它。ccc 负责初始化项目、建索引、搜索、体检,也能直接跑成 MCP 服务器;另一个命令 ccc grep 做结构化模式匹配,完全不用索引、守护进程或嵌入模型。建索引是一个写在 CocoIndex v1 之上的应用:文件按 include、exclude、.gitignore 与体积上限筛出来,在 tree-sitter 给出的边界处切开、目标块长约一千字符,由本地的 sentence-transformers 或经 LiteLLM 的云端提供方嵌入,最后写进 SQLite,向量检索交给 sqlite-vec 扩展。一个常驻守护进程把模型留在内存里,同时服务 CLI 与 MCP 服务器。它以 Claude Code 与 Grok 的插件市场、Oh My Pi 的扩展以及一个 skill 的形式发布;README 的说法是省下 70% 的 token、一分钟装完,Apache-2.0 许可,而包自己仍标着 Alpha。

谁做的仓库属于 GitHub 组织 cocoindex-io——也就是它赖以构建的 CocoIndex 数据转换引擎背后的团队——README 结尾给出的维护者邮箱就在同一域名下。249 次提交来自 23 个账号,其中两个人几乎包办了全部:georgeh0(Jiangzhou He,133 次,除少数几次外都挂在 jiangzhou@cocoindex.io 名下)与 badmonster0(Linghua Jin,35 次,邮箱 linghua@cocoindex.io,也就是 README 里留给企业支持的那个地址)。其后是 mareurs(Marius Ailinca,23 次)与依赖机器人(22 次),外部贡献的份额小而具体。统计到的 77 条共同作者尾注里,53 条署名某个 Claude 模型——Claude Sonnet 4.6 十五条、Claude Opus 4.6 (1M context) 十五条、Claude Opus 4.6 九条、Claude Fable 5 八条——另有 22 条来自依赖机器人,一条署名一个叫 Clawdbot 的机器人,还有一条署了维护者本人。

它是怎么搭起来的

组成 · 6

形状是一个守护进程加一层薄客户端,中间夹着一个 CocoIndex 应用。贵的那件东西——嵌入模型——只加载一次,放进一个长期驻留的后台进程,它在 Unix socket(Windows 上是命名管道)上监听;命令行工具与 MCP 服务器每次只连一个请求,递过去一个用 messagepack 编码、在同一个协议模块里定义好的标签联合,然后断开。建索引被写成一个按内容记忆结果的函数,所以第二次跑只重算变化过的部分;又因为记忆状态存在数据库文件里而不是进程里,重启本身不会让任何东西失效。搜索是对一张 SQLite 向量表的查询,通过一个扩展读取,有两条计划:不带路径过滤时走最近邻计划,带过滤时走全表扫描。agent 触到的每一层都是刻意做薄的——命令行层除了解析参数和转交之外没有逻辑——而必须快的那部分活在底下的 Rust 引擎里,这也是它敢提供第二种完全不需要索引、守护进程和嵌入模型的搜索方式的原因。

src/cocoindex_code/
一个扁平包里二十个文件、214 KB。最大的是命令行层 39,570 字节,其后是守护进程 31,661、客户端 27,130,设置与路径解析 26,226。其余一个文件管一件事:结构化搜索命令 16,211 字节、MCP 服务器 13,760、包在 CocoIndex 环境外的项目包装 12,603、文件匹配 7,819、消息协议 7,305、上下文键与嵌入器工厂 6,614、向量查询 6,021,再往后是嵌入器默认值与参数、守护进程 socket 路径、限流的云端嵌入器、3,615 字节的建索引应用、1,133 字节的 chunker 注册表和一个小 schema 模块。
tests/
二十三个文件、221 KB,重心在面向用户命令的端到端测试而不是单元测试:单个最大的文件 37,690 字节,设置与文件匹配那套 33,724 字节,另有专门覆盖命令行辅助函数、结构化搜索、客户端、守护进程、守护进程空闲行为、协议,以及两条端到端路径的套件。Docker 端到端测试作为单独标记的一组存在,默认测试命令不跑它们;项目自己的指引说测试失败时要去修底层问题,而不是跳过测试。
scripts/
两个文件,只为用证据回答一个问题:一个 15,274 字节的脚本查询实时的 MTEB 结果数据集,渲染出一张按参数量、架构与估算 CPU 速度排序的嵌入模型表;以及它产出的那份 7,236 字节的报告,一并提交在树里,好让 README 里的数字能追回到一条命令。
skills/ccc/ 与插件清单
面向 agent 的那一层:一份 3,298 字节的 skill,告诉 agent 它对这个项目的初始化、建索引与搜索负责,不该让用户去做;另有两份参考,一份讲设置(4,493 字节),一份讲管理与排障(3,501 字节)。周围放着两份插件市场目录,分别给 Claude Code 和 Oh My Pi,各带一份清单,以及一行 MCP 服务器定义,两边都读它。
hooks/ 与 extensions/
索引如何在不被要求的情况下保持新鲜。一份 947 字节的钩子文件注册在会话开始时以及编辑之后,好让项目目录存在时自动跑一次增量索引;又因为有一个 agent 平台不执行这种文件格式,同样的行为被重新实现成一个 2,518 字节的 TypeScript 扩展,并用一份小清单声明它监听哪些事件。
docker/、.github/workflows/ 与文档
一份 4,719 字节的镜像定义,带两个变体——一个只装云端路径,一个把本地模型栈一起打包——外加 compose 文件、一个在挂载路径上对齐文件归属的入口脚本,以及 7,678 字节的发布工作流对着 1,616 字节的检查工作流。文档是 41,991 字节的 README、12,958 字节的嵌入指南,和 7,677 字节、写给在这个仓库里干活的编码 agent 的指引。

取舍,以及它替代了什么

  • 内嵌的 SQLite 索引,而不是另装一个数据库 替代 要求单独跑一个向量数据库服务

    功能列表把它写成「Embedded: Portable and just works, no database setup required!」,整个索引就是项目目录里的两个文件。这个选择的代价是写下来而不是藏起来的:增量状态放在一个 LMDB 库里,它的最大尺寸在守护进程启动时固定,默认 4 GiB,所以大仓库会撞上环境变量里的 mapsize 上限报错,而文档给的答案是一个环境变量加重启守护进程,直到上游那个「按需自动扩容」的 issue 落地。

  • 常驻守护进程,让嵌入模型留在内存里 替代 每条命令都重新加载一次模型

    README 直说守护进程把模型留在内存里,所以它在一段可配置的空闲时间(默认 180 分钟)没有客户端活动后退出,并在下一条命令时被透明地重新拉起。这个取舍是按会话协商而不是固定的:MCP 客户端默认发心跳把守护进程焐热,而一个写明的设置只关掉那个心跳,好让长时间开着的编辑器会话在真实请求之间把模型放掉。

  • 一个完全不用索引的结构化搜索命令 替代 让每一次查找都走向量索引

    按示例写模式的命令通过引擎的结构化匹配能力去匹配语法树,用 README 的话说「runs entirely locally: no index, daemon, or embeddings required」。这里也是项目依赖尚未发布之物的地方:同一份 README 注明,在那个能力于上游发布之前,这条命令需要对着引擎的本地构建来跑。

  • 两种安装形态,其中一种不带本地模型栈 替代 一个总是拉上 sentence-transformers 的包

    「全都要」的那个 extra 会把 sentence-transformers 带进来,于是本地嵌入不需要 API key,这也是推荐的默认值;瘦装只走云端,是给不愿意在机器上放大约一个 GB 本地推理栈的人准备的。同一刀切法也镜像到每次发布都会出的两种 Docker 镜像变体上。

  • 先用 skill 教会 agent,其次才提供 MCP 替代 只提供一个 MCP 服务器

    README 把 skill 标为推荐集成方式,并说装上它之后不需要初始化或建索引的步骤,因为这套生命周期由 agent 自己管;skill 文件也明确要求 agent 自行初始化、建索引与刷新,而不是去问用户。MCP 作为替代路径被记录在文档里,它的搜索工具默认先刷新索引。

依据README.md(41,991 字符)、EMBEDDINGS.md(12,958 字符)、CLAUDE.md(7,677 字符)、pyproject.toml、scripts/find_best_models.py、scripts/MTEB-RANKINGS.md、.github/workflows/release.yml、.github/workflows/pre-commit.yml、skills/ccc/SKILL.md,以及完整的 81 个文件树及其体积。

制作过程

6 个阶段
  1. 01

    四个月的开发,和一条停在八月的发布线

    仓库创建于 2026-02-01,最早一次提交的日期是 2026-01-31,提交信息是 feat: initial version。八个月里一共 249 次提交,曲线明显前重后轻:二月 90 次、三月 73 次,此后是 20、7、15、27、13,最后是有活动的最后一个月九月 3 次。它发了 20 个 release,没有一个草稿或预发布,从 2026-04-09 的 v0.2.22 到 2026-08-07 的 v0.2.41,tag 列表上的名字与这二十个完全一致。结尾那段空档是关于节奏最直白的事实:最后一次推送是 2026-09-22,比最后一个 release 晚六周,也就是说在停止切版本之后,开发还继续了相当一段时间。发布机制是端到端自动的——GitHub 上发布一个 release 会触发构建、经标准发布动作推到 PyPI、把同一批产物挂回 GitHub release,并为两种架构构建 Docker 镜像,推送到 Docker Hub 与 GHCR 两处,缓存也放在 registry 上。那条工作流里的一句注释记下了一次真出过的竞态:镜像从检出的源码树安装而不是从 PyPI 装,因为在 v0.2.24 那次发布时,刚发布的版本还没在 PyPI 的 CDN 上传开,而从 tag 构建也顺带保证了镜像与它一致。周围还有 2,729 个星、225 个 fork、17 个 watcher 与 44 个未关的 issue。

  2. 02

    一个文件怎么变成块,索引又存在哪里

    建索引就是一个 CocoIndex 函数 process_file,它被装饰成按内容记忆结果,所以第二次跑只重算变化过的部分。它通过一个匹配器遍历项目,同时应用 include 规则、exclude 规则、嵌套的 .gitignore 以及可选的 max_file_size;语言由扩展名判断,也可以按扩展名改判;随后切分文本——如果该扩展名在 settings.yml 里配了自定义 chunker 就用它,否则用默认的递归切分器。切分方式是 README 专门论证过的一点:块切在 tree-sitter 的边界上,于是一个函数或一个类通常保持完整,目标块长约一千字符、大致三百 token,这个数字是为了让一块能塞进多数本地模型 512 token 的窗口,而云端模型给的是八千到三万二。嵌入器在守护进程启动时创建一次,两个参数字典随请求传递,好让非对称模型在文档侧与查询侧拿到不同的参数。存储是项目 .cocoindex_code/ 目录下的两个文件:一个是 LMDB 支撑、保存增量状态的库,一个是保存向量、通过 sqlite-vec 扩展检索的 SQLite 文件。它们的位置可以按路径前缀重映射,这是给容器用的推荐做法,因为 LMDB 在绑定挂载上表现不好。

  3. 03

    标题里那个百分比,以及它旁边那些数字的方法

    README 开篇就是一个百分比:「Instant token saving by 70%」。README 里没有任何地方说明它是怎么得到的——没有基准、没有数据集、没有读者可以照跑的复现命令——所以那是作者的自述,本记录也只把它当自述来记。仓库确实为相邻的那个问题——该选哪个嵌入模型——提供了一个可复现的入口:scripts/find_best_models.py 是一个单文件 Python 脚本,依赖写在脚本头的内联声明里,它查询 Hugging Face 上实时的 MTEB 结果数据集,渲染出一份提交在树里的报告 scripts/MTEB-RANKINGS.md。这份报告用一条写明的命令就能重新生成:uv run scripts/find_best_models.py --clear-cache --output MTEB-RANKINGS.md;它还会给自己盖上数据新鲜度,这一份上写的是数据集更新于 2026-06-23。这份报告对自身性质的谨慎比它那张表更值得记:CPU 速度被标为「estimated from parameter count and architecture」,而最容易被引用的那个数字——同样参数量的 decoder 模型在 CPU 上可能慢三到十倍——是估算而不是实测,它硬编码在脚本自己的架构表里,又在 README 里被当作对纯 CPU 建索引者的预期重复了一遍。材料里唯一一个实测的延迟数字来自项目之外:2026-09-09 的一份 issue 报告说,在一个十万块的合成数据集上,带路径过滤的宽泛查询在 384 维下大约要 450 毫秒,而不带路径过滤的查询走的是向量索引的最近邻计划。

  4. 04

    19:34 的一份崩溃报告,00:15 的一个 release,和一次分歧

    材料里最清楚的一次过招从 issue #270 开始。一位用 pipx 装了 v0.2.39 的用户报告说,任何带 --path 的 ccc search 都会把守护进程打崩,报 TypeError: unsupported operand type(s) for *: "NoneType" and "NoneType";只用 --lang 没事;而客户端只是把守护进程回的报错原样抛出来。他还给了一个根因:带路径过滤的请求会被分派到全表扫描那条路,于是 NULL 距离跑到了打分函数里。当晚就有修复落地——pull request #271 把守护进程侧的调用栈送到客户端(填进错误结构的一个字段),把五处重复的抛错点合并成一个 _daemon_error() 帮助函数,并拦下 distance 为 NULL 的行;理由是这一列是隐藏列,只在最近邻计划下被填充,所以出现 NULL 就意味着真跑的计划不是要的那个计划;全表扫描那条路不需要这道拦截,因为距离是它自己算的。v0.2.41 在 00:15 发布,距离报告不到五小时。十分钟后维护者回帖说复现不了、认为那份根因判断不对——因为距离函数遇到坏输入是抛错而不是返回 NULL——并请用户升级后重跑,好拿到真正的栈帧。用户照做,确认堆栈修复生效,并报告同一个 TypeError 依然存在。

  5. 05

    记忆键忘掉的两个输入

    分量最重的外部贡献,是两份关于增量索引「算错」而不是「算慢」的报告。第一份指出:process_file 是被记忆的,但它有两个输入从来不在记忆键里——语言覆盖值是在函数内部从项目设置文件读的,自定义 chunker 则来自一个不受跟踪的上下文键。于是改动其中任何一个,内容没变的文件都会继续留在旧的块和旧的语言上,而重启守护进程也救不了,因为记忆状态持久化在数据库文件里。那份报告把 README 自己的承诺引回来对照:改完这些设置之后,不需要删索引,也不需要重启守护进程。它提出的修法是算一枚指纹——对生效后的语言覆盖值,以及对每个注册过的后缀取 chunker 所在模块、限定名与模块源码——然后把它作为额外参数传进被记忆的函数,这个参数存在的唯一意义就是进入记忆键。同一贡献者的第二份报告讲清了慢路径的代价,以及它为什么比那个开关看起来更常被走到:从子目录里发起的搜索会默认把范围限定到该子目录,而任何路径过滤都会把查询送去全表扫描。第三份仍开着的 pull request 则是个内存问题而非正确性问题:切分器把尺寸设置当作目标而不是上限,于是一行里找不到任何可切分符时,它整行返回、多长都返回,而在场景文件、压缩过的产物或编码过的二进制块里,一行六万字符很常见;嵌入器又会把一批补齐到最长那条的长度,代价是平方级的,一个文件就足以把守护进程打死。这三份到九月底都还开着。

  6. 06

    谁在审,一份好 pull request 要等多久

    贡献者列表有 23 个名字,但实际上的审阅者只有一个人,这条队列在帖子里看得见。对于「让诊断命令遵守文件体积上限」的那处修复,答复是一句谢谢加一个指路:「thanks @shixi-li, @georgeh0 can help take a look!」。那份 pull request 开于 2026-08-10,六周多之后才有人回来——分支干净可合、四条持续集成检查全绿、被点名的审阅者仍未看过——至今开着。另一份提议给下一次建索引加预演的改动,收到的回应更有内容:维护者先问动机是什么,提醒说它只会列出新增和删除的文件、不会列出被修改的文件,这在很多场景下「may be unexpected and misleading for many cases」,并指向 CocoIndex 自己在做的影子运行与预演 API;贡献者回答说他的动机是想知道那些意外文件是从哪来的——是另一个 worktree,由一个 agent 创建的——随后改了 pull request。外部的活也确实落地了:Elixir 支持,Oh My Pi 的插件市场目录(换回一句「Thanks a lot!」),一处修掉「深目录下守护进程 socket 路径超出系统上限」的补丁,以及一个让守护进程可以在 MCP 客户端连着时退出的设置。还有一份 issue 展示了另一面:一位用户请维护者不要再把 .gitignore 当成应用自己的规则来源,它被关掉了,没有留下回复。

相关档案

全部档案 →