注册表 79 条、顶层 82 个目录:差的那几个是什么

2026-08-10

CLI-Anything 这个仓库有两个数字特别容易被摆在一起看:registry.json 里的条目数,和 clone 下来之后根目录的目录数。我们采集时(2026-08-10,对应仓库快照 39634a6)前者是 79,后者是 82。

这两个数放在一起,第一反应几乎都是做减法:82 减 79 等于 3,那就是有 3 个目录没进注册表。这个结论是错的,而且错得比较彻底——这两个集合的交集只有 68 个名字,注册表这一侧多出 11 条,目录那一侧多出 14 个。差 3,只是两个方向的差额相减之后的残值,它掩盖了 25 个名字的错位。

本文就把这两份差集逐个摆出来,顺带说清一个会让你数出不同结果的比对坑。

先把两个数各自数清楚

79 这个数来自 registry.json。这份文件顶层只有 metaclis 两个键,clis 是数组,长度 79。meta 里还有一个 updated 字段,值是 2026-06-19

82 这个数来自根目录本身,ls -d */ | wc -l 得到 82。注意这 82 个里面本来就包含 assets/docs/skills/ 这种一看就不是 harness 的目录。

也就是说,这两个数从一开始就不是在数同一类东西:一个数的是注册表登记了多少个 CLI,一个数的是仓库根下摆了多少个文件夹。把它们直接相减,等于默认「一个条目对应一个目录、一个目录对应一个条目」,而这个假设两头都不成立。

注册表有、目录没有的 11 条

用 Python 把 79 个 name 和 82 个目录名各做成一个集合,取差集,注册表这一侧剩下 11 条:

clibrowserhacker-feeds-cliinkstitchmagnificmeerk40topenwebuipalmierstatatinyfishueatelierve-twini

这 11 条有一个共同点,而且是可以在同一份 JSON 里直接核对的:它们要么填了外部 source_url,要么安装串本身就指向外部仓。其中 10 条走的是前一种,clibrowser 走的是后一种——它的 source_urlnull,但 install_cmd 写成 cargo install --git .../clibrowser,代码同样不在这个仓库里,自然也不会在根目录留下同名文件夹。

这里有个特别容易顺手写错的地方:source_urlnull 的条目也正好是 11 条,但不是同一批名字。两个 11 条差的是 zoteroclibrowser 这一对——zoterosource_url 非 null,可它在根目录有同名文件夹,属于交集那 68 个;clibrowser 反过来,source_urlnull 却没有本地目录。数量相同不等于集合相同,这正是本文开头那个「82 减 79」错误的同一种形状。

这一点也能从安装串上对上号。仓库内条目的 install_cmd 形如 pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=jumpserver/agent-harnessregistry.json:16,第 1 条 jumpserver),指的就是本仓某个子目录。外部条目走的是别的地址。

顺带一提,skill_md 那一侧也有 11 条是远程 URL 而不是仓库内相对路径:zoteroopenwebuiueatelierve-twinistatainkstitchhacker-feeds-clitinyfishmeerk40tpalmiermagnific。这又是第三个「11 条」,名单和上面「没有本地目录」的 11 条仍然不重合——它含 zotero,不含 clibrowserclibrowserskill_md 是 5 条 null 之一,另外四条是 adguardhomecomfyuimermaidsketch)。所以:只 clone 这个仓库,没有本地目录的那 11 条翻不到源码;skill_md 为远程 URL 的那 11 条则是连技能文档都要另外去外部地址取。三处都是 11,比对时请按名字比,别按数量比。

目录有、注册表没有的 14 个

反方向的差集是 14 个:

assetsdocsskillscli-anything-plugincli-hubcli-hub-matrixcli-hub-meta-skillcodex-skillhermes-skillopencode-commandsqoder-pluginreasonix-skillskill_generationrekordbox

