`public_registry.json` 与 `registry.json` 差在哪

2026-08-10

CLI-Anything 的仓库根上并排躺着两份 JSON,名字只差一个前缀:registry.jsonpublic_registry.json。第一次翻这个仓库的人很容易把后者当成前者的公开子集或者精选版,然后按同一套字段去解析它——这个假设一开始就不成立。我们采集时(2026-08-10,对应仓库快照 39634a6)用 Python 的 json.load 把两份文件都读了一遍,下面的数都是这么数出来的。

先把两份文件的骨架摆平

两份文件的顶层键是一样的:metaclis。分歧从 meta.description 就开始了。

registry.jsonmeta.description 原文是 “CLI-Hub — Agent-native stateful CLI interfaces for softwares, codebases, and Web Services”;public_registry.json 的原文是 “Public CLI Registry — Third-party and official CLIs managed by CLI-Hub across npm, bundled, brew, and other install methods”(两处均在各自文件的第 3 到 5 行)。前者描述的是「这个项目自己造的那层 agent 化接口」,后者描述的是「CLI-Hub 顺带纳管的、本来就存在于 npm / brew 这些生态里的命令行」。

条目数:registry.jsonclis 数组是 79 条,public_registry.json22 条,后者比前者少 57 条,两份合起来 101 条。

有意思的是 meta.updated:两份文件都写的是 2026-06-19,同一天。

差异一:条目从哪来,直接写在 source_url

这是两份表最本质的分工,而且有一个字段可以直接验证。

registry.json 的 79 条里,source_urlnull 的有 68 条——也就是这 68 条的代码就在本仓库里,对应的是各软件目录下的 agent-harness/。第 1 条 jumpserver 的 install_cmd 长这样(registry.json:16):

pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=jumpserver/agent-harness

这里只声明这一条。其余条目的 install_cmd 我们没有逐条核对,别把这一行当成全表通式去写解析代码。

剩下 11 条 source_url 非空,指向各自的独立仓库。这 11 条也正好是根目录下找不到同名目录的那 11 个——关于 79 条条目与 82 个顶层目录之间那道差,我们另有一篇专门讲,这里不展开。

public_registry.json 这边,22 条全部存在 source_url 这个键registry.json 里则有 1 条连键都没有)。注意这句话的措辞:我们核对的是「键在不在」,键的值是否为空我们没有逐条核实。

差异二:字段并集,一个 15 个一个 25 个

registry.json 的 79 条条目,字段并集是 15 个。public_registry.json 的 22 条,字段并集是 25 个。两边共有 13 个键,registry.json 独有的两个是 contributorcontributor_url——那是 qgis 那一条特有的写法(其余 78 条用的是数组形式的 contributors)。

public 独有的 12 个键,摆在一起看方向非常清楚:

字段出现条数(分母 22)
package_manager20
npm_package10
npx_cmd10
uninstall_cmd5
update_cmd5
docs_url2
install_notes2
uninstall_notes2
update_notes2
detect_cmd2
platform2
engines1

十二个字段没有一个是在描述「这个 CLI 能干什么」,全部在描述「它怎么装、怎么卸、怎么更新、怎么探测、能在哪个平台跑」。

原因在分发层那一侧有直接依据。先交代一句前提:cli-hub 的打包分类器把自身标为 Development Status :: 4 - Betacli-hub/setup.py:88),下面凡是提到 cli-hub 行为的地方,都以我们采集时的源码为准。cli-hub 的默认推断规则里,_source == "harness" 的条目一律走 pip 策略(cli-hub/cli_hub/installer.py:106-119),而 harness 的安装动作实际执行的是 sys.executable -m pip install …installer.py:184-192)。也就是说,registry.json 这一侧的安装方式由条目的来源直接决定,不需要在条目里用 package_manager 这类字段去区分——全表也只有 siyuan 一条带了 install_strategy,值是 "pip"。而 public 这 22 条来自完全不同的六种生态,我们统计的 package_manager 取值分布是:npm 10 条、pip 4 条、brew 2 条、bundled 2 条、uv 1 条、script 1 条,另有 2 条根本没有这个字段。安装方式不统一,就只能靠字段一条条写清楚。

还有一个字段是两边都有、但分量完全不同的:install_strategyregistry.json 79 条里只有 siyuan 一条带它,public_registry.json 22 条里有 7 条带它。同一个键在一张表里是例外、在另一张表里是常规写法,这也是不能拿同一套解析假设去套两份文件的原因之一。

至于 cli-hub 拿到这两份表之后怎么合并、怎么按这些字段推断安装策略、会往你机器上写哪些目录,那是 cli-hub 分发层的话题,我们另有专门篇目讲。这里只交代一句与本篇直接相关的:这两份表在 cli-hub 里不是二选一,而是合并使用的,list 子命令的 --source 选项给出的可选值里就同时有 harnesspublic

这 22 条都是谁

这张表条目不多,索性全列出来。这是本篇最值得留档的一份原始数据,读者拿它对着 public_registry.json 逐条比对最省事(按文件里的原始顺序,package_manager 一列照抄字段值):

namedisplay_namecategorypackage_manager
feishuFeishu/Lark CLIcommunicationnpm
minimax-cliMiniMax CLIainpm
wecomWeCom CLIcommunicationnpm
contentfulContentful CLIwebnpm
sanitySanity CLIwebnpm
shopifyShopify CLIwebnpm
sentrySentry CLIdevopsnpm
1password-cli1Password CLIdevopsbrew
android-cliAndroid CLImobilebundled
generate-veo-videoGenerate Veo Videoaiuv
sunoSuno CLImusicpip
elevenlabsElevenLabs CLIaudionpm
jimengJimeng / Dreamina CLIaiscript
obsidian-cliObsidian CLIknowledgebundled
py4csrTraceCSR / Py4CSR CLIdata-sciencepip
deployhqDeployHQ CLIdevopsbrew
obsidian-agent-cliObsidian CLIproductivity无该字段
arcgis-proArcGIS Proscientificpip
cloakbrowserCloakBrowser CLIwebnpm
smithue-cliSmithUE CLIdevtoolsnpm
browser-cdpBrowser CDPwebpip
piecesPieces OS CLIproductivity无该字段

看这张表的方式是:先看第四列。同一份注册表里同时并排着 npm 包、brew formula、pip 包、uv 工具、一段脚本,以及两条根本不走包管理器的 bundled。这就是它和 registry.json 那 79 条最直观的区别:package_manager 是 public 这份表独有的字段,那 79 条根本没有这一列可画——它们的安装方式不写在条目里,而是由 cli-hub 按 _source 直接推断出来的。

反直觉的那一处:22 条里,字段是稀疏的

把上面那张频次表再读一遍,注意分母是 22。

namedisplay_nameversiondescriptioncategoryrequireshomepagesource_urlskill_mdentry_pointcontributors 这 11 个键,22 条全有。但从 package_manager 开始就掉下来了:20 条有 package_manager,20 条有 install_cmduninstall_cmd 的只有 5 条,有 update_cmd 的也只有 5 条,有 detect_cmd 的只有 2 条,engines 只有 1 条。

也就是说,这张表里超过四分之三的条目没有写卸载命令,也没有写更新命令。

这一点值得单独强调,因为它正是「注册表里有一条 ≠ 你装上就能用」这句话最具体的形态。22 条不是 22 个开箱即用的能力,它们各自的完整度差得很远:

  • 有 2 条(obsidian-agent-clipieces)连 package_manager 都没有;
  • 有 2 条的 package_managerbundledandroid-cliobsidian-cli),而 cli-hub 对 bundled 这一策略的处理方式是不真的执行安装,只检测对应命令在不在 PATH 里,不在就提示你去上游 App 里自行启用;
  • 有 1 条的 package_managerscriptjimeng)。

另外要如实说清楚的一点:这一层比 registry.json 更贴近「在你本机执行第三方软件」。表里 10 条走 npm、2 条走 brew、4 条走 pip,装的是各自上游发布的包;而包装真实桌面软件的那些条目,README 也明确提醒需要用户自行安装上游应用(README.md:236)。这两件事叠起来,意味着按这张表装东西等于同意在本机拉取并运行外部程序,是否使用请自行评估。

