mcpField
C
cli-design

cli-design

github.com/joelhooks开发65149 次安装5 天前更新
代码仓库

关于 cli-design

面向 Agent 的命令行设计规范,涵盖 JSON 响应、命令发现、上下文相关的后续操作,以及持续任务的 NDJSON 流。适用于构建便于 Agent 调用和处理的 CLI。

主要功能

  1. 01仅使用 JSON 响应封装
  2. 02上下文相关的 HATEOAS 后续操作
  3. 03自描述命令树
  4. 04限制输出以保护上下文
  5. 05类型化 NDJSON 流
  6. 06提供可执行的错误修复建议

适用场景

  1. 01设计供 Agent 使用的 CLI
  2. 02为跟踪和监视命令添加流
  3. 03统一命令错误与恢复方式
  4. 04控制大型输出的上下文占用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/joelhooks/joelclaw --skill cli-design

安装后在 Claude Code 中输入/cli-design确认已加载。

附带文件agents/openai.yamlassets/small-logo.svg

详细介绍

面向 Agent 的优先 CLI 设计

此系统中的 CLI 优先面向 Agent,其次才面向人类。每条命令都会返回 Agent 可以解析、执行操作并据此继续执行的结构化 JSON。欢迎人类通过 jq 进行管道处理。

核心原则

1. 始终使用 JSON

每条命令都会返回 JSON。没有纯文本。没有表格。没有颜色代码。Agent 解析 JSON,而不是解析散文。

# This is the ONLY output format
joelclaw status
# → { "ok": true, "command": "joelclaw status", "result": {...}, "next_actions": [...] }

没有 --json 标志。没有 --human 标志。JSON 是默认且唯一的格式。

2. HATEOAS——每条响应都会告诉你下一步该做什么

每条响应都包含 next_actions——Agent 接下来可以运行的命令模板。模板使用标准 POSIX/docopt 占位符语法:

  • <placeholder> — 必需参数
  • [--flag <value>] — 带值的可选标志
  • [--flag] — 可选布尔标志
  • 不存在 params 字段 — 字面命令(原样运行)
  • 存在 params — 模板(由 Agent 填入占位符)
  • params.*.value — 根据上下文预填的值(Agent 可以覆盖)
  • params.*.default — 省略时使用的值
  • params.*.enum — 有效选项
{
  "ok": true,
  "command": "joelclaw send pipeline/video.download",
  "result": {
    "event_id": "01KHF98SKZ7RE6HC2BH8PW2HB2",
    "status": "accepted"
  },
  "next_actions": [
    {
      "command": "joelclaw run <run-id>",
      "description": "Check run status for this event",
      "params": {
        "run-id": { "value": "01KHF98SKZ7RE6HC2BH8PW2HB2", "description": "Run ID (ULID)" }
      }
    },
    {
      "command": "joelclaw logs <source> [--lines <lines>] [--grep <text>] [--follow]",
      "description": "View worker logs",
      "params": {
        "source": { "enum": ["worker", "errors", "server"], "default": "worker" }
      }
    },
    {
      "command": "joelclaw status",
      "description": "Check system health"
    }
  ]
}

next_actions 是上下文相关的——它们会根据刚刚发生的情况而变化。命令失败时建议的后续步骤与命令成功时不同。模板是 Agent 的可供操作选项——它们展示哪些内容可参数化、哪些值有效,以及当前上下文预填了哪些内容。

3. 自我说明的命令树

Agent 通过两种途径发现命令:根命令(JSON 树)和 --help(由 Effect CLI 自动生成)。两者都必须实用。

**根命令(无参数)**以 JSON 格式返回完整命令树:

{
  "ok": true,
  "command": "joelclaw",
  "result": {
    "description": "JoelClaw — personal AI system CLI",
    "health": { "server": {...}, "worker": {...} },
    "commands": [
      { "name": "send", "description": "Send event to Inngest", "usage": "joelclaw send <event> -d '<json>'" },
      { "name": "status", "description": "System status", "usage": "joelclaw status" },
      { "name": "gateway", "description": "Gateway operations", "usage": "joelclaw gateway status" }
    ]
  },
  "next_actions": [...]
}

