{"id":48963831,"url":"https://github.com/paperfoot/agent-cli-framework","last_synced_at":"2026-04-18T03:04:33.788Z","repository":{"id":346702831,"uuid":"1191200996","full_name":"paperfoot/agent-cli-framework","owner":"paperfoot","description":"Build Rust CLIs that AI agents can discover, call, and learn from. No MCP server needed.","archived":false,"fork":false,"pushed_at":"2026-04-17T20:43:00.000Z","size":186,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-17T22:29:22.021Z","etag":null,"topics":["agent-cli-framework","agent-friendly-cli","agent-protocol","ai-agent-tools","ai-agents","ai-coding","cli-architecture","cli-framework","command-line-tools","developer-tools","llm-tools","mcp-alternative","rust","rust-cli-framework","rust-developer-tools","structured-output","tool-discovery"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/paperfoot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-25T02:32:17.000Z","updated_at":"2026-04-17T20:43:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/paperfoot/agent-cli-framework","commit_stats":null,"previous_names":["199-biotechnologies/agent-cli-framework","paperfoot/agent-cli-framework"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/paperfoot/agent-cli-framework","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fagent-cli-framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fagent-cli-framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fagent-cli-framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fagent-cli-framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/paperfoot","download_url":"https://codeload.github.com/paperfoot/agent-cli-framework/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/paperfoot%2Fagent-cli-framework/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31954738,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-cli-framework","agent-friendly-cli","agent-protocol","ai-agent-tools","ai-agents","ai-coding","cli-architecture","cli-framework","command-line-tools","developer-tools","llm-tools","mcp-alternative","rust","rust-cli-framework","rust-developer-tools","structured-output","tool-discovery"],"created_at":"2026-04-18T03:04:29.684Z","updated_at":"2026-04-18T03:04:33.778Z","avatar_url":"https://github.com/paperfoot.png","language":"Rust","readme":"\u003cdiv align=\"center\"\u003e\n\n# Agent CLI Framework\n\n**Build Rust CLIs that AI agents can discover, call, and learn from.**\n\n\u003cbr /\u003e\n\n[![Star this repo](https://img.shields.io/github/stars/paperfoot/agent-cli-framework?style=for-the-badge\u0026logo=github\u0026label=%E2%AD%90%20Star%20this%20repo\u0026color=yellow)](https://github.com/paperfoot/agent-cli-framework/stargazers)\n\u0026nbsp;\u0026nbsp;\n[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge\u0026logo=x\u0026logoColor=white)](https://x.com/longevityboris)\n\n\u003cbr /\u003e\n\n[![CI](https://github.com/paperfoot/agent-cli-framework/actions/workflows/ci.yml/badge.svg)](https://github.com/paperfoot/agent-cli-framework/actions/workflows/ci.yml)\n[![Rust](https://img.shields.io/badge/Rust-000000?style=for-the-badge\u0026logo=rust\u0026logoColor=white)](https://www.rust-lang.org/)\n[![MSRV 1.85+](https://img.shields.io/badge/MSRV-1.85%2B-orange?style=for-the-badge)](https://www.rust-lang.org/)\n[![MIT License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)](LICENSE)\n[![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-brightgreen?style=for-the-badge)](CONTRIBUTING.md)\n\n---\n\nEight patterns turn any Rust CLI into a tool AI agents can pick up and use without documentation, MCP servers, or skill files. The binary describes itself, returns structured output, uses semantic exit codes, teaches usage through rich help, diagnoses its own dependencies, and guards against duplicate runs. Your CLI becomes the tool, the documentation, and the API -- all in one binary.\n\n[Philosophy](#philosophy) | [Why This Exists](#why-this-exists) | [Patterns](#patterns) | [Reusable Modules](#reusable-modules) | [Getting Started](#getting-started-build-your-own) | [Example](#example) | [Invariants](#invariants)\n\n\u003c/div\u003e\n\n---\n\n## Philosophy\n\nThese principles govern every CLI built with this framework. They are not suggestions. When an agent or developer faces a decision not covered by a specific pattern, reason from these principles.\n\n### 1. The binary is the interface\n\nNo MCP servers. No protocol layers. No separate documentation that drifts. The CLI describes itself (`agent-info`), explains its errors (`suggestion`), and signals its state (exit codes). If an agent has the binary on PATH, it has everything it needs.\n\n### 2. Local-first, zero-infrastructure\n\nNo databases to spin up. No services to connect to. Config is a TOML file. State is SQLite when needed. Cache is a directory you can delete. Everything lives on the machine, in standard directories:\n\n| Purpose | Path | Lifecycle |\n|---------|------|-----------|\n| Config | `~/.config/\u003capp\u003e/config.toml` | User-authored, version-controlled |\n| Secrets | Env vars or `~/.config/\u003capp\u003e/config.toml` | Never in state DB, masked on display |\n| State | `~/.local/share/\u003capp\u003e/` | Mutable operational data |\n| Cache | `~/.cache/\u003capp\u003e/` | Disposable -- `rm -rf` is always safe |\n| Logs | `~/.local/share/\u003capp\u003e/logs/` | Append-only, daily rotation |\n\n`rm -rf ~/.config/mycli ~/.local/share/mycli ~/.cache/mycli` resets to factory.\n\n### 3. Two audiences, one stdout\n\nHumans get colored, human-readable output. Agents get JSON envelopes. The binary detects which and adapts automatically. Both paths are first-class. If a command writes to stdout, it respects the output format -- no exceptions, no code paths that leak raw text.\n\n### 4. Errors are instructions\n\nAn error is not a report -- it's a recovery plan. Every error has three parts: a machine-readable code, a human sentence, and a concrete suggestion the agent can follow literally. Suggestions are tested instructions, not hints. A wrong suggestion is a bug.\n\n### 5. Exit codes are contracts\n\n| Code | Meaning | Agent action |\n|------|---------|-------------|\n| `0` | Success | Continue |\n| `1` | Transient error (IO, network) | Retry with backoff |\n| `2` | Config error (missing key, bad file) | Fix setup, do not retry |\n| `3` | Bad input (invalid args) | Fix arguments |\n| `4` | Rate limited | Wait, then retry |\n\nCodes 5-125 are reserved for future framework use. Do not invent custom exit codes. If your error doesn't fit 1-4, it's a `1` (transient). Codes 126-255 are reserved by POSIX.\n\n**`--help` and `--version` always exit 0.** They are informational requests, not errors.\n\n### 6. Less code, right problem\n\nDon't add features nobody asked for. Don't add abstractions for one call site. Don't add error handling for impossible scenarios. Three similar lines beat a premature abstraction. Delete code when it's no longer needed.\n\n### 7. Consistency across all CLIs\n\nIf `inbox list` works, `account list` works. If `--json` forces JSON in one CLI, it does in every CLI. If config lives in `~/.config/\u003capp\u003e/`, it does everywhere. An agent that learns one CLI built with this framework has learned them all.\n\n### 8. Self-contained and portable\n\nThe binary carries its own skill file as an embedded constant (via `const` or `include_str!`). `skill install` deploys it. `update` replaces the binary from GitHub Releases. One artifact. The self-update mechanism is opt-in -- CLIs distributed via package managers or in managed environments should disable it.\n\n### 9. Speed is a feature\n\nSingle-binary Rust. No runtime. No JIT warmup. Cold start under 10ms. If an agent shells out to this tool 50 times in a session, each call should feel instant.\n\n### 10. Never prompt, never block\n\nAgent-friendly means non-interactive. No \"are you sure?\" prompts. No stdin waits. No pagers. If it needs input, it takes flags. If it's destructive, require `--confirm` as a flag, not an interactive prompt. If auth is missing, exit 2 (config error) with a suggestion -- never hang waiting for input.\n\n---\n\n## Why This Exists\n\nAgents need tools. Not connections to tools. Not descriptions of tools. Actual tools they can pick up and use.\n\nAn MCP server is a connection -- it tells the agent \"there's a service over there, here's its schema, here's how to call it.\" A skill file is an instruction manual. Neither is the tool itself. The agent reads about capabilities without having them. It's the difference between handing someone a hammer and handing them a pamphlet about hammers.\n\nA CLI is the tool. It sits on the machine, does one job, and explains itself when asked. An agent that has `search` on its PATH can search. An agent that has `labparse` can parse lab results. No intermediary, no server process, no protocol layer. The agent shells out, gets structured JSON back, and moves on.\n\n### The numbers back this up\n\nScalekit benchmarked 75 tasks: the simplest cost **1,365 tokens via CLI** and **44,026 via MCP** -- a 32x overhead. Each MCP tool definition burns 550-1,400 tokens just to describe itself. A typical setup dumps 55,000 tokens into the context window before any real work starts.\n\nSpeakeasy found that at 107 tools, models struggled to select the right one and started hallucinating tool names that didn't exist. GitHub Copilot [cut from 40 tools to 13](https://github.blog/ai-and-ml/github-copilot/how-were-making-github-copilot-smarter-with-fewer-tools/) and got better results.\n\nLLMs already know how to use CLIs. They were trained on millions of shell examples from Stack Overflow, GitHub, and man pages. The grammar of `tool subcommand --flag value` is baked into their weights. Eugene Petrenko at JetBrains documented agents autonomously discovering and using the `gh` CLI -- handling auth, reading PRs, managing issues -- without being told it existed.\n\n---\n\n## Patterns\n\n### Pattern 1: `agent-info` -- Capability Discovery\n\nThe binary describes itself. One command returns a JSON manifest of everything the tool can do.\n\n```json\n{\n  \"name\": \"mycli\",\n  \"version\": \"1.2.0\",\n  \"description\": \"What this CLI does in one sentence\",\n  \"commands\": {\n    \"search \u003cquery\u003e\": \"Search for items. Modes: web, academic, news.\",\n    \"config show\": \"Display current configuration.\",\n    \"config set \u003ckey\u003e \u003cvalue\u003e\": \"Set a configuration value.\",\n    \"agent-info | info\": \"This manifest.\",\n    \"skill install\": \"Install skill file to agent platforms.\",\n    \"update [--check]\": \"Self-update from GitHub Releases.\"\n  },\n  \"flags\": {\n    \"--json\": \"Force JSON output (auto-enabled when piped)\",\n    \"--quiet\": \"Suppress non-essential output\"\n  },\n  \"exit_codes\": {\n    \"0\": \"Success\",\n    \"1\": \"Transient error (IO, network) -- retry\",\n    \"2\": \"Config error -- fix setup\",\n    \"3\": \"Bad input -- fix arguments\",\n    \"4\": \"Rate limited -- wait and retry\"\n  },\n  \"envelope\": {\n    \"version\": \"1\",\n    \"success\": \"{ version, status, data }\",\n    \"error\": \"{ version, status, error: { code, message, suggestion } }\"\n  },\n  \"config_path\": \"~/.config/mycli/config.toml\",\n  \"auto_json_when_piped\": true,\n  \"env_prefix\": \"MYCLI_\"\n}\n```\n\n`agent-info` always outputs raw JSON (not wrapped in the envelope). It IS the schema definition, not a command that returns data.\n\n**Known limitation: manifest drift.** The `agent-info` manifest is hand-maintained. It can desync from the actual clap definition. Mitigation: treat `agent-info` as a tested contract. If `agent-info` advertises a command, that command must work. If it doesn't, that's a P0 bug. Add integration tests that verify every command listed in `agent-info` is routable.\n\n### Pattern 2: Structured Output -- JSON Envelope\n\nAuto-detected via `std::io::IsTerminal`:\n- **Terminal (TTY):** Colored table for humans\n- **Piped/redirected:** JSON envelope for agents\n\n**Success envelope** (stdout):\n```json\n{\n  \"version\": \"1\",\n  \"status\": \"success\",\n  \"data\": { }\n}\n```\n\n**Error envelope** (stderr):\n```json\n{\n  \"version\": \"1\",\n  \"status\": \"error\",\n  \"error\": {\n    \"code\": \"invalid_input\",\n    \"message\": \"Name cannot be empty\",\n    \"suggestion\": \"Provide a non-empty name as the first argument\"\n  }\n}\n```\n\n**Extended status values** for operations that talk to multiple sources:\n\n| Status | Meaning |\n|--------|---------|\n| `success` | All operations completed, results returned |\n| `partial_success` | Some operations completed, some failed -- results + errors returned |\n| `all_failed` | Every operation failed -- no results |\n| `no_results` | Operations completed but returned no matches |\n\n**Stderr contract:** Errors always go to stderr (both JSON and human-readable). This ensures `tool search \"foo\" | jq` never breaks, even on error. Agents that need to read errors should check both the exit code and stderr.\n\n### Pattern 3: Semantic Exit Codes\n\nSee [Philosophy #5](#5-exit-codes-are-contracts). Every command, every code path, every error -- maps to one of `0, 1, 2, 3, 4`. No exceptions.\n\n### Pattern 4: Skill Self-Install\n\nThe binary carries a minimal SKILL.md as an embedded constant (via `const` or `include_str!`). One command writes it to agent platform directories:\n\n```\n~/.claude/skills/\u003cname\u003e/SKILL.md\n~/.codex/skills/\u003cname\u003e/SKILL.md\n~/.gemini/skills/\u003cname\u003e/SKILL.md\n```\n\nThe skill is a signpost -- a few lines saying \"this tool exists, run `agent-info` for everything else.\" All workflow knowledge lives in the binary. Binary update = skill update. No drift.\n\n### Pattern 6: Rich Help with Tips and Examples\n\n`--help` is the first thing an agent reads. Clap's auto-generated help lists flags but doesn't teach usage. Add contextual tips and real-world examples using clap's `after_long_help`:\n\n```rust\n#[derive(Parser)]\n#[command(\n    name = \"mycli\",\n    about = \"What this CLI does in one sentence\",\n    after_long_help = HELP_FOOTER,\n)]\npub struct Cli { /* ... */ }\n\nconst HELP_FOOTER: \u0026str = \"\\\nTips:\n  • Run `mycli agent-info | jq` to see the full capability manifest\n  • Pipe output to jq for structured data: `mycli search \\\"query\\\" | jq '.data.results'`\n  • Config is 3-tier: defaults \u003c config.toml \u003c env vars (MYCLI_ prefix)\n  • Use --quiet to suppress human output while keeping JSON intact\n  • doctor checks dependencies before you start: `mycli doctor`\n\nExamples:\n  mycli search \\\"CRISPR gene therapy\\\" --mode academic\n    Search academic sources for gene therapy papers\n\n  mycli config set keys.api_key sk-proj-abc123\n    Set your API key (stored in ~/.config/mycli/config.toml)\n\n  mycli search \\\"latest news\\\" | jq '.data.results[0]'\n    Get the first result as structured JSON\";\n```\n\nTips should be 3-8 bullets covering the most common agent workflows. Examples should be 3-5 real commands with one-line descriptions. Both survive into `--help` output where agents and humans read them.\n\n### Pattern 7: Doctor -- Dependency Diagnostics\n\nFor CLIs with external dependencies (API keys, binaries on PATH, network endpoints), a `doctor` command tells agents \"can this tool actually work right now?\" before they attempt real work.\n\n```bash\n# Agent runs doctor before first use\nmycli doctor --json | jq '.data.checks[] | select(.status == \"fail\")'\n\n# Human output\nmycli doctor\n```\n\nReturns structured pass/warn/fail checks:\n\n```json\n{\n  \"version\": \"1\",\n  \"status\": \"success\",\n  \"data\": {\n    \"checks\": [\n      { \"name\": \"config_file\", \"status\": \"pass\", \"message\": \"~/.config/mycli/config.toml\" },\n      { \"name\": \"api_key\",     \"status\": \"pass\", \"message\": \"MYCLI_API_KEY set (sk-p...1234)\" },\n      { \"name\": \"ffmpeg\",      \"status\": \"fail\", \"message\": \"ffmpeg not found on PATH\",\n        \"suggestion\": \"Install ffmpeg: brew install ffmpeg\" }\n    ],\n    \"summary\": { \"pass\": 2, \"warn\": 0, \"fail\": 1 }\n  }\n}\n```\n\nExit code: `0` if all checks pass, `2` (config error) if any fail. Agents use this to self-diagnose before retrying.\n\n### Pattern 8: Duplicate Guard\n\nPrevent expensive or irreversible operations from running twice accidentally. Use a lock file in the state directory with PID tracking and staleness detection.\n\nWhen an agent retries a failed command, or two agents target the same CLI concurrently, the guard catches it and suggests `--force` instead of silently doubling the work (or cost).\n\n```bash\nmycli deploy                  # Creates lock, runs deploy\nmycli deploy                  # \"Operation already running. Use --force to override.\" (exit 3)\nmycli deploy --force          # Bypasses guard\n```\n\n### Pattern 5: Self-Update\n\nThree install paths, one update mechanism:\n\n```bash\n# Install (pick any):\nbrew tap your-org/tap \u0026\u0026 brew install your-cli\ncargo install your-cli\ncurl -fsSL https://your-cli.dev/install.sh | sh\n\n# Self-update (built into the binary):\nyour-cli update --check      # check for new version\nyour-cli update              # pull latest from GitHub Releases\nyour-cli skill install       # re-deploy updated skill\n```\n\nSelf-update should be disableable via config (`update.enabled = false`) for managed environments.\n\n---\n\n## Reusable Modules\n\nThese are battle-tested patterns extracted from production CLIs. Each module is self-contained -- copy the pattern into your CLI and adapt.\n\n### Output Format Detection and Context\n\nDetect whether to output JSON or human-readable, based on `--json` flag or pipe detection. Bundle format + quiet into a `Ctx` that gets passed to all commands.\n\n```rust\n#[derive(Clone, Copy)]\npub enum Format {\n    Json,\n    Human,\n}\n\nimpl Format {\n    pub fn detect(json_flag: bool) -\u003e Self {\n        if json_flag || !std::io::stdout().is_terminal() {\n            Format::Json\n        } else {\n            Format::Human\n        }\n    }\n}\n\n/// Output context: bundles format + quiet so commands take one parameter.\n#[derive(Clone, Copy)]\npub struct Ctx {\n    pub format: Format,\n    pub quiet: bool,\n}\n\nimpl Ctx {\n    pub fn new(json_flag: bool, quiet: bool) -\u003e Self {\n        Self { format: Format::detect(json_flag), quiet }\n    }\n}\n```\n\n### JSON Envelope Helpers\n\n`print_success_or` is the workhorse -- it handles JSON automatically and lets you provide a closure for human output. `--quiet` suppresses human output; JSON always emits. `print_error` sends errors to stderr in both formats (never suppressed by `--quiet`).\n\n```rust\nuse serde::Serialize;\n\n/// Safe serialization: never panics, never produces invalid JSON.\nfn safe_json_string\u003cT: Serialize\u003e(value: \u0026T) -\u003e String {\n    match serde_json::to_string_pretty(value) {\n        Ok(s) =\u003e s,\n        Err(e) =\u003e {\n            let fallback = serde_json::json!({\n                \"version\": \"1\",\n                \"status\": \"error\",\n                \"error\": {\n                    \"code\": \"serialize\",\n                    \"message\": e.to_string(),\n                    \"suggestion\": \"Retry the command\",\n                },\n            });\n            serde_json::to_string_pretty(\u0026fallback).unwrap_or_else(|_| {\n                r#\"{\"version\":\"1\",\"status\":\"error\",\"error\":{\"code\":\"serialize\",\"message\":\"serialization failed\",\"suggestion\":\"Retry the command\"}}\"#.to_string()\n            })\n        }\n    }\n}\n\npub fn print_success_or\u003cT: Serialize, F: FnOnce(\u0026T)\u003e(ctx: Ctx, data: \u0026T, human: F) {\n    match ctx.format {\n        Format::Json =\u003e {\n            let envelope = serde_json::json!({\n                \"version\": \"1\",\n                \"status\": \"success\",\n                \"data\": data,\n            });\n            println!(\"{}\", safe_json_string(\u0026envelope));\n        }\n        Format::Human if !ctx.quiet =\u003e human(data),\n        Format::Human =\u003e {} // quiet: suppress human output\n    }\n}\n\npub fn print_error(format: Format, err: \u0026AppError) {\n    let envelope = serde_json::json!({\n        \"version\": \"1\",\n        \"status\": \"error\",\n        \"error\": {\n            \"code\": err.error_code(),\n            \"message\": err.to_string(),\n            \"suggestion\": err.suggestion(),\n        },\n    });\n    match format {\n        Format::Json =\u003e eprintln!(\"{}\", safe_json_string(\u0026envelope)),\n        Format::Human =\u003e {\n            eprintln!(\"error: {err}\");\n            eprintln!(\"  {}\", err.suggestion());\n        }\n    }\n}\n```\n\n### Error Type\n\nEvery CLI error enum implements three methods. This is the contract that makes semantic exit codes and error envelopes work together.\n\n```rust\n#[derive(thiserror::Error, Debug)]\npub enum AppError {\n    #[error(\"Invalid input: {0}\")]\n    InvalidInput(String),\n\n    #[error(\"Configuration error: {0}\")]\n    Config(String),\n\n    #[error(\"{0}\")]\n    Transient(String),\n\n    #[error(\"Rate limited: {0}\")]\n    RateLimited(String),\n\n    #[error(\"IO error: {0}\")]\n    Io(#[from] std::io::Error),\n\n    #[error(\"Update failed: {0}\")]\n    Update(String),\n}\n\nimpl AppError {\n    /// Maps to process exit code: 1=transient, 2=config, 3=input, 4=rate-limited\n    pub fn exit_code(\u0026self) -\u003e i32 {\n        match self {\n            Self::InvalidInput(_) =\u003e 3,\n            Self::Config(_) =\u003e 2,\n            Self::Transient(_) | Self::Io(_) | Self::Update(_) =\u003e 1,\n            Self::RateLimited(_) =\u003e 4,\n        }\n    }\n\n    /// Machine-readable code for JSON: \"invalid_input\", \"config_error\", etc.\n    pub fn error_code(\u0026self) -\u003e \u0026str {\n        match self {\n            Self::InvalidInput(_) =\u003e \"invalid_input\",\n            Self::Config(_) =\u003e \"config_error\",\n            Self::Transient(_) =\u003e \"transient_error\",\n            Self::RateLimited(_) =\u003e \"rate_limited\",\n            Self::Io(_) =\u003e \"io_error\",\n            Self::Update(_) =\u003e \"update_error\",\n        }\n    }\n\n    /// Tested recovery instruction. Agents follow this literally.\n    pub fn suggestion(\u0026self) -\u003e \u0026str {\n        match self {\n            Self::InvalidInput(_) =\u003e \"Check arguments with: mycli --help\",\n            Self::Config(_) =\u003e \"Check config with: mycli config show\",\n            Self::Transient(_) | Self::Io(_) =\u003e \"Retry the command\",\n            Self::RateLimited(_) =\u003e \"Wait a moment and retry\",\n            Self::Update(_) =\u003e \"Retry later, or install manually via cargo install mycli\",\n        }\n    }\n}\n```\n\nAdapt the variants to your domain. The three methods (`exit_code`, `error_code`, `suggestion`) are the contract -- keep those consistent.\n\n### Entry Point Runner\n\nThe `main()` function follows a strict pattern: pre-scan for `--json`, parse with `try_parse()`, handle help/version as success, wrap clap errors in the envelope (never let clap own the exit code), detect format, dispatch, exit with semantic code.\n\n```rust\n/// Pre-scan argv for --json before clap parses. This ensures --json is\n/// honored on help, version, and parse-error paths.\nfn has_json_flag() -\u003e bool {\n    std::env::args_os().any(|a| a == \"--json\")\n}\n\nfn main() {\n    let json_flag = has_json_flag();\n\n    let cli = match Cli::try_parse() {\n        Ok(cli) =\u003e cli,\n        Err(e) =\u003e {\n            // Help and --version are not errors. Exit 0.\n            if matches!(\n                e.kind(),\n                clap::error::ErrorKind::DisplayHelp\n                    | clap::error::ErrorKind::DisplayVersion\n            ) {\n                let format = Format::detect(json_flag);\n                match format {\n                    Format::Json =\u003e {\n                        print_help_json(e);\n                        std::process::exit(0);\n                    }\n                    Format::Human =\u003e e.exit(),\n                }\n            }\n\n            // Parse errors -- we own the exit code, not clap. Always exit 3.\n            let format = Format::detect(json_flag);\n            print_clap_error(format, \u0026e);\n            std::process::exit(3);\n        }\n    };\n\n    let ctx = Ctx::new(cli.json, cli.quiet);\n\n    if let Err(e) = run(cli, ctx) {\n        print_error(ctx.format, \u0026e);\n        std::process::exit(e.exit_code());\n    }\n}\n```\n\n### Config Loading\n\nThree-tier precedence: compiled defaults, then TOML file, then environment variables. Environment variables use a prefix (`MYCLI_`) and map to dotted keys (`MYCLI_KEYS_BRAVE` -\u003e `keys.brave`).\n\n```rust\nuse figment::{Figment, providers::{Env, Format as _, Serialized, Toml}};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Config {\n    pub keys: Keys,\n    pub settings: Settings,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Keys {\n    pub api_key: Option\u003cString\u003e,\n}\n\n#[derive(Debug, Serialize, Deserialize)]\npub struct Settings {\n    pub timeout: u64,\n}\n\nimpl Default for Config {\n    fn default() -\u003e Self {\n        Self {\n            keys: Keys { api_key: None },\n            settings: Settings { timeout: 30 },\n        }\n    }\n}\n\npub fn load_config(config_path: \u0026std::path::Path) -\u003e Result\u003cConfig, figment::Error\u003e {\n    Figment::from(Serialized::defaults(Config::default()))\n        .merge(Toml::file(config_path))\n        .merge(Env::prefixed(\"MYCLI_\").split(\"_\"))\n        .extract()\n}\n```\n\nConfig path: `~/.config/\u003capp\u003e/config.toml`. Use the `directories` crate to resolve platform-appropriate paths:\n\n```rust\npub fn config_dir(app_name: \u0026str) -\u003e std::path::PathBuf {\n    directories::ProjectDirs::from(\"\", \"\", app_name)\n        .map(|d| d.config_dir().to_path_buf())\n        .unwrap_or_else(|| {\n            let home = std::env::var(\"HOME\")\n                .or_else(|_| std::env::var(\"USERPROFILE\"))\n                .unwrap_or_else(|_| \".\".into());\n            std::path::PathBuf::from(home).join(\".config\").join(app_name)\n        })\n}\n```\n\n### Secret Handling\n\nSecrets resolve through a priority chain: explicit flag, then environment variable, then config file. Never store secrets in state databases. Always mask on display.\n\n```rust\n/// Resolve a secret from multiple sources. First non-empty value wins.\npub fn resolve_secret(\n    flag_value: Option\u003c\u0026str\u003e,\n    env_var: \u0026str,\n) -\u003e Option\u003cString\u003e {\n    // 1. Explicit flag\n    if let Some(v) = flag_value {\n        let v = v.trim();\n        if !v.is_empty() {\n            return Some(v.to_string());\n        }\n    }\n    // 2. Environment variable\n    if let Ok(v) = std::env::var(env_var) {\n        let v = v.trim().to_string();\n        if !v.is_empty() {\n            return Some(v);\n        }\n    }\n    None\n}\n\n/// Mask a secret for display: \"sk-proj-abc...xyz1234\"\n/// Uses char boundaries (not byte offsets) to avoid panics on non-ASCII input.\npub fn mask_secret(value: \u0026str) -\u003e String {\n    if value.is_empty() {\n        return \"(not set)\".to_string();\n    }\n    let chars: Vec\u003cchar\u003e = value.chars().collect();\n    if chars.len() \u003c= 8 {\n        let prefix: String = chars[..2.min(chars.len())].iter().collect();\n        format!(\"{prefix}***\")\n    } else {\n        let prefix: String = chars[..4].iter().collect();\n        let suffix: String = chars[chars.len() - 4..].iter().collect();\n        format!(\"{prefix}...{suffix}\")\n    }\n}\n```\n\n### Standard Paths (XDG)\n\nConsistent directory layout across all CLIs:\n\n```rust\nuse std::path::PathBuf;\n\npub struct AppPaths {\n    pub config_dir: PathBuf,\n    pub data_dir: PathBuf,\n    pub cache_dir: PathBuf,\n}\n\nimpl AppPaths {\n    pub fn new(app_name: \u0026str) -\u003e Self {\n        let dirs = directories::ProjectDirs::from(\"\", \"\", app_name);\n        Self {\n            config_dir: dirs.as_ref()\n                .map(|d| d.config_dir().to_path_buf())\n                .unwrap_or_else(|| home().join(\".config\").join(app_name)),\n            data_dir: dirs.as_ref()\n                .map(|d| d.data_dir().to_path_buf())\n                .unwrap_or_else(|| home().join(\".local/share\").join(app_name)),\n            cache_dir: dirs.as_ref()\n                .map(|d| d.cache_dir().to_path_buf())\n                .unwrap_or_else(|| home().join(\".cache\").join(app_name)),\n        }\n    }\n\n    pub fn config_file(\u0026self) -\u003e PathBuf {\n        self.config_dir.join(\"config.toml\")\n    }\n\n    pub fn ensure_dirs(\u0026self) -\u003e std::io::Result\u003c()\u003e {\n        std::fs::create_dir_all(\u0026self.config_dir)?;\n        std::fs::create_dir_all(\u0026self.data_dir)?;\n        std::fs::create_dir_all(\u0026self.cache_dir)?;\n        Ok(())\n    }\n}\n\nfn home() -\u003e PathBuf {\n    std::env::var(\"HOME\")\n        .or_else(|_| std::env::var(\"USERPROFILE\"))\n        .map(PathBuf::from)\n        .unwrap_or_else(|_| PathBuf::from(\".\"))\n}\n```\n\n### Command Naming Conventions\n\nAgents learn patterns from one subcommand group and apply them everywhere. Two rules:\n\n**1. Always alias CRUD subcommands.**\n\n| Operation | Primary | Alias | Attribute |\n|-----------|---------|-------|-----------|\n| List | `list` | `ls` | `#[command(visible_alias = \"ls\")]` |\n| Create | `create` | `new` | `#[command(visible_alias = \"new\")]` |\n| Delete | `delete` | `rm` | `#[command(visible_alias = \"rm\")]` |\n| Show | `show` | `get` | `#[command(visible_alias = \"get\")]` |\n\n**2. Be consistent across subcommand groups.** If `inbox list` works, `account list` must also work. Same names, same aliases, same argument patterns.\n\nDocument aliases in `agent-info` using `\"list | ls\"` format so agents discover both forms.\n\n### Doctor Command\n\nStructured dependency checker. Each check returns pass/warn/fail with a message and optional suggestion. The doctor command itself always exits 0 on all-pass, 2 on any failure.\n\n```rust\nuse serde::Serialize;\n\n#[derive(Serialize)]\npub struct DoctorCheck {\n    pub name: \u0026'static str,\n    pub status: CheckStatus,\n    pub message: String,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub suggestion: Option\u003cString\u003e,\n}\n\n#[derive(Serialize, PartialEq)]\n#[serde(rename_all = \"lowercase\")]\npub enum CheckStatus { Pass, Warn, Fail }\n\n#[derive(Serialize)]\npub struct DoctorReport {\n    pub checks: Vec\u003cDoctorCheck\u003e,\n    pub summary: DoctorSummary,\n}\n\n#[derive(Serialize)]\npub struct DoctorSummary {\n    pub pass: usize,\n    pub warn: usize,\n    pub fail: usize,\n}\n\nimpl DoctorReport {\n    pub fn has_failures(\u0026self) -\u003e bool {\n        self.summary.fail \u003e 0\n    }\n}\n\n/// Check if a binary exists on PATH.\npub fn check_binary(name: \u0026str) -\u003e DoctorCheck {\n    match which::which(name) {\n        Ok(path) =\u003e DoctorCheck {\n            name: \"binary\",\n            status: CheckStatus::Pass,\n            message: format!(\"{name} found at {}\", path.display()),\n            suggestion: None,\n        },\n        Err(_) =\u003e DoctorCheck {\n            name: \"binary\",\n            status: CheckStatus::Fail,\n            message: format!(\"{name} not found on PATH\"),\n            suggestion: Some(format!(\"Install {name}: brew install {name}\")),\n        },\n    }\n}\n\n/// Check if an env var is set and non-empty.\npub fn check_env_var(var: \u0026str) -\u003e DoctorCheck {\n    match std::env::var(var) {\n        Ok(v) if !v.trim().is_empty() =\u003e DoctorCheck {\n            name: \"env_var\",\n            status: CheckStatus::Pass,\n            message: format!(\"{var} set ({})\", mask_secret(\u0026v)),\n            suggestion: None,\n        },\n        _ =\u003e DoctorCheck {\n            name: \"env_var\",\n            status: CheckStatus::Fail,\n            message: format!(\"{var} not set\"),\n            suggestion: Some(format!(\"Set {var} in your environment or config file\")),\n        },\n    }\n}\n\n/// Check if config file exists.\npub fn check_config_file(path: \u0026std::path::Path) -\u003e DoctorCheck {\n    if path.exists() {\n        DoctorCheck {\n            name: \"config_file\",\n            status: CheckStatus::Pass,\n            message: format!(\"{}\", path.display()),\n            suggestion: None,\n        }\n    } else {\n        DoctorCheck {\n            name: \"config_file\",\n            status: CheckStatus::Warn,\n            message: format!(\"{} not found (using defaults)\", path.display()),\n            suggestion: Some(format!(\"Create config: mycli config show \u003e {}\", path.display())),\n        }\n    }\n}\n```\n\nAdd `which = \"7\"` to dependencies if checking binaries on PATH. Compose checks in your doctor command:\n\n```rust\npub fn run_doctor(ctx: Ctx, config: \u0026Config) -\u003e Result\u003c(), AppError\u003e {\n    let mut checks = vec![\n        check_config_file(\u0026config.path),\n        check_env_var(\"MYCLI_API_KEY\"),\n    ];\n    // Add domain-specific checks\n    if config.features.transcription {\n        checks.push(check_binary(\"ffmpeg\"));\n    }\n    let summary = DoctorSummary {\n        pass: checks.iter().filter(|c| c.status == CheckStatus::Pass).count(),\n        warn: checks.iter().filter(|c| c.status == CheckStatus::Warn).count(),\n        fail: checks.iter().filter(|c| c.status == CheckStatus::Fail).count(),\n    };\n    let report = DoctorReport { checks, summary };\n    let has_failures = report.has_failures();\n    print_success_or(ctx, \u0026report, |r| {\n        for check in \u0026r.checks {\n            let icon = match check.status {\n                CheckStatus::Pass =\u003e \"✓\",\n                CheckStatus::Warn =\u003e \"!\",\n                CheckStatus::Fail =\u003e \"✗\",\n            };\n            eprintln!(\"  {icon} {}: {}\", check.name, check.message);\n        }\n    });\n    if has_failures {\n        return Err(AppError::Config(\"Doctor found issues. Run with --json for details.\".into()));\n    }\n    Ok(())\n}\n```\n\n### Duplicate Guard\n\nLock file pattern for expensive operations. Uses PID tracking to detect stale locks from crashed processes.\n\n```rust\nuse serde::{Deserialize, Serialize};\nuse std::path::PathBuf;\n\n#[derive(Serialize, Deserialize)]\nstruct LockFile {\n    pid: u32,\n    started_at: String,\n    operation: String,\n}\n\nconst STALE_THRESHOLD_SECS: u64 = 3600; // 1 hour\n\npub struct DuplicateGuard {\n    lock_path: PathBuf,\n}\n\nimpl DuplicateGuard {\n    pub fn new(data_dir: \u0026std::path::Path, operation: \u0026str) -\u003e Self {\n        let lock_dir = data_dir.join(\"locks\");\n        let _ = std::fs::create_dir_all(\u0026lock_dir);\n        Self {\n            lock_path: lock_dir.join(format!(\"{operation}.lock\")),\n        }\n    }\n\n    /// Check if the operation is already running. Returns Ok(()) if safe to proceed.\n    pub fn acquire(\u0026self, force: bool) -\u003e Result\u003c(), AppError\u003e {\n        if let Ok(contents) = std::fs::read_to_string(\u0026self.lock_path) {\n            if let Ok(lock) = serde_json::from_str::\u003cLockFile\u003e(\u0026contents) {\n                // Check if the process is still alive\n                let pid_alive = unsafe { libc::kill(lock.pid as i32, 0) == 0 };\n                let is_stale = chrono::Utc::now()\n                    .signed_duration_since(\n                        chrono::DateTime::parse_from_rfc3339(\u0026lock.started_at)\n                            .unwrap_or_default()\n                    )\n                    .num_seconds() \u003e STALE_THRESHOLD_SECS as i64;\n\n                if pid_alive \u0026\u0026 !is_stale \u0026\u0026 !force {\n                    return Err(AppError::InvalidInput(format!(\n                        \"Operation '{}' already running (pid {}). Use --force to override.\",\n                        lock.operation, lock.pid\n                    )));\n                }\n            }\n        }\n        // Write new lock\n        let lock = LockFile {\n            pid: std::process::id(),\n            started_at: chrono::Utc::now().to_rfc3339(),\n            operation: self.lock_path.file_stem()\n                .unwrap_or_default().to_string_lossy().into(),\n        };\n        std::fs::write(\u0026self.lock_path, serde_json::to_string(\u0026lock).unwrap())?;\n        Ok(())\n    }\n\n    /// Release the lock. Call on completion (success or failure).\n    pub fn release(\u0026self) {\n        let _ = std::fs::remove_file(\u0026self.lock_path);\n    }\n}\n\nimpl Drop for DuplicateGuard {\n    fn drop(\u0026mut self) {\n        self.release();\n    }\n}\n```\n\nUsage in a command:\n\n```rust\npub fn run_deploy(ctx: Ctx, config: \u0026Config, force: bool) -\u003e Result\u003c(), AppError\u003e {\n    let guard = DuplicateGuard::new(\u0026config.data_dir, \"deploy\");\n    guard.acquire(force)?;\n    // ... expensive work happens here ...\n    // guard.release() called automatically via Drop\n    Ok(())\n}\n```\n\nAdd `chrono = \"0.4\"` and `libc = \"0.2\"` to dependencies if using this pattern. The `Drop` impl ensures cleanup even on early returns or panics.\n\n### HTTP Retry with Backoff\n\nFor CLIs that make network calls:\n\n```rust\nuse std::time::Duration;\n\n/// Linear backoff: 700ms * (attempt + 1)\npub fn backoff(attempt: usize) -\u003e Duration {\n    Duration::from_millis(700 * (attempt as u64 + 1))\n}\n\n/// Respect the server's Retry-After header, fall back to backoff\npub fn retry_delay(headers: \u0026reqwest::header::HeaderMap, attempt: usize) -\u003e Duration {\n    headers\n        .get(\"retry-after\")\n        .and_then(|v| v.to_str().ok())\n        .and_then(|v| v.parse::\u003cu64\u003e().ok())\n        .map(Duration::from_secs)\n        .unwrap_or_else(|| backoff(attempt))\n}\n\n/// Should we retry this request error?\npub fn should_retry(err: \u0026reqwest::Error) -\u003e bool {\n    err.is_timeout() || err.is_connect() || err.is_request()\n}\n```\n\n---\n\n## Getting Started: Build Your Own\n\n**1. Copy the scaffold:**\n\n```bash\ncp -r example/ my-cli/\ncd my-cli/\n```\n\n**2. Rename the binary** in `Cargo.toml`:\n\n```toml\n[package]\nname = \"my-cli\"                      # Your binary name\nversion = \"0.1.0\"\nedition = \"2024\"\nrust-version = \"1.85\"\n```\n\nUpdate the `[[bin]]` section and the `#[command(name = \"...\")]` in `cli.rs`.\n\n**3. Replace the `hello` command** with your domain logic. Keep the same structure:\n\n```\nsrc/\n  main.rs           # Entry point (barely changes between CLIs)\n  cli.rs            # clap derive definitions\n  config.rs         # 3-tier config loading\n  error.rs          # AppError with exit_code(), error_code(), suggestion()\n  output.rs         # Format detection + envelope helpers\n  commands/\n    mod.rs\n    agent_info.rs   # Update: list YOUR commands\n    your_command.rs  # Your domain logic\n    skill.rs        # Skill content auto-derived from CARGO_PKG_NAME\n    config.rs       # config show/path (works out of the box)\n    update.rs       # Self-update (just change repo owner/name in config)\n```\n\n**4. Update `agent-info`** to list your actual commands with argument schemas. This is the contract agents bootstrap from.\n\n**5. Write tests and run them:**\n\n```bash\ncargo test                           # All integration tests\ncargo run -- agent-info              # Verify manifest\ncargo run -- config show             # Verify config loading\necho '{}' | cargo run -- hello Test  # Verify JSON envelope in pipe\n```\n\n**6. Ship it:**\n\n```bash\ncargo build --release                # Single binary, sub-10ms cold start\n./target/release/my-cli skill install  # Deploy to Claude/Codex/Gemini\n```\n\nThe framework conventions (`env!(\"CARGO_PKG_NAME\")`, config loading, skill install) adapt automatically when you rename the package. No find-and-replace needed.\n\n---\n\n## Example\n\nThe `example/` directory contains a modular `greeter` CLI demonstrating all core patterns: agent-info with argument schemas, JSON envelope, semantic exit codes (0-4), `--json` pre-scan, `--quiet` flag, config loading via Figment, skill self-install, and self-update. It includes 40 integration tests that verify every contract.\n\n```\nexample/\n  src/\n    main.rs           # Entry point -- pre-scan --json, parse, dispatch, exit\n    cli.rs            # Clap definitions: Cli, Commands, Style (ValueEnum)\n    config.rs         # 3-tier config loading (defaults -\u003e TOML -\u003e env vars)\n    error.rs          # AppError with exit_code(), error_code(), suggestion()\n    output.rs         # Format detection, Ctx struct, envelope helpers\n    commands/\n      mod.rs          # Command router\n      hello.rs        # Domain command (the actual feature)\n      agent_info.rs   # Enriched capability manifest with arg schemas\n      config.rs       # config show / config path\n      skill.rs        # Skill install + status\n      update.rs       # Self-update\n      contract.rs     # Hidden: deterministic exit-code trigger for tests\n  tests/\n    exit_code_contracts.rs    # All 5 exit codes verified\n    output_contracts.rs       # JSON envelope shape, quiet flag, help wrapping\n    agent_info_contract.rs    # Manifest fields, routable commands, arg schemas\n    robustness.rs             # Malformed config resilience, edge cases\n  Cargo.toml\n```\n\nBuild and run:\n\n```bash\ngit clone https://github.com/paperfoot/agent-cli-framework.git\ncd agent-cli-framework/example\ncargo build --release\n\n# Human output (terminal)\n./target/release/greeter hello Boris --style pirate\n\n# Agent output (piped)\n./target/release/greeter hello Boris | jq\n\n# Capability discovery\n./target/release/greeter agent-info\n\n# Semantic exit code on error\n./target/release/greeter hello \"\"\necho $?  # 3 (bad input)\n\n# Skill installation\n./target/release/greeter skill install\n```\n\n---\n\n## Invariants\n\nThese are non-negotiable rules. If a CLI violates any of these, it is broken.\n\n1. **Every code path that writes to stdout respects the output format.** No raw text leaks when piped. Not from `config show`. Not from `update --check`. Not from error recovery paths.\n\n2. **`--help` and `--version` exit 0.** Always. Even when piped. Wrap in success envelope when not a TTY.\n\n3. **`agent-info` matches reality.** Every command listed is routable. Every flag described works. Every env var is named correctly. If it drifts, that's a P0 bug.\n\n4. **Errors include suggestions.** Every error envelope has a `suggestion` field. The suggestion is a tested, executable instruction. \"Try running with elevated permissions\" is not acceptable -- be specific.\n\n5. **Exit codes match the documented contract.** 0 means success. 1-4 mean what they say. Nothing else.\n\n6. **JSON on stdout, errors on stderr.** An agent running `tool command | jq` must never see error text on stdout. Errors go to stderr in both formats.\n\n7. **No interactive prompts.** The CLI never reads from stdin, never opens a pager, never asks \"are you sure?\" Destructive operations take `--confirm` as a flag.\n\n8. **Secrets are never logged or displayed in plain text.** Use `mask_secret()` for any display. Never include raw secrets in error messages, suggestions, or JSON output.\n\n---\n\n## Mistakes We Made\n\nThese came from shipping CLIs with these patterns. Every one went to production before we caught it.\n\n**Wrong suggestions.** Our search CLI told agents to set `SEARCH_BRAVE_KEY` when the actual env var was `SEARCH_KEYS_BRAVE`. The agent followed the suggestion exactly, set the wrong variable, and reported auth still broken. Suggestions are instructions. Test them.\n\n**JSON only on the main command.** The primary `search` command returned proper JSON envelopes. But `config show`, `update --check`, and cache-miss paths printed raw text. An agent piping stdout into a JSON parser got a crash instead of data. Every code path must respect the output format.\n\n**Success that was failure.** All eleven providers errored out. The response: `{\"status\": \"success\", \"results\": []}`. The agent saw success and moved on. We added `partial_success` and `all_failed` as additional status values.\n\n**Dead features in agent-info.** The manifest advertised search modes that existed in code but were never wired into the dispatch path. An agent called `search --mode deep` and got \"unknown mode\" despite agent-info promising it worked. If agent-info says the tool can do something, it must actually do it.\n\n**`--help` returned exit code 3.** We used `try_parse()` and routed all clap errors through the JSON error handler. But `--help` and `--version` aren't errors. An agent ran `tool --help`, got exit code 3 and a suggestion to \"check arguments with --help.\" It thought it had made a mistake. The fix: check `e.kind()` for `DisplayHelp` and `DisplayVersion`, exit 0.\n\n**Inconsistent subcommand names.** Our `inbox` group used `ls` but `account` used `list`. An agent that learned `inbox ls` tried `account ls` and failed. Use `visible_alias` to accept both forms everywhere.\n\n**Permission error suggested escalation.** An IO error with `PermissionDenied` suggested \"try running with elevated permissions.\" An agent ran `sudo` on a search CLI. The suggestion should have been \"check file permissions on ~/.config/mycli/\" -- specific and safe.\n\n---\n\n## Standard Dependencies\n\nCurated set of crates for agent-friendly CLIs:\n\n```toml\n[dependencies]\n# CLI\nclap = { version = \"4\", features = [\"derive\", \"env\"] }\n\n# Output\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\ncomfy-table = \"7\"            # Human-readable tables\nowo-colors = \"4\"             # Terminal colors\n\n# Errors\nthiserror = \"2\"\nanyhow = \"1\"                 # For internal/unexpected errors\n\n# Config\nfigment = { version = \"0.10\", features = [\"toml\", \"env\"] }\ntoml = \"0.8\"                 # For config file mutations\n\n# Paths\ndirectories = \"6\"\n\n# Doctor (if checking binaries on PATH)\nwhich = \"7\"\n\n# Duplicate guard (if using lock files with timestamps)\nchrono = \"0.4\"\nlibc = \"0.2\"\n\n# HTTP (if making network calls)\nreqwest = { version = \"0.12\", features = [\"json\", \"rustls-tls\"] }\n\n# Self-update (optional)\nself_update = { version = \"0.42\", features = [\"archive-tar\", \"compression-flate2\"] }\n\n[profile.release]\nlto = true\ncodegen-units = 1\nstrip = true\nopt-level = 3\n```\n\n---\n\n## Production CLIs Using This Architecture\n\n| CLI | What it does | Install |\n|-----|-------------|---------|\n| [search-cli](https://github.com/paperfoot/search-cli) | 11 search providers, 14 modes, one binary | `cargo install agent-search` |\n| [autoresearch](https://github.com/paperfoot/autoresearch-cli) | Autonomous experiment loops for any metric | `cargo install autoresearch` |\n| [xmaster](https://github.com/paperfoot/xmaster-cli) | X/Twitter CLI with dual backends | `cargo install xmaster` |\n| [email-cli](https://github.com/paperfoot/email-cli) | Agent-friendly email via Resend API | `cargo install email-cli` |\n\n---\n\n## Further Reading\n\n- [MCP vs CLI: Benchmarking AI Agent Cost \u0026 Reliability](https://www.scalekit.com/blog/mcp-vs-cli-use) -- Scalekit\n- [Your MCP Server Is Eating Your Context Window](https://www.apideck.com/blog/mcp-server-eating-context-window-cli-alternative) -- Apideck\n- [CLI Is the New API and MCP](https://jonnyzzz.com/blog/2026/02/20/cli-tools-for-ai-agents/) -- Eugene Petrenko\n- [Reducing MCP Token Usage by 100x](https://www.speakeasy.com/blog/how-we-reduced-token-usage-by-100x-dynamic-toolsets-v2) -- Speakeasy\n\n## Contributing\n\nContributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n## License\n\nMIT -- see [LICENSE](LICENSE).\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\nBuilt by [Boris Djordjevic](https://github.com/longevityboris) at [199 Biotechnologies](https://github.com/199-biotechnologies) | [Paperfoot AI](https://paperfoot.ai)\n\n\u003cbr /\u003e\n\n**If this is useful to you:**\n\n[![Star this repo](https://img.shields.io/github/stars/paperfoot/agent-cli-framework?style=for-the-badge\u0026logo=github\u0026label=%E2%AD%90%20Star%20this%20repo\u0026color=yellow)](https://github.com/paperfoot/agent-cli-framework/stargazers)\n\u0026nbsp;\u0026nbsp;\n[![Follow @longevityboris](https://img.shields.io/badge/Follow_%40longevityboris-000000?style=for-the-badge\u0026logo=x\u0026logoColor=white)](https://x.com/longevityboris)\n\n\u003c/div\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperfoot%2Fagent-cli-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpaperfoot%2Fagent-cli-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpaperfoot%2Fagent-cli-framework/lists"}