CLI 与库包入口
agent-bundle 之于智能体插件,正如 Rslib 之于库:一份 agent-bundle.config.ts、一个 CLI、由框架拥有
的入口生命周期,以及一个被祝福的、通往打包器的逃生舱。输出宿主产物的同一份配置,也负责 npm 包构建,
因此同时作为 CLI 或库发布的插件不需要第二份打包器配置。
bin 与 lib
配置沉默时,src/cli.ts 与 src/index.ts 这两个约定会补齐它们。配置总是胜出,而 bin: false /
lib: false 则用于退出。
由于包输出位于 dist/,宿主产物必须放在别处:命令行默认的产物根目录是 artifact/,而在带有包入口的项目上把
output.distPath 或 --output 指向 dist 就是 AB4706。dist 是强制忽略的目录:包输出
永远不会进入项目源码快照,也不会进入 Skill 与资源发现。
输出会被暂存并原子发布,其 provenance,字节数、SHA-256,以及排序后的项目相对源码输入,会像产物文件 一样在构建结果中报告。
声明文件
lib.dts 默认为 true。声明生成会从项目中解析 typescript,因此请把它加为 devDependency。它把 lib
入口所在的源码目录编译为独立的 program:编译选项通过 extends 来自项目的 tsconfig.json,rootDir
固定为入口所在目录,并且只纳入该子树,测试文件永远不会让包构建失败,也不会污染它。声明文件平铺落在
dist/ 下,每个源码模块一个 .d.ts。
lib profile 刻意做得很薄。需要多格式矩阵(UMD、多入口、逐格式 tsconfig)的包已经超出这个 profile,
它真正想要的是 Rslib。那是唯一一种仍然保留第二份打包器配置的情形,而且是主动选择的。
可执行封套
一个 bin 条目,或一个产物脚本,只要其模块导出了 main,或默认导出一个函数,就会获得生成的进程
封套:
封套会 await main(process.argv.slice(2), { terminal }),把数值返回值作为进程退出码,并让逃逸的
rejection 走 Node 的顶层失败路径(堆栈打到 stderr,退出码 1)。terminal 是该进程的
终端能力:stdout 与 stderr 各自的 TTY 属性、颜色与尺寸,以及两者是否指向同一
目标,由封套内联的一个无依赖模块在 main 运行前探测一次,普通脚本或 bin 不会为此加载任何运行时。
包 bin 的 hostSurface 为 cli,产物脚本的为 script,因此同时发布到两个表面的模块看到的是它实际
被启动的那个表面。只声明一个参数的 main 照常工作,第二个参数只是没有被读取。没有 main 导出的
自执行模块则逐字节直接打包,也不会收到探测结果。
路由式 CLI
src/cli/** 表面会编译成一个由框架生成的可执行文件,取代手写的分发器,也取代该项目的 src/cli.ts
bin 约定。嵌套即身份:src/cli/library/audit.ts 以 <bin> library audit 运行。
编译器把 config.inputJsonSchema 静态投影到 argv 上,生成分层帮助(每一层都有 --help,根层有 --version),
并在组合产物中输出 bin/<plugin-name>.mjs。运行时,外壳解析命令路径、
解析并强制转换 argv、用模块自身的 schema 校验、在带类型的 Agent 请求上下文中执行默认函数、向 stdout
写出一行规范 JSON,并确定性地映射退出码:
当 inputSchema 拒绝解析后的 argv 时,shell 会用 CLI 的措辞逐条说明问题,而不是打印原始的 schema issue
JSON:按用户输入的形式指出参数(--max-files、<root>,或投影 MCP 命令的 --input.<path>)、期望值与
实际收到的值,每个问题一行,随后是该命令的准确用法行与 --help 提示,全部写到 stderr,退出码为 2:
在 --json 下 stdout 保持为空,stderr 只输出一行规范化的
{"error":{"code":"CLI_INPUT_INVALID","issues":[...],"usage":"..."}};在 --ndjson 下,事件流中只包含一个
携带相同 error 对象的 type: "error" 事件。
.tsx 命令路由把默认函数换成一个具有相同 props 的 默认 Server Component,并通过运行时分发器
针对同级的 bin/<plugin-name>-flight.mjs worker 渲染。它由此获得
脚本与资源中描述的四种输出模式。路由式 CLI 项目需要把 @agent-bundle/runtime
作为依赖,因为生成的可执行文件通过它装配请求上下文。
渲染式命令运行在一个挂钟默认为 60 秒的渲染会话中。合理运行时间更长的命令,例如在两小时的构建上执行
<bin> await <ticket>,以 config.render: { maxElapsedMs } 声明自己的预算(以毫秒计的正整数,最多 24
小时,否则报 AB4835);编译后的命令把它带进生成的可执行文件,投影的 MCP 命令(routes.mcpCommands)
则继承其工具的预算。纯 .ts 命令没有渲染会话,会拒绝该键。参见渲染预算。
退出方式:bin: false、routes.cli: 'conventional',或给某一段路径加 _ 前缀。
在 MCP 与 CLI 之间共享同一份 schema
运行时的 inputSchema 和 resultSchema 可以导入,也可以由工厂创建。编译器检查导出是否存在,
生成的可执行文件加载真实 schema。TypeScript 从这些导出推导输入和输出类型,包括默认值和转换。
若需要不执行模块的检查、Workbench 表单或命名 CLI flag,请在工具的 defineTool 对象中声明字面量
inputJsonSchema。
它接受 additionalProperties: false 的对象、字符串/数字/布尔标量、字符串枚举、标量数组,
以及可选的 required、description 和 default 元数据。
运行时 schema 仍负责校验、默认值和转换,元数据不会替代它。
没有这些元数据的独立 CLI 路由或工具投影默认接受 --input JSON。
显式声明 input: 'json' 可在已有元数据时选择 JSON 模式。
flag 和位置参数映射需要元数据;工具缺少元数据时报告 AB4845。
元数据或独立 argv 规则无效时报告 AB4814。编译器不解释 Zod 表达式,也不跟随 schema 别名链;
AB4838 和 AB4839 已退役。
将一个工具投影为惯用命令
同级 <tool>.cli.ts 是同一工具的 CLI 表面投影,不是新路由。它可以声明命令路径、位置参数和 flag 名称,
操作身份仍为 tool:<server>/<tool>。
该模块位于工具旁边,绝不放在 src/cli/** 下。Status 只需重命名;解析器已经发出规范键,因此不需要
mapInput:
Request 需要位置参数,以及一个同步或异步的 mapInput:它会用 process.cwd() 填充规范必填的 cwd,并拆分
逗号分隔的 --after 列表:
CliProjectionConfig<Schema> 从 agent-bundle/routes 导出。flags 与 positionals 的键受限于
keyof z.input<Schema>。config 使用与路由模块相同的静态提取语法(会解包 satisfies)。
mapInput 是普通的命名导出,其存在性会被静态记录,并且只由 CLI bin 加载;MCP worker 永远不会看到
该模块。mapInput 是表面适配器,不是领域逻辑:它只把 argv 重塑或补默认值成规范输入(重命名、拆分
列表、派生工作目录)。领域校验与行为留在操作里,它的 inputSchema 精化以及它的组件。在 mapper
里重写一遍命令逻辑,正是投影要去掉的重复。由于 mapper 仍可能拆分或重命名一个值,投影导出了 mapInput
的命令,其 z.enum 取值由规范 schema 在 mapInput 运行之后判定,因此 --regions us,uk 能到达
mapper,而枚举之外的值仍以 2 退出,并以 CLI 拼写报告为
Invalid value for --<option>[<index>]: expected one of: …。没有 mapInput 的投影则像 src/cli
路由一样在解析 argv 时就拒绝未知的枚举值。
flag 无法拼写的 schema:JSON 输入
命名 flag 使用显式输入元数据。嵌套输入可以直接使用 JSON;没有元数据时默认采用该模式,
也可以声明 input: 'json':
该对象原样到达工具自己的 inputSchema,因此默认值、refinement 与 transform 的应用方式与
tools/call 完全一致;schema 问题以 --input.<path> 报告,非对象的 --input 会在任何工具运行之前
以退出码 2 被拒绝。命令保留投影控制的其余一切,command、aliases、description、confirm
与 exitCode,帮助、inspect 与制品清单都会记录这一模式(projection.input: "json")。
策展器的先计划后执行转换设置 confirm: false,嵌套的 conversion.apply 字段仍是显式修改门禁。
flags 以工具的 RouteContract.input 的规范键为键。发现阶段会在派生身份之前排除 .cli.{ts,tsx},
并将该文件与同级的 .{ts,tsx} 配对。这个后缀在 src/mcp/** 下保留;加 _ 前缀可以停用它。
孤立文件,或位于 resources/、prompts/ 或 apps/ 下的 .cli.*,会触发 AB4843。缺失或动态的
config、封闭集合之外的键、形状错误的字段、或在没有 mapInput 时对
规范必填键使用 required: false / CLI default,会触发 AB4844。契约中不存在的
flags/positionals 键、不符合 kebab-case、为保留名(help、json、ndjson、version,以及
启用确认时的 yes)或相互冲突的 name/别名、位置参数键上的 name 或 aliases(裸参数没有
--spelling)、启用确认的命令所投影的契约含有键 yes(无论如何拼写,shell 都会把 yes 当作确认
剥离),以及不是安全身份段的 command 段,会触发 AB4845。
每条消息都是 CLI projection <module> for tool:<server>/<tool>: <detail>.(位置错误的模块没有可指名的
工具:CLI projection <module>: <detail>.)。缺少工具 config.inputJsonSchema 的 flag 映射报告 AB4845。
运行时检查 mapInput 是否可调用并等待其结果。
agent-bundle inspect --routes 会转储编译后的路由图,因此无需额外的 renderer 即可看到
cli.commands[].projection(module、mapInput、input?、defaults?、relaxed?)与
options[].{key,option,aliases}。Workbench 会在选中操作的 inspector 中展示这些事实。
编译后命令的 routeId 是工具 id;运行时的 invocation.kind 为 'cli',operationId 则为该工具
id。只要路由从生成的 CLI 可执行文件运行,无论命令由哪种投影机制产生,它观察到的都是
kind: 'cli'。生成的 MCP 服务器仍传入 kind: 'tool'。
没有投影模块,就不会通过这条路径生成命令;批量选择加入的
routes.mcpCommands 对其他所有工具仍照常工作。任何投影字段都不会改变 MCP
inputSchema、tools/list、annotations 或 _meta;ToolConfig.inputJsonSchema 提供可选 flag 元数据。
短格式 -x 别名仍不受支持。mapInput 可以是同步或异步函数;shell 会等待它完成,然后执行 schema 验证。
每种分发中的同一个路由式 CLI
hook、Skill、脚本与 npm 消费者都使用插件根目录,因此构建把编译后的命令图只发射一次到产物根目录,
路径为 bin/<plugin-name>.mjs(若有任一命令需要渲染,则再加上
bin/<plugin-name>-flight.mjs),由所有选中宿主共享。每个内置 target 都发布了准入它的 cli 能力。产物 bin 是一个
带 Node shebang 和可执行位的自包含 ESM 模块。npm 根目录原样复制它,并让 package.json 的 bin
直接指向它,因此帮助、argv 解析、输出模式、退出码、信号与完整命令集合都逐字节相同。两种形式都从
bin/ 的上级根目录解析插件代码,并从该代码根目录推导出同一个框架状态根目录,
~/.agent-bundle/state/<plugin>-<digest>(或对应的 XDG_STATE_HOME 路径),或已设置的
AGENT_BUNDLE_STATE_ROOT,因此 CLI 与 MCP 服务器共享同一份存储。
从其他表面引用它时使用插件根 token,Claude Skill Markdown 与 hook 命令中写
${CLAUDE_PLUGIN_ROOT}/bin/<plugin-name>.mjs,Codex hook 中写 ${PLUGIN_ROOT}/…,Cursor hook 中写
${CURSOR_PLUGIN_ROOT}/…,或者在编译后的脚本里用同级引用
new URL('../bin/<plugin-name>.mjs', import.meta.url)。inspect 把该 bin 记为每个 target 的一个 cli
组件,产物清单会记录这两个文件。不具备 cli 能力的 target 会省略该 bin 并报告 AB4765;宿主在同一路径
发射的文件(例如 claude.bin 目录中交付的 <plugin-name>.mjs)是 AB4766。npm 根目录复制清单声明的
这个可执行文件,不再编译另一份路由式 CLI。
把 MCP 工具投影进 CLI
routes.mcpCommands 把生成式 MCP 服务器的工具加入同一张命令图与同一个可执行文件,即使项目完全没有
src/cli/** 路由也可以。true 选中每个符合条件的工具;对象形式接受匹配 <server>:<tool> 身份的
include 与 exclude 模式,其中 * 是唯一的通配符。已经有同位置 <tool>.cli.ts 投影模块的工具会从这次
批量投影中排除:一个操作只编译成一个命令。仅匹配这类工具的 include 模式会触发 AB4822,并点名
投影模块。
每个被批量投影的工具以 <plugin-bin> <server> <tool> 运行,协议工具名逐字保留。它唯一的输入选项是
--input,接受一个 JSON 对象。批量 routes.mcpCommands 投影与显式的 <tool>.cli.ts 都是 CLI
表面,因此只要路由从生成的 CLI 可执行文件运行,它观察到的都是 kind: 'cli';生成的 MCP 服务器仍
传入 kind: 'tool'。只有当工具的静态 MCP annotations 明确设置了 readOnlyHint: true 时
它才是只读的;其余工具都被视为可变更,并在没有 --yes 时失败关闭。每个声明的模式都必须至少匹配一个
符合条件的工具,拼写错误会以 AB4822 失败,并列出可用的身份。
发布标识
插件代码通过框架读取自身标识,而不是维护一个手写的版本模块:
version 是解析出的插件版本,name 是宿主原生的插件 slug,绝不是 npm 包名,packageName 与
packageVersion 则是经过校验的 npm 轴,对未打包的开发期项目为 undefined。编译器会在每个被编译的
表面替换该 specifier:产物脚本、路由式 CLI、MCP 入口、钩子包装层、包构建,以及浏览器 MCP App 包。
它是保留 specifier,因此 tools 逃生舱无法把它外部化,任何输出包也不可能残留对它的未解析导入。
在 agent-bundle 编译之外,已发布的该模块会抛出错误,而不是报告一个伪造的标识;完全没有发布版本号的
发布构建会被拒绝(AB4013)。
打包 npm 根目录
启用包输出后,dist/ 包含完整的已校验组合产物,以及包元数据、标准文档和包专用入口。生成的路由式 CLI
不会再次编译:已安装的 package.json 指向 agent-bundle.manifest.json 声明并从产物原样复制的
bin/<plugin-name>.mjs。因此 tarball 只有一个插件根和一个路由式 CLI,没有嵌套的 artifact/ 目录或
相对包的安装器包装器。没有任何 npm 生命周期会执行宿主安装。
生成的根目录拥有自己的文件清单。源项目的 files 与 bin 字段会被替换;包入口路径
(exports、imports、main、module、types 与 typesVersions)会从 dist/ 重定位到
生成根目录。pack 与 publish 钩子会被移除,其他脚本保持原样,因此必须已经使用相对该根目录的路径。
只有清单拥有的产物文件、编译后的包入口以及标准 README、license 与 notice 文件会被携带;任意的
源项目额外文件不会被复制。
prepack 运行发布构建,并通过 process.execPath 启动 npm 的 npm-cli.js
执行 npm pack --dry-run --json --ignore-scripts(不是 PATH 上的裸 npm),随后对精确的包与产物清单、
清单哈希、包 bin 目标以及发布版本一致性设卡。生成的包元数据会移除 pack 与 publish 生命周期钩子,
避免源项目的 prepack 从 dist/ 递归;消费者安装脚本保持不变。请直接运行门禁,不要依赖源包的
生命周期钩子,然后发布它已经校验的 npm 根目录:
预构建 payload
有些项目确实自行掌控编译,例如逐入口逃生舱无法表达的、协调多环境的打包器拓扑,但仍希望使用框架
自有的宿主打包。顶层 payload 块声明构建将按字节、按稳定路径打包的已构建目录树,而
entry: { prebuilt: './dist/…' } 或 handler: { prebuilt: './dist/…' } 则把生成的宿主清单指向这些
树中的文件,而不编译它们。
每个 payload 文件都保留其精确的相对路径,因为框架并没有编译这些文件,也就无法改写它们内部的引用。
完整性依然是内容寻址的:每个 payload 文件都会带着自己的 SHA-256 与 prebuilt 文件类别进入产物清单,
并参与项目 revision 的哈希。请先运行你自己的构建,缺失或为空的 payload 在 dev 下只是警告,以便
干净检出也能工作,但 agent-bundle build 会拒绝它。
编译器不会打开 payload 文件,因此请用 definePrebuilt 和 runtimeDependencies 列出它们加载的裸包名。
npm 不会将其读作裸包名的条目,或 package.json 未为消费者安装的包名(dependencies、
optionalDependencies,或未标记为可选的 peer),会报 AB4751;package.json 缺失(静默)、
无法解析或位于项目根之外(AB4011)时跳过声明检查。已声明的包名在 AB7014 中视为已使用。
打包器逃生舱
tools.rsbuild(一个 Rsbuild 环境配置片段)与 tools.rspack(一个 Rspack 配置对象、mutator 函数或
数组)会最后合并进 agent-bundle 合成的每一份打包器配置:产物脚本、MCP 入口、钩子包装层、MCP App
视图与包构建。这正是消费者永远不需要第二份打包器配置文件的原因。
这个逃生舱是有边界的。框架的 invariant 钩子在你的 tools.rspack 之后运行,解析后的配置断言也仍在
合并之后运行。破坏产物契约的取值,异步 chunk、输出根目录、自包含性,会以硬诊断让构建失败,而不是
悄悄覆盖契约。保留的模块 specifier 也以同样方式受到保护:把 agent-bundle/mcp-entry、
agent-bundle/meta 或 agent-bundle/mcp-apps 外部化的逃生舱会让构建失败。外部化整体上也受到同样的
边界约束:AB4725 会拒绝静态可见、点名某个包的 externals,相对路径、函数形式以及 mutator 添加的
externals 则由编译过程自身的证据判断(AB6005)。运行时路径引用也以同样方式得到保留:你的代码或
生成入口中的 new URL(…, import.meta.url) 或 new Worker(new URL(…)) 指向的是产物旁的一个文件,因此
不变量层会在逃生舱之后关闭打包器对 URL 与 worker 的静态资源处理,让该表达式原样进入产物。逃生舱定制的是
代码如何编译,绝不是产物承诺了什么。
逃生舱是并列合并到框架配置旁边的:plugins 数组会拼接,而 Rsbuild 从不按名称去重插件。通过
tools.rsbuild.plugins 再次添加框架已经注册的插件,@rsbuild/plugin-react(rsbuild:react),
每个合成入口与每个 MCP App 视图都带着它,会让它运行两次,因此 agent-bundle validate
会以 AB4724 报告,并点名该插件及其包名。删除这一项即可;框架会替你注册它。
有一个引擎身份注意事项:产物脚本、MCP 入口、钩子包装层与包构建通过 Rslib 编译,MCP App 视图通过
@rsbuild/core 编译;agent-bundle 固定了两者的版本,使项目只安装一个 @rsbuild/core 与一个
@rspack/core,但这个引擎是 agent-bundle 的依赖,而不是你的依赖。因此,从单独安装的 @rspack/core
导入的类,与实际执行配置的那个引擎的类身份可能并不相同。切勿针对导入的 @rspack/core 构造插件或执行
instanceof 检查,请改用传给 mutator 函数的 utils 参数:
想确切看到逃生舱产生了什么:
它会导出构建所编译的每个输出降级后的 Rspack 配置,每个编译器一条记录,由构建自己的 Rslib(可执行
文件)或 Rsbuild(MCP App 视图)降级流程产出,并在构建即将开始编译的位置停下,因此这份导出不可能与
真实编译结果发生漂移。你能看到解析后的 resolve.alias 映射(框架运行时模块与你自己的别名并列)、
externals 列表、以 [object <ClassName>] 渲染的插件、以 [function <name>] 渲染的函数,以及
output.path:产物根目录显示为 <output>,包构建显示为 <项目根>/dist。被构建不变量拒绝的逃生舱值
(例如给保留的 agent-bundle/* 说明符设置别名)会让检查变为无效,并以 AB7001 指出被拒绝的原因。
被框架包装的入口还会附带生成的包装模块源码。
本页所有内容的完整契约见 Entry conventions。