Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `****<last4>`. 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

Expand Down
7 changes: 6 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
Expand Down
175 changes: 175 additions & 0 deletions src/commands/agent/context.md
Original file line number Diff line number Diff line change
@@ -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 <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 <KEY>` on every invocation, or
- Write the key once: `qn auth login --api-key <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 `****<last4>` 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 <id>` 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 <bash|zsh|fish|...>`.

## 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 <id> # inspect before modifying
qn endpoint rate-limit set <id> --rps 50
qn endpoint show <id> # 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 <id> # inspect while paused
qn stream activate <id>
```

**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 <id> # inspect before activating
qn webhook activate <id>
```

**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}}.
94 changes: 94 additions & 0 deletions src/commands/agent/mod.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
4 changes: 4 additions & 0 deletions src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub struct CreateArgs {
/// dataset_batch_size (defaults to 1).
#[arg(long)]
pub batch_size: Option<i64>,
/// 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<bool>,
/// elastic_batch_enabled.
Expand Down
Loading