Skip to content

Commit 4cdd0ac

Browse files
authored
feat(agent): add qn agent context + agent discovery breadcrumbs (DX-5704) (#35)
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.
1 parent 12b685a commit 4cdd0ac

7 files changed

Lines changed: 282 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ For TTY-specific behavior (color, prompting), run `./target/debug/qn ...` in a r
183183
- **No secrets in logs**: API keys never go to stdout. `auth whoami` redacts to `****<last4>`. Never `dbg!` or `println!` an `SdkFullConfig`.
184184
- **`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.
185185
- **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.
186+
- **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.
186187

187188
## Anti-patterns we already hit and fixed
188189

src/cli.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ use crate::output::Format;
3535
qn endpoint create --chain ethereum --network mainnet\n \
3636
qn endpoint list -o json\n \
3737
qn endpoint logs ep-1234 --from 1h\n \
38-
qn chain list",
38+
qn chain list\n\n\
39+
AI agents: run 'qn agent context' for a machine-readable usage guide.",
3940
// Group the global flags under their own heading in every subcommand's
4041
// --help, so command-specific flags surface first under "Options".
4142
next_help_heading = "Global options"
@@ -109,6 +110,9 @@ pub enum Command {
109110
/// Manage CLI authentication (API key).
110111
Auth(commands::auth::Args),
111112

113+
/// Resources for AI agents and automated tools.
114+
Agent(commands::agent::Args),
115+
112116
/// Manage RPC endpoints on your account.
113117
#[command(visible_alias = "endpoints")]
114118
Endpoint(commands::endpoint::Args),
@@ -186,6 +190,7 @@ impl Cli {
186190
Ok(())
187191
}
188192
Command::Auth(args) => commands::auth::run(args, global).await,
193+
Command::Agent(args) => commands::agent::run(args, global).await,
189194
Command::Endpoint(args) => {
190195
commands::endpoint::run(args, Ctx::from_global(global)?).await
191196
}

src/commands/agent/context.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# qn — usage guide for agents
2+
3+
`qn` is the Quicknode command-line interface. It manages endpoints, streams,
4+
webhooks, the KV store, usage, metrics, billing, and teams.
5+
6+
This guide describes qn v{{VERSION}}. It prints as Markdown by default — no flag
7+
needed to read it. For a structured envelope (`{version, guide}`), pass `-o json`.
8+
Read the control-flow sections (auth, output, exit codes, confirmation, retry)
9+
before the command catalog: they decide whether you can run unattended without
10+
hanging or double-acting.
11+
12+
## 1. Auth
13+
14+
Resolution order for the API key:
15+
16+
1. `--api-key <KEY>` flag (highest precedence).
17+
2. Config file: `[api] key = "..."` in `~/.config/qn/config.toml` (or the path
18+
passed to `--config-file`).
19+
3. If neither resolves, the command exits **4** (`no API key found`).
20+
21+
There is **no environment-variable fallback** by design — a key left exported in
22+
a shell is invisible state that outlives the session.
23+
24+
Non-interactive paths:
25+
26+
- Pass `--api-key <KEY>` on every invocation, or
27+
- Write the key once: `qn auth login --api-key <KEY>` (saves the config file).
28+
29+
Config file location:
30+
31+
- Linux/macOS: `$XDG_CONFIG_HOME/qn/config.toml`, else `~/.config/qn/config.toml`.
32+
- Windows: `%USERPROFILE%\.config\qn\config.toml`.
33+
34+
Verify the resolved key against the API: `qn auth whoami` (prints the key redacted
35+
to `****<last4>` and confirms it works). `qn auth status` does the same without the
36+
network call.
37+
38+
## 2. Output contract
39+
40+
- Default format is `table` on a TTY and **`toon`** when stdout is not a TTY (piped).
41+
- Data goes to **stdout**; diagnostics, prompts, and ✓ confirmations go to **stderr**.
42+
- Formats: `table`, `md`, `json`, `yaml`, `toon`. The structured forms
43+
(`json`/`yaml`/`toon`) always include every field — `--wide` is not needed and
44+
only affects `table`/`md`.
45+
- Config file can set defaults: `[output] format = "json"`, `wide = true`.
46+
47+
## 3. Exit codes
48+
49+
Branch on these — especially **4** and **5**.
50+
51+
| Code | Meaning |
52+
|------|---------|
53+
| 0 | Success. |
54+
| 1 | Generic CLI error (bad arguments, I/O, unclassified failure). |
55+
| 2 | API error — the server returned a non-2xx response. |
56+
| 3 | HTTP error — network failure (connect/timeout). |
57+
| 4 | Auth/config — no API key, or a config file that can't be read or written. |
58+
| 5 | Cancelled, or confirmation required and not granted (see §4). |
59+
| 130 | Interrupted (SIGINT). |
60+
61+
## 4. Non-interactive & confirmation behavior
62+
63+
Destructive commands are gated. On a TTY they prompt `y/N`. To proceed without a
64+
prompt, pass `--yes` (`-y`).
65+
66+
In a non-TTY **a gated command without `--yes` exits 5 before any request is sent**
67+
nothing is changed. Pass `-y` to proceed, or `--no-input` to force non-interactive
68+
behavior everywhere (it fails fast instead of prompting). `--quiet` (`-q`) suppresses
69+
the ✓ state-change notes on stderr; it does not affect stdout.
70+
71+
Gated command classes:
72+
73+
- `endpoint archive`, `endpoint bulk pause`
74+
- `endpoint tag delete`
75+
- `endpoint security` removals (token/jwt/ip/referrer/domain-mask remove, and
76+
`set-options` toggles that disable a protection)
77+
- `endpoint rate-limit delete-override`
78+
- `stream delete`, `webhook delete`, `team delete`
79+
- `kv set delete`, `kv list delete`
80+
81+
There is **no account-wide wipe command** — that is intentional; use the API directly
82+
if you need it.
83+
84+
## 5. Retry & idempotency
85+
86+
- **Read-only** commands auto-retry transient failures (HTTP 429/500/502/503/504 and
87+
connect/timeout errors) with exponential backoff and jitter. Tune with `--retries N`
88+
(default 3; `0` = a single attempt, no retries).
89+
- **Mutations never auto-retry.** A retried create/update/delete could apply twice.
90+
- When a mutation fails transiently, its outcome is unknown until verified — e.g.
91+
`qn endpoint show <id>` reflects whether it took effect.
92+
- `qn stream test-filter` evaluates a filter against historical data and changes
93+
nothing — it is read-only and safe to retry.
94+
95+
## 6. Command catalog
96+
97+
Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliases):
98+
99+
- `auth` — login, logout, whoami, status
100+
- `endpoint` — list, show, create, update, archive, pause, resume, urls, logs,
101+
log-details, metrics, enable-multichain, disable-multichain; nested:
102+
`tag`, `security`, `rate-limit`, `bulk`
103+
- `team` — list, create, show, delete, endpoints, set-endpoints; nested: `member`
104+
- `usage` — summary, by-endpoint, by-method, by-chain, by-tag
105+
- `metrics` — account, endpoint
106+
- `chain` — list
107+
- `billing` — invoices, payments
108+
- `stream` — list, show, create, update, delete, activate, pause, test-filter,
109+
enabled-count
110+
- `webhook` — list, show, create, update, update-template, delete, activate, pause,
111+
enabled-count
112+
- `kv``set` (put, get, list, delete, bulk) and `list` (list, get, create, append,
113+
contains, remove-item, update, delete)
114+
115+
Drill into any level with `--help`: `qn endpoint --help`, `qn endpoint security --help`,
116+
`qn endpoint rate-limit --help`. Shell completions: `qn completions <bash|zsh|fish|...>`.
117+
118+
## 7. Common workflows
119+
120+
Capture the `id` (and any URL) from each create response and chain it into the next call.
121+
Run `show` before a state change so you act on the current state, not an assumed one.
122+
123+
**Provision an endpoint and rate-limit it:**
124+
125+
```sh
126+
qn endpoint create --chain ethereum --network mainnet # → id, http_url, wss_url
127+
qn endpoint show <id> # inspect before modifying
128+
qn endpoint rate-limit set <id> --rps 50
129+
qn endpoint show <id> # verify
130+
```
131+
132+
**Create a stream (paused), inspect it, then activate:**
133+
134+
```sh
135+
qn stream create --name my-stream --network ethereum-mainnet \
136+
--dataset block --start 15301579 --end 25301589 \
137+
--batch-size 2 --fix-block-reorgs 1 \
138+
--notification-email you@example.com --status paused \
139+
--webhook https://hook.example.com --region usa-east # → id
140+
qn stream show <id> # inspect while paused
141+
qn stream activate <id>
142+
```
143+
144+
**Create a webhook from a template:**
145+
146+
```sh
147+
qn webhook create --name wallet-watch --network ethereum-mainnet \
148+
--url https://hook.example.com --template evm-wallet \
149+
--wallet 0xabc... # → id
150+
qn webhook show <id> # inspect before activating
151+
qn webhook activate <id>
152+
```
153+
154+
**KV put / get / list:**
155+
156+
```sh
157+
qn kv set put my-key my-value
158+
qn kv set get my-key
159+
qn kv set list
160+
```
161+
162+
## 8. Gotchas & safety rails
163+
164+
- Mutations are never retried; re-running a failed create can double-provision (§5).
165+
- No account-wide wipe command exists by design (§4).
166+
- Piped output defaults to `toon`, not `json` (§2).
167+
- `--base-url` overrides the API host; it exists for testing.
168+
- For *this* command, `-o yaml`/`-o toon`/`-o table` print Markdown (with a note on
169+
stderr); `-o json` produces the `{version, guide}` envelope.
170+
171+
## 9. More
172+
173+
- `qn --help`, and `--help` at every noun/verb level, document flags exhaustively.
174+
- Docs: https://www.quicknode.com/docs
175+
- This guide self-describes its version: it matches qn v{{VERSION}}.

src/commands/agent/mod.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! `qn agent context` — print an embedded, version-stamped usage guide for
2+
//! AI agents and automated tools.
3+
//!
4+
//! This command builds no SDK and makes no network call: an agent will often
5+
//! run it *before* it has a key, so it must never trigger the API-key path.
6+
//! Dispatched directly from `GlobalArgs` in `cli.rs`, alongside `auth` and
7+
//! `completions`.
8+
//!
9+
//! The guide lives in `context.md` (embedded via `include_str!`) and carries a
10+
//! `{{VERSION}}` placeholder filled in at print time. See the sync rule in
11+
//! CLAUDE.md: the prose makes version-stamped claims about the command surface,
12+
//! so any change to that surface must update `context.md` in the same commit.
13+
14+
use std::io::Write;
15+
16+
use clap::{Args as ClapArgs, Subcommand};
17+
use serde::Serialize;
18+
19+
use crate::context::GlobalArgs;
20+
use crate::errors::CliError;
21+
use crate::output::Format;
22+
23+
/// The embedded guide. `{{VERSION}}` is replaced with the crate version at print time.
24+
const GUIDE: &str = include_str!("context.md");
25+
26+
#[derive(Debug, ClapArgs)]
27+
#[command(
28+
about = "Resources for AI agents and automated tools.",
29+
long_about = "Resources for AI agents and automated tools.\n\n\
30+
Run `qn agent context` for a single, self-contained usage guide\n\
31+
(auth, output formats, exit codes, confirmation, retry/idempotency,\n\
32+
the command catalog, and common workflows)."
33+
)]
34+
pub struct Args {
35+
#[command(subcommand)]
36+
pub cmd: AgentCmd,
37+
}
38+
39+
#[derive(Debug, Subcommand)]
40+
pub enum AgentCmd {
41+
/// Print a machine-readable usage guide for agents (no auth, no network).
42+
Context,
43+
}
44+
45+
pub async fn run(args: Args, global: GlobalArgs) -> Result<(), CliError> {
46+
match args.cmd {
47+
AgentCmd::Context => context(global),
48+
}
49+
}
50+
51+
/// JSON envelope for `-o json`: the guide stays self-describing even after the
52+
/// `guide` string is extracted.
53+
#[derive(Serialize)]
54+
struct ContextView<'a> {
55+
version: &'a str,
56+
guide: &'a str,
57+
}
58+
59+
fn context(global: GlobalArgs) -> Result<(), CliError> {
60+
let version = env!("CARGO_PKG_VERSION");
61+
let guide = GUIDE.replace("{{VERSION}}", version);
62+
63+
// Read `global.format` directly (the raw Option), NOT `resolve_format` —
64+
// here `None` means "print markdown", not the piped-default TOON. This
65+
// command's stdout is prose to read, not data to parse.
66+
match global.format {
67+
Some(Format::Json) => {
68+
let view = ContextView {
69+
version,
70+
guide: &guide,
71+
};
72+
let mut out = std::io::stdout().lock();
73+
serde_json::to_writer_pretty(&mut out, &view)?;
74+
writeln!(out)?;
75+
}
76+
other => {
77+
print!("{guide}");
78+
// An explicit non-markdown format (yaml/toon/table) can't carry the
79+
// guide usefully, so we print markdown and say why on stderr.
80+
if matches!(other, Some(Format::Yaml | Format::Toon | Format::Table)) && !global.quiet {
81+
let fmt = match other {
82+
Some(Format::Yaml) => "yaml",
83+
Some(Format::Toon) => "toon",
84+
_ => "table",
85+
};
86+
let _ = writeln!(
87+
std::io::stderr(),
88+
"ℹ '-o {fmt}' isn't supported by 'qn agent context'; printing markdown. Use '-o json' for structured output."
89+
);
90+
}
91+
}
92+
}
93+
Ok(())
94+
}

src/commands/auth.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ async fn login(args: LoginArgs, global: GlobalArgs) -> Result<(), CliError> {
9595
config::save_api_key(&path, &key)?;
9696
if !global.quiet {
9797
let _ = writeln!(std::io::stderr(), "✓ Saved API key to {}", path.display());
98+
let _ = writeln!(
99+
std::io::stderr(),
100+
"Tip: run 'qn agent context' for a machine-readable usage guide."
101+
);
98102
}
99103
Ok(())
100104
}

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! One module per top-level noun. Each exposes `Args` (the clap-derived
44
//! argument struct) and `run(args, ctx) -> Result<(), CliError>`.
55
6+
pub mod agent;
67
pub mod auth;
78
pub mod billing;
89
pub mod chain;

src/commands/stream/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub struct CreateArgs {
131131
/// dataset_batch_size (defaults to 1).
132132
#[arg(long)]
133133
pub batch_size: Option<i64>,
134-
/// Fix block reorgs (true/false).
134+
/// Fix block reorgs (true/false). Defaults to false (disabled).
135135
#[arg(long, value_parser = clap::builder::BoolishValueParser::new())]
136136
pub fix_block_reorgs: Option<bool>,
137137
/// elastic_batch_enabled.

0 commit comments

Comments
 (0)