agentsclimarketplace

Agent cli design

Skill Blushyes/skills/skills/agent-cli-design

Pan's personal agent skills, skills.sh-compatible

Install
npx -y skills add Blushyes/skills --skill agent-cli-design

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

设计与编写命令行工具(CLI),尤其是给 LLM agent 驱动的 CLI —— 选对命令模型、输出形态、 护栏、文件架构。当用户在设计或编写 CLI、新增或重构 subcommand/verb、选 CLI 框架或库、 设计 flags / 寻址 / 批量操作、构建一个会被 agent 调用的工具,或重构一个长烂了的 CLI (上帝文件、纠缠的命令 handler、复制粘贴的 op 构造)时使用。触发词:"写/设计一个 CLI"、 "加个命令"、"CLI 架构"、"agent 友好的工具"、"命令行工具"、"做个我的 agent 能用的工具"、 "梳理我的 CLI 结构" 等 —— 即使用户没说"CLI"这个词、但明显在搭一个终端/命令工具,也应触发。 目前 TypeScript(commander)有完整参考,Go / Rust / Python 参考待补。

SKILL.md

18.5 KB, as published. Nobody here has run it

agent-cli-design

核心论点

现代 CLI 的首要使用者是 LLM agent,不是人。 人运行一条命令、用眼睛读屏幕、用一个不按 token 计费的脑子决定下一步。而 agent 运行你的命令后,输出直接回灌进一个有限且昂贵的 context window,并自动驱动下一步决策。仅这一点就重排了所有设计优先级:

一次就对(one-shot correctness)、token 经济、批量友好、失败可恢复、可审计 排序 —— 漂亮的 help 文本和聪明的交互式提问几乎一文不值;干净的结构化结果、以及一条「告诉 agent 接下来该干什么」的错误,才是全部。

人当然也读这工具(而且好的 agent-facing 设计往往也是好的 human-facing 设计),但 agent 是热路径。为它而设计。

本 skill 分两半:

  • 原则(本文件) —— 语言无关。怎么思考 命令模型、输出、护栏、可组合性、架构。
  • 语言参考 —— 具体技术栈 + 可直接复制的构件。按用户的栈读对应那份: references/typescript.md(commander,最深),以及 references/{go,rust,python}.md(待填的骨架)。

真要动手时:先读下面的原则,再打开语言参考、把 harness 抄过来,别重造。


第 1 部分 —— 输出即产品

agent 永远看不到你的代码;它看到的是你的输出和错误。把这两样做对,其余水到渠成。

1. 地址(address)是通用货币。 你的工具寻址什么 —— 文件、行、区间、实体 id —— 那个 token 就是从「查询」流向下一次「变更」的东西。所以查询必须 吐出 地址、动词必须 地址。 最大的罪是把地址埋进人类可读的一行里(第1集 第3场 第5句:"..."),逼 agent 用脆弱的 awk 把它切回来。给地址一个独立字段。

2. 输出分档,按需付重量。