--help 输出由 Effect CLI 根据 Command.withDescription() 自动生成。每个子命令都必须有描述——Agent 总会调用 --help,没有描述的命令列表毫无用处。

// ❌ Agents see a blank command list
const status = Command.make("status", {}, () => ...)

// ✅ Agents see what each command does
const status = Command.make("status", {}, () => ...).pipe(
  Command.withDescription("Active sessions, queue depths, Redis health")
)
COMMANDS
  - status                          Active sessions, queue depths, Redis health
  - diagnose [--hours integer]      Layer-by-layer health check
  - review [--hours integer]        Recent session context
4. 保护上下文的输出

Agent 的上下文窗口大小有限。CLI 输出不得使其超出限制。

规则:

  • 默认简洁——只输出最低限度的可用内容
  • 对大型输出(日志、列表)自动截断至合理长度
  • 截断时,提供完整输出的文件路径
  • 切勿倾倒原始日志、完整对话记录或不受限制的列表
{
  "ok": true,
  "command": "joelclaw logs",
  "result": {
    "lines": 20,
    "total": 4582,
    "truncated": true,
    "full_output": "/var/folders/.../joelclaw-logs-abc123.log",
    "entries": ["...last 20 lines..."]
  },
  "next_actions": [
    {
      "command": "joelclaw logs <source> [--lines <lines>]",
      "description": "Show more log lines",
      "params": {
        "source": { "enum": ["worker", "errors", "server"], "default": "worker" },
        "lines": { "default": 20, "description": "Number of lines" }
      }
    }
  ]
}
5. 错误应提供修复建议

发生故障时,响应应包含 fix 字段,用通俗语言告诉 Agent 如何处理。

{
  "ok": false,
  "command": "joelclaw send pipeline/video.download",
  "error": {
    "message": "Inngest server not responding",
    "code": "SERVER_UNREACHABLE"
  },
  "fix": "Start the Inngest server pod: kubectl rollout restart statefulset/inngest -n joelclaw",
  "next_actions": [
    { "command": "joelclaw status", "description": "Re-check system health after fix" },
    {
      "command": "kubectl get pods [--namespace <ns>]",
      "description": "Check pod status",
      "params": { "ns": { "default": "joelclaw" } }
    }
  ]
}

响应封装

每条命令都使用以下准确结构:

成功
{
  ok: true,
  command: string,          // the command that was run
  result: object,           // command-specific payload
  next_actions: Array<{
    command: string,        // command template (POSIX syntax) or literal
    description: string,    // what it does
    params?: Record<string, {   // presence = command is a template
      description?: string,     // what this param means
      value?: string | number,  // pre-filled from current context
      default?: string | number,// value if omitted
      enum?: string[],          // valid choices
      required?: boolean        // true for <positional> args
    }>
  }>
}
错误
{
  ok: false,
  command: string,
  error: {
    message: string,        // what went wrong
    code: string            // machine-readable error code
  },
  fix: string,              // plain-language suggested fix
  next_actions: Array<{
    command: string,        // command template or literal
    description: string,
    params?: Record<string, { ... }>  // same schema as success
  }>
}
参考实现
  • joelclaw — ~/Code/joelhooks/joelclaw/packages/cli/(Effect CLI,操作界面)
  • joelclaw otel — 使用相同响应封装的结构化遥测命令

请将这些作为当前响应封装的事实来源。旧版 slog 系统日志 CLI 已退役。

实现

框架:Effect CLI (@effect/cli)

现有的 joelclaw CLI 软件包使用 @effect/cli 和其配置的运行时。遵循目标仓库的软件包管理器和固定依赖项。对于新项目,使用操作人员当前的技术栈默认设置;本技能不授权迁移运行时。

import { Command, Options } from "@effect/cli"
import { NodeContext, NodeRuntime } from "@effect/platform-node"

const send = Command.make("send", {
  event: Options.text("event"),
  data: Options.optional(Options.text("data").pipe(Options.withAlias("d"))),
}, ({ event, data }) => {
  // ... execute, return JSON envelope
})

const root = Command.make("joelclaw", {}, () => {
  // Root: return health + command tree
}).pipe(Command.withSubcommands([send, status, logs]))
二进制分发

