cli-design

安装量: 92
排名: #8764

安装

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

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.

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:
— required argument
[--flag ]
— 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
{
"ok"
:
true
,
"command"
:
"joelclaw send pipeline/video.download"
,
"result"
:
{
"event_id"
:
"01KHF98SKZ7RE6HC2BH8PW2HB2"
,
"status"
:
"accepted"
}
,
"next_actions"
:
[
{
"command"
:
"joelclaw run "
,
"description"
:
"Check run status for this event"
,
"params"
:
{
"run-id"
:
{
"value"
:
"01KHF98SKZ7RE6HC2BH8PW2HB2"
,
"description"
:
"Run ID (ULID)"
}
}
}
,
{
"command"
:
"joelclaw logs [--lines ] [--grep ] [--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: { "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 -d ''" } , { "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. // ❌ 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 { "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 [--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. { "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 ]" , "description" : "Check pod status" , "params" : { "ns" : { "default" : "joelclaw" } } } ] } Response Envelope Every command uses this exact shape: Success { 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 args }

}

} Error { 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) slog — system log CLI (same envelope patterns) Use these as the current envelope source-of-truth. Implementation Framework: Effect CLI (@effect/cli) All CLIs use @effect/cli with Bun. This is non-negotiable — consistency across the system matters more than framework preference. 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/ : bun build src/cli.ts --compile --outfile joelclaw cp joelclaw ~/.bun/bin/ Adding a new command Define the command with Command.make Return the standard JSON envelope (ok, command, result, next_actions) Include contextual next_actions — what makes sense AFTER this specific command Handle errors with the error envelope (ok: false, error, fix, next_actions) Add to the root command's subcommands Add to the root command's commands array in the self-documenting output 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 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 [ ] } Emitting stream events Use the emit() helper — one JSON line per call, flushed immediately: 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 subscription pattern Streaming commands subscribe to the same Redis pub/sub channels the gateway extension uses. pushGatewayEvent() middleware is the emission point — the CLI is just another subscriber. 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" , } ) Composable with Unix tools NDJSON is pipe-native. Agents and humans can filter streams:

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 consumption pattern
Agents consuming streams read lines as they arrive and can make decisions mid-execution:
Start the stream:
joelclaw send video/download --follow
Read lines incrementally
React to early signals (cancel if error, escalate if slow, log progress)
The terminal
result
/
error
line contains
next_actions
for what to do after
This eliminates the
polling tax
— no wasted tool calls checking "is it done yet?"
Cleanup
Streaming commands hold a Redis connection. They
must
:
Handle SIGINT/SIGTERM gracefully (disconnect Redis, emit terminal event)
Use
connectTimeout
and
commandTimeout
to prevent hangs
Clean up the subscription on stream end (success, error, or signal)
Anti-Patterns
Don't
Do
Plain text output
JSON envelope
Tables with ANSI colors
JSON arrays
--json
flag to opt into JSON
JSON is the only format
Dump 10,000 lines
Truncate + file pointer
Error: something went wrong
{ ok: false, error: {...}, fix: "..." }
Undiscoverable commands
Root returns full command tree
Static help text
HATEOAS next_actions
console.log("Success!")
{ ok: true, result: {...} }
Exit code as the only error signal
Error in JSON + exit code
Require the agent to read --help
Root command self-documents
Subcommand with no
withDescription
Every command gets a description for
--help
Poll in a loop for temporal data
Stream NDJSON via Redis sub (ADR-0058)
Plain text in streaming commands
Every line is a typed JSON object
Hold Redis connections without cleanup
SIGINT handler + connection timeout
Naming Conventions
Commands are
nouns or verbs
, lowercase, no hyphens:
send
,
status
,
logs
,
gateway
Subcommands follow naturally:
joelclaw search "query"
,
joelclaw loop start
Flags use
--kebab-case
:
--max-quality
,
--follow
Short flags for common options:
-d
for
--data
,
-f
for
--follow
Event names use
domain/action
:
pipeline/video.download
,
content/summarize
Checklist for New Commands
Returns JSON envelope (ok, command, result, next_actions)
Command.withDescription()
set (shows in
--help
)
Error responses include fix field
Root command lists this command in its tree
Output is context-safe (truncated if potentially large)
next_actions are contextual to what just happened
next_actions with variable parts use template syntax (
,
[--flag ]
) +
params
Context-specific values pre-filled via
params.*.value
No plain text output anywhere
No ANSI colors or formatting
Works when piped (no TTY detection)
Builds and installs to ~/.bun/bin/
TODO
OAuth device flow pattern for CLI auth
Document the GitHub device flow → broker → session token → env materialization pattern proven in the skillrecordings/support repo. Covers: device code polling loop, org/team membership gating, short-lived AES-GCM session tokens, age-encrypted secret delivery to CLI ephemeral keypairs, in-memory-only env injection. Reference implementation: apps/front/lib/broker/ + apps/front/app/api/auth/device/ in skillrecordings/support . This eliminates 1Password CLI as a developer dependency while keeping server-side secret management intact.
返回排行榜