两张表之间还有名字上的交叠

这是解析这两份文件时另一处容易翻车的地方:同一个软件在两张表里可能各有条目,而且名字只差一点

registry.json 里有 minimax(category ai,v1.1.0),public_registry.json 里有 minimax-cli(category ai,npm)。registry.json 里有 obsidian(category knowledge,v1.1.0),而 public_registry.json 里有两条:obsidian-cli(category knowledge,package_manager 为 bundled)和 obsidian-agent-cli(category productivity,无 package_manager 字段)。

后面这两条更值得看一眼:它们的 name 不同、category 不同,但 display_name 都是 “Obsidian CLI”。你如果按 display_name 去做键或者去人工辨认,这两条是分不开的。这是两份表里可以各自核对的事实,我们只陈述到这里。

想往哪张表里提条目,先看 CONTRIBUTING 写的是什么

CONTRIBUTING.md 第 59 到 72 行给注册表条目定了 11 个「Required」字段,其中 contributors 要求是形如 {"name","url"} 的对象数组,skill_md 则要求仓库内的 harness 写成 repo 根 skills/ 树下的相对路径。

这段要求是围绕仓库内 harness 的形态写的。public 那 22 条的 skill_md 键虽然 22 条全有,但它们的取值是否也落在同一棵 skills/ 树下,我们没有核实。这里只把文档写了什么摆出来:你要提一条新记录,先弄清楚自己提的是「仓库内新造的 harness」还是「外部生态里已有的 CLI」,这决定了你该往哪份文件里加,以及那 12 个安装相关字段要不要填。

顺带一个能自洽的交叉验证

registry.json 那 79 条的 category 取值共 31 种。public_registry.json 这 22 条的 category 取值共 12 种,其中有 4 种是 registry.json 那 31 种里没有的:mobiledata-scienceproductivitydevtools。31 + 4 = 35,而两份表合并去重后的 category 值确实是 35 个。

这条不是什么大发现,但它是个好用的自检:你解析完两份文件,如果合并去重的分类数不是 35,说明你有条目漏读了。

你可以自己跑一遍的核对动作

上面每个数都可以在 clone 下来的仓库根目录用几行 Python 复现。注意用 encoding='utf-8' 显式打开:两份文件的 meta.description 里各有一处非 ASCII 字符,我们采集时它在本机 shell 下输出为乱码,原字符未确认。

import json, io, collections

r = json.load(io.open('registry.json', encoding='utf-8'))
p = json.load(io.open('public_registry.json', encoding='utf-8'))

print(len(r['clis']), len(p['clis']))            # 条目数
print(r['meta']['updated'], p['meta']['updated'])  # 两份的 updated

c = collections.Counter()
for e in p['clis']:
    c.update(e.keys())
print(len(c), c.most_common())                    # 字段并集与频次

print(collections.Counter(e.get('package_manager') for e in p['clis']))

rc = {e['category'] for e in r['clis']}
pc = {e['category'] for e in p['clis']}
print(len(rc), len(pc), sorted(pc - rc), len(rc | pc))

我们采集时用 json.load 解析后数出来的就是这些值:79 / 22、两个 2026-06-19、25 个字段、package_manager 的六种取值加 2 条缺省,以及 31 / 12 / 四个新分类 / 35。你按上面这几行复现,应当得到同样的结果。

记住哪几件事

如果只带走三条:

第一,两份表不是包含关系,而是按来源与安装方式分的家。registry.json 装的是本仓库自己造的 harness,public_registry.json 纳管的是外部生态里已经存在的 CLI。

第二,字段差异全落在「怎么装、怎么卸、怎么更新」这一簇上,而且是稀疏的——22 条里只有 5 条写了卸载命令。你要写工具去消费这两份 JSON,.get()[] 安全得多。

第三,条目数不是能力数。这句话在 registry.json 那 79 条上成立,在 public 这 22 条上更成立:有 2 条按定义就不执行安装,只做 PATH 探测。

数字会随上游更新变动,重要的不是记住 79 和 22,而是记住这两份文件各自在回答什么问题,以及解析时哪些键可能不存在。


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

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