使用 Bun 构建并安装到 ~/.bun/bin/:

bun build src/cli.ts --compile --outfile joelclaw
cp joelclaw ~/.bun/bin/
添加新命令
  1. 使用 Command.make 定义命令
  2. 返回标准 JSON 封装(ok、command、result、next_actions)
  3. 添加上下文相关的 next_actions——即在执行此特定命令后合理的下一步操作
  4. 使用错误封装处理错误(ok: false、error、fix、next_actions)
  5. 将命令添加到根命令的子命令中
  6. 将命令添加到自我说明输出中的根命令 commands 数组
  7. 重新构建并安装

流式协议(NDJSON)——ADR-0058

请求-响应覆盖空间维度(当前状态如何?)。流式 NDJSON 覆盖时间维度(随时间发生了什么?)。两者结合,通过一种协议即可观测整个系统。

何时使用流式传输

当命令涉及时间相关操作(监听、持续跟进、持续读取)时,使用流式传输。并非所有命令都需要流式传输。时间点查询(status、functions、runs)仍使用单个信封。

流式传输由命令语义(--follow、watch、gateway stream)激活,绝不通过全局 --stream 标志激活。

协议:带 HATEOAS 终止项的类型化 NDJSON

每一行都是一个带有 type 判别字段的独立 JSON 对象。最后一行始终是标准 HATEOAS 信封(result 或 error)。不理解流式传输的工具读取最后一行,也能获得完全符合预期的内容。

{"type":"start","command":"joelclaw send video/download --follow","ts":"2026-02-19T08:25:00Z"}
{"type":"step","name":"download","status":"started","ts":"..."}
{"type":"progress","name":"download","percent":45,"ts":"..."}
{"type":"step","name":"download","status":"completed","duration_ms":3200,"ts":"..."}
{"type":"step","name":"transcribe","status":"started","ts":"..."}
{"type":"log","level":"warn","message":"Large file, chunked transcription","ts":"..."}
{"type":"step","name":"transcribe","status":"completed","duration_ms":45000,"ts":"..."}
{"type":"result","ok":true,"command":"...","result":{...},"next_actions":[...]}
流事件类型
类型含义终止项?
start流已开始,回显命令否
stepInngest 步骤生命周期(已启动/已完成/已失败)否
progress进度更新(百分比、字节数、消息)否
log诊断消息(info/warn/error 级别)否
event已发出 Inngest 事件(扇出可见性)否
resultHATEOAS 成功信封——始终位于最后是
errorHATEOAS 错误信封——始终位于最后是
TypeScript 类型
import type { NextAction } from "./response"

type StreamEvent =
  | { type: "start"; command: string; ts: string }
  | { type: "step"; name: string; status: "started" | "completed" | "failed"; duration_ms?: number; error?: string; ts: string }
  | { type: "progress"; name: string; percent?: number; message?: string; ts: string }
  | { type: "log"; level: "info" | "warn" | "error"; message: string; ts: string }
  | { type: "event"; name: string; data: unknown; ts: string }
  | { type: "result"; ok: true; command: string; result: unknown; next_actions: NextAction[] }
  | { type: "error"; ok: false; command: string; error: { message: string; code: string }; fix: string; next_actions: NextAction[] }
发出流事件

使用 emit() 辅助函数——每次调用输出一行 JSON,并立即刷新:

import { emit, emitResult, emitError } from "../stream"

// Progress events
emit({ type: "start", command: "joelclaw send video/download --follow", ts: new Date().toISOString() })
emit({ type: "step", name: "download", status: "started", ts: new Date().toISOString() })
emit({ type: "step", name: "download", status: "completed", duration_ms: 3200, ts: new Date().toISOString() })

// Terminal — always last
emitResult("send --follow", { videoId: "abc123" }, [
  { command: "joelclaw run abc123", description: "Inspect the completed run" },
])
Redis 订阅模式

流式命令会订阅网关扩展使用的相同 Redis 发布/订阅频道。pushGatewayEvent() 中间件是事件的发出点——CLI 只是另一个订阅者。

import { streamFromRedis } from "../stream"

