diff --git a/src/handlers/project/invoke/index.tsx b/src/handlers/project/invoke/index.tsx index c9bd1d8d27..24b2089dff 100644 --- a/src/handlers/project/invoke/index.tsx +++ b/src/handlers/project/invoke/index.tsx @@ -120,6 +120,27 @@ export function createProjectInvokeHandler(core: Core, io: AppIO) { description: "invoke a Runtime, harness, or Gateway", flags: invokeFlags, middlewares: [withProject({ projectManager: core.projectManager, optional: true })], + examples: [ + { description: "Choose a project resource to invoke", command: "agentcore invoke" }, + { + description: "Send a payload to a project Runtime", + command: `agentcore invoke --runtime checkout --payload '{"prompt":"Hello"}'`, + }, + { + description: "Send a prompt to a harness by ID", + command: `agentcore invoke --harness support-AbCdEf1234 --prompt "Hello"`, + }, + { + description: "List the tools on a Gateway by ARN", + command: + "agentcore invoke --gateway arn:aws:bedrock-agentcore:us-west-2:111122223333:gateway/tools-AbCdEf1234 " + + `--path /mcp --payload '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'`, + }, + { + description: "Invoke a Runtime on the local development server", + command: `agentcore invoke --runtime checkout --local --payload '{"prompt":"Hello"}'`, + }, + ], handle: async (ctx, flags) => { const project = ctx.value(ProjectKey); const headless = ctx.require(JsonKey) || Object.values(flags).some(isSet); diff --git a/src/router/flags.tsx b/src/router/flags.tsx index 0e40a9326a..d8ee8a5a95 100644 --- a/src/router/flags.tsx +++ b/src/router/flags.tsx @@ -1,7 +1,7 @@ import { Option } from "commander"; import { InputValidationError } from "../errors"; import type { Context } from "./context"; -import type { Flag, GlobalFlag } from "./handler"; +import type { Flag, GlobalFlag, HelpExample } from "./handler"; import { coerce, formatZodError, inspect } from "./schema"; // toOption builds a Commander Option from a flag's schema. A boolean that defaults @@ -54,6 +54,12 @@ export function formatParameterDetails(flags: Flag[]): string | undefined { return `\nParameter details:\n\n${sections.join("\n\n")}\n`; } +export function formatExamples(examples: HelpExample[]): string | undefined { + if (examples.length === 0) return undefined; + const sections = examples.map(({ description, command }) => ` ${description}\n $ ${command}`); + return `\nExamples:\n\n${sections.join("\n\n")}\n`; +} + // attributeName mirrors how Commander camelCases an option name into the key it // stores on the parsed options object (e.g. "harness-id" -> "harnessId"). export function attributeName(name: string): string { diff --git a/src/router/handler.tsx b/src/router/handler.tsx index 95f63a1a6a..8073d49109 100644 --- a/src/router/handler.tsx +++ b/src/router/handler.tsx @@ -101,6 +101,19 @@ export interface Handler { children(): Handler[]; } +export interface HelpExample { + description: string; + command: string; +} + +export interface ExamplesProvider { + examples(): HelpExample[]; +} + +export function isExamplesProvider(h: Handler): h is Handler & ExamplesProvider { + return typeof (h as Partial).examples === "function"; +} + type CreateHandlerInput< F extends readonly Flag[], A extends readonly Argument[], @@ -112,6 +125,7 @@ type CreateHandlerInput< handle?: HandleFn; children?: Handler[]; middlewares?: Middleware[]; + examples?: HelpExample[]; }; const noOpHandler = async (_ctx: Context, _flags: any, _args: any): Promise => {}; @@ -124,6 +138,7 @@ class BaseHandler implements Handler { _handle: HandleFn; _children: Handler[]; _middlewares: Middleware[]; + _examples: HelpExample[]; constructor( input: CreateHandlerInput[], readonly Argument[]>, @@ -135,6 +150,7 @@ class BaseHandler implements Handler { this._handle = (input.handle ?? noOpHandler) as HandleFn; this._children = input.children ?? []; this._middlewares = input.middlewares ?? []; + this._examples = input.examples ?? []; } name(): string { @@ -168,6 +184,10 @@ class BaseHandler implements Handler { middlewares(): Middleware[] { return this._middlewares; } + + examples(): HelpExample[] { + return this._examples; + } } // createHandler infers the flags tuple from `flags` (the `const` type parameter diff --git a/src/router/index.tsx b/src/router/index.tsx index b0d7fe9e0f..8777f0c621 100644 --- a/src/router/index.tsx +++ b/src/router/index.tsx @@ -20,6 +20,7 @@ export { type GlobalFlag, type Argument, type FlagsOf, + type HelpExample, createHandler, flag, globalFlag, diff --git a/src/router/router.test.ts b/src/router/router.test.ts index e7134d1084..3426f89637 100644 --- a/src/router/router.test.ts +++ b/src/router/router.test.ts @@ -810,6 +810,27 @@ test("commands without long-form flag help have no Parameter details section", a expect(out).toContain("--id"); expect(out).not.toContain("Parameter details:"); + expect(out).not.toContain("Examples:"); +}); + +test("handlers with examples render an Examples section", async () => { + const get = createHandler({ + name: "get", + description: "", + examples: [ + { description: "Get a thing", command: "app get --id a" }, + { description: "Get it as JSON", command: "app get --id a --json" }, + ], + handle: async () => {}, + }); + const root = new Router("app"); + root.handler(get); + + const out = await helpOutput(root, ["app", "get", "--help"]); + + expect(out).toContain( + "Examples:\n\n Get a thing\n $ app get --id a\n\n Get it as JSON\n $ app get --id a --json\n", + ); }); // --- telemetry: command path recording ------------------------------------- diff --git a/src/router/router.tsx b/src/router/router.tsx index ac528ed096..fd8a23efe8 100644 --- a/src/router/router.tsx +++ b/src/router/router.tsx @@ -1,9 +1,16 @@ -import type { Argument, Flag, GlobalFlag, Handler } from "./handler"; +import { + isExamplesProvider, + type Argument, + type Flag, + type GlobalFlag, + type Handler, +} from "./handler"; import { type Middleware, type MiddlewareProvider, isMiddlewareProvider } from "./middleware"; import { type Context, type ContextKey, ValueContext, contextKey } from "./context"; import { applyGlobalFlags, attributeName, + formatExamples, formatParameterDetails, parseFlags, toOption, @@ -227,6 +234,11 @@ export function compile( ); } + const examples = formatExamples(isExamplesProvider(node) ? node.examples() : []); + if (examples) { + c.addHelpText("after", examples); + } + // Flags with long-form documentation get a "Parameter details" section after // the option list in `--help` output. const parameterDetails = formatParameterDetails(ownFlags);