AgentSkillsCN

cli-patterns

打造生产级 CLI 工具的模式,确保工具行为可预测、输出格式可解析,并支持代理式工作流程。适用场景包括:CLI 工具、命令行工具、构建 CLI 工具、CLI 设计模式、代理式 CLI、CLI 设计、Typer CLI、Click CLI 等。

SKILL.md
--- frontmatter
name: cli-patterns
description: "Patterns for building production-quality CLI tools with predictable behavior, parseable output, and agentic workflows. Triggers: cli tool, command line tool, build cli, cli patterns, agentic cli, cli design, typer cli, click cli."
compatibility: "Python 3.11+, Typer, Click"
allowed-tools: "Read, Write, Edit"
depends-on: []
related-skills: [python-cli-patterns, python-async-patterns]

CLI Patterns for Agentic Workflows

Patterns for building CLI tools that AI assistants and power users can chain, parse, and rely on.

Philosophy

Build CLIs for agentic workflows - AI assistants and power users who chain commands, parse output programmatically, and expect predictable behavior.

Core Principles

PrincipleMeaningWhy It Matters
Self-documenting--help is comprehensive and always currentLLMs discover capabilities without external docs
PredictableSame patterns across all commandsLearn once, use everywhere
ComposableUnix philosophy - do one thing wellTools chain together naturally
Parseable--json always available, always validMachine consumption without parsing hacks
Quiet by defaultData only, no decoration unless requestedScripts don't break on unexpected output
Fail fastInvalid input = immediate errorNo silent failures or partial results

Design Axioms

  1. stdout is sacred - Only data. Never progress, never logging, never decoration.
  2. stderr is for humans - Progress bars, colors, tables, warnings live here.
  3. Exit codes have meaning - Scripts can branch on failure mode.
  4. Help includes examples - The fastest path to understanding.
  5. JSON shape is predictable - Same structure across all commands.

Command Architecture

Structural Pattern

code
<tool> [global-options] <resource> <action> [options] [arguments]

Every CLI follows this hierarchy:

code
<tool>
├── --version, --help              # Global flags
├── auth                           # Authentication (if required)
│   ├── login
│   ├── status
│   └── logout
└── <resource>                     # Domain resources (plural nouns)
    ├── list                       # Get many
    ├── get <id>                   # Get one by ID
    ├── create                     # Make new (if supported)
    ├── update <id>                # Modify existing (if supported)
    ├── delete <id>                # Remove (if supported)
    └── <custom-action>            # Domain-specific verbs

Naming Conventions

ElementConventionValid ExamplesInvalid Examples
Tool namelowercase, 2-12 charsmytool, datactlMyTool, my-tool-cli
Resourceplural noun, lowercaseinvoices, usersInvoice, user
Actionverb, lowercaselist, get, synclisting, getter
Long flagskebab-case--dry-run, --output-format--dryRun, --output_format
Short flagssingle letter-n, -q, -v-num, -quiet

Standard Resource Actions

ActionHTTP EquivReturnsIdempotent
listGET /resourcesArrayYes
get <id>GET /resources/:idObjectYes
createPOST /resourcesCreated objectNo
update <id>PATCH /resources/:idUpdated objectYes
delete <id>DELETE /resources/:idConfirmationYes
searchGET /resources?q=ArrayYes

Flags & Options

Mandatory Flags

Every command MUST support:

FlagShortBehaviorOutput
--help-hShow help with examplesHelp text to stdout, exit 0
--jsonMachine-readable outputJSON to stdout

Root command MUST additionally support:

FlagShortBehaviorOutput
--version-VShow version<tool> <version> to stdout, exit 0

Recommended Flags

FlagShortTypePurposeDefault
--quiet-qboolSuppress non-essential stderrfalse
--verbose-vboolIncrease detail levelfalse
--dry-runboolPreview without executingfalse
--limit-nintMax results to return20
--output-opathWrite output to filestdout
--format-fenumOutput formatvaries

Flag Behavior Rules

  1. Boolean flags take no value: --json not --json=true
  2. Short flags can combine: -vq equals -v -q
  3. Unknown flags are errors: Never silently ignore
  4. Repeated flags: Last value wins (or error if inappropriate)

Output Specification

Stream Separation

This is the most critical rule:

StreamContentWhen
stdoutData onlyAlways
stderrEverything elseInteractive mode

stdout receives:

  • JSON when --json is set
  • Minimal text output when interactive
  • Nothing else. Ever.

stderr receives:

  • Progress indicators (spinners, bars)
  • Status messages ("Fetching...", "Done")
  • Warnings
  • Rich formatted tables
  • Colors and decoration
  • Debug information (--verbose)

Interactive Detection

python
import sys

def is_interactive() -> bool:
    """True if connected to a terminal, not piped."""
    return sys.stdout.isatty() and sys.stderr.isatty()