前 13 个都不是被封装的软件:assets/docs/ 是资源与文档,skills/ 放的是给 agent 读的技能包。剩下的 cli-anything-plugincli-hubcli-hub-matrixcli-hub-meta-skillcodex-skillhermes-skillopencode-commandsqoder-pluginreasonix-skillskill_generation,从目录名看对得上 README 里列出的那批平台分节与工具入口——README 一共列了 10 个 agent 平台分节加 1 个「更多平台」分节,其中 OpenCode、Goose、Codex、Hermes、Reasonix 5 个标了 Experimental,Cursor 和 Windsurf 标的是 “coming soon”。这些目录的内容我们没有逐个读过,这里只按名字对应关系说到这一步。不管怎样,它们都不是注册表要登记的那种「被封装的软件」。

真正值得单独看的是第 14 个:rekordbox。它是这 14 个里唯一一个看起来就是 harness 的目录,skills/ 下也有对应的 cli-anything-rekordbox,但 79 个 name 里查不到 rekordbox 这个名字。而 README 的新闻区里写着 “Rekordbox CLI merged (#252)“(README.md:66)。目录在、skill 包在、README 新闻在,注册表条目不在——这是四个可以各自核对的事实,我们只陈述到这里。

把这一条并进来算一笔账就清楚了:根目录下真正带 agent-harness/ 子目录的有 69 个(find . -type d -name agent-harness | wc -l),其中 68 个能在注册表里找到同名条目,多出来的那一个正是 rekordbox。82 减去 13 个非 harness 目录,剩下的就是这 69。

会让你数出不同结果的那个坑:大小写

如果你自己跑一遍集合比对,很可能得到的不是 11,而是 13。差别只在一处:比不比大小写

根目录里有两个目录名和注册表 name 的大小写不一致——目录是 3MF/,条目是 3mf;目录是 QGIS/,条目是 qgisqgis 条目的 skill_md 也写成 QGIS/agent-harness/... 这种带大写的路径)。

于是:

  • 直接拿字符串比(区分大小写),注册表这侧的差集是 13 条,多出 3mfqgis
  • .lower() 再比,差集是 11 条,交集 68。

这两种数法都能自圆其说,但含义完全不同:13 里面那两条是有目录的,只是名字大小写对不上;11 里面则全是真的没有本地目录。你在 macOS 或 Windows 这类默认不区分大小写的文件系统上按目录名去找文件不会有感觉,一旦写脚本做集合比对,或者在 Linux 上按注册表里的路径去 open(),这两个名字就会变成两个不同的东西。所以做这类核对时,先决定用哪种口径,并且把口径写进结论里。

79 条不等于 79 个能用

到这里差集算清楚了,但还有一层更要紧的换算:注册表里有 79 条,不代表你能拿到 79 个可用的能力。

至少要扣掉这么几处:

第一,宿主软件得你自己装。README 明确写了,包装真实桌面软件的 CLI 需要用户自行安装上游应用(README.md:236)。注册表里登记的是那层 harness,不是被封装的软件本体。

第二,harness 本身的齐整度是有分布的。我们采集时按四份文档(顶层 <APP>.md、包内 README.mdSKILL.mdTEST.md)逐个检查 69 个 harness,四件套全齐的 51 个、有缺件的 18 个。其中 sketchSKILL.md*_cli.py 都没有,它的 agent-harness/ 整体不是 Python 结构,而是 package.json + src/cli.js 那一套。缺 TEST.md 的有 12 个,rekordbox 也在其中。这些结构统计另有专门篇目在讲,本文只借它说明「条目数」和「可用能力」不是一回事。

第三,仓库自己给的成熟度口径很克制:打包文件里 45 处标 Development Status :: 4 - Beta、2 处标 3 - Alpha,全仓没有任何一个标 5 - Production/Stable

第四,也是使用前必须掂量的一点:这类 harness 的工作方式就是在你本机上执行外部程序与脚本——起子进程调二进制、调配套命令行工具、发 HTTP 请求、或者拉起 MCP 服务端。仓库的 SECURITY.md 自己列了 5 类攻击面(子进程参数、Script-Fu 注入、XML/SVG 内容、路径穿越、凭据泄露)及对应缓解手段(SECURITY.md:14-22),并声明该文件是给人类贡献者与安全评审看的,“NOT part of the CLI generation methodology”(SECURITY.md:3-5)。装不装、在什么机器上装,请结合自身环境评估。

