mcpField
C
cli-design

cli-design

github.com/joelhooks開発65149 回インストール5 日前に更新
リポジトリ

cli-design について

エージェント向け CLI の設計スキルです。JSON 応答、コマンドの発見性、状況に応じた次の操作、継続処理向け NDJSON ストリームを定義します。

主な機能

  1. 01JSON のみの応答形式
  2. 02状況に応じた HATEOAS 次アクション
  3. 03自己記述型コマンドツリー
  4. 04コンテキストを守る出力制限
  5. 05型付き NDJSON ストリーミング
  6. 06実行可能なエラー修正案

活用シーン

  1. 01エージェント向け 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

詳細

エージェントファーストの 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/
新しいコマンドの追加
  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 は時間的な側面(時間とともに何が起きているか?)を扱います。これらを組み合わせることで、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ストリームを開始し、コマンドをエコーしますいいえ
stepInngest のステップのライフサイクル(開始/完了/失敗)いいえ
progress進捗の更新(割合、バイト数、メッセージ)いいえ
log診断メッセージ(info/warn/error レベル)いいえ
eventInngest イベントが発行されました(ファンアウトを可視化)いいえ
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 を 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
エージェントによる利用パターン

ストリームを利用するエージェントは、到着した行を読み取り、実行中に判断できます。

  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 と終了コードの両方でエラーを示す
エージェントに --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 に依存する必要がなくなります。

コメント