Contextstdout.isatty()Behavior
TerminalTrueRich output to stderr, summary to stdout
Piped (| jq)FalseMinimal/JSON to stdout
Redirected (> file)FalseMinimal to stdout
--json flagAnyJSON to stdout, suppress stderr noise

JSON Output Schema

See references/json-schemas.md for complete JSON response patterns.

Key conventions:

  • List responses: {"data": [...], "meta": {...}}
  • Single item: {"data": {...}}
  • Errors: {"error": {"code": "...", "message": "..."}}
  • ISO 8601 dates, decimal money, string IDs

Exit Codes

Semantic exit codes that scripts can rely on:

CodeNameMeaningWhen
0SUCCESSOperation completedEverything worked
1ERRORGeneral/unknown errorUnexpected failures
2AUTH_REQUIREDNot authenticatedNo token, token expired
3NOT_FOUNDResource missingID doesn't exist
4VALIDATIONInvalid inputBad arguments, failed validation
5FORBIDDENPermission deniedAuthenticated but not authorized
6RATE_LIMITEDToo many requestsAPI throttling
7CONFLICTState conflictConcurrent modification, duplicate

Usage

bash
# Script can branch on exit code
mytool items get item-001 --json
case $? in
  0) echo "Success" ;;
  2) echo "Need to authenticate" && mytool auth login ;;
  3) echo "Item not found" ;;
  *) echo "Error occurred" ;;
esac

Implementation

python
# Constants
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_AUTH_REQUIRED = 2
EXIT_NOT_FOUND = 3
EXIT_VALIDATION = 4
EXIT_FORBIDDEN = 5
EXIT_RATE_LIMITED = 6
EXIT_CONFLICT = 7

# Usage
raise typer.Exit(EXIT_NOT_FOUND)

Error Handling

Error Output Format

With --json, errors output structured JSON to stdout AND a message to stderr:

stderr:

code
Error: Item not found

stdout:

json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Item not found",
    "details": {
      "item_id": "bad-id"
    }
  }
}

Error Codes

CodeExitMeaning
AUTH_REQUIRED2Must authenticate first
TOKEN_EXPIRED2Token needs refresh
FORBIDDEN5Insufficient permissions
NOT_FOUND3Resource doesn't exist
VALIDATION_ERROR4Invalid input
INVALID_ARGUMENT4Bad argument value
MISSING_ARGUMENT4Required argument missing
RATE_LIMITED6Too many requests
CONFLICT7State conflict
ALREADY_EXISTS7Duplicate resource
INTERNAL_ERROR1Unexpected error
API_ERROR1Upstream API failed
NETWORK_ERROR1Connection failed

Implementation Pattern

python
def _error(
    message: str,
    code: str = "ERROR",
    exit_code: int = EXIT_ERROR,
    details: dict = None,
    as_json: bool = False,
):
    """Output error and exit."""
    error_obj = {"error": {"code": code, "message": message}}
    if details:
        error_obj["error"]["details"] = details

    if as_json:
        print(json.dumps(error_obj, indent=2))

    # Always print human message to stderr
    console.print(f"[red]Error:[/red] {message}")
    raise typer.Exit(exit_code)

Help System

Help Requirements

Every --help output MUST include:

  1. Brief description (one line)
  2. Usage syntax
  3. Options with descriptions
  4. Examples (critical for discovery)

Help Format Template

code
<one-line description>

Usage: <tool> <resource> <action> [OPTIONS] [ARGS]

Arguments:
  <arg>          Description of positional argument

Options:
  -s, --status TEXT    Filter by status
  -n, --limit INTEGER  Max results [default: 20]
  --json               Output as JSON
  -h, --help           Show this help

Examples:
  <tool> <resource> <action>
  <tool> <resource> <action> --status active
  <tool> <resource> <action> --json | jq '.[0]'

Examples Are Critical

Examples should show:

  1. Basic usage - Simplest invocation
  2. Common filters - Most-used options
  3. JSON piping - How to chain with jq
  4. Real-world scenarios - Actual use cases

Authentication

Auth Commands

Tools requiring authentication MUST implement:

code
<tool> auth login      # Interactive authentication
<tool> auth status     # Check current state
<tool> auth logout     # Clear credentials

Credential Storage Priority

Recommended: OS keyring with fallbacks for maximum security

  1. Environment variable (CI/CD, testing)

    • MYTOOL_API_TOKEN or similar
    • Highest priority, overrides all other sources
  2. OS Keyring (primary storage - secure)

    • Windows: Credential Manager
    • macOS: Keychain
    • Linux: Secret Service (GNOME Keyring, KWallet)
    • Encrypted at rest, per-user isolation
  3. .env file (development fallback)

    • Plain text in current directory
    • Convenient for local development
    • Must be in .gitignore