顺手提醒:别拿 README 的目录树当清单

如果你打算靠 README 里的 Project Structure 树来核对目录,会对不上。那棵树(README.md:1409-1479)只列了约 44 个 agent-harness/rekordbox/siyuan/live2d/macrocli/intelwatch/firefly-iii/ 这些实际存在的目录都不在里面;assets/ 子树也只列了两个文件,实际还有 architecture.pngcli-typing.gifdemos/。README 演示表格里带 cli-anything-* 入口的行是 44 行,正文另写「Tested across 18 diverse, complex applications」(README.md:999,README 自称)。

这些数字和 79、82 都对不上,属于同一类事实的不同口径。要数目录就直接数文件系统,要数条目就直接解析 JSON。

你可以自己复现的核查动作

三条,从粗到细:

ls -d */ | wc -l
find . -type d -name agent-harness | wc -l
python -c "import json,io; print(len(json.load(io.open('registry.json',encoding='utf-8'))['clis']))"

我们采集时这三条依次得到 82、69、79。

再做集合比对,注意上面说的大小写口径:

import json, io, os
d = json.load(io.open('registry.json', encoding='utf-8'))
names = {c['name'].lower() for c in d['clis']}
dirs = {x.lower() for x in os.listdir('.') if os.path.isdir(x) and not x.startswith('.')}
print(len(names & dirs))        # 交集
print(sorted(names - dirs))     # 注册表有、目录没有
print(sorted(dirs - names))     # 目录有、注册表没有

我们采集时依次得到 68、上面那 11 条、上面那 14 个。把 .lower() 去掉再跑一遍,第二行会变成 13 条。注意 dirs 那行里的 not x.startswith('.') 不能省——os.listdir 会把 .git.github 这类隐藏目录一并列出来,不过滤的话第三行会变成 18 项,和 ls -d */ 的口径对不上。

最后一条,用来确认某条记录是不是外部仓托管:

print(sum(1 for c in d['clis'] if c.get('source_url') is None))

得到 68。这里有两个细节要交代清楚,否则这个 68 很容易被误读:

一是 .get() 的语义。79 条里显式写了 source_url: null 的其实是 67 条,另有 1 条(qgis)压根没有 source_url 这个键——事实卡记的字段频次也是 source_url 只出现 78 次。c.get('source_url') is None 会把「值为 null」和「没这个键」一并算成 None,所以才得到 68。你要是改用 c['source_url'] is None,这行会直接抛 KeyError。写口径时得说明用的是哪一种。

二是别把这个 68 和上面的交集 68 当成同一批名字。两个数确实都是 68,但集合不同,差的就是前面说的 zoteroclibrowser 那一对:zotero 有本地目录、却填了外部 source_urlclibrowsersource_urlnull、却没有本地目录。而 qgis 恰好是有本地目录(QGIS/)又缺键的那一条。所以「source_url 为 null 等于代码在本仓」是个近似说法,不是可以直接拿来当判据的规则。

什么情况下说明不是这个原因

如果你数出来的三个数不是 82 / 69 / 79,先别急着当成不一致:

  • registry.jsonmeta.updated2026-06-19,我们读的仓库快照是 39634a6(提交日期 2026-08-03),注册表本身就可能落后于目录;上游继续合并 PR 之后,两侧都会变。
  • 如果只有目录数对不上、条目数没变,先看看是不是把 .git.github 这类隐藏目录也数进去了——ls -d */ 默认不含隐藏目录。
  • 如果交集数不是 68 而是 66,多半是大小写口径没统一,3MFQGIS 掉出去了。
  • 如果某个名字在注册表里有、目录里没有,别急着断定它是外部仓条目:先看 source_url,再看 install_cmdclibrowser 就是 source_urlnull 而安装串指向外部仓的那种。两个字段都指不到外部,才是一个和本文这 11 条性质不同的新情况,值得单独记一笔。

这三个数本身会随上游变动,值得记住的不是 79、82、68,而是这两个集合从来不是一一对应,以及做差集之前先把大小写口径定下来。


本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.jsondocs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。 本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness, 也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。 这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。 注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。 安全相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。