关于 cli-build
该技能用于规划和构建面向 Agent、由人类监督的命令行工具,强调机器可读契约、非交互安全、按风险设置防护,以及易读的人类输出。
主要功能
- 01定义或调查命令契约
- 02JSON 输出与 schema 命令
- 03结构化错误且不阻塞提示
- 04按风险为写操作设置防护
- 05采用名词-动词命令结构
- 06区分人类与机器输出
适用场景
- 01设计由 Agent 驱动的命令行工具
- 02封装外部 API 或现有系统
- 03构建执行高影响写入的命令
- 04制定稳定的 JSON 输出契约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ crafter-station/ skills --skill cli-build安装后在 Claude Code 中输入/cli-build确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
cases/conventions.mdosmo.mdportfolio-shape.mdREADME.mdsunat-cli-renta.mdreferences/audit-log-patterns.mdauth-patterns.mdbuild-and-runtime.mdcompounding-surface.mdhuman-output.mdjson-contract.mdtesting.mdtrust-ladder-patterns.md详细介绍
cli-build
构建一个主要面向 Agent、由人工监督的 CLI。这会颠倒通常的默认设置:机器可读输出不是事后添加的标志,在非交互场景中提示是故障模式,而每次写入都会留下回执。
Phase 0:契约从何而来
你编写的每条命令都会读取或修改具有特定结构的内容。本阶段要明确这种结构由谁定义,因为这决定了你是去查找契约,还是编写契约。
已发现。 CLI 封装的是你无法控制的系统:第三方 API、需要登录才能访问的门户、设备或未记录的文件格式。契约确实存在,但尚未验证,因此猜测会比梳理更浪费时间。先运行 surface-recon,并从其报告着手。
已定义。 模型由你负责:对自有数据库执行 CRUD、搭建脚手架、进行本地转换,或封装一个你已经掌握其类型的库。无需逆向工程,因此 surface-recon 没有可用输入,跳过它也没有任何损失。改为编写契约,并在 Phase 1 开始前一次性完成:实体、字段及类型、每个实体允许执行的操作,以及哪些写入不可逆。这个产物就是 recon 报告原本会提供的内容。
混合型。 基于自有数据库构建的 CLI 同时调用支付服务提供商:边界处的部分属于已发现,中间部分属于已定义。只对已发现的部分进行 recon;不要因为一次外部调用,就让整个构建都变成 recon 任务。
在 Phase 1 开始前,明确说出属于哪一种,并将其写入 friction.md。未说明分类时,默认按已发现处理,这会导致一个已定义的构建受阻,等待一份根本没人能提供的报告。
完成条件: 已明确来源,并且已有 recon 报告,或已记录实体和操作。
现在就把 friction.md 放在代码旁边,边做边追加记录。
你拒绝的区块及原因、不适用于此领域的约定、内容单薄的参考资料。这些记录最终会汇总到案例中,也正是它们让这项技能得以改进:从旧版本继承的七条说法与源内容不符,而只有在有人写下它们不匹配后,这些问题才浮现出来。
面向 Agent 的设计究竟意味着什么
每个 CLI,无一例外:
- 提供
--json;当 stdout 不是 TTY 时,即使没有该标志,也自动输出 JSON。 三个 CLI 各自独立地采用了这一做法;这是这里最有价值的默认设置。 - 非交互式运行绝不会被提示阻塞。 任何需要提示用户的操作都会返回结构化错误,而不是一直挂起。
- **提供带版本字段的
schema命令,**让 Agent 在运行时自省,而不是解析--help。在 14 个语料库 CLI 中,只有 3 个提供了该命令:这是价值最高、采用率最低的约定。 - 退出码应有明确含义。 零表示成功;用户错误与系统故障可以区分。
- 数据始终输出到 stdout,诊断信息始终输出到 stderr。 包括横幅,因此
banner是一个代码块,而不是console.log。 - **从终端读取的密钥绝不会回显;**没有 TTY 时则返回
null,而不是一直挂起。这就是prompt-secret。
现在,这些规则已在 surfacer 仓库中通过测试强制执行,而不再只是依赖文字说明;该仓库根据 recon 描述符编译 CLI。每项测试都会指出所检查的规则,因此二者可以对照阅读。
了解这次实践的发现很有价值,因为只要这份清单仅仅写在文档里,很可能到处都会出现同样的问题。将这些规则编码到生成器的测试中,发现了三处违规,其中包括这里最有价值的两项默认设置。随后,对生成器自身的 CLI 应用完全相同的清单,又发现了四处问题;而这个工具刚刚才被教会如何强制执行这些规则。一个能生成面向 Agent 的 CLI、自己却并非面向 Agent 的编译器,是最容易被忽略的情况:所有测试都通过了,但每项测试检查的都是生成结果。
因此,这份清单要检查两次;测试之所以分布在两个文件中,只是因为一个测试无法调用另一个 crate 中的二进制文件:crates/surfacer-emit-cli/tests/agent_first_rules.rs 检查生成结果,crates/surfacer-app/tests/agent_first_rules.rs 检查执行生成的工具。如果你构建的工具会生成 CLI,也要用这份清单检查生成工具本身。文字说明不会让构建失败,而你用来要求别人的规则,往往正是你对自己停止检查的规则。
当领域中的操作会产生实际后果时(金钱、不可逆写入、第三方副作用、个人数据):
- 使用信任阶梯,按风险对命令进行分类。
- 每个变更操作都提供
--dry-run,并返回将会发生什么。 - 在网络调用之前写入只追加的审计日志,而不是之后。
- 提供人工可通过带外方式触发的紧急停止开关。
**当 API 是异步的:**每个提交命令都提供 --wait,并由即使轮询进程被终止也能保留的作业台账提供支持。把轮询循环交给调用方,会迫使 Agent 编写退避逻辑,而它很可能会在不易察觉的地方写错。参见 compounding-surface.md。
让防护成本与损害程度相匹配,而“无需防护”也是一个合理选项。 先问一次错误调用会造成什么代价,再据此选择。针对公共数据集的只读 CLI,只需提供 --json 和 schema 命令即可:重复运行查询不会造成任何损失。意图令牌适用于错误调用会造成经济损失的情形。对于完全无法造成伤害的工具,采用完整的信任阶梯并非谨慎,而是 Agent 每次调用都要支付的成本,而且会让人工不看内容就批准。
要考虑的是损害,而不是是否涉及远程操作。即使没有网络、金钱或第三方,自有本地数据库上的 delete 也不可逆,因此应为破坏性动词提供 --dry-run 和确认机制,而旁边的读取操作则无需防护。可以从你确实拥有的备份中恢复的写入操作,所需防护则较少。按命令评估,而不是按项目评估。参见 trust-ladder-patterns.md。
阶段 1:先决定分发目标
这一步要先做,因为它会限制你编写的代码,事后再改会很麻烦。
| 谁来安装 | 目标 | 方式 |
|---|---|---|
| 只有你自己,从源码安装 | 运行时 shebang,无需构建 | 迭代最快,无需打包 |
| JS 生态的开发者 | 构建为 Node 程序,发布到 npm | 无需额外安装即可使用 npx |
| 任何人,无需运行时 | 编译为原生二进制文件 | 完全没有前置条件 |
让这三种方式都可行的规则:基于 Node 的 API 进行开发(fs、path、process、http),而不是基于特定运行时的 API。这样一来,目标就成了构建时的决定,而不是重写代码。
**完成标准:**明确受众,并据此确定目标,而不是凭偏好决定。
这比看起来更重要:没有什么能阻止你发布一个 shebang 指向安装者并未安装的运行时的包。本技能所依据的语料库中,四个已发布的包有三个正是如此。直到没有安装该运行时的人安装它时,这一切才会暴露出来;他们会看到 env: <runtime>: No such file or directory。这条消息既没有说明原因,也没有提供解决办法,因此包看起来像是坏了,而不是说明不够充分。参见 build-and-runtime.md。
阶段 2:设计命令界面
始终采用名词-动词命名,并通过 CI 强制执行,而不是只写在风格指南里。Agent 对 CLI 的工作方式有一套通用认知;如果其他命令都用 get,某个工具却用 info,Agent 仍然能完成任务,但会花费 token 查看 --help,因此效率低下。语料库中的两个 CLI 都各自把 --json 用作输入选项,这正是缺少检查机制的后果。参见 compounding-surface.md。
{cli} {noun} {verb} [args] [flags]
{cli} order preview --ticker AAPL --amount 100
{cli} order submit --intent-token <token>
为最常见的操作提供一个简写。如果 90% 的使用都集中在一个命令上,那么这个命令就应该是最容易输入的。
**筛选值得作为命令的操作。**并非每种 UI 功能都应该成为命令。保留那些会重复执行、适合脚本化、输入和输出明确且能与其他工具组合的操作。舍弃那些本质上依赖视觉呈现或探索性的操作。
**编写代码前,先为每个命令定义 JSON 契约。**包括字段名称、类型和嵌套结构。这是 Agent 所依赖的 API,之后更改会在悄无声息中破坏它们。请注意,按照惯例,--json 表示输出模式:如果需要 JSON 输入,请使用其他选项名称。语料库中的两个 CLI 把 --json 用作输入选项,违背了 Agent 预期的惯例。参见 json-contract.md。
**完成标准:**每个命令都有名词-动词名称,并且有书面定义的 JSON 输出结构。输出结构尚未确定的命令,就还没有设计完成。
**在结构化输出中加入 nextSteps。**在响应中告诉 Agent 接下来可以运行哪些操作,可以避免一类无所适从的情况;--help 无法解决这一问题。
**分别设计人类可读视图。**机器模式返回所有内容,因为 Agent 会进行筛选;人类模式则返回用户所需的内容。试图用同一种输出满足两者,结果往往两边都不讨好。关于易读性、读者对指标方向的默认理解、量表阈值的放置位置,以及何时应将重复内容改为标题等所有问题,详见 human-output.md。本技能的其他检查关卡都无法发现难以阅读的表格:第一个使用本技能构建的 CLI 通过了所有检查,却在有人询问当晚正在播放什么时,输出了 275 行。
**精致的人类可读输出应是默认范围。**除非用户明确要求 MVP、极简、无头或纯机器 CLI,否则应包含清晰的视觉层次、语义化颜色、易读的表格、耗时操作的进度提示,以及仅在 TTY 中显示的横幅(如果命令面向用户)。命令数量少,不是输出朴素的理由;这只能说明展示形式应保持紧凑。
阶段 3:基于经过验证的模块组装
每个 CLI 都需要相同的基础功能:标志解析、配置路径、原子写入、审计日志、TTY 检测、审批关卡和错误格式。每次都从头编写这些功能既耗时,也是 bug 的来源。
TypeScript CLI 基础设施默认使用 cligentic。 它的注册表提供纯 TypeScript 代码,安装后由项目自行拥有,并且会解析传递的模块和包依赖,不需要 cligentic 运行时依赖。没有 components.json 的项目会在 package.json 中注册命名空间,因此不再需要先具备组件工具。
规范的采用流程见 cligentic 技能。如果已能使用 cligentic,请遵循该流程进行发现、逐模块决策、安装、接线和验证。如果没有,且实现属于任务范围,请在修改文件前提出安装建议:
bunx --bun skills add Railly/cligentic --skill cligentic
安装技能会更改项目或用户环境,因此请等待用户同意。如果用户拒绝、任务为只读,或无法安装技能,请从 GitHub 阅读规范技能,并遵循其采用分支,不要安装。
逐个模块选择启用,并说明理由。 每个模块都应取代你原本需要凭记忆编写的内容,才值得纳入。对于面向用户的 TypeScript CLI,除非用户明确缩小范围,否则 detect、style 和 banner 是默认的展示模块集。在一次实际改造中,7 个模块被完整采用,2 个采用混合方案,7 个被拒绝并说明了理由。其中最重要的一项拒绝,是某个输出辅助工具的格式与已发布给 Agent 的封装格式冲突。已发布的契约优先于共享模块。
完成标准: 每个模块都已采用、混合采用,或被拒绝,并在仓库中记录理由。没有说明拒绝原因,后续接手的人就会以为这是疏忽。
如果你不在 TypeScript 项目中,或安装方式不适用,请阅读模块源码并重新实现相应模式。模式才是关键;文件只是捷径。
阶段 4:安全措施应与损害程度相称
首先说退出条件:如果工具界面中的命令都不会导致数据丢失、产生费用或影响第三方,那么本阶段只需在 friction.md 中用一行说明这一事实,然后继续。只读的 CLI,或只会写入可重写文件的 CLI,无需设置防护门槛。记录这一结论,让后续读者知道这是经过判断的,而不是被跳过了。
否则,请阅读 trust-ladder-patterns.md 并选择一种方案。真实代码中有三种实质上不同的方案,正确选择取决于领域,而非时尚。
领域存在后果时,以下要求不可妥协:
先审计,再执行。 写入待处理记录,执行调用,再用相同的 id 写入最终记录。如果进程中途退出,你留下的是可审计的待处理条目,而不是一片空白。按日分桶的 JSONL、仅追加、严格的文件权限。
Dry-run 必须走真实路径。 返回硬编码结构的 --dry-run 毫无证明力。最可靠的语料库实现会调用提供方自己的预览端点并返回结果。
同意门槛必须比信任门槛更严格。 法律条款的接受操作应要求真实的 TTY,并且完全不能提供 --yes 绕过选项。如果 Agent 能代替人类接受条款,这道门槛就只是摆设。
执行操作前,先验证返回内容。 某个语料库 CLI 会拒绝提交,除非提供方的预览界面中明确包含预期的名称、金额和币种。这项检查既能发现提供方的错误,也能发现你构造请求时的错误。
完成条件: 每个会修改数据的命令都有一个信任级别,且阶梯方案是根据领域选择的,而非照搬;或者 friction.md 记录了工具界面中的命令均不会产生后果。
将第三方响应文本视为不可信输入。 外部 API 的自由文本可能包含针对读取输出的 Agent 的提示注入。应先对其进行转义,再让它进入任何可能被理解为指令的地方。
阶段 5:接通你提到的每一项安全功能
当一项功能存在调用点时,它才算接通。在此之前,它只是一项定义;而 --help 对它的描述,只是代码无法兑现的承诺。
审计日志模块已导入,却从未调用。--dry-run 被解析到标志对象中,但没有任何命令主体读取它。--help 却把两者都列了出来。这种情况如今就存在于可安装的软件包中,而且严格来说比两项功能都没有更糟,因为操作人员(无论是人类还是 Agent)会以为存在审计轨迹和预览,实际上却什么都没有。
检查方法:用 grep 搜索调用点。若只有定义处命中,就说明功能尚未接通。
完成条件: --help 或文档中提到的每一项安全功能,都有经 grep 确认的调用点。
同一语料库中还有两种情况,都存在于已发布的软件包中:
- 发布时没有任何测试。 最低限度应覆盖:身份验证流程、每个命令的 JSON 输出契约,以及任何签名或加密代码。
- 重命名产品留下的死代码仍可从入口点触达。 弃用意味着移除,而不只是停止提及。
阶段 6:验证,然后记录下来
先全局链接。 在验证任何内容之前,先将 CLI 加入 PATH,这样你调用它的方式才与用户一致:
bun link # or npm link
which mycli && mycli --help
运行 bun run src/cli.ts 验证的是一个文件。运行 mycli 验证的才是实际交付的内容:bin 入口、shebang、解析后的依赖项和横幅。这些内容中有三项无法通过调用源文件来检查,而错误的 bin 路径会在用户首次安装时暴露出来。
完成的定义是观察到的行为。 运行命令、阅读输出,再读一遍:第一遍检查正确性,第二遍站在不了解该领域的人角度阅读。最大的数字代表什么,第一眼会注意到什么,是否有一列的每一行都显示相同内容。参见 human-output.md。
对于面向用户的 CLI,在宣布完成之前,请检查真实的 TTY 路径并确认以下各项:
- 横幅和诊断信息输出到 stderr,而机器数据输出到 stdout;
--json、管道输出和其他机器模式中不含 ANSI 转义序列;NO_COLOR会禁用样式,但不会改变内容;- 彩色列按可见宽度而非字符串长度对齐;
- 长时间运行的操作会根据已完成的工作报告真实进度,而不是播放计时器动画。
验证新功能时,也要探查与它共享输出流的旧功能。 确认新横幅没有污染 stdout 时,发现那里有 810 个字节的内容,但没有一个字节来自横幅:从上一轮开始,直接调用命令就一直会将帮助文本写入 stdout,并以非零状态退出。没人注意到这一点,因为直接调用并不是人们会测试的场景。
测试套件全绿所能证明的,比看上去少。它会遗漏什么,以及如何弥补每个缺口,请参见 testing.md。
将 Agent 手册与 CLI 一同交付。 在 CLI 自己的代码库中提供 skills/<name>/SKILL.md:命令、JSON 信封、重要标志以及值得组合使用的工作流。如果没有它,每个 Agent 都得通过 --help 重新摸清功能,这正是创建此技能要消除的成本。它与代码放在一起,因此不会过时,也让 CLI 可以通过 npx skills add <owner>/<repo> 安装。
然后在 cases/ 中记录构建过程:目标、环境、分发方式、采纳或拒绝了哪些模块及原因、出了什么问题,以及你会如何改进。将 friction.md 的内容整合进来。将重复出现的发现提炼到 conventions.md,并阅读 portfolio-shape.md,了解整体情况。
谈自己的代码,不谈别人的代码。 对于自己编写的 CLI,可以具体描述出了什么问题。对于别人的软件包,只保留缺陷描述,不提对象。完整的界定规则见 cases/README.md。
完成条件: CLI 已全局链接,并通过自身名称运行;已阅读其真实输出;skills/<name>/SKILL.md 存在且包含有效的 frontmatter;case 文件存在,且已整合 friction.md 的内容。
在远程仓库存在之前,无法检查 npx skills add <owner>/<repo>;而对于尚未发布的仓库,它的失败提示与格式错误的 SKILL.md 无法区分,除非你阅读错误消息。有效的 frontmatter 可以在本地检查;是否可安装,则要推送之后才能确认。
这是所有人都会跳过的部分,而跳过它,才会让相同的经验一再被重新发现。在有人把这些经验记录下来之前,四个 CLI 各自独立地重新设计了相同的审计日志。
参考资料
- cligentic skill:实时发现、采用、接线、验证和编写 block
- trust-ladder-patterns.md:三种真实模式,按领域选择
- json-contract.md:输出模式、TTY 检测,以及实际实现的 NDJSON
- audit-log-patterns.md:两阶段写入,以及死代码陷阱
- auth-patterns.md:观察到的四种架构、轮换和密钥处理
- build-and-runtime.md:详细介绍发行矩阵
- testing.md:绿色测试套件无法证明什么
- human-output.md:面向读者,而非监督者。指标方向、校准后的量表,以及人类用户的默认设置应该是什么
- cases/README.md:案例的撰写方式,以及案例中可以点名哪些人
- compounding-surface.md:异步
--wait、在 CI 中强制执行的词汇、--deliver和feedback。内容取材于已发表的作品,而非语料库,并已明确标注
边界
上面的阶段说明了该做什么。以下两条原则适用于不明显适用某个阶段的情况:
标识符存储起来,密钥从环境中获取。 密码应保存在会话内存中,或存入密钥链。持久化配置保存账户 ID 和用户名。
只声明已实现的功能。 实现了 NDJSON、按天分桶的审计日志和 dry-run 后,才能提及这些功能,不能提前声明。