Dependencies:

toml
dependencies = [
    "keyring>=24.0.0",      # OS keyring access
    "python-dotenv>=1.0.0", # .env file support
]

Simple alternative: Just config file in ~/.config/<tool>/

  • Good for tools without sensitive credentials
  • Or when OS keyring adds too much complexity

See references/implementation.md for complete credential storage implementations.

Unauthenticated Behavior

When auth is required but missing:

bash
$ mytool items list
Error: Not authenticated. Run: mytool auth login
# exit code: 2
bash
$ mytool items list --json
# stderr: Error: Not authenticated. Run: mytool auth login
{"error": {"code": "AUTH_REQUIRED", "message": "Not authenticated. Run: mytool auth login"}}
# exit code: 2

Data Conventions

Date Handling

Input (Flexible): Accept multiple formats for user convenience

FormatExampleInterpretation
ISO date2025-01-15Exact date
ISO datetime2025-01-15T10:30:00ZExact datetime
Relativetoday, yesterday, tomorrowCurrent/previous/next day
Relativelast, this (with context)Previous/current period

Output (Strict): Always output ISO 8601

json
{
  "created_at": "2025-01-15T10:30:00Z",
  "due_date": "2025-02-15",
  "month": "2025-01"
}

Money

  • Store as decimal number, not cents
  • Include currency when ambiguous
  • Never format (no "$" or "," in JSON)
json
{
  "total": 1250.50,
  "currency": "USD"
}

IDs

  • Always strings (even if numeric)
  • Preserve exact format from source
json
{
  "id": "abc_123",
  "legacy_id": "12345"
}

Enums

  • UPPER_SNAKE_CASE in JSON
  • Case-insensitive input
bash
# All equivalent
--status DRAFT
--status draft
--status Draft
json
{"status": "IN_PROGRESS"}

Filtering & Pagination

Common Filter Patterns

bash
# By status
--status DRAFT
--status active,pending    # Multiple values

# By date range
--from 2025-01-01 --to 2025-01-31
--month 2025-01
--month last

# By related entity
--user "Alice"
--project "Project X"

# Text search
--search "keyword"
-q "keyword"

# Boolean filters
--archived
--no-archived
--include-deleted

Pagination

bash
# Limit results
--limit 50
-n 50

# Offset-based
--page 2
--offset 20

# Cursor-based
--cursor "eyJpZCI6MTIzfQ=="
--after "item_123"

Implementation

See references/implementation.md for complete Python implementation templates including:

  • CLI skeleton with Typer
  • Client pattern with httpx
  • Error handling
  • Authentication flows
  • Testing patterns

Anti-Patterns

❌ Output Pollution

bash
# BAD: Progress to stdout
$ bad-tool items list --json
Fetching items...
[{"id": "1"}]
Done!

# GOOD: Only JSON to stdout
$ good-tool items list --json
[{"id": "1"}]

❌ Interactive Prompts

bash
# BAD: Prompts in non-interactive context
$ bad-tool items create
Enter name: _

# GOOD: Fail fast with required flags
$ good-tool items create
Error: --name is required

❌ Inconsistent Flags

bash
# BAD: Different flags for same concept
$ tool1 list -j
$ tool2 list --format=json

# GOOD: Same flags everywhere
$ tool1 list --json
$ tool2 list --json

❌ Silent Failures

bash
# BAD: Success exit code on failure
$ bad-tool items delete bad-id
Item not found
$ echo $?
0

# GOOD: Semantic exit code
$ good-tool items delete bad-id
Error: Item not found: bad-id
$ echo $?
3

Quick Reference

Must-Have Checklist

  • <tool> --version
  • <tool> --help with examples
  • <tool> <resource> list [--json]
  • <tool> <resource> get <id> [--json]
  • Semantic exit codes (0, 1, 2, 3, 4, 5, 6, 7)
  • Errors to stderr, data to stdout
  • Valid JSON on --json
  • Stream separation (stdout = data, stderr = UI)

Recommended Additions

  • Authentication commands (auth login, auth status, auth logout)
  • Create/Update/Delete operations
  • --quiet and --verbose modes
  • --dry-run for mutations
  • Pagination (--limit, --page)
  • Filtering (status, date range, search)
  • Automated tests

Framework Choice

Typer (preferred for new tools):

  • Type hints provide automatic validation
  • Built-in help generation
  • Rich integration for beautiful output
  • Less boilerplate than Click

Click (acceptable for existing tools):

  • Typer is built on Click (100% compatible)
  • Well-structured Click code doesn't need migration
  • Both must follow same output conventions
python
# Typer (preferred)
import typer
from rich.console import Console

app = typer.Typer()
console = Console(stderr=True)  # UI to stderr

# Click (acceptable)
import click
from rich.console import Console

console = Console(stderr=True)  # Same pattern