cli-design について
エージェント向け CLI の設計スキルです。JSON 応答、コマンドの発見性、状況に応じた次の操作、継続処理向け NDJSON ストリームを定義します。
主な機能
- 01JSON のみの応答形式
- 02状況に応じた HATEOAS 次アクション
- 03自己記述型コマンドツリー
- 04コンテキストを守る出力制限
- 05型付き NDJSON ストリーミング
- 06実行可能なエラー修正案
活用シーン
- 01エージェント向け CLI の設計
- 02追跡・監視コマンドへのストリーム追加
- 03コマンドのエラーと復旧方法の統一
- 04大量出力によるコンテキスト消費の抑制
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ joelhooks/ joelclaw --skill cli-designインストール後、Claude Code で/cli-designを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlassets/small-logo.svg
詳細
エージェントファーストの CLI 設計
このシステムの CLI は、エージェントファースト、人間は二の次です。すべてのコマンドは、エージェントが解析し、操作し、次へ進める構造化 JSON を返します。人間は jq を介して出力を処理できます。
基本原則
1. 常に JSON
すべてのコマンドは JSON を返します。プレーンテキストは使いません。表も使いません。カラーコードも使いません。エージェントは JSON を解析します。文章は解析しません。
# This is the ONLY output format
joelclaw status
# → { "ok": true, "command": "joelclaw status", "result": {...}, "next_actions": [...] }
--json フラグはありません。--human フラグもありません。JSON がデフォルトであり、唯一の形式です。
2. HATEOAS — すべての応答が次に行うことを示す
すべての応答には、エージェントが次に実行できるコマンドのテンプレートを格納した配列 next_actions が含まれます。テンプレートでは、標準的な POSIX/docopt のプレースホルダー構文を使います。
<placeholder>— 必須の引数[--flag <value>]— 値を伴う任意のフラグ[--flag]— 任意のブール値フラグparamsフィールドなし — そのまま実行するリテラルコマンドparamsあり — テンプレート(エージェントがプレースホルダーを埋める)params.*.value— コンテキストから事前に入力される値(エージェントは上書き可能)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 は状況に応じて変化します。何が起きたかによって変わります。失敗したコマンドが提示する次の手順は、成功したコマンドの場合とは異なります。テンプレートはエージェントにとっての操作可能な選択肢です。何をパラメーター化できるか、有効な値は何か、現在のコンテキストからどの値が事前入力されるかを示します。
3. 自己記述型のコマンドツリー
エージェントは2 つの方法でコマンドを見つけます。ルートコマンド(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 の出力は、Command.withDescription() から Effect CLI によって自動生成されます。すべてのサブコマンドには説明が必要です。エージェントは必ず --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. コンテキストを保護する出力
エージェントのコンテキストウィンドウには上限があります。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 フィールドを含め、エージェントが何をすべきかを平易な言葉で伝えます。
{
"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— 同じ応答エンベロープを使用する構造化テレメトリーコマンド
現行のエンベロープの正確な定義には、これらを使用してください。以前のシステムログ CLI slog は廃止されています。
実装
フレームワーク: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/
新しいコマンドの追加
Command.makeでコマンドを定義する- 標準の JSON エンベロープ(ok、command、result、next_actions)を返す
- 状況に応じた
next_actionsを含める — このコマンドの実行後に適切な次の操作を示す - エラーはエラーエンベロープ(ok: false、error、fix、next_actions)で処理する
- ルートコマンドのサブコマンドに追加する
- 自己記述型の出力で、ルートコマンドの
commands配列に追加する - 再ビルドしてインストールする
ストリーミングプロトコル(NDJSON)— ADR-0058
リクエスト/レスポンスは空間的な側面(現在の状態は?)を扱います。ストリーミング NDJSON は時間的な側面(時間とともに何が起きているか?)を扱います。これらを組み合わせることで、1 つのプロトコルを通じてシステム全体を可観測にできます。
ストリーミングするタイミング
監視、追跡、テーリングなど、時間的な操作を伴うコマンドではストリーミングします。すべてのコマンドでストリーミングが必要なわけではありません。特定時点のクエリ(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 | ストリームを開始し、コマンドをエコーします | いいえ |
step | Inngest のステップのライフサイクル(開始/完了/失敗) | いいえ |
progress | 進捗の更新(割合、バイト数、メッセージ) | いいえ |
log | 診断メッセージ(info/warn/error レベル) | いいえ |
event | Inngest イベントが発行されました(ファンアウトを可視化) | いいえ |
result | HATEOAS 成功エンベロープ — 常に最後 | はい |
error | HATEOAS エラーエンベロープ — 常に最後 | はい |
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 を 1 行出力し、即座にフラッシュします。
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 pub/sub チャネルをサブスクライブします。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 はパイプでの利用を前提としています。エージェントも人間も、ストリームをフィルタリングできます。
# 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
エージェントによる利用パターン
ストリームを利用するエージェントは、到着した行を読み取り、実行中に判断できます。
- ストリームを開始します:
joelclaw send video/download --follow - 行を順次読み取ります
- 早期の兆候に応じて対応します(エラーならキャンセル、遅ければエスカレーション、進捗をログに記録)
- 終端の
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 と終了コードの両方でエラーを示す |
| エージェントに --help の確認を要求する | ルートコマンドで自己説明する |
withDescription のないサブコマンド | すべてのコマンドに --help 用の説明を付ける |
| 時間的なデータをループでポーリングする | Redis sub 経由で NDJSON をストリーミングする(ADR-0058) |
| ストリーミングコマンドでプレーンテキストを使う | すべての行を型付き JSON オブジェクトにする |
| クリーンアップせずに Redis 接続を維持する | SIGINT ハンドラーと接続タイムアウトを設定する |
命名規則
- コマンドには名詞または動詞を使用し、小文字でハイフンは使いません:
send、status、logs、gateway - サブコマンドは自然な形にします:
joelclaw search "query"、joelclaw loop start - フラグには
--kebab-caseを使用します:--max-quality、--follow - よく使うオプションには短いフラグを使います:
--dataの-d、--followの-f - イベント名には
domain/actionを使用します:pipeline/video.download、content/summarize
新規コマンドのチェックリスト
- JSON エンベロープを返す(ok、command、result、next_actions)
-
Command.withDescription()を設定する(--helpに表示されます) - エラーレスポンスに修正方法を示すフィールドを含める
- ルートコマンドのツリーにこのコマンドを追加する
- 出力がコンテキストに配慮している(サイズが大きくなる可能性がある場合は切り詰める)
- next_actions が直前に発生した内容に即している
- 可変部分を含む next_actions ではテンプレート構文(
<required>、[--flag <value>])とparamsを使用する - コンテキスト固有の値を
params.*.valueで事前入力する - プレーンテキストの出力が一切ない
- ANSI カラーや書式設定を使用しない
- パイプ経由で動作する(TTY 検出を行わない)
- ビルドされ、~/.bun/bin/ にインストールされる
TODO
- CLI 認証用の OAuth デバイスフローパターン:
skillrecordings/supportリポジトリで実証された、GitHub デバイスフロー → ブローカー → セッショントークン → 環境変数への展開パターンを文書化します。対象:デバイスコードのポーリングループ、組織/チームへの所属確認、短期間有効な AES-GCM セッショントークン、一時的な鍵ペアを使った age 暗号化済みシークレットの CLI への配信、メモリ内限定の環境変数注入。リファレンス実装:skillrecordings/supportのapps/front/lib/broker/+apps/front/app/api/auth/device/。これにより、サーバー側のシークレット管理を維持しながら、開発者が 1Password CLI に依存する必要がなくなります。