For AI agents: the complete documentation index is available at https://scriptedalchemy.github.io/agent-bundle/llms.txt, the full documentation bundle is available at https://scriptedalchemy.github.io/agent-bundle/llms-full.txt, and this page is available as Markdown at https://scriptedalchemy.github.io/agent-bundle/guide/authoring/package-entries.md.
Skip to main content
  • English
  • CLI and library package entries

    agent-bundle is the build product for agent plugins the way Rslib is for libraries: one agent-bundle.config.ts, one CLI, framework-owned entry lifecycles, and a single blessed escape hatch into the bundler. The same config that emits host artifacts also owns the npm package build, so a plugin that also ships as a CLI or a library needs no second bundler config.

    bin and lib

    import { 
    const defineConfig: (config: AgentBundleConfig | ConfigFactory) => AgentBundleConfig | ConfigFactory
    defineConfig
    } from 'agent-bundle/config';
    export default
    function defineConfig(config: AgentBundleConfig | ConfigFactory): AgentBundleConfig | ConfigFactory
    defineConfig
    ({
    AgentBundleConfig.bin?: AgentBundleBinConfig | undefined
    bin
    : { 'my-plugin': './src/cli.ts' },
    AgentBundleConfig.lib?: AgentBundleLibConfig | undefined
    lib
    : {
    AgentBundleLibEntry.dts?: boolean | undefined

    Emit type declarations next to the library output — Rslib's bundleless dts mode, a .d.ts graph beside the bundle rather than one rolled-up file. Defaults to true.

    dts
    : true,
    AgentBundleLibEntry.entry: string
    entry
    : './src/index.ts' },
    AgentBundleConfig.output?: AgentBundleOutputConfig | undefined
    output
    : {
    AgentBundleOutputConfig.distPath?: string | undefined

    The artifact output directory of agent-bundle build, relative to the project root. Defaults to dist. The per-invocation CLI --output flag still wins, but remains subject to the same project-root containment check; absolute and external output paths are unsupported.

    distPath
    : 'artifact' },
    AgentBundleConfig.plugin: AgentBundlePluginConfig
    plugin
    : {
    AgentBundlePluginConfig.description?: string | undefined
    description
    : 'A CLI plus a plugin.',
    AgentBundlePluginConfig.name: string
    name
    : 'my-plugin' },
    AgentBundleConfig.targets?: string[] | undefined
    targets
    : ['portable', 'claude'],
    });
    ConfigOutputNotes
    bin: { '<name>': './src/cli.ts' }dist/bin/<name>.jsSelf-executing ESM bundle, #!/usr/bin/env node shebang, executable bit.
    lib: { entry: './src/index.ts', dts: true }dist/<stem>.js plus dist/**/*.d.tsSingle-entry ESM profile, node target, es2022 syntax.

    The conventions src/cli.ts and src/index.ts fill these in when the config is silent. Config always wins, and bin: false / lib: false opt out.

    Because package outputs live in dist/, host artifacts must live somewhere else: the CLI's default artifact root is artifact/, and pointing output.distPath or --output at dist on a project with package entries is AB4706. dist is a mandatory-ignored directory: package outputs never enter project source snapshots or Skill and asset discovery.

    Outputs are staged and published atomically, and their provenance, bytes, SHA-256, and sorted project-relative source inputs, is reported on the build result exactly like artifact files.

    Declarations

    lib.dts defaults to true. Declaration generation resolves typescript from the project, so add it as a devDependency. It compiles the lib entry's source directory as its own program: compiler options come from the project tsconfig.json via extends, rootDir is pinned to the entry's directory, and only that subtree is included, test files never fail or pollute the package build. Declarations land flat under dist/, one .d.ts per source module.

    The lib profile is deliberately thin. A package that needs a multi-format matrix (UMD, multiple entries, per-format tsconfig) has outgrown the profile and genuinely wants Rslib. That is the one case where a second bundler config remains, by choice.

    The executable envelope

    A bin entry, or an artifact script, whose module exports main, or default-exports a function, receives the generated process envelope:

    // src/cli.ts — the whole CLI entry a consumer writes
    import type { ExecutableMainContext } from 'agent-bundle';
    
    export const main = async (
      argv: readonly string[],
      { terminal }: ExecutableMainContext,
    ): Promise<number> => {
      if (terminal.stderr.color !== 'none') { /* paint progress on stderr */ }
      return 0;
    };

    The envelope awaits main(process.argv.slice(2), { terminal }), adopts a numeric return as the process exit code, and lets an escaped rejection surface through Node's top-level failure path (stack to stderr, exit code 1). terminal is the process's terminal capability: TTY-ness, color, and size for stdout and stderr, plus whether the two share one target, probed once before main runs by a dependency-free module the envelope inlines, a plain script or bin loads no runtime for it. hostSurface is cli for a package bin and script for an artifact script, so a module shipped on both surfaces sees the one it was launched from. A main declared with one parameter keeps working; the second argument is simply unread. Self-executing modules with no main export bundle directly, byte for byte, and receive no probe.

    The routed CLI

    A src/cli/** surface compiles into one framework-generated executable instead of a hand-written dispatcher, superseding the src/cli.ts bin convention for the project. Nesting is identity: src/cli/library/audit.ts runs as <bin> library audit.

    // src/cli/inspect.ts — the whole command a consumer writes
    import type { CliRouteConfig, 
    interface CliRouteProps<InputSchema extends RouteSchema>

    Props received by every routed CLI command's default function.

    Read transport-owned invocation, host, session, actor, and workspace axes with await agent() from @agent-bundle/runtime. Every identity axis is Observed; unavailable axes carry a typed reason, and parsed command input cannot override request identity.

    CliRouteProps
    } from 'agent-bundle';
    import {
    import z
    z
    } from 'zod';
    export const
    const config: {
        description: string;
        positionals: string[];
        inputJsonSchema: {
            type: "object";
            additionalProperties: false;
            properties: {
                maxFiles: {
                    type: "number";
                };
                root: {
                    type: "string";
                };
            };
            required: string[];
        };
    }
    config
    = {
    CliRouteConfig.description?: string | undefined
    description
    : 'Inspect a bounded source tree without changing it.',
    CliRouteConfig.positionals?: readonly string[] | undefined

    The inputSchema keys consumed as bare arguments, in order. All but the last must be scalar; a trailing z.array(...) key is variadic. Keys not named here become --options.

    positionals
    : ['root'],
    CliRouteConfig.inputJsonSchema?: RouteInputSchema | undefined

    Execution-free input metadata for forms and CLI flags; the original schema owns validation.

    inputJsonSchema
    : {
    RouteInputSchema.type: "object"
    type
    : 'object',
    RouteInputSchema.additionalProperties: false
    additionalProperties
    : false,
    RouteInputSchema.properties: Readonly<Record<string, RouteInputPropertySchema>>
    properties
    : {
    maxFiles: {
        type: "number";
    }
    maxFiles
    : {
    RouteInputNumberSchema.type: "number"
    type
    : 'number' },
    root: {
        type: "string";
    }
    root
    : {
    RouteInputStringSchema.type: "string"
    type
    : 'string' } },
    RouteInputSchema.required?: readonly string[] | undefined
    required
    : ['root'],
    }, } satisfies CliRouteConfig; export const
    const inputSchema: z.ZodObject<{
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    }, z.core.$strict>
    inputSchema
    =
    import z
    z
    .
    function object<{
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    }>(shape?: {
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    } | undefined, params?: string | {
        error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
        message?: string | undefined | undefined;
    } | undefined): z.ZodObject<{
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    }, z.core.$strip>
    object
    ({
    maxFiles: z.ZodOptional<z.ZodNumber>
    maxFiles
    :
    import z
    z
    .
    function number(params?: string | z.core.$ZodNumberParams): z.ZodNumber
    number
    ().
    _ZodNumber<$ZodNumberInternals<number>>.int(params?: string | z.core.$ZodCheckNumberFormatParams): z.ZodNumber

    Consider z.int() instead. This API is considered legacy; it will never be removed but a better alternative exists.

    int
    ().
    _ZodNumber<$ZodNumberInternals<number>>.min(value: number, params?: string | z.core.$ZodCheckGreaterThanParams): z.ZodNumber
    min
    (1).
    _ZodNumber<$ZodNumberInternals<number>>.max(value: number, params?: string | z.core.$ZodCheckLessThanParams): z.ZodNumber
    max
    (256).
    ZodType<any, any, $ZodNumberInternals<number>>.optional(): z.ZodOptional<z.ZodNumber>
    optional
    (),
    root: z.ZodString
    root
    :
    import z
    z
    .
    function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)
    string
    ().
    _ZodString<$ZodStringInternals<string>>.min(minLength: number, params?: string | z.core.$ZodCheckMinLengthParams): z.ZodString
    min
    (1),
    }).
    ZodObject<{ maxFiles: ZodOptional<ZodNumber>; root: ZodString; }, $strip>.strict(): z.ZodObject<{
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    }, z.core.$strict>

    Consider z.strictObject(A.shape) instead

    strict
    ();
    export const
    const resultSchema: z.ZodObject<{
        scanned: z.ZodNumber;
    }, z.core.$strict>
    resultSchema
    =
    import z
    z
    .
    function object<{
        scanned: z.ZodNumber;
    }>(shape?: {
        scanned: z.ZodNumber;
    } | undefined, params?: string | {
        error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
        message?: string | undefined | undefined;
    } | undefined): z.ZodObject<{
        scanned: z.ZodNumber;
    }, z.core.$strip>
    object
    ({
    scanned: z.ZodNumber
    scanned
    :
    import z
    z
    .
    function number(params?: string | z.core.$ZodNumberParams): z.ZodNumber
    number
    ().
    _ZodNumber<$ZodNumberInternals<number>>.int(params?: string | z.core.$ZodCheckNumberFormatParams): z.ZodNumber

    Consider z.int() instead. This API is considered legacy; it will never be removed but a better alternative exists.

    int
    () }).
    ZodObject<{ scanned: ZodNumber; }, $strip>.strict(): z.ZodObject<{
        scanned: z.ZodNumber;
    }, z.core.$strict>

    Consider z.strictObject(A.shape) instead

    strict
    ();
    export default async function
    function inspect({ input, signal }: CliRouteProps<typeof inputSchema>): Promise<{
        scanned: number;
    }>
    inspect
    (
    {
    input: {
        root: string;
        maxFiles?: number | undefined;
    }
    input
    ,
    signal: AbortSignal
    signal
    }:
    interface CliRouteProps<InputSchema extends RouteSchema>

    Props received by every routed CLI command's default function.

    Read transport-owned invocation, host, session, actor, and workspace axes with await agent() from @agent-bundle/runtime. Every identity axis is Observed; unavailable axes carry a typed reason, and parsed command input cannot override request identity.

    CliRouteProps
    <typeof
    const inputSchema: z.ZodObject<{
        maxFiles: z.ZodOptional<z.ZodNumber>;
        root: z.ZodString;
    }, z.core.$strict>
    inputSchema
    >,
    ) {
    signal: AbortSignal
    signal
    .
    AbortSignal.throwIfAborted(): void

    The throwIfAborted() method throws the signal's abort reason if the signal has been aborted; otherwise it does nothing.

    MDN Reference

    throwIfAborted
    ();
    return {
    scanned: number
    scanned
    :
    input: {
        root: string;
        maxFiles?: number | undefined;
    }
    input
    .
    maxFiles?: number | undefined
    maxFiles
    ?? 0 };
    }

    The compiler statically projects config.inputJsonSchema onto argv, generates nested help (--help at every level, --version at the root), and emits bin/<plugin-name>.mjs in the composite artifact. At run time the shell resolves the command path, parses and coerces argv, validates through the module's own schemas, executes the default function inside the typed Agent request context, writes one canonical JSON line to stdout, and maps exit codes deterministically:

    Exit codeMeaning
    0Success, or the result's exitCode under config.exitCode: 'result'.
    1Execution failure.
    2Usage or input failure.
    130 / 143SIGINT / SIGTERM, which also reach the route's AbortSignal.

    When inputSchema rejects the parsed argv, the shell spells each issue in CLI terms instead of printing the raw schema issue JSON: the argument as typed (--max-files, <root>, or --input.<path> for a projected MCP command), the expectation, and the received value, one line per issue, then the exact usage line and the --help hint, all on stderr, exit code 2:

    Invalid value for --max-files: expected number <= 55000; received 300000.
    Usage: curator doctor [options] <root>
    Run 'curator doctor --help' for usage.

    Under --json stdout stays empty and stderr carries one canonical {"error":{"code":"CLI_INPUT_INVALID","issues":[...],"usage":"..."}} line; under --ndjson the stream carries one type: "error" event with the same error object.

    A .tsx command route swaps the default function for a default Server Component with the same props and renders through the runtime dispatcher against a sibling dist/bin/<plugin-name>-flight.mjs worker. It gains the four output modes described in Scripts and assets. Routed CLI projects need @agent-bundle/runtime as a dependency, because the generated executable installs the request context through it.

    A rendered command runs inside a render session whose wall clock defaults to 60 seconds. A command that legitimately runs longer, <bin> await <ticket> over a two-hour build, declares its budget as config.render: { maxElapsedMs } (a positive integer of milliseconds up to 24 hours, AB4835 otherwise); the compiled command carries it into the generated executable, and a projected MCP command (routes.mcpCommands) inherits its tool's. A plain .ts command executes without a render session and rejects the key. See Render budget.

    Opt out with bin: false, routes.cli: 'conventional', or by prefixing a path segment with _.

    Share one schema between MCP and CLI

    Runtime inputSchema and resultSchema may be imported or created by a factory. The compiler checks export presence; the generated executable loads the actual schemas. TypeScript infers route input/output types from those exports, including defaults and transforms.

    For execution-free inspection, Workbench forms, and named CLI flags, declare literal inputJsonSchema in the tool's defineTool object. It accepts an object with additionalProperties: false, scalar string/number/boolean properties, string enums, arrays of scalars, and optional required, description, and default metadata. The runtime schema remains authoritative for validation, defaults, and transforms; metadata does not replace it.

    import { defineTool } from 'agent-bundle/routes';
    
    export default defineTool({
      inputJsonSchema: {
        type: 'object',
        additionalProperties: false,
        properties: {
          laneKey: { type: 'string' },
          limit: { type: 'number' },
        },
      },
      inputSchema,
      resultSchema,
    }, handler);

    A standalone CLI route or tool projection without this metadata accepts --input JSON. Declare input: 'json' explicitly to select that mode even when metadata exists. Flag or positional mappings require metadata; missing tool metadata reports AB4845. Malformed metadata or invalid standalone argv policy reports AB4814. The compiler does not interpret Zod expressions or follow schema alias chains; AB4838 and AB4839 are retired.

    Project one tool as an idiomatic command

    A separate CLI route has its own operation identity. A colocated <tool>.cli.ts projection module is the CLI surface projection of the same operation, identity stays tool:<server>/<tool>, and is never a route. Host projection (targets) is unchanged and orthogonal.

    The module sits beside the tool, never under src/cli/**. Status needs only renames; the parser already emits canonical keys, so there is no mapInput:

    // src/mcp/hauler/tools/hauler_status.cli.ts
    import type { CliProjectionConfig } from 'agent-bundle/routes';
    import type { inputSchema } from './hauler_status.js';
    
    export const config = {
      command: ['status'],
      confirm: false,
      description:
        'Show the queue, in-flight cargo work, lanes, and admission state.',
      flags: {
        laneKey: {
          description: 'Only requests in this lane key',
          name: 'lane',
        },
        limit: { description: 'Recent rows to show' },
        statuses: {
          description: 'Only these statuses (repeatable)',
          name: 'status',
        },
        tickets: {
          description: 'Only these tickets (repeatable)',
          name: 'ticket',
        },
      },
    } satisfies CliProjectionConfig<typeof inputSchema>;

    Request needs positionals plus a sync or async mapInput that fills canonical-required cwd from process.cwd() and splits comma-separated --after lists:

    // src/mcp/hauler/tools/hauler_request.cli.ts
    import type { CliProjectionConfig } from 'agent-bundle/routes';
    import type { z } from 'zod';
    
    import { parseTicketList } from '../../../client/parse.js';
    import type { inputSchema } from './hauler_request.js';
    
    export const config = {
      command: ['request'],
      confirm: false,
      description:
        'Submit a background cargo request and print its ticket: ' +
        'hauler request [--after cc-N] -- cargo check -p foo',
      flags: {
        after: {
          description:
            'Tickets that must finish first (repeatable, or comma-separated)',
        },
        cwd: {
          description: 'Workspace directory (default: current directory)',
          required: false,
        },
        host: { description: 'Agent host name for attribution' },
        session: { description: 'Agent session id for attribution' },
      },
      positionals: ['argv'],
    } satisfies CliProjectionConfig<typeof inputSchema>;
    
    type CliInput = Omit<z.input<typeof inputSchema>, 'cwd' | 'after'> & {
      readonly after?: readonly string[];
      readonly cwd?: string;
    };
    
    export const mapInput = (
      input: CliInput,
    ): z.input<typeof inputSchema> => {
      const after = parseTicketList(input.after ?? []);
      return {
        ...input,
        cwd: input.cwd ?? process.cwd(),
        ...(after.length === 0 ? {} : { after }),
      };
    };

    CliProjectionConfig<Schema> is exported from agent-bundle/routes. flags and positionals keys are constrained to keyof z.input<Schema>. config uses the same static extract grammar as a route module (satisfies unwraps). mapInput is an ordinary named export whose presence is recorded statically and loaded only by the CLI bin; the MCP worker never sees the module. mapInput is a surface adapter, not domain logic: it only reshapes or defaults argv into the canonical input (renames, splitting lists, deriving a working directory). Domain validation and behaviour stay in the operation, its inputSchema refinements and its component. A mapper that recreates command logic is the duplication the projection exists to remove. Because the mapper may still split or rename a value, a command whose projection exports mapInput has its z.enum choices judged by the canonical schema after mapInput runs, so --regions us,uk reaches the mapper, and a value outside the enum is still exit 2, reported as Invalid value for --<option>[<index>]: expected one of: … under the CLI spelling. A projection without mapInput rejects an unknown enum value while parsing argv, as a src/cli route does.

    KeyMeaning
    commandCommand path segments; default [tool]. Each must pass safeIdentitySegment.
    descriptionHelp text; default: the tool's config.description.
    positionalsCanonical keys consumed as bare arguments, in order (same rules as a src/cli route).
    flags.<key>.nameCLI spelling (kebab-case, no leading dashes); default kebab(key).
    flags.<key>.aliasesExtra long-form --spellings, kebab-case, no leading dashes.
    flags.<key>.descriptionOverrides the input metadata description.
    flags.<key>.defaultCLI-only default the shell applies before mapInput (recorded in projection.defaults; a schema .default() is zod's to apply, after mapInput).
    flags.<key>.requiredfalse relaxes a canonical-required key; legal only when mapInput is exported.
    aliasesCommand aliases (same rules as a src/cli route's config.aliases).
    confirmDefault: !(tool config.annotations.readOnlyHint === true).
    exitCode'result' or 'zero'; default: the tool's config.exitCode ?? 'zero'.
    input'json': the command takes the tool's canonical input as one JSON object through --input, like the bulk mcpCommands projection, instead of per-field flags. For a schema the argv grammar cannot express. Cannot be combined with flags, positionals, or a mapInput export (AB4844).

    JSON input for a schema flags cannot spell

    Named flags use explicit input metadata. Nested input can use JSON directly; commands without metadata default to this mode. Declare input: 'json' to select it explicitly:

    examples/audiobook-curator/src/mcp/curator/tools/convert_audiobook.cli.ts
    import type { CliProjectionConfig } from 'agent-bundle/routes';
    
    import type { inputSchema } from './convert_audiobook.js';
    
    export const config = {
      command: ['convert'],
      confirm: false,
      description: 'Plan or apply a verified conversion to one chaptered M4B.',
      input: 'json',
    } satisfies CliProjectionConfig<typeof inputSchema>;
    audiobook-curator convert --input \
      '{"conversion":{"author":"Frank Herbert","output":"./Dune.m4b","selection":"./selection.json","title":"Dune"}}' \
      --json

    The object reaches the tool's own inputSchema unchanged, so its defaults, refinements, and transforms apply exactly as they do for a tools/call; a schema issue is reported under --input.<path> and a non-object --input is refused with exit 2 before any tool runs. The command keeps everything else the projection controls, command, aliases, description, confirm, and exitCode, and help, inspect, and the artifact manifest record the mode (projection.input: "json"). The curator's plan-first conversion sets confirm: false; its nested conversion.apply field remains the explicit mutation gate.

    flags is keyed by the canonical key of the tool's RouteContract.input. Discovery excludes .cli.{ts,tsx} before identity derivation and pairs the file with the sibling .{ts,tsx}. The suffix is reserved under src/mcp/**; prefix _ parks it. An orphan, or a .cli.* under resources/, prompts/, or apps/, is AB4843. A missing or dynamic config, a key outside the closed set, a field of the wrong shape, required: false / a CLI default on a canonical-required key without mapInput, is AB4844. A flags/positionals key absent from the contract, a name/alias that is not kebab-case, is reserved (help, json, ndjson, version, and yes when confirm), or collides, name or aliases on a positional key (a bare argument has no --spelling), a contract key yes on a confirming command (the shell strips yes as the confirmation, whatever it is spelled), or a command segment that is not a safe identity segment, is AB4845. Every message is CLI projection <module> for tool:<server>/<tool>: <detail>. (a misplaced module has no tool to name: CLI projection <module>: <detail>.). A flag mapping without tool config.inputJsonSchema reports AB4845. The runtime checks mapInput callability and awaits its result.

    agent-bundle inspect --routes dumps the compiled graph, so cli.commands[].projection (module, mapInput, input?, defaults?, relaxed?) and options[].{key,option,aliases} appear with no extra renderer. The Workbench will surface those facts in the selected-operation inspector. The compiled command's routeId is the tool id; at run time invocation.kind is 'cli' and operationId is that tool id. A route observes kind: 'cli' whenever it runs from the generated CLI executable, whichever projection mechanism produced the command. The generated MCP server still passes kind: 'tool'.

    No projection module means no command from this path, the bulk routes.mcpCommands opt-in keeps working for every other tool. The MCP inputSchema, tools/list, annotations, and _meta are untouched by any projection field. ToolConfig.inputJsonSchema supplies optional flag metadata.

    Short -x aliases remain unsupported. mapInput may be sync or async; the shell awaits it before schema validation.

    The same routed CLI in every distribution

    Hooks, Skills, scripts, and npm consumers all use the plugin root, so the build emits the compiled command graph once into the artifact root as bin/<plugin-name>.mjs (plus bin/<plugin-name>-flight.mjs when any command renders), shared by every selected host. Every built-in target publishes the cli capability that admits it. The artifact bin is a self-contained executable ESM module with a Node shebang. The npm root copies it unchanged and points package.json bin directly at it, so help, argv parsing, output modes, exit codes, signals, and the complete command set are byte-for-byte the same. Both forms resolve plugin code from the root above bin/ and derive the same framework state root from that code root, ~/.agent-bundle/state/<plugin>-<digest> (or the XDG_STATE_HOME equivalent), or AGENT_BUNDLE_STATE_ROOT when set, so the CLI and MCP server share one store.

    Reach it from the other surfaces with the plugin-root token, ${CLAUDE_PLUGIN_ROOT}/bin/<plugin-name>.mjs in Claude Skill Markdown and hook commands, ${PLUGIN_ROOT}/… in Codex hooks, ${CURSOR_PLUGIN_ROOT}/… in Cursor hooks, or, from a compiled script, as the sibling new URL('../bin/<plugin-name>.mjs', import.meta.url). inspect accounts for the bin as one cli component per target and the artifact manifest records both files. A target without the cli capability omits the bin and reports AB4765; a host-emitted file at the same path (a claude.bin directory shipping <plugin-name>.mjs) is AB4766. The npm root copies this manifest-declared executable; it does not compile another routed CLI.

    Projecting MCP tools into the CLI

    routes.mcpCommands adds tools from generated MCP servers to the same command graph and executable, including in projects with no src/cli/** routes at all. true selects every eligible tool; the object form takes include and exclude patterns matching the <server>:<tool> identity, with * as the only wildcard. A tool that already has a colocated <tool>.cli.ts projection module is excluded from this bulk projection: one operation compiles to one command. An include pattern that matches only such tools is AB4822 and names the projection module.

    Each bulk-projected tool runs as <plugin-bin> <server> <tool> with the protocol tool name preserved verbatim. Its only input option is --input taking one JSON object. Both the bulk routes.mcpCommands projection and an explicit <tool>.cli.ts are CLI surfaces, so a route observes kind: 'cli' whenever it runs from the generated CLI executable; the generated MCP server still passes kind: 'tool'. A tool is read-only only when its static MCP annotations explicitly set readOnlyHint: true; every other tool is mutation-capable and fails closed unless --yes is present. Every declared pattern must match at least one eligible tool, and a misspelling fails with AB4822 listing the available identities.

    Release identity

    Plugin code reads its own identity from the framework instead of maintaining a hand-written version module:

    import meta, { name, packageName, packageVersion, version } from 'agent-bundle/meta';

    version is the resolved plugin version, name is the host-native plugin slug, never the npm package name, and packageName / packageVersion are the validated npm axes, undefined for an unpackaged development project. The compiler replaces the specifier in every compiled surface: artifact scripts, the routed CLI, MCP entries, hook wrappers, the package build, and browser MCP App bundles. It is a reserved specifier, so the tools hatch cannot externalize it and no emitted bundle can carry an unresolved import of it.

    Outside an agent-bundle compilation the published module throws rather than reporting a fabricated identity, and a release build refuses a project with no release version at all (AB4013).

    Packaging the npm root

    When package outputs are enabled, dist/ contains the complete validated composite artifact plus the package metadata, standard documentation, and package-only entries. A generated routed CLI is not compiled a second time: the installed package.json points at the bin/<plugin-name>.mjs executable declared by agent-bundle.manifest.json and copied unchanged from the artifact. The tarball therefore has one plugin root and one routed CLI, with no nested artifact/ directory or package-relative installer wrapper. No npm lifecycle performs a host installation.

    The generated root owns its inventory. Source files and bin fields are replaced; package entry paths (exports, imports, main, module, types, and typesVersions) are rebased from dist/ to the generated root. Pack and publish hooks are removed, while other scripts are copied unchanged and must already name paths relative to that root. Only manifest-owned artifact files, compiled package entries, and standard README, license, and notice files are carried; arbitrary source-project extras are not.

    npx agent-bundle prepack --root . --output artifact --json

    prepack runs the release build and npm pack --dry-run --json --ignore-scripts (npm's npm-cli.js via process.execPath, not a bare npm PATH spawn), then gates the exact package and artifact inventory, manifest hashes, package bin targets, and release-version agreement. It removes pack and publish lifecycle hooks from the generated package metadata, so a source-project prepack cannot recurse from dist/; consumer install scripts remain intact. Run the gate directly rather than relying on a source-package lifecycle hook, then publish the npm root it validated:

    npm publish ./dist --ignore-scripts

    Prebuilt payloads

    Some projects legitimately own their compilation, a coordinated multi-environment bundler topology the per-entry hatch cannot express, but still want framework-owned host packaging. The top-level payload block names already-built directory trees the build packages byte-for-byte at stable paths, and entry: { prebuilt: './dist/…' } or handler: { prebuilt: './dist/…' } points the generated host manifests at files inside them without compiling them.

    Every payload file keeps its exact relative path, because the framework did not compile these files and cannot rewrite the references inside them. Integrity stays content-addressed anyway: each payload file lands in the artifact manifest with its SHA-256 and the prebuilt file kind, and hashes into the project revision. Run your own build first, a missing or empty payload is a warning under dev so a clean checkout works, but agent-bundle build refuses it.

    Because the compiler never opens payload files, use definePrebuilt and runtimeDependencies to list the bare package names they load. A name npm would not read as a bare package name, or one package.json does not install for a consumer (dependencies, optionalDependencies, or a peer not marked optional), is AB4751; the declaration check is skipped when package.json is missing (silent), unparsable, or outside the root (AB4011). Declared names count as used for AB7014.

    import { defineConfig, definePrebuilt } from 'agent-bundle';
    
    export default defineConfig({
      payload: {
        tools: definePrebuilt({
          source: './built/tools',
          targets: ['claude'],
          runtimeDependencies: ['sharp'],
        }),
      },
    });

    The bundler escape hatch

    tools.rsbuild (an Rsbuild environment-config fragment) and tools.rspack (an Rspack config object, mutator function, or array) merge last into every bundler config agent-bundle synthesizes: artifact scripts, MCP entries, hook wrappers, MCP App views, and the package build. This is why a consumer never needs a second bundler config file.

    The hatch is bounded. The framework invariant hook runs after your tools.rspack, and the resolved-config assertions still run after the merge. A value that breaks an artifact contract, async chunks, output roots, self-containment, fails the build with a hard diagnostic instead of silently overriding the contract. Reserved module specifiers are protected the same way: a hatch that externalizes agent-bundle/mcp-entry, agent-bundle/meta, or agent-bundle/mcp-apps fails the build. Externalization in general is bounded too: AB4725 rejects statically visible package externals, while relative, function-form, and mutator externals are judged from the compilation's own evidence (AB6005). Run-time path references are kept the same way: a new URL(…, import.meta.url) or new Worker(new URL(…)) in your code or a generated entry names a file beside the artifact, so the invariant layer turns the bundler's URL and worker asset processing off after the hatch and the expression reaches the artifact verbatim. The hatch customizes how code compiles, never what the artifact promises.

    The hatch merges beside the framework profile: plugins arrays concatenate, and Rsbuild never dedupes plugins by name. Re-adding a plugin the framework already registers, @rsbuild/plugin-react (rsbuild:react), which every synthesized entry and every MCP App view carries, through tools.rsbuild.plugins would run it twice, so agent-bundle validate reports it as AB4724 naming the plugin and its package. Remove the entry; the framework registers it for you.

    One engine-identity caveat: artifact scripts, MCP entries, hook wrappers, and the package build compile through Rslib, and MCP App views through @rsbuild/core; agent-bundle pins both so a project installs a single @rsbuild/core and a single @rspack/core, but that engine is agent-bundle's dependency, not yours. A class imported from a separately installed @rspack/core can therefore have a different identity than the engine executing the config. Never construct plugins or run instanceof checks against an imported @rspack/core, use the utils argument passed to mutator functions instead:

    tools: {
      rspack: (config, { rspack }) => {
        // `rspack` is always the executing engine's own object.
        return config;
      },
    },

    To see exactly what the hatch produced:

    npx agent-bundle inspect --bundler --root .
    npx agent-bundle inspect --bundler --root . --target claude --json

    That dumps the lowered Rspack configuration of every output the build compiles, one entry per compiler, produced by the build's own Rslib (executables) or Rsbuild (MCP App views) lowering and stopped where the build would start compiling, so the dump cannot drift from what compiles. You see the resolved resolve.alias map (framework runtime modules beside your own aliases), the externals list, the plugins as [object <ClassName>], functions as [function <name>], and output.path as <output> (the artifact root) or <project root>/dist (the package build). A hatch value the build's invariants refuse, aliasing a reserved agent-bundle/* specifier, for instance, makes the inspection invalid with AB7001 naming the refusal. Entries the framework wraps also carry the generated wrapper module source.

    The full contract for everything on this page is in Entry conventions.

    Next