From 3c72a983e82aabeec03ac6184745279ee4822d74 Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Mon, 15 Jun 2026 08:30:53 -0400 Subject: [PATCH] feat(agent): add `qn agent context` + agent discovery breadcrumbs (DX-5704) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give coding agents one self-contained place to learn how to operate `qn` correctly on the first try. `qn agent context` prints an embedded, version-stamped usage guide to stdout and exits 0 with no auth and no network — it joins the no-SDK dispatch group (auth/completions) so it never triggers the API-key path. The guide lives in src/commands/agent/context.md (included via include_str!) and leads with the control-flow-critical material (auth, output contract, exit codes, confirmation, retry/idempotency) before the command catalog and workflow recipes. Output contract: the command reads global.format directly rather than resolve_format, so no flag means markdown (not the piped-default toon). `-o json` emits a `{version, guide}` envelope; `-o yaml`/`-o toon`/`-o table` print markdown plus a one-line stderr notice (suppressed by --quiet). Discovery breadcrumbs point to the command from the two places agents look: the top-level `qn --help` footer and a stderr tip after a successful `qn auth login`, using one canonical pointer phrase. Also documents the false (disabled) default on `stream create --fix-block-reorgs`, and adds a CLAUDE.md rule to keep context.md in sync with the command surface. Verified: release build, clippy -D warnings, and fmt --check clean; 229 tests pass. --- CLAUDE.md | 1 + src/cli.rs | 7 +- src/commands/agent/context.md | 175 ++++++++++++++++++++++++++++++++++ src/commands/agent/mod.rs | 94 ++++++++++++++++++ src/commands/auth.rs | 4 + src/commands/mod.rs | 1 + src/commands/stream/mod.rs | 2 +- 7 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 src/commands/agent/context.md create mode 100644 src/commands/agent/mod.rs diff --git a/CLAUDE.md b/CLAUDE.md index 36b1527..64b02db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -183,6 +183,7 @@ For TTY-specific behavior (color, prompting), run `./target/debug/qn ...` in a r - **No secrets in logs**: API keys never go to stdout. `auth whoami` redacts to `****`. Never `dbg!` or `println!` an `SdkFullConfig`. - **`ctx.out.note`** for ✓ state-change confirmations on stderr (auto-suppressed by `--quiet`). The actual resource still goes to stdout via `emit` so pipelines see it. - **Don't read `std::env` inside command bodies.** Resolve through `Ctx` / `GlobalArgs`. Tests need to set behavior without mutating process env (which races across parallel tests). `OutputCtx::detect_with` is the testable form of `OutputCtx::detect` for this reason. +- **Keep `src/commands/agent/context.md` in sync.** It's the embedded guide `qn agent context` prints, and it makes version-stamped claims about the command surface. Any change to the command catalog (the `Command` enum or a module's verbs), exit codes (`errors.rs`), gating (`confirm.rs`), retry behavior (`retry.rs`), the output contract (`output.rs` `Format`), or auth/config resolution (`config.rs`) **must** update `context.md` in the same commit. The version stamp is automatic (`CARGO_PKG_VERSION`); the prose accuracy is not — that's this rule's job. ## Anti-patterns we already hit and fixed diff --git a/src/cli.rs b/src/cli.rs index bfa5f2c..daf6411 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,7 +35,8 @@ use crate::output::Format; qn endpoint create --chain ethereum --network mainnet\n \ qn endpoint list -o json\n \ qn endpoint logs ep-1234 --from 1h\n \ - qn chain list", + qn chain list\n\n\ + AI agents: run 'qn agent context' for a machine-readable usage guide.", // Group the global flags under their own heading in every subcommand's // --help, so command-specific flags surface first under "Options". next_help_heading = "Global options" @@ -109,6 +110,9 @@ pub enum Command { /// Manage CLI authentication (API key). Auth(commands::auth::Args), + /// Resources for AI agents and automated tools. + Agent(commands::agent::Args), + /// Manage RPC endpoints on your account. #[command(visible_alias = "endpoints")] Endpoint(commands::endpoint::Args), @@ -186,6 +190,7 @@ impl Cli { Ok(()) } Command::Auth(args) => commands::auth::run(args, global).await, + Command::Agent(args) => commands::agent::run(args, global).await, Command::Endpoint(args) => { commands::endpoint::run(args, Ctx::from_global(global)?).await } diff --git a/src/commands/agent/context.md b/src/commands/agent/context.md new file mode 100644 index 0000000..75baff0 --- /dev/null +++ b/src/commands/agent/context.md @@ -0,0 +1,175 @@ +# qn — usage guide for agents + +`qn` is the Quicknode command-line interface. It manages endpoints, streams, +webhooks, the KV store, usage, metrics, billing, and teams. + +This guide describes qn v{{VERSION}}. It prints as Markdown by default — no flag +needed to read it. For a structured envelope (`{version, guide}`), pass `-o json`. +Read the control-flow sections (auth, output, exit codes, confirmation, retry) +before the command catalog: they decide whether you can run unattended without +hanging or double-acting. + +## 1. Auth + +Resolution order for the API key: + +1. `--api-key ` flag (highest precedence). +2. Config file: `[api] key = "..."` in `~/.config/qn/config.toml` (or the path + passed to `--config-file`). +3. If neither resolves, the command exits **4** (`no API key found`). + +There is **no environment-variable fallback** by design — a key left exported in +a shell is invisible state that outlives the session. + +Non-interactive paths: + +- Pass `--api-key ` on every invocation, or +- Write the key once: `qn auth login --api-key ` (saves the config file). + +Config file location: + +- Linux/macOS: `$XDG_CONFIG_HOME/qn/config.toml`, else `~/.config/qn/config.toml`. +- Windows: `%USERPROFILE%\.config\qn\config.toml`. + +Verify the resolved key against the API: `qn auth whoami` (prints the key redacted +to `****` and confirms it works). `qn auth status` does the same without the +network call. + +## 2. Output contract + +- Default format is `table` on a TTY and **`toon`** when stdout is not a TTY (piped). +- Data goes to **stdout**; diagnostics, prompts, and ✓ confirmations go to **stderr**. +- Formats: `table`, `md`, `json`, `yaml`, `toon`. The structured forms + (`json`/`yaml`/`toon`) always include every field — `--wide` is not needed and + only affects `table`/`md`. +- Config file can set defaults: `[output] format = "json"`, `wide = true`. + +## 3. Exit codes + +Branch on these — especially **4** and **5**. + +| Code | Meaning | +|------|---------| +| 0 | Success. | +| 1 | Generic CLI error (bad arguments, I/O, unclassified failure). | +| 2 | API error — the server returned a non-2xx response. | +| 3 | HTTP error — network failure (connect/timeout). | +| 4 | Auth/config — no API key, or a config file that can't be read or written. | +| 5 | Cancelled, or confirmation required and not granted (see §4). | +| 130 | Interrupted (SIGINT). | + +## 4. Non-interactive & confirmation behavior + +Destructive commands are gated. On a TTY they prompt `y/N`. To proceed without a +prompt, pass `--yes` (`-y`). + +In a non-TTY **a gated command without `--yes` exits 5 before any request is sent** — +nothing is changed. Pass `-y` to proceed, or `--no-input` to force non-interactive +behavior everywhere (it fails fast instead of prompting). `--quiet` (`-q`) suppresses +the ✓ state-change notes on stderr; it does not affect stdout. + +Gated command classes: + +- `endpoint archive`, `endpoint bulk pause` +- `endpoint tag delete` +- `endpoint security` removals (token/jwt/ip/referrer/domain-mask remove, and + `set-options` toggles that disable a protection) +- `endpoint rate-limit delete-override` +- `stream delete`, `webhook delete`, `team delete` +- `kv set delete`, `kv list delete` + +There is **no account-wide wipe command** — that is intentional; use the API directly +if you need it. + +## 5. Retry & idempotency + +- **Read-only** commands auto-retry transient failures (HTTP 429/500/502/503/504 and + connect/timeout errors) with exponential backoff and jitter. Tune with `--retries N` + (default 3; `0` = a single attempt, no retries). +- **Mutations never auto-retry.** A retried create/update/delete could apply twice. +- When a mutation fails transiently, its outcome is unknown until verified — e.g. + `qn endpoint show ` reflects whether it took effect. +- `qn stream test-filter` evaluates a filter against historical data and changes + nothing — it is read-only and safe to retry. + +## 6. Command catalog + +Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliases): + +- `auth` — login, logout, whoami, status +- `endpoint` — list, show, create, update, archive, pause, resume, urls, logs, + log-details, metrics, enable-multichain, disable-multichain; nested: + `tag`, `security`, `rate-limit`, `bulk` +- `team` — list, create, show, delete, endpoints, set-endpoints; nested: `member` +- `usage` — summary, by-endpoint, by-method, by-chain, by-tag +- `metrics` — account, endpoint +- `chain` — list +- `billing` — invoices, payments +- `stream` — list, show, create, update, delete, activate, pause, test-filter, + enabled-count +- `webhook` — list, show, create, update, update-template, delete, activate, pause, + enabled-count +- `kv` — `set` (put, get, list, delete, bulk) and `list` (list, get, create, append, + contains, remove-item, update, delete) + +Drill into any level with `--help`: `qn endpoint --help`, `qn endpoint security --help`, +`qn endpoint rate-limit --help`. Shell completions: `qn completions `. + +## 7. Common workflows + +Capture the `id` (and any URL) from each create response and chain it into the next call. +Run `show` before a state change so you act on the current state, not an assumed one. + +**Provision an endpoint and rate-limit it:** + +```sh +qn endpoint create --chain ethereum --network mainnet # → id, http_url, wss_url +qn endpoint show # inspect before modifying +qn endpoint rate-limit set --rps 50 +qn endpoint show # verify +``` + +**Create a stream (paused), inspect it, then activate:** + +```sh +qn stream create --name my-stream --network ethereum-mainnet \ + --dataset block --start 15301579 --end 25301589 \ + --batch-size 2 --fix-block-reorgs 1 \ + --notification-email you@example.com --status paused \ + --webhook https://hook.example.com --region usa-east # → id +qn stream show # inspect while paused +qn stream activate +``` + +**Create a webhook from a template:** + +```sh +qn webhook create --name wallet-watch --network ethereum-mainnet \ + --url https://hook.example.com --template evm-wallet \ + --wallet 0xabc... # → id +qn webhook show # inspect before activating +qn webhook activate +``` + +**KV put / get / list:** + +```sh +qn kv set put my-key my-value +qn kv set get my-key +qn kv set list +``` + +## 8. Gotchas & safety rails + +- Mutations are never retried; re-running a failed create can double-provision (§5). +- No account-wide wipe command exists by design (§4). +- Piped output defaults to `toon`, not `json` (§2). +- `--base-url` overrides the API host; it exists for testing. +- For *this* command, `-o yaml`/`-o toon`/`-o table` print Markdown (with a note on + stderr); `-o json` produces the `{version, guide}` envelope. + +## 9. More + +- `qn --help`, and `--help` at every noun/verb level, document flags exhaustively. +- Docs: https://www.quicknode.com/docs +- This guide self-describes its version: it matches qn v{{VERSION}}. diff --git a/src/commands/agent/mod.rs b/src/commands/agent/mod.rs new file mode 100644 index 0000000..71fc49b --- /dev/null +++ b/src/commands/agent/mod.rs @@ -0,0 +1,94 @@ +//! `qn agent context` — print an embedded, version-stamped usage guide for +//! AI agents and automated tools. +//! +//! This command builds no SDK and makes no network call: an agent will often +//! run it *before* it has a key, so it must never trigger the API-key path. +//! Dispatched directly from `GlobalArgs` in `cli.rs`, alongside `auth` and +//! `completions`. +//! +//! The guide lives in `context.md` (embedded via `include_str!`) and carries a +//! `{{VERSION}}` placeholder filled in at print time. See the sync rule in +//! CLAUDE.md: the prose makes version-stamped claims about the command surface, +//! so any change to that surface must update `context.md` in the same commit. + +use std::io::Write; + +use clap::{Args as ClapArgs, Subcommand}; +use serde::Serialize; + +use crate::context::GlobalArgs; +use crate::errors::CliError; +use crate::output::Format; + +/// The embedded guide. `{{VERSION}}` is replaced with the crate version at print time. +const GUIDE: &str = include_str!("context.md"); + +#[derive(Debug, ClapArgs)] +#[command( + about = "Resources for AI agents and automated tools.", + long_about = "Resources for AI agents and automated tools.\n\n\ + Run `qn agent context` for a single, self-contained usage guide\n\ + (auth, output formats, exit codes, confirmation, retry/idempotency,\n\ + the command catalog, and common workflows)." +)] +pub struct Args { + #[command(subcommand)] + pub cmd: AgentCmd, +} + +#[derive(Debug, Subcommand)] +pub enum AgentCmd { + /// Print a machine-readable usage guide for agents (no auth, no network). + Context, +} + +pub async fn run(args: Args, global: GlobalArgs) -> Result<(), CliError> { + match args.cmd { + AgentCmd::Context => context(global), + } +} + +/// JSON envelope for `-o json`: the guide stays self-describing even after the +/// `guide` string is extracted. +#[derive(Serialize)] +struct ContextView<'a> { + version: &'a str, + guide: &'a str, +} + +fn context(global: GlobalArgs) -> Result<(), CliError> { + let version = env!("CARGO_PKG_VERSION"); + let guide = GUIDE.replace("{{VERSION}}", version); + + // Read `global.format` directly (the raw Option), NOT `resolve_format` — + // here `None` means "print markdown", not the piped-default TOON. This + // command's stdout is prose to read, not data to parse. + match global.format { + Some(Format::Json) => { + let view = ContextView { + version, + guide: &guide, + }; + let mut out = std::io::stdout().lock(); + serde_json::to_writer_pretty(&mut out, &view)?; + writeln!(out)?; + } + other => { + print!("{guide}"); + // An explicit non-markdown format (yaml/toon/table) can't carry the + // guide usefully, so we print markdown and say why on stderr. + if matches!(other, Some(Format::Yaml | Format::Toon | Format::Table)) && !global.quiet { + let fmt = match other { + Some(Format::Yaml) => "yaml", + Some(Format::Toon) => "toon", + _ => "table", + }; + let _ = writeln!( + std::io::stderr(), + "ℹ '-o {fmt}' isn't supported by 'qn agent context'; printing markdown. Use '-o json' for structured output." + ); + } + } + } + Ok(()) +} diff --git a/src/commands/auth.rs b/src/commands/auth.rs index 3826a83..9cd849a 100644 --- a/src/commands/auth.rs +++ b/src/commands/auth.rs @@ -95,6 +95,10 @@ async fn login(args: LoginArgs, global: GlobalArgs) -> Result<(), CliError> { config::save_api_key(&path, &key)?; if !global.quiet { let _ = writeln!(std::io::stderr(), "✓ Saved API key to {}", path.display()); + let _ = writeln!( + std::io::stderr(), + "Tip: run 'qn agent context' for a machine-readable usage guide." + ); } Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 89255e3..3d6209b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,6 +3,7 @@ //! One module per top-level noun. Each exposes `Args` (the clap-derived //! argument struct) and `run(args, ctx) -> Result<(), CliError>`. +pub mod agent; pub mod auth; pub mod billing; pub mod chain; diff --git a/src/commands/stream/mod.rs b/src/commands/stream/mod.rs index b740613..6b1a97b 100644 --- a/src/commands/stream/mod.rs +++ b/src/commands/stream/mod.rs @@ -131,7 +131,7 @@ pub struct CreateArgs { /// dataset_batch_size (defaults to 1). #[arg(long)] pub batch_size: Option, - /// Fix block reorgs (true/false). + /// Fix block reorgs (true/false). Defaults to false (disabled). #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub fix_block_reorgs: Option, /// elastic_batch_enabled.