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
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:
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.
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:
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:
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.
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:
Request needs positionals plus a sync or async mapInput that fills canonical-required cwd
from process.cwd() and splits comma-separated --after lists:
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.
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:
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:
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.
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:
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.
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:
To see exactly what the hatch produced:
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
- Configuration model: the rest of the config surface.
- Project structure: where each of these entries lives.