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. 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. 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..6f4231c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,58 @@ 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 +``` + +#### 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 `. @@ -103,6 +155,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 @@ -176,13 +229,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 +274,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 +333,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 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"]);