形态用于
默认按数据形状输出简洁文本:同构二维数据用带表头 TSV;单对象用紧凑 key=value;正文原样输出agent 自己的 read→decide 循环 + 人扫一眼;便宜
--json结构化信封 + rows[] 数组(addr/type/… 各为字段)程序(jq / 脚本 / MCP host 的结构化 tool)做确定性解析 —— 不是 agent 喂自己的下一步
--format addr(或 -0裸的、换行/NUL 分隔的地址流喂 xargs 或批量构造器 —— 只要货币,不让正文再进 context

默认走简洁文本档 —— 而且这正是 agent 自己 read→decide 循环该读的档。这点最反直觉,单独讲清: agent 的「消费者」不是 JSON.parse 的程序,而是把你的输出读进 context window 的 LLM。对 LLM:

  • JSON 更费 token —— 每行重复键名 + 括号引号;纯文本把键名变隐式,同样信息常省 20–50%。
  • JSON 更费「脑力」 —— 已有基准显示,把同样信息从纯文本换成 JSON 编码会让模型推理 显著退化 (观察到 ~10–15%);结构化包装对 LLM 是噪声,不是帮助。
  • LLM 本就母语级读自然文本 —— 何必逼它先解析一层再理解。

所以:agent 的「发现 → 决策 → 下一条命令」全程读简洁文本档;--json 只留给确定性 程序 (jq 管道、脚本、MCP host 的结构化 tool)。 把「机器 = 吃 JSON」当默认,是把「程序消费者」的 前提错套到「LLM 消费者」身上 —— 这是 agent-facing CLI 最常见、最隐蔽的设计错误之一(连"机器就该 输出 JSON"的肌肉记忆都会把你带过去)。

同构二维数据默认用带表头 TSV。 当一个命令返回多条同 schema 记录(典型是 list / search / doctor / 状态矩阵),TSV 比逐行 key=value 更省 token:字段名只在表头出现 一次;也比终端对齐表格更稳定,因为列边界是 tab,不依赖空格宽度。比如:

addr	provider	kind	model	maturity
qiling:image:nano-banana-2	qiling	image	nano-banana-2	documented
chatfire:image:gpt-image-1.5	chatfire	image	gpt-image-1.5	documented

实现 TSV 时遵守这些规则:

  • 表头是 schema。 使用稳定的 snake_case 列名和固定列序;即使结果为零行也输出表头。把 addr / id 放在独立列里,通常放第一列。
  • stdout 只装 TSV。 不混入标题、计数、空行、next:、进度或诊断;把这些写到 stderr,或放进 --json 的 metadata。这样 cut / awk / CSV reader 可以直接消费 stdout。
  • 颜色只属于交互式表头。 仅在 stdout 是 TTY 且未设置 NO_COLOR 时给表头加 ANSI;管道、 重定向和非 TTY 输出必须是无 ANSI 的纯 TSV。
  • 用合规的 CSV/TSV writer 转义。 字段含 tab、换行或双引号时按 RFC 4180 风格加引号和转义, 不用裸 join("\t")。短小、值域受控的标量列表可在单元格内用逗号连接;嵌套对象、异构记录和长 多行正文改用 --json 或更窄的 exact-read 命令。
  • 同一份 rows 驱动 TSV 与 JSON。 为表格声明列序,由 renderer 从结构化 rows[] 取值;不要在 handler 里维护一套 TSV 字符串、另一套 JSON row,避免字段与默认值漂移。

简洁文本仍按数据形状选择。 单个对象或一次 mutation 的结果用一行紧凑 key=value;自然语言 正文按原样输出;同构二维记录用 TSV。三者都要字段稳定、地址可直接抬起、正文按需付费。

3. --help 是导航,不是手册。 顶层 help 只负责让 agent 快速选对下一条命令:usage 形状、 主要分组、命令名列表、全局输出/目标 flag,以及「继续看哪个子命令 help」的指针。不要在顶层内联 某个 workflow 的全部子命令、长选项表、细节说明和例子;那会把每次探索都变成一次 context dump。 分层写:顶层列地图,group help 一行一个子命令,leaf help 才放 required flags、局部语义、副作用、 退出码和少量例子。help 一旦像手册,就把它拆到更深一层。

4.「一条命令覆盖一片」省的是 context,不是调用次数。 这是批量操作对 agent 重要的最深 理由。N 条单点命令的循环会产生 N 份结果回显 堆进 context window —— N 份几乎一样的输出。 一条带 selector 的命令把它压成一份命中摘要。你省的不是 5 秒,是 50 份 context。

5. 错误即下一条命令。 出错时,错误体里应当装着 修法,而不只是诊断:

  • 命中歧义 → 回候选地址 + 消歧的 flag
  • 无命中 → 回当前有效的区间/范围
  • 缺/多参数 → 回那条改好的调用
  • 前置条件未满足 → 回该先跑什么

agent 读完错误一回合就自纠,而不是三次试探性调用。把 nextSteps 焊进你的错误类型 (见参考里的 CliError)。

6. 护栏住在工具里,不住在文档里。 agent 会 自信地宣称成功 ——「我已经把所有标记清掉 了!」—— 不管它有没有发生。你只写在 help 字符串或注释里的东西,都会被跳过。在代码里强制: 宽写之前回显 blast radius、默认拒绝歧义命中、让 --dry-run 与真跑共用完全相同的匹配逻辑、 用 语义化退出码(见下)让脚本能按结果分支。


第 2 部分 —— 命令模型:selector × mutation

多数 CLI 攒了一堆零散动词。有威力、好学的形态是一个 笛卡尔积:一套「选哪些」的语言 (selector),正交地喂给每个「做什么」的动词(mutation)。N 动词 × M selector 变成 N + M 要学的东西,而不是 N × M。

选择轴(选哪些),按威力粗排:

  • 单点 —— 一个地址
  • 区间 —— a..b(用 ..;它在 Rust/Ruby/Kotlin 里就是闭区间)
  • 多区间 —— 逗号连接 a..b,c..d(用 ,;它读起来就是列表)
  • 谓词 —— 按属性筛(--type X --owner Y,或 --where k=v
  • 模式 —— 对内容做 regex / glob
  • 全部 —— 显式 的「一切」(--all--in '*')。绝不让空 selector 静默等于「全部」 —— 那正是 agent 漏个 flag 就把整份文档清空的方式。让全本范围是个刻意的 token。

变更轴(做什么):正文编辑 / 属性设置 / 状态与关系 / 实体(改名/合并/删除) / 结构 (插入/移动/拆分)。

让它成立的两条规则:

  • 同一套选择语言既读又写。 agent 用 list --in a..b --type X 发现,用 set-type --in a..b --type X <value> 变更 —— 完全相同 的选择 flag。选择语法学一次、两边用。 这是整个模型里杠杆最高的一步。
  • 结构性动词留单点。 插/删/移会让后面所有元素的下标漂移,所以预先展开的 selector 会在 sweep 中途指错。要诚实:在结构性动词上拒绝 selector,改走事务 surface(第 3 部分)。别假装 一个会损坏数据的能力。

第 3 部分 —— 可组合性:三个 surface,一个引擎

agent 爱管道。管道适合 发现变换,对 写入范畴错误

为什么管道串写是错的: 一份有引用完整性的文档是 共享可变状态edit A | edit B 是两个 进程各自独立 read→mutate→write 文件;管道里流的是报告、不是文档;并发写还会 last-write-wins 互相覆盖。写不是流水线 stage,是对同一份状态的顺序覆写。

正确的形态是 discover → plan → apply,按 谁来写 分成三个 authoring surface,全部编译 到同一个事务 op 引擎:

Surface作者何时
selector-verbset --in a..b …agent,交互一片同构改动(80% 场景)
verb-scriptdo - / do edits.txtagent,手写一组各不相同的异构改动
machine JSONpatch -程序 / jq管道里的结构化变换

verb-script 的洞察(这点最多人漏): 批量应当 就是 agent 平时敲的那套动词,一行一条命令 —— 而不是另一套要 agent 翻译过去的 JSON op schema。把 replace ep#3 --from x --to "" 逼成 {"op":"content.replace","at":"ep#3","from":"x","to":""} 既是翻译税、又是 分叉陷阱(两套 编码会漂移;见坑 #4)。让 agent 写它会敲的行;用同一个 tokenizer 解析;编译到同一批 op。 JSON 留给 jq 管道的 机器 接口、从 stdin(-)读,而不是给人/agent 手写。

事务 = 持久化边界。 把所有 op 应用到一份内存副本;任一失败就 不保存 —— 仅这一点就给了 全成或全不成的原子性,不需要回滚引擎。末尾再对内存里的 将成之态 重新校验(见坑 #3)。


第 4 部分 —— 护栏默认值(面对一个会宣称成功的 actor)

  • 多点写默认 dry-run。 一旦写操作选中超过一个东西(区间 / 谓词 / --all),就默认 dry-run、 回显 blast radius(数量 + 样例地址 + would-skip),要 --apply 才落盘。一条好记的规则: 「选中不止一个 → 必须确认」。单个显式地址直接写(blast radius 低)。
  • replace 默认字面量、regex opt-in、多命中拒绝。 字面量 --from 在一个项里命中两次就是 歧义 —— 默认拒绝、要么 --all、要么写更长更唯一的串。对应 sed -F / 不带 -i
  • 语义化退出码 让 agent 能 && / 分支: 0 = 有改动/成功 · 1 = 无命中、什么都没做(grep 风格) · 2 = 写了但校验需修复 · >2 = 致命/用法错。agent 就能链 discover && apply 并信任退出码。
  • 幂等。 重跑一次清理("strip the [xxx] prefixes")必须无害、第二次报 "0 changed"。agent 会重试,为此而设计。
  • 一套选择语法,校验一次。 区间端点同 kind、禁止跨层裸数字(3..7 有歧义 —— 强制 ep_3..ep_7)、reversed 区间自动 swap(id 单调,无歧义、比报错更对 agent 友好)。

第 5 部分 —— 架构(让它扛得住生长)

CLI 烂成上帝文件时,agent 维护者都付代价。下面的结构让 5 个命令的玩具和 100 个命令的怪兽 一样好导航。

  • 单向分层: bin(入口 shim)→ cli(只解析/路由)→ usecases(编排)→ domain (纯、无框架的核心)→ infra(IO 适配)。依赖只指向 ;用环检测(如 madge --circular) 在 CI 守住。永远无环。
  • 薄命令壳。 一个命令 handler 解析 flag、校验、委派、格式化结果。业务逻辑住在 usecases/domain,绝不内联进 handler。(gh、kubectl、Cobra 官方指南都这么做。)
  • 一动词一文件。 commands/<verb>.ts,共享 helper 进 lib/。这是主流(oclif、Cobra)且有 道理:每个命令都好找、互相隔离。
  • deep-import,别用 barrel。 内部 结构就 import 具体模块(commands/foo.js),别用 re-export 的 index 门面 —— barrel 招循环依赖、糊 go-to-definition。(gh/kubectl 直接 import 子包。)barrel 只在你刻意要一个稳定的 对外 API 面时才值。
  • 用 Command/registry 取代大 if/else 派发器。 一个 1000 行的 if (op==='a'){…} else if (op==='b'){…} 是经典上帝函数。改成 dispatch mapMap<opKind, handler>、一个 op(或一组)一文件、一个查表即调的小 driver。(这个重构有名字: Replace Conditional Dispatcher with Command。)参考里有具体写法。
  • op 构造单一真相源。 如果一个 CLI handler 和一个 script translator 都在造「同一个 op」, 它们 一定 会漂移 —— 一边丢了互斥校验、一边字段默认值不同。抽一个共享 buildXTemplate() 两边都调。(见坑 #4 —— 这是这里最贵的一课。)

第 6 部分 —— 选框架

为该语言挑 最主流、最低开销 的库,除非你真需要更重框架的特性,否则别上。

对 agent-facing CLI,startup latency 是真实成本。 agent 每次调用 都 spawn 一次二进制, 常常几十次。一个在 15–25 ms 基线上多加 50–120 ms 冷启动(插件发现、manifest 加载)的框架, 是在 每一次 调用上交这笔税。对交互式人类工具它隐形;对 agent 循环它是可测量的拖累。挑精瘦的。

  • TypeScriptcommander(事实标准,每周数亿下载)。除非你需要插件生态,否则别上 oclif —— 它约 5–7× 的启动开销。想要框架的结构又不要重量,citty(UnJS)是精瘦的现代选择。 完整细节 + harness:references/typescript.md
  • Gocobra(kubectl/gh/docker 标准)。references/go.md
  • Rustclap(derive API)。references/rust.md
  • Pythontyper(或 click)。references/python.md

上面的 原则 四种语言完全一致;只有 harness 不同。


一个完整示例:「跨整份文档清掉一个标记」

一个真实、反复出现的 agent 任务:「我很多文件开头有 [draft] 之类的标记,全帮我去掉。」它把 整个 skill 浓缩成一例。

坏路径(朴素 CLI 逼你走的):grep 找出来,然后对每个命中循环跑单点字面量 replace,每条的 括号内容都不同、要 agent 读回来原样传 → N 条命令、N 份 context dump、没有预演。

好路径(本 skill 搭出来的):

tool replace --in '*' --regex --from '^\s*\[[^\]]*\]\s*' --to ''            # dry-run:"matched 312, would change 88"
tool replace --in '*' --regex --from '^\s*\[[^\]]*\]\s*' --to '' --apply    # 落地

一条命令。全本范围显式(--in '*')。regex 是 opt-in。默认 dry-run 带 blast radius、只在 --apply 时落盘、幂等(重跑 → "0 changed")、吐一个干净的计数 —— 不是 88 份回显。 才是 agent-facing CLI。


坑(每一条都付过一个真 bug)

  1. 引号陷阱。 verb-script 的 shell 式 tokenizer 若 剥掉 引号,就再也分不清引号值 "--x" 和真 flag --x。要逐 token 记录「是否有字符来自引号/转义」,并把引号 token 当作永不是 flag。 否则像 --content "-- Act I --" 这样的内容会被当成 flag 而被拒。
  2. 布尔 --flag=false 陷阱。 --force=false 必须变成 false,而不是真值字符串 "false"。 把 = 形式的布尔 flag 解析成布尔。
  3. dry-run 校验错了状态。 dry-run 改的是 内存 副本、不保存。若你的校验回去读 磁盘 文件, 它校验的是改之前的状态、报一个误导的 "would pass"。要校验内存里的将成之态。
  4. 一个 op 两套真相源。 CLI verb handler 和 verb-script translator 都造「同一个 op」一定会 分叉 —— 丢互斥校验、字段默认不同(如单 speaker 的 delivery 默认值不一致)。抽一个共享 template builder 两边都调。
  5. 全局 flag 预扫吞掉了某 flag 的值。 若你在命令运行前扫 整个 argv 来预解析 --format / --json,那么 --from --format 会让扫描偷走 --format。只在下一个 token 不是 flag 时才把它 当值,并支持 = 形式。
  6. 假装结构性批量。 让 selector 驱动 insert/delete/move 会随下标漂移而静默指错。拒绝它,走 事务 surface。

如何把它装成可用 skill

本项目在 ~/Projects/skills/skills/agent-cli-design/。要让它成为活跃 skill,symlink 或拷进你的 skills 目录: ln -s ~/Projects/skills/skills/agent-cli-design ~/.claude/skills/agent-cli-design (或用 skill-creator 的 package_skill.py 打包)。

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.