// Subscribe to a channel, transform events, emit NDJSON
await streamFromRedis({
  channel: `joelclaw:notify:gateway`,
  command: "joelclaw gateway stream",
  transform: (event) => ({
    type: "event" as const,
    name: event.type,
    data: event.data,
    ts: new Date().toISOString(),
  }),
  // Optional: end condition
  until: (event) => event.type === "loop.complete",
})
与 Unix 工具组合使用

NDJSON 原生支持管道。Agent 和用户可以筛选流:

# Only step events
joelclaw watch | jq --unbuffered 'select(.type == "step")'

# Only failures
joelclaw send video/download --follow | jq --unbuffered 'select(.type == "error" or (.type == "step" and .status == "failed"))'

# Count steps
joelclaw send pipeline/run --follow | jq --unbuffered 'select(.type == "step" and .status == "completed")' | wc -l
Agent 消费模式

消费流的 Agent 会在每行到达时读取,并可在执行过程中做出决策:

  1. 启动流:joelclaw send video/download --follow
  2. 逐行读取
  3. 根据早期信号采取行动(出错时取消、速度变慢时升级处理、记录进度)
  4. 终止的 result/error 行包含后续操作指南 next_actions

这样就消除了轮询开销——无需浪费工具调用来检查“是否已完成”。

清理

流式命令会占用 Redis 连接。它们必须:

  • 正常处理 SIGINT/SIGTERM(断开 Redis 连接并发出终止事件)
  • 使用 connectTimeout 和 commandTimeout 防止挂起
  • 在流结束时清理订阅(成功、出错或收到信号时)

反模式

不要应该
输出纯文本使用 JSON 信封
使用带 ANSI 颜色的表格使用 JSON 数组
使用 --json 标志选择 JSON 格式JSON 是唯一格式
输出 10,000 行截断输出并提供文件指针
Error: something went wrong{ ok: false, error: {...}, fix: "..." }
使用无法发现的命令根命令返回完整命令树
使用静态帮助文本使用 HATEOAS next_actions
console.log("Success!"){ ok: true, result: {...} }
仅将退出代码作为错误信号在 JSON 中提供错误信息,并设置退出代码
要求 Agent 阅读 --help根命令提供自文档说明
使用没有 withDescription 的子命令为每个命令添加描述,供 --help 使用
通过循环轮询时间相关数据通过 Redis 订阅流式传输 NDJSON(ADR-0058)
在流式命令中输出纯文本每一行都是类型化 JSON 对象
持有 Redis 连接但不清理使用 SIGINT 处理程序和连接超时

命名约定

  • 命令使用名词或动词,全部小写,不带连字符:send、status、logs、gateway
  • 子命令应符合自然表达:joelclaw search "query"、joelclaw loop start
  • 标志使用 --kebab-case:--max-quality、--follow
  • 常用选项使用短标志:-d 表示 --data,-f 表示 --follow
  • 事件名称使用 domain/action:pipeline/video.download、content/summarize

新命令检查清单

  • 返回 JSON 信封(ok、command、result、next_actions)
  • 设置 Command.withDescription()(显示在 --help 中)
  • 错误响应包含 fix 字段
  • 根命令在命令树中列出此命令
  • 输出对上下文安全(可能过长时进行截断)
  • next_actions 与刚刚发生的情况相关
  • next_actions 中包含可变部分时,使用模板语法(<required>、[--flag <value>])和 params
  • 通过 params.*.value 预填充特定上下文的值
  • 不在任何位置输出纯文本
  • 不使用 ANSI 颜色或格式
  • 支持管道传输(不检测 TTY)
  • 构建并安装到 ~/.bun/bin/

待办事项

  • CLI 身份验证的 OAuth 设备流模式:记录在 skillrecordings/support 仓库中验证过的 GitHub 设备流 → broker → 会话令牌 → 环境变量注入模式。涵盖:设备代码轮询循环、组织/团队成员资格门控、短时 AES-GCM 会话令牌、通过 age 加密将密钥交付给 CLI 临时密钥对,以及仅在内存中注入环境变量。参考实现:skillrecordings/support 中的 apps/front/lib/broker/ + apps/front/app/api/auth/device/。此模式可在保留服务器端密钥管理的同时,消除对 1Password CLI 的开发者依赖。

评论

支持 Markdown