From 271c200ada75b835adc216bb2c9257c952cb224d Mon Sep 17 00:00:00 2001 From: Cody McCabe Date: Thu, 12 Mar 2026 19:59:12 -0400 Subject: [PATCH 1/5] update docs for user-first readme flow Refocus README on end-user onboarding and command usage while moving maintainer-focused development and release operations into MAINTAINERS.md. Made-with: Cursor --- MAINTAINERS.md | 119 ++++++++++++++++++++++++++++++++++++ README.md | 163 ++++++++++++++----------------------------------- 2 files changed, 166 insertions(+), 116 deletions(-) create mode 100644 MAINTAINERS.md diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..ff74dd9 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,119 @@ +# Maintainers Guide + +Maintainer-focused operational docs for release management and advanced local testing. +For general usage and contributor setup, see `README.md`. + +## Local Development + +Prerequisites: + +- [Node.js 22+](https://nodejs.org/) +- [pnpm](https://pnpm.io/) + +### Setup + +```bash +git clone https://github.com/alchemyplatform/alchemy-cli.git +cd alchemy-cli +pnpm install +pnpm build +pnpm link --global +``` + +This makes the local `alchemy` build available globally for testing. +To unlink later: `pnpm unlink --global`. + +### Common Commands + +Run during development: + +```bash +# Run without building +npx tsx src/index.ts balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + +# Build in watch mode +pnpm dev +``` + +Build: + +```bash +pnpm build +``` + +Test: + +```bash +pnpm test +pnpm test:e2e +``` + +Type check: + +```bash +pnpm lint +``` + +Coverage: + +```bash +pnpm test:coverage +``` + +## Changesets And Releasing + +This project uses [Changesets](https://github.com/changesets/changesets) for versioning and release notes. + +**When to add a changeset:** Any PR with user-facing changes (new commands, bug fixes, flag changes, output format changes) needs a changeset. Internal changes (CI, refactors with no behavior change, docs) can skip by adding the `no-changeset` label. + +**How to add a changeset:** + +```bash +pnpm changeset +``` + +You will be prompted to pick the bump type: +- **patch** - bug fixes, small tweaks (for example fixing `--json` output for a command) +- **minor** - new commands, new flags, new capabilities +- **major** - breaking changes (removed commands, changed flag behavior, output format changes) + +This creates a file like `.changeset/cool-dogs-fly.md`: + +```markdown +--- +"@alchemy/cli": minor +--- + +Add `alchemy portfolio transactions` command for portfolio transaction history. +``` + +Write a 1-2 sentence summary of the change from a user's perspective. Commit this file with your PR. + +**How releases work:** When PRs with changesets merge to `main`, the publish workflow automatically: +1. Verifies the build (typecheck, build, test) +2. Applies version bumps and updates `CHANGELOG.md` via `changeset version` +3. Creates a signed release commit via the GitHub Git Database API (using a GitHub App token) +4. Publishes to npm using OIDC trusted publishing (no long-lived npm token) +5. Creates a GitHub release/tag with notes extracted from `CHANGELOG.md` + +If no changesets are pending, the workflow exits cleanly and no release is created. + +**Release infrastructure:** +- Repository write operations use a GitHub App (`APP_ID` variable + `APP_PRIVATE_KEY` secret) +- npm publish uses [trusted publishing](https://docs.npmjs.com/generating-provenance-statements) (OIDC), so no `NPM_TOKEN` secret is required +- Required GitHub repo settings: `APP_ID` (variable), `APP_PRIVATE_KEY` (secret) +- Required npm-side: configure trusted publishing for this repo/workflow at npm package settings + +## Endpoint Override Env Vars (Local Testing Only) + +These are for local/mock testing, not normal production usage: + +- `ALCHEMY_RPC_BASE_URL` +- `ALCHEMY_ADMIN_API_BASE_URL` +- `ALCHEMY_X402_BASE_URL` + +Safety constraints: + +- Only localhost targets are accepted (`localhost`, `127.0.0.1`, `::1`) +- Non-HTTPS transport is allowed only for localhost +- Production defaults are unchanged when unset diff --git a/README.md b/README.md index ffd71d7..b5c0ab2 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,47 @@ Or run without installing globally: npx @alchemy/cli ``` +## Getting Started + +### Authentication Quick Start + +Authentication is required before making requests. Configure auth first, then run commands. + +If you are using the CLI as a human in an interactive terminal, the easiest path is: + +```bash +alchemy +``` + +Then follow the setup flow in the terminal UI to configure auth. + +Know which auth method does what: + +- **API key** - direct auth for blockchain queries (`balance`, `tx`, `block`, `nfts`, `tokens`, `rpc`) +- **Access key** - Admin/API app management; app setup/selection can also provide API key auth for blockchain queries +- **x402 wallet auth** - wallet-authenticated, pay-per-request model for supported blockchain queries + +If you use Notify webhooks, add webhook auth on top via `alchemy config set webhook-api-key `, `--webhook-api-key`, or `ALCHEMY_WEBHOOK_API_KEY`. + +For setup commands, env vars, and resolution order, see [Authentication Reference](#authentication-reference). + +### Usage By Workflow + +After auth is configured, use the CLI differently depending on who is driving it: + +- **Humans (interactive terminal):** start with `alchemy` and use the terminal UI/setup flow; this is the recommended path for human usage +- **Agents/scripts (automation):** always use `--json` and prefer non-interactive execution (`--no-interactive`) + +Quick usage examples: + +```bash +# Human recommended entrypoint +alchemy + +# Agent/script-friendly command +alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --json --no-interactive +``` + ## Command Reference Run commands as `alchemy `. @@ -176,13 +217,13 @@ Additional env vars: | `network list` | `--configured`, `--app-id ` | | `config reset` | `-y, --yes` | -## Authentication +## Authentication Reference The CLI supports three auth inputs: - API key for blockchain queries (`balance`, `tx`, `block`, `nfts`, `tokens`, `rpc`) -- Access key for Admin API operations (`apps`, `chains`, configured network lookups) -- x402 wallet key for wallet-authenticated blockchain queries +- Access key for Admin API operations (`apps`, `chains`, configured network lookups`) and app setup/selection, which can also supply the API key used by blockchain query commands +- x402 wallet key for wallet-authenticated blockchain queries in a pay-per-request model Notify/webhook commands use a webhook API key with resolution order: `--webhook-api-key` -> `ALCHEMY_WEBHOOK_API_KEY` -> `ALCHEMY_NOTIFY_AUTH_TOKEN` -> config `webhook-api-key` -> configured app webhook key. @@ -221,6 +262,9 @@ Resolution order: `--access-key` -> `ALCHEMY_ACCESS_KEY` -> config file. #### x402 wallet auth +x402 is a wallet-authenticated, pay-per-request usage model for supported blockchain queries. +The CLI can generate or import the wallet key used for these requests. + ```bash # Generate/import a wallet managed by CLI alchemy wallet generate @@ -277,116 +321,3 @@ Errors are structured JSON in JSON mode: } } ``` - -## Development - -Prerequisites: - -- [Node.js 22+](https://nodejs.org/) -- [pnpm](https://pnpm.io/) - -### Local development setup - -```bash -git clone https://github.com/alchemyplatform/alchemy-cli.git -cd alchemy-cli -pnpm install -pnpm build -pnpm link --global -``` - -This makes the local `alchemy` build available globally for testing. -To unlink later: `pnpm unlink --global`. - -Run during development: - -```bash -# Run without building -npx tsx src/index.ts balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 - -# Build in watch mode -pnpm dev -``` - -Build: - -```bash -pnpm build -``` - -Test: - -```bash -pnpm test -pnpm test:e2e -``` - -Type check: - -```bash -pnpm lint -``` - -Coverage: - -```bash -pnpm test:coverage -``` - -### Changesets & Releasing - -This project uses [Changesets](https://github.com/changesets/changesets) for versioning and release notes. - -**When to add a changeset:** Any PR with user-facing changes (new commands, bug fixes, flag changes, output format changes) needs a changeset. Internal changes (CI, refactors with no behavior change, docs) can skip by adding the `no-changeset` label. - -**How to add a changeset:** - -```bash -pnpm changeset -``` - -You'll be prompted to pick the bump type: -- **patch** — bug fixes, small tweaks (e.g. fixing `--json` output for a command) -- **minor** — new commands, new flags, new capabilities -- **major** — breaking changes (removed commands, changed flag behavior, output format changes) - -This creates a file like `.changeset/cool-dogs-fly.md`: - -```markdown ---- -"@alchemy/cli": minor ---- - -Add `alchemy portfolio transactions` command for portfolio transaction history. -``` - -Write a 1-2 sentence summary of the change from a user's perspective. Commit this file with your PR. - -**How releases work:** When PRs with changesets merge to `main`, the publish workflow automatically: -1. Verifies the build (typecheck, build, test) -2. Applies version bumps and updates `CHANGELOG.md` via `changeset version` -3. Creates a signed release commit via the GitHub Git Database API (using a GitHub App token) -4. Publishes to npm using OIDC trusted publishing (no long-lived npm token) -5. Creates a GitHub release/tag with notes extracted from `CHANGELOG.md` - -If no changesets are pending, the workflow exits cleanly — no release is created. - -**Release infrastructure:** -- Repository write operations use a GitHub App (`APP_ID` variable + `APP_PRIVATE_KEY` secret) -- npm publish uses [trusted publishing](https://docs.npmjs.com/generating-provenance-statements) (OIDC) — no `NPM_TOKEN` secret required -- Required GitHub repo settings: `APP_ID` (variable), `APP_PRIVATE_KEY` (secret) -- Required npm-side: configure trusted publishing for this repo/workflow at npmjs.com package settings - -### Endpoint Override Env Vars (Local Testing Only) - -These are for local/mock testing, not normal production usage: - -- `ALCHEMY_RPC_BASE_URL` -- `ALCHEMY_ADMIN_API_BASE_URL` -- `ALCHEMY_X402_BASE_URL` - -Safety constraints: - -- Only localhost targets are accepted (`localhost`, `127.0.0.1`, `::1`) -- Non-HTTPS transport is allowed only for localhost -- Production defaults are unchanged when unset From ee8a0cd0a3c39854e2f6cec2fee1eb0f9f5eaf44 Mon Sep 17 00:00:00 2001 From: Cody McCabe Date: Thu, 12 Mar 2026 20:03:09 -0400 Subject: [PATCH 2/5] update agents styling guidance Align AGENTS.md with current shared formatting modules by including src/lib/error-format.ts in the preferred helper paths. Made-with: Cursor --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d76a408..5f8d218 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Keep all terminal output visually consistent with existing Alchemy CLI conventions used by help, tables, and command output. ## Core Principles -- Prefer shared style helpers in `src/lib/ui.ts` and `src/lib/output.ts` over ad-hoc ANSI strings. +- Prefer shared style helpers in `src/lib/ui.ts`, `src/lib/output.ts`, and `src/lib/error-format.ts` over ad-hoc ANSI strings. - Use one visual voice: indented blocks, compact spacing, and consistent symbols (`◆` for section headers, `✗` for errors, `✓` for success). - Keep copy short and actionable. From c22e24e25717c03e1aa8c185c2d09c46cb0a46cf Mon Sep 17 00:00:00 2001 From: Cody McCabe Date: Thu, 12 Mar 2026 20:38:19 -0400 Subject: [PATCH 3/5] Add `agent-prompt` command for self-describing CLI usage Adds `alchemy agent-prompt` which emits a complete, machine-readable contract (JSON or plain-text) containing execution policy, preflight, auth matrix, full command tree, error codes with recovery actions, examples, and docs URL -- so agents can learn how to use the CLI entirely from the CLI itself. Made-with: Cursor --- README.md | 1 + src/commands/agent-prompt.ts | 263 ++++++++++++++++++++++++++++ src/commands/interactive.ts | 1 + src/index.ts | 4 +- tests/commands/agent-prompt.test.ts | 149 ++++++++++++++++ tests/e2e/cli.e2e.test.ts | 20 +++ 6 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 src/commands/agent-prompt.ts create mode 100644 tests/commands/agent-prompt.test.ts diff --git a/README.md b/README.md index b5c0ab2..11e7e61 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Use `alchemy help` or `alchemy help ` for generated command help. | `config get ` | Gets one config value | `alchemy config get network` | | `config list` | Lists all config values | `alchemy config list` | | `config reset [key]` | Resets one or all config values | `alchemy config reset --yes` | +| `agent-prompt` | Emits complete agent/automation usage instructions | `alchemy --json agent-prompt` | | `version` | Prints CLI version | `alchemy version` | ## Flags diff --git a/src/commands/agent-prompt.ts b/src/commands/agent-prompt.ts new file mode 100644 index 0000000..b6d0607 --- /dev/null +++ b/src/commands/agent-prompt.ts @@ -0,0 +1,263 @@ +import type { Command } from "commander"; +import { ErrorCode, EXIT_CODES, type ErrorCodeType } from "../lib/errors.js"; +import { isJSONMode, printJSON, printHuman } from "../lib/output.js"; + +interface CommandSchema { + name: string; + description: string; + aliases?: string[]; + arguments?: Array<{ name: string; description: string; required: boolean }>; + options?: Array<{ flags: string; description: string }>; + subcommands?: CommandSchema[]; +} + +interface ErrorEntry { + exitCode: number; + retryable: boolean; + recovery: string; +} + +interface AgentPrompt { + executionPolicy: string[]; + preflight: { command: string; description: string }; + auth: Array<{ + method: string; + envVar: string; + flag: string; + configKey: string; + commandFamilies: string[]; + }>; + commands: CommandSchema[]; + errors: Record; + examples: string[]; + docs: string; +} + +const RETRYABLE_CODES: ReadonlySet = new Set([ + ErrorCode.RATE_LIMITED, + ErrorCode.NETWORK_ERROR, +]); + +const ERROR_RECOVERY: Record = { + AUTH_REQUIRED: + "Set ALCHEMY_API_KEY env var or run: alchemy config set api-key ", + INVALID_API_KEY: + "Check your API key and set a valid one: alchemy config set api-key ", + NETWORK_NOT_ENABLED: + "Enable the target network for your app at dashboard.alchemy.com", + INVALID_ACCESS_KEY: + "Check your access key: https://dashboard.alchemy.com/", + ACCESS_KEY_REQUIRED: + "Set ALCHEMY_ACCESS_KEY env var or run: alchemy config set access-key ", + APP_REQUIRED: + "Select an app: alchemy config set app ", + ADMIN_API_ERROR: + "Check the error message for details; verify access key permissions", + NETWORK_ERROR: + "Check internet connection and retry", + RPC_ERROR: + "Check RPC method, params, and network; verify API key has access", + INVALID_ARGS: + "Check command usage via: alchemy --json help ", + NOT_FOUND: + "Verify the resource identifier (address, hash, id) is correct", + RATE_LIMITED: + "Wait and retry; consider upgrading your Alchemy plan", + PAYMENT_REQUIRED: + "Fund your x402 wallet or switch to API key auth", + SETUP_REQUIRED: + "Run preflight: alchemy --json setup status, then follow nextCommands", + INTERNAL_ERROR: + "Unexpected error; retry or report a bug", +}; + +function buildCommandSchema(cmd: Command): CommandSchema { + const schema: CommandSchema = { + name: cmd.name(), + description: cmd.description(), + }; + + const aliases = cmd.aliases(); + if (aliases.length > 0) { + schema.aliases = aliases; + } + + const args = cmd.registeredArguments; + if (args.length > 0) { + schema.arguments = args.map((a) => ({ + name: a.name(), + description: a.description, + required: a.required, + })); + } + + const opts = cmd.options; + if (opts.length > 0) { + schema.options = opts.map((o) => ({ + flags: o.flags, + description: o.description, + })); + } + + const subs = cmd.commands; + if (subs.length > 0) { + schema.subcommands = subs.map(buildCommandSchema); + } + + return schema; +} + +function buildAgentPrompt(program: Command): AgentPrompt { + const errors: Record = {}; + for (const [code, exitCode] of Object.entries(EXIT_CODES)) { + errors[code] = { + exitCode, + retryable: RETRYABLE_CODES.has(code), + recovery: ERROR_RECOVERY[code as ErrorCodeType] ?? "Check error message", + }; + } + + const commands = program.commands + .filter((cmd) => cmd.name() !== "agent-prompt") + .map(buildCommandSchema); + + return { + executionPolicy: [ + "Always pass --json --no-interactive", + "Parse stdout as JSON on exit code 0", + "Parse stderr as JSON on nonzero exit code", + "Never run bare 'alchemy' without --json --no-interactive", + ], + preflight: { + command: "alchemy --json setup status", + description: + "Check auth readiness before first command. If complete is false, follow nextCommands in the response to configure auth.", + }, + auth: [ + { + method: "API key", + envVar: "ALCHEMY_API_KEY", + flag: "--api-key ", + configKey: "api-key", + commandFamilies: [ + "balance", + "tx", + "block", + "rpc", + "trace", + "debug", + "tokens", + "nfts", + "transfers", + "prices", + "portfolio", + "simulate", + "solana", + ], + }, + { + method: "Access key", + envVar: "ALCHEMY_ACCESS_KEY", + flag: "--access-key ", + configKey: "access-key", + commandFamilies: ["apps", "chains", "network list --configured"], + }, + { + method: "Webhook API key", + envVar: "ALCHEMY_WEBHOOK_API_KEY", + flag: "--webhook-api-key ", + configKey: "webhook-api-key", + commandFamilies: ["webhooks"], + }, + { + method: "x402 wallet", + envVar: "ALCHEMY_WALLET_KEY", + flag: "--x402 --wallet-key-file ", + configKey: "x402", + commandFamilies: [ + "balance", + "tx", + "block", + "rpc", + "trace", + "debug", + "tokens", + "nfts", + "transfers", + ], + }, + ], + commands, + errors, + examples: [ + "alchemy --json --no-interactive setup status", + "alchemy --json --no-interactive balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --api-key $ALCHEMY_API_KEY", + "alchemy --json --no-interactive apps list --access-key $ALCHEMY_ACCESS_KEY", + "alchemy --json --no-interactive rpc eth_blockNumber --api-key $ALCHEMY_API_KEY", + "alchemy --json --no-interactive network list", + ], + docs: "https://www.alchemy.com/docs", + }; +} + +function formatAsSystemPrompt(payload: AgentPrompt): string { + const lines: string[] = []; + + lines.push("Alchemy CLI agent instructions"); + lines.push("=============================="); + lines.push(""); + + lines.push("Execution policy:"); + for (const rule of payload.executionPolicy) { + lines.push(` - ${rule}`); + } + lines.push(""); + + lines.push("Preflight:"); + lines.push(` Command: ${payload.preflight.command}`); + lines.push(` ${payload.preflight.description}`); + lines.push(""); + + lines.push("Auth methods:"); + for (const auth of payload.auth) { + lines.push(` ${auth.method}:`); + lines.push(` env: ${auth.envVar}`); + lines.push(` flag: ${auth.flag}`); + lines.push(` config: alchemy config set ${auth.configKey} `); + lines.push(` commands: ${auth.commandFamilies.join(", ")}`); + } + lines.push(""); + + lines.push("Error codes:"); + for (const [code, entry] of Object.entries(payload.errors)) { + const retry = entry.retryable ? " [retryable]" : ""; + lines.push(` ${code} (exit ${entry.exitCode})${retry}: ${entry.recovery}`); + } + lines.push(""); + + lines.push("Examples:"); + for (const example of payload.examples) { + lines.push(` ${example}`); + } + lines.push(""); + + lines.push(`Docs: ${payload.docs}`); + lines.push(" For RPC method signatures, parameters, and supported networks."); + lines.push(""); + lines.push( + "For full command tree, run: alchemy --json agent-prompt", + ); + lines.push(""); + + return lines.join("\n"); +} + +export function registerAgentPrompt(program: Command) { + program + .command("agent-prompt") + .description("Emit complete agent/automation usage instructions") + .action(() => { + const payload = buildAgentPrompt(program); + printHuman(formatAsSystemPrompt(payload), payload); + }); +} diff --git a/src/commands/interactive.ts b/src/commands/interactive.ts index 15a0c20..1325cc2 100644 --- a/src/commands/interactive.ts +++ b/src/commands/interactive.ts @@ -93,6 +93,7 @@ const COMMAND_NAMES = [ "tokens metadata", "tokens allowance", "tx", + "agent-prompt", "version", "wallet", "wallet generate", diff --git a/src/index.ts b/src/index.ts index 7d36ddd..b93caf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ import { registerWebhooks } from "./commands/webhooks.js"; import { registerBundler } from "./commands/bundler.js"; import { registerGasManager } from "./commands/gas-manager.js"; import { registerSolana } from "./commands/solana.js"; +import { registerAgentPrompt } from "./commands/agent-prompt.js"; import { isInteractiveAllowed } from "./lib/interaction.js"; import { getSetupStatus, isSetupComplete, shouldRunOnboarding } from "./lib/onboarding.js"; @@ -71,7 +72,7 @@ const ROOT_COMMAND_PILLARS = [ }, { label: "Admin", - commands: ["apps", "config", "setup", "version", "help"], + commands: ["apps", "config", "setup", "agent-prompt", "version", "help"], }, ] as const; @@ -361,6 +362,7 @@ registerApps(program); registerSetup(program); registerConfig(program); registerSolana(program); +registerAgentPrompt(program); registerVersion(program); program .command("help [command...]") diff --git a/tests/commands/agent-prompt.test.ts b/tests/commands/agent-prompt.test.ts new file mode 100644 index 0000000..d6b1793 --- /dev/null +++ b/tests/commands/agent-prompt.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { Command } from "commander"; +import { ErrorCode } from "../../src/lib/errors.js"; + +describe("agent-prompt command", () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.resetModules(); + }); + + it("emits JSON payload with all required top-level keys", async () => { + const printHuman = vi.fn(); + vi.doMock("../../src/lib/output.js", () => ({ + isJSONMode: () => true, + printJSON: vi.fn(), + printHuman, + })); + + const { registerAgentPrompt } = await import( + "../../src/commands/agent-prompt.js" + ); + const program = new Command(); + program.command("balance").description("Get ETH balance"); + program.command("apps").description("Manage apps"); + registerAgentPrompt(program); + + await program.parseAsync(["node", "test", "agent-prompt"], { + from: "node", + }); + + expect(printHuman).toHaveBeenCalledTimes(1); + const payload = printHuman.mock.calls[0][1]; + + expect(payload).toHaveProperty("executionPolicy"); + expect(payload).toHaveProperty("preflight"); + expect(payload).toHaveProperty("auth"); + expect(payload).toHaveProperty("commands"); + expect(payload).toHaveProperty("errors"); + expect(payload).toHaveProperty("examples"); + expect(payload).toHaveProperty("docs"); + }); + + it("errors map contains all ErrorCode values", async () => { + const printHuman = vi.fn(); + vi.doMock("../../src/lib/output.js", () => ({ + isJSONMode: () => true, + printJSON: vi.fn(), + printHuman, + })); + + const { registerAgentPrompt } = await import( + "../../src/commands/agent-prompt.js" + ); + const program = new Command(); + registerAgentPrompt(program); + + await program.parseAsync(["node", "test", "agent-prompt"], { + from: "node", + }); + + const payload = printHuman.mock.calls[0][1]; + for (const code of Object.values(ErrorCode)) { + expect(payload.errors).toHaveProperty(code); + expect(payload.errors[code]).toHaveProperty("exitCode"); + expect(payload.errors[code]).toHaveProperty("retryable"); + expect(payload.errors[code]).toHaveProperty("recovery"); + } + }); + + it("commands array excludes agent-prompt itself", async () => { + const printHuman = vi.fn(); + vi.doMock("../../src/lib/output.js", () => ({ + isJSONMode: () => true, + printJSON: vi.fn(), + printHuman, + })); + + const { registerAgentPrompt } = await import( + "../../src/commands/agent-prompt.js" + ); + const program = new Command(); + program.command("balance").description("Get ETH balance"); + registerAgentPrompt(program); + + await program.parseAsync(["node", "test", "agent-prompt"], { + from: "node", + }); + + const payload = printHuman.mock.calls[0][1]; + const commandNames = payload.commands.map( + (c: { name: string }) => c.name, + ); + expect(commandNames).toContain("balance"); + expect(commandNames).not.toContain("agent-prompt"); + }); + + it("marks RATE_LIMITED and NETWORK_ERROR as retryable", async () => { + const printHuman = vi.fn(); + vi.doMock("../../src/lib/output.js", () => ({ + isJSONMode: () => true, + printJSON: vi.fn(), + printHuman, + })); + + const { registerAgentPrompt } = await import( + "../../src/commands/agent-prompt.js" + ); + const program = new Command(); + registerAgentPrompt(program); + + await program.parseAsync(["node", "test", "agent-prompt"], { + from: "node", + }); + + const payload = printHuman.mock.calls[0][1]; + expect(payload.errors.RATE_LIMITED.retryable).toBe(true); + expect(payload.errors.NETWORK_ERROR.retryable).toBe(true); + expect(payload.errors.AUTH_REQUIRED.retryable).toBe(false); + expect(payload.errors.INVALID_ARGS.retryable).toBe(false); + }); + + it("includes auth entries for all method types", async () => { + const printHuman = vi.fn(); + vi.doMock("../../src/lib/output.js", () => ({ + isJSONMode: () => true, + printJSON: vi.fn(), + printHuman, + })); + + const { registerAgentPrompt } = await import( + "../../src/commands/agent-prompt.js" + ); + const program = new Command(); + registerAgentPrompt(program); + + await program.parseAsync(["node", "test", "agent-prompt"], { + from: "node", + }); + + const payload = printHuman.mock.calls[0][1]; + const methods = payload.auth.map( + (a: { method: string }) => a.method, + ); + expect(methods).toContain("API key"); + expect(methods).toContain("Access key"); + expect(methods).toContain("Webhook API key"); + expect(methods).toContain("x402 wallet"); + }); +}); diff --git a/tests/e2e/cli.e2e.test.ts b/tests/e2e/cli.e2e.test.ts index 2dc1359..d0a44e5 100644 --- a/tests/e2e/cli.e2e.test.ts +++ b/tests/e2e/cli.e2e.test.ts @@ -371,6 +371,26 @@ describe("CLI mock E2E", () => { }); }); + it("agent-prompt returns full agent contract JSON", async () => { + const result = await runCLI(["--json", "agent-prompt"]); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + const payload = parseJSON(result.stdout) as Record; + expect(payload).toHaveProperty("executionPolicy"); + expect(payload).toHaveProperty("preflight"); + expect(payload).toHaveProperty("auth"); + expect(payload).toHaveProperty("commands"); + expect(payload).toHaveProperty("errors"); + expect(payload).toHaveProperty("examples"); + expect(payload).toHaveProperty("docs"); + + const commands = payload.commands as Array<{ name: string }>; + expect(commands.length).toBeGreaterThan(10); + expect(commands.some((c) => c.name === "balance")).toBe(true); + expect(commands.some((c) => c.name === "agent-prompt")).toBe(false); + }); + it("bare no-interactive returns SETUP_REQUIRED with remediation data", async () => { const result = await runCLI(["--json", "--no-interactive"]); From b30fdc2127f7ac9b0e80b27bc2c1669a7591e75e Mon Sep 17 00:00:00 2001 From: Cody McCabe Date: Thu, 12 Mar 2026 20:40:17 -0400 Subject: [PATCH 4/5] Add changeset for agent-prompt command Made-with: Cursor --- .changeset/add-agent-prompt-command.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/add-agent-prompt-command.md diff --git a/.changeset/add-agent-prompt-command.md b/.changeset/add-agent-prompt-command.md new file mode 100644 index 0000000..1722cae --- /dev/null +++ b/.changeset/add-agent-prompt-command.md @@ -0,0 +1,5 @@ +--- +"@alchemy/cli": minor +--- + +Add `agent-prompt` command that emits complete automation/agent usage instructions including execution policy, auth methods, error codes, and command schema. From ae0ca504abd57ef21f7f9f6bff9695349651495d Mon Sep 17 00:00:00 2001 From: Cody McCabe Date: Thu, 12 Mar 2026 20:44:15 -0400 Subject: [PATCH 5/5] Add agent bootstrap example to README Show agents how to run `alchemy --json agent-prompt` as their first step to get the full CLI contract without external docs. Made-with: Cursor --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 11e7e61..6f4231c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,17 @@ alchemy alchemy balance 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --json --no-interactive ``` +#### Agent bootstrap + +Have your agent run `agent-prompt` as its first step to get a complete, machine-readable contract describing every command, auth method, error code, and execution rule: + +```bash +# Agent runs this once to learn everything the CLI can do +alchemy --json agent-prompt +``` + +This returns a single JSON document with execution policy, preflight instructions, auth matrix, the full command tree with all arguments and options, error codes with recovery actions, and example invocations. No external docs required. + ## Command Reference Run commands as `alchemy `.