注册表 79 条、顶层 82 个目录:差的那几个是什么
CLI-Anything 这个仓库有两个数字特别容易被摆在一起看:registry.json 里的条目数,和 clone 下来之后根目录的目录数。我们采集时(2026-08-10,对应仓库快照 39634a6)前者是 79,后者是 82。
这两个数放在一起,第一反应几乎都是做减法:82 减 79 等于 3,那就是有 3 个目录没进注册表。这个结论是错的,而且错得比较彻底——这两个集合的交集只有 68 个名字,注册表这一侧多出 11 条,目录那一侧多出 14 个。差 3,只是两个方向的差额相减之后的残值,它掩盖了 25 个名字的错位。
本文就把这两份差集逐个摆出来,顺带说清一个会让你数出不同结果的比对坑。
先把两个数各自数清楚
79 这个数来自 registry.json。这份文件顶层只有 meta 和 clis 两个键,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 条:
clibrowser、hacker-feeds-cli、inkstitch、magnific、meerk40t、openwebui、palmier、stata、tinyfish、ueatelier、ve-twini。
这 11 条有一个共同点,而且是可以在同一份 JSON 里直接核对的:它们要么填了外部 source_url,要么安装串本身就指向外部仓。其中 10 条走的是前一种,clibrowser 走的是后一种——它的 source_url 是 null,但 install_cmd 写成 cargo install --git .../clibrowser,代码同样不在这个仓库里,自然也不会在根目录留下同名文件夹。
这里有个特别容易顺手写错的地方:source_url 非 null 的条目也正好是 11 条,但不是同一批名字。两个 11 条差的是 zotero 与 clibrowser 这一对——zotero 的 source_url 非 null,可它在根目录有同名文件夹,属于交集那 68 个;clibrowser 反过来,source_url 是 null 却没有本地目录。数量相同不等于集合相同,这正是本文开头那个「82 减 79」错误的同一种形状。
这一点也能从安装串上对上号。仓库内条目的 install_cmd 形如 pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=jumpserver/agent-harness(registry.json:16,第 1 条 jumpserver),指的就是本仓某个子目录。外部条目走的是别的地址。
顺带一提,skill_md 那一侧也有 11 条是远程 URL 而不是仓库内相对路径:zotero、openwebui、ueatelier、ve-twini、stata、inkstitch、hacker-feeds-cli、tinyfish、meerk40t、palmier、magnific。这又是第三个「11 条」,名单和上面「没有本地目录」的 11 条仍然不重合——它含 zotero,不含 clibrowser(clibrowser 的 skill_md 是 5 条 null 之一,另外四条是 adguardhome、comfyui、mermaid、sketch)。所以:只 clone 这个仓库,没有本地目录的那 11 条翻不到源码;skill_md 为远程 URL 的那 11 条则是连技能文档都要另外去外部地址取。三处都是 11,比对时请按名字比,别按数量比。
目录有、注册表没有的 14 个
反方向的差集是 14 个:
assets、docs、skills、cli-anything-plugin、cli-hub、cli-hub-matrix、cli-hub-meta-skill、codex-skill、hermes-skill、opencode-commands、qoder-plugin、reasonix-skill、skill_generation、rekordbox。
前 13 个都不是被封装的软件:assets/ 和 docs/ 是资源与文档,skills/ 放的是给 agent 读的技能包。剩下的 cli-anything-plugin、cli-hub、cli-hub-matrix、cli-hub-meta-skill、codex-skill、hermes-skill、opencode-commands、qoder-plugin、reasonix-skill、skill_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/,条目是 qgis(qgis 条目的 skill_md 也写成 QGIS/agent-harness/... 这种带大写的路径)。
于是:
- 直接拿字符串比(区分大小写),注册表这侧的差集是 13 条,多出
3mf和qgis; - 先
.lower()再比,差集是 11 条,交集 68。
这两种数法都能自圆其说,但含义完全不同:13 里面那两条是有目录的,只是名字大小写对不上;11 里面则全是真的没有本地目录。你在 macOS 或 Windows 这类默认不区分大小写的文件系统上按目录名去找文件不会有感觉,一旦写脚本做集合比对,或者在 Linux 上按注册表里的路径去 open(),这两个名字就会变成两个不同的东西。所以做这类核对时,先决定用哪种口径,并且把口径写进结论里。
79 条不等于 79 个能用
到这里差集算清楚了,但还有一层更要紧的换算:注册表里有 79 条,不代表你能拿到 79 个可用的能力。
至少要扣掉这么几处:
第一,宿主软件得你自己装。README 明确写了,包装真实桌面软件的 CLI 需要用户自行安装上游应用(README.md:236)。注册表里登记的是那层 harness,不是被封装的软件本体。
第二,harness 本身的齐整度是有分布的。我们采集时按四份文档(顶层 <APP>.md、包内 README.md、SKILL.md、TEST.md)逐个检查 69 个 harness,四件套全齐的 51 个、有缺件的 18 个。其中 sketch 连 SKILL.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.png、cli-typing.gif 和 demos/。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,但集合不同,差的就是前面说的 zotero 与 clibrowser 那一对:zotero 有本地目录、却填了外部 source_url;clibrowser 的 source_url 是 null、却没有本地目录。而 qgis 恰好是有本地目录(QGIS/)又缺键的那一条。所以「source_url 为 null 等于代码在本仓」是个近似说法,不是可以直接拿来当判据的规则。
什么情况下说明不是这个原因
如果你数出来的三个数不是 82 / 69 / 79,先别急着当成不一致:
registry.json的meta.updated是2026-06-19,我们读的仓库快照是39634a6(提交日期 2026-08-03),注册表本身就可能落后于目录;上游继续合并 PR 之后,两侧都会变。- 如果只有目录数对不上、条目数没变,先看看是不是把
.git、.github这类隐藏目录也数进去了——ls -d */默认不含隐藏目录。 - 如果交集数不是 68 而是 66,多半是大小写口径没统一,
3MF与QGIS掉出去了。 - 如果某个名字在注册表里有、目录里没有,别急着断定它是外部仓条目:先看
source_url,再看install_cmd。clibrowser就是source_url为null而安装串指向外部仓的那种。两个字段都指不到外部,才是一个和本文这 11 条性质不同的新情况,值得单独记一笔。
这三个数本身会随上游变动,值得记住的不是 79、82、68,而是这两个集合从来不是一一对应,以及做差集之前先把大小写口径定下来。
本文依据 CLI-Anything 官方仓库(github.com/HKUDS/CLI-Anything)的 README、registry.json、
docs/ 与各 agent-harness/ 下的源码整理,核对日 2026-08-10,对应仓库快照 39634a6。
本文内容为仓库源码与文档口径,我们没有安装或运行过其中任何一个 harness,
也没有在本机驱动过任何一款宿主软件,因此不涉及实际操控效果的任何描述。
这类 harness 会在本机执行外部程序与脚本,是否使用请结合自身环境评估。
注册表与 harness 内容随上游更新而变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。