Skip to main content

cli-design

Design and build agent-first CLIs with HATEOAS JSON responses, context-protecting output, and self-documenting command trees. Use when creating new CLI tools, adding commands to existing CLIs such as joelclaw, or reviewing CLI design for agent-friendliness. Triggers on 'build a CLI', 'add a command', 'CLI design', 'agent-friendly output', or any task involving command-line tool creation.

Datos de origen

Repositorio
joelhooks/joelclaw
Última actividad en el origen
5 de septiembre de 2026 a las 14:53
Idioma detectado de SKILL.md
inglés
Estrellas
64
Forks
2

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
4 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
cli-design
displayName
CLI Design
description
Design and build agent-first CLIs with HATEOAS JSON responses, context-protecting output, and self-documenting command trees. Use when creating new CLI tools, adding commands to existing CLIs such as joelclaw, or reviewing CLI design for agent-friendliness. Triggers on 'build a CLI', 'add a command', 'CLI design', 'agent-friendly output', or any task involving command-line tool creation.
version
1.1.0
author
Joel Hooks
tags
["joelclaw","cli","agentic","ux","json"]
disable-model-invocation
true
# Agent-First CLI Design CLIs in this system are **agent-first, human-distant-second**. Every command returns structured JSON that an agent can parse, act on, and follow. Humans are welcome to pipe through `jq`. ## Core Principles ### 1. JSON always Every command returns JSON. No plain text. No tables. No color codes. Agents parse JSON; they don't parse prose. ```bash # This is the ONLY output format joelclaw status # → { "ok": true, "command": "joelclaw status", "result": {...}, "next_actions": [...] } ``` No `--json` flag. No `--human` flag. JSON is the default and only format. ### 2. HATEOAS — every response tells you what to do next Every response includes `next_actions` — an array of command **templates** the agent can run next. Templates use standard POSIX/docopt placeholder syntax: - `<placeholder>` — required argument - `[--flag <value>]` — optional flag with value - `[--flag]` — optional boolean flag - No `params` field — literal command (run as-is) - `params` present — template (agent fills placeholders) - `params.*.value` — pre-filled from context (agent can override) - `params.*.default` — value if omitted - `params.*.enum` — valid choices ```json { "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` are **contextual** — they change based on what just happened. A failed command suggests different next steps than a successful one. Templates are the agent's **affordances** — they show what's parameterizable, what values are valid, and what the current context pre-fills. ### 3. Self-documenting command tree Agents discover commands via **two paths**: the root command (JSON tree) and `--help` (Effect CLI auto-generated). Both must be useful. **Root command (no args)** returns the full command tree as JSON: ```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` output** is auto-generated by Effect CLI from `Command.withDescription()`. Every subcommand **must** have a description — agents always call `--help` and a bare command list with no descriptions is useless. ```typescript // ❌ 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. Context-protecting output Agents have finite context windows. CLI output must not blow them up. **Rules:** - Terse by default — minimum viable output - Auto-truncate large outputs (logs, lists) at a reasonable limit - When truncated, include a file path to the full output - Never dump raw logs, full transcripts, or unbounded lists ```json { "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. Errors suggest fixes When something fails, the response includes a `fix` field — plain language telling the agent what to do about it. ```json { "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" } } } ] } ``` ## Response Envelope Every command uses this exact shape: ### Success ```typescript { 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 }> }> } ``` ### Error ```typescript { 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 }> } ``` ### Reference implementations - `joelclaw` — `~/Code/joelhooks/joelclaw/packages/cli/` (Effect CLI, operational surface) - `joelclaw otel` — structured telemetry commands using the same response envelope Use these as the current envelope source-of-truth. The former `slog` system-log CLI is retired. ## Implementation ### Framework: Effect CLI (@effect/cli) Existing joelclaw CLI packages use `@effect/cli` with their configured runtime. Follow the target repository package manager and pinned dependencies. For a new project, use the operator’s current stack defaults; this skill does not authorize a runtime migration. ```typescript 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])) ``` ### Binary distribution Build with Bun, install to `~/.bun/bin/`: ```bash bun build src/cli.ts --compile --outfile joelclaw cp joelclaw ~/.bun/bin/ ``` ### Adding a new command 1. Define the command with `Command.make` 2. Return the standard JSON envelope (ok, command, result, next_actions) 3. Include contextual `next_actions` — what makes sense AFTER this specific command 4. Handle errors with the error envelope (ok: false, error, fix, next_actions) 5. Add to the root command's subcommands 6. Add to the root command's `commands` array in the self-documenting output 7. Rebuild and install ## Streaming Protocol (NDJSON) — ADR-0058 Request-response covers the **spatial** dimension (what's the state now?). Streamed NDJSON covers the **temporal** dimension (what's happening over time?). Together they make the full system observable through one protocol. ### When to stream Stream when the command involves **temporal operations** — watching, following, tailing. Not every command needs streaming. Point-in-time queries (`status`, `functions`, `runs`) stay as single envelopes. Streaming is activated by command semantics (`--follow`, `watch`, `gateway stream`), never by a global `--stream` flag. ### Protocol: typed NDJSON with HATEOAS terminal Each line is a self-contained JSON object with a `type` discriminator. The **last line is always the standard HATEOAS envelope** (`result` or `error`). Tools that don't understand streaming read the last line and get exactly what they expect. ``` {"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":[...]} ``` ### Stream event types | Type | Meaning | Terminal? | |------|---------|-----------| | `start` | Stream begun, echoes command | No | | `step` | Inngest step lifecycle (started/completed/failed) | No | | `progress` | Progress update (percent, bytes, message) | No | | `log` | Diagnostic message (info/warn/error level) | No | | `event` | An Inngest event was emitted (fan-out visibility) | No | | `result` | HATEOAS success envelope — always last | **Yes** | | `error` | HATEOAS error envelope — always last | **Yes** | ### TypeScript types ```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 }
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub