caveman create-caveman-agent 初始化器详解:一条命令生成带预算治理与 Eval 门禁的 TypeScript Agent 项目
caveman 仓库中的 packages/create-caveman-agent 是 @caveman-ai/agent 框架的原子初始化器(npm 包名 @caveman-ai/create-agent),它用一条 npm create 命令生成一个依赖极少、结构严格、内置预算上限与 Eval 门禁的 TypeScript Agent 项目。读完本文,你能完整掌握该初始化器的命令行用法、Provider 凭据选择策略、生成项目的每个文件的作用与预算配置语义,并能理解其"原子写入 + 环境变量白名单"的安全设计,以及生成项目从 dev 到 build、check 的完整落地流程。
初始化器定位与快速上手
README(packages/create-caveman-agent/README.md)对初始化器的定义非常克制:@caveman-ai/agent 的原子初始化器(Atomic initializer)。它只负责四件事——创建一个必需的源码文件、一个 starter eval、一份严格的构建配置、一个 provider 选择,并安装依赖。
标准使用流程:
npm create @caveman-ai/agent@latest my-agent
cd my-agent
npm run doctor
npm run dev
npm run doctor 是一个零 provider 调用的就绪检查:如果它报告缺少签名运行时工件(signed runtime artifacts),按它给出的唯一动作 caveman setup --install 处理即可。npm run dev 启动交互式会话,且无需任何 Caveman 账户或托管服务——只要本机有 Node.js 22.19+ 和一个 provider 凭据即可运行。
初始化器的运行时约束在 packages/create-caveman-agent/package.json 中明确声明:
bin入口为create-caveman-agent,指向./dist/index.js,即npm create实际调用的是编译后的 Node 脚本;engines要求node >= 22.19.0;- 发布前通过
prepack: npm run build(tsc 编译)保证打包产物可执行,测试(packages/create-caveman-agent/tests/initializer.test.mjs)会校验bin目标文件首行是#!/usr/bin/env nodeshebang。
从源码结构看,初始化器自身是零运行时依赖的:全部逻辑(参数解析、文件模板、凭据检测、子进程安装)都写在约 320 行的 packages/create-caveman-agent/src/index.ts 中,仅使用 node:child_process、node:fs/promises、node:path、node:readline/promises 等 Node 内置模块。
命令行参数与两种使用模式
非交互(脚本化)使用
README 给出的非交互形式是显式指定 provider:
npm create @caveman-ai/agent@latest my-agent -- --provider anthropic
支持的 provider 为 anthropic、openai、google。当依赖安装由其他工具接管时,跳过初始化器自带的安装:
npm create @caveman-ai/agent@latest my-agent -- --provider openai --no-install
参数解析逻辑在 parseArgs()(src/index.ts,L55-L88)中实现,语义严格:
| 参数 | 行为 |
|---|---|
<project> |
唯一的必填位置参数;目录名会被 safeName() 规范化为小写字母/数字/-/_,最长 96 字符,必须含至少一个字母或数字 |
--provider <anthropic|openai|google> |
显式指定 provider;值缺失或以下划线 -- 开头时报错 --provider requires a value;值大小写不敏感,非法值报 unsupported provider |
--no-install |
关闭初始化器内置的 npm install,输出提示中会补一行 npm install 供调用方自行处理 |
未知 -- 选项 |
立即抛错 unknown option <flag>,且不产生任何部分目录 |
--help / -h |
打印 usage 行后退出,不读取凭据、不写文件系统 |
位置参数必须恰好一个:多给或少给都会打印 usage 并以非零码退出。这种"未知选项即失败"的设计在测试 initializer rejects unknown options without writing target 中被验证:传入 --wat 时退出码为 1、stderr 匹配 unknown option --wat,且目标目录不存在。
Provider 选择:静默、提问与失败三种路径
README 中的一句产品描述——"恰好检测到 1 个 provider 凭据时静默选择;0 个或多个时只问一次"——在 chooseProvider()(src/index.ts,L131-L152)中逐字落地:
- 若命令行已传
--provider,直接解析使用,不再检测环境; - 否则检测环境变量凭据:
ANTHROPIC_API_KEY→anthropicOPENAI_API_KEY→openaiGEMINI_API_KEY或GOOGLE_API_KEY→google
- 恰好 1 个 → 静默选中该 provider(对应测试
exactly one credential selects provider without question); - 0 个或多个 → 若 stdin/stdout 是 TTY,交互式提问一次
Provider (anthropic/openai/google):;若非 TTY(CI/管道等),直接失败:无凭据时提示no provider credential detected; pass --provider,多凭据时提示multiple provider credentials detected; pass --provider。
多凭据的非交互失败场景有专门的测试保证:同时设置 ANTHROPIC_API_KEY 和 OPENAI_API_KEY 时,退出码为 1、stderr 匹配 multiple provider credentials,并且目标目录不会有任何残留(assert.rejects(stat(target), { code: "ENOENT" })。
选择结果写入生成项目的 .caveman/provider.json,内容为 { provider, model }。每个 provider 对应一个基线模型,定义在源码头部的 MODELS 表中:
| provider | 基线模型 | 生成的凭据提示 |
|---|---|---|
anthropic |
anthropic/claude-haiku-4-5 |
ANTHROPIC_API_KEY |
openai |
openai/gpt-5.4-mini |
OPENAI_API_KEY |
google |
google/gemini-2.5-flash |
GEMINI_API_KEY (or GOOGLE_API_KEY) |
测试用 openai 场景断言了 .caveman/provider.json 中 model 等于 openai/gpt-5.4-mini。生成的项目 README 也会写明"在 shell 中设置对应环境变量,绝不提交 provider 凭据"。
凭据的三重保密约束
README 承诺"Secrets are never printed or written"(秘密永不打印、永不落盘)。实现层面有三重约束:
- 输出脱敏:测试
initializer writes one required agent source and deterministic provider choice设置OPENAI_API_KEY: "secret-never-print",然后断言 stdout+stderr 中不出现该值; - 不落盘:
.caveman/provider.json只写 provider 与 model 字段,不含任何密钥; - 子进程隔离:初始化器安装依赖时不继承父进程完整环境,见下一节的环境白名单。
原子写入:临时目录 + rename
生成过程不是直接往目标目录里写文件,而是走"临时目录 → rename"的原子路径(src/index.ts,L29-L43):
const temporary = resolve(dirname(target), `.${basename(target)}.${process.pid}.${crypto.randomUUID()}.tmp`);
try {
// 依次创建 src/、evals/、.caveman/ 并在临时目录中写入全部模板文件
if (parsed.install) await installDependencies(temporary);
await rename(temporary, target);
} catch (error) {
await rm(temporary, { recursive: true, force: true });
throw error;
}
设计要点:
- 临时目录名带 PID 与 UUID,避免同目录并发初始化互相踩踏;
- 所有模板文件以
flag: "wx"(排他创建)、mode: 0o600写入; - 依赖安装在临时目录内完成、成功后才 rename 到目标路径——这意味着失败的运行不会留下"装了一半依赖的半成品项目";
- 目标路径事先通过
assertAbsent()校验必须不存在(已存在则报target already exists),配合 rename 保证幂等语义。
安装子进程本身的实现(installDependencies(),L90-L115)也有讲究:
- 优先复用
npm_execpath(当初始化器本身经由npm create启动时)直接以node <npm-cli.js> install ...执行,保证与用户 npm 版本一致;Windows 下回落到ComSpec调cmd.exe /d /s /c,其他平台直接npm; - 安装参数固定为
install --no-audit --no-fund --ignore-scripts——跳过审计与资金信息、禁止执行依赖包的安装脚本,缩小供应链攻击面; - 退出码非 0 时抛错并清理临时目录。
环境变量白名单:秘密到不了安装器
dependencyInstallEnv()(L117-L129)构造子进程环境时,只白名单透传约 27 个键:PATH、HOME、代理变量(HTTP_PROXY 等)、locale 与终端颜色变量,以及 npm 自身的 NPM_CONFIG_CACHE/REGISTRY/USERCONFIG(含小写形式)。其余一切——包括 OPENAI_API_KEY、CAVE_API_KEY、AWS_SECRET_ACCESS_KEY——都不会传给安装子进程。
测试 initializer installs dependencies by default and leaves two-command first-run flow 用假 npm 捕获了子进程环境并逐一断言:三个密钥均不在安装器环境中,而 PATH 在;同时断言安装参数恰好是 ["install", "--no-audit", "--no-fund", "--ignore-scripts"]。
生成项目逐文件解读
初始化器一次性写出 9 个文件(projectFiles(),src/index.ts,L154-L291)。下面按文件说明其内容与设计意图。
package.json:六个 npm 脚本
生成的 package.json 是 ESM("type": "module"),engines.node >= 22.19.0,依赖面极小:
{
"scripts": {
"doctor": "caveman-agent doctor",
"dev": "caveman-agent dev src/agent.ts",
"run": "node --experimental-strip-types src/run.ts",
"build": "caveman-agent build caveman.config.ts",
"check": "caveman-agent check caveman.config.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": { "@caveman-ai/agent": "^0.1.0" },
"devDependencies": { "typescript": "5.9.3" }
}
脚本分工(与 packages/agent/README.md 的 CLI 章节一致:dev、build、check、doctor):
doctor:零模型调用的就绪检查;dev:交互式开发会话,入口固定src/agent.ts;run:用node --experimental-strip-types直接跑src/run.ts做一次性程序化调用(这也是"最小依赖"跑法的来源——Node 原生类型剥离);build:基于caveman.config.ts执行有限搜索并尝试写锁定构建(Cave Build);check:部署前校验,拒绝漂移(drift),在模型调用前就拦截配置/构建不一致。
测试断言了上述每个脚本字符串与 typescript: 5.9.3 的精确版本。
tsconfig.json:严格构建配置
{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts", "evals/**/*.ts", "caveman.config.ts"]
}
这就是 README 所说的"strict build config":strict: true + noEmit,allowImportingTsExtensions 配合 Node 原生 strip-types 支持 import ... from "./agent.ts" 这类带扩展名导入。include 精确覆盖三个位置,不额外纳入其他目录。
src/agent.ts:唯一必需的源码文件
import { agent, auto, schema, tool } from "@caveman-ai/agent";
const readiness = tool({
name: "readiness",
description: "Read fixture readiness before answering.",
input: schema.object({}),
effect: "read",
execute: () => ({ status: "ready" }),
});
export default agent({
id: "<project-name>",
instructions: "Call readiness once. Then reply with exactly: ready",
model: auto(),
tools: [readiness],
});
要点:
- 工具必须显式声明副作用类别
effect: "read"——这是框架对工具的强制要求(效果声明:read/write/idempotent/external,见 packages/agent/README.md Tools 章节); model: auto()按CAVE_MODEL→.caveman/provider.json→ 唯一凭据的基线模型顺序解析,不做任务分类或模型路由——所以初始化器写入的provider.json就是auto()的直接依据;id为规范化后的项目名(测试以support-agent为例断言生成文件匹配agent()。
src/run.ts:预算"经济旋钮"第一天就摆上台面
这是生成模板中注释最密集的文件,它把预算配置写成显式代码而不是留给用户日后发现:
const result = await run(definition, "Reply with exactly: ready", {
rootDir,
entryPath: "src/agent.ts",
budget: {
// 准入上限:每次调用先把"估算最坏情况"计入该上限做预留;
// 若 provider 最终用量超过预留,收据记录 capBreached/overspent 并停止运行。
maxUsd: 0.5,
// 预留余量逼近上限时的策略。"compact"(默认)会重写上下文、
// 让同一上限在压缩后继续支撑有用工作;
// "stop" 则跳过压缩,直接钳制输出并停止。
onExhausted: "compact",
},
});
console.log(result.stopReason, result.text);
console.log(`estimated list-price subtotal: $${result.receipt.totalEstimatedUsd}`);
这些字段不是模板自造的概念,而是 @caveman-ai/agent 的真实预算 API。在 packages/agent/src/budget.ts 中可以确认:
maxUsd与maxTokens互斥(二选一声明计量单位);onExhausted: "compact" | "stop",规范化时默认值就是"compact"(const onExhausted = budget.onExhausted ?? "compact");- 预留机制逐次调用执行:"run reserves each call's estimated worst case against this before the request",超额由收据(receipt)以
capBreached/overspent字段诚实地记录,而不是掩盖。
另外两个来自框架文档的重要语义在模板注释里同样被写明:所有金额都是"公开目录价估算小计(estimated public-catalog list-price subtotal),从不是发票";当准入触顶时,run 返回带 reason 的部分结果,而 provider 报告的超额仍然保留在收据中可见。测试会用 mock 版 @caveman-ai/agent 实际执行 src/run.ts,断言 run() 收到的 rootDir 解析到项目根、entryPath 为 src/agent.ts、prompt 为 Reply with exactly: ready——验证了模板与框架调用契约的对接。
evals/smoke.eval.ts:初始状态为"未批准"
import { eval as defineEval } from "@caveman-ai/agent";
export const smoke = defineEval({
id: "smoke",
approved: false,
input: "Reply with exactly: ready",
quality: [
{ type: "exact_match", expected: "ready" },
{ type: "tool_called", tools: ["readiness"] },
],
});
approved: false 是有意为之(README 原文:"Generated eval starts unapproved"):强制用户先审阅预期行为、确认后再改为 approved: true。这对应框架的构建门禁——npm run build 对每个已批准 fixture 做有限搜索,只有在声明的 eval 全部通过时才写锁定的 Cave Build。
caveman.config.ts 与 .caveman/provider.json
import { defineBuild } from "@caveman-ai/agent/build";
export default defineBuild({
entry: "src/agent.ts",
evals: "evals/*.eval.ts",
maxSearchCostUsd: 2,
});
构建配置把入口、eval glob 与搜索成本上限(maxSearchCostUsd: 2)显式固定。.caveman/provider.json 则保存上文的 provider 与基线模型二元组。
.gitignore 与生成的 README.md
.gitignore 只排除三类内容:node_modules/、.caveman/traces/、.env*——与"凭据永不落盘、trace 是本地运行产物"的原则一致。生成的 README.md 按 provider 注入对应的凭据变量名,并给出两段操作说明:doctor 报缺签名工件时跑 caveman setup --install 后重跑 doctor;以及"确认 eval 行为后把 approved: false 改为 true,再 npm run build → npm run check"。测试还特意断言生成 README 不包含 "never spend past" 这类绝对化措辞,而包含 capBreached/overspent——即文档必须描述预算机制的真实语义(超额会被记录而非"保证绝不多花")。
Eval 批准流程与"诚实数字"原则
把 README 末段与框架文档串起来,生成项目上线前的完整路径是:
npm run doctor:零 provider 调用检查 Node 版本、沙箱包含性、engine/gateway 可达性等(缺 engine 只是 WARN,因为 observe-only 运行仍可用);npm run dev:迭代 Agent 行为,确认 starter eval 的预期输出;- 在
evals/smoke.eval.ts中将approved: false改为approved: true; npm run build:执行 eval 门禁构建。按 packages/agent/README.md,当 usage 缺失、模型无法定价、缓存回退、恢复失败、沙箱/隐私失败、质量下降、搜索未完成或超成本上限时,不会写出优化锁;- 锁定构建之后、部署之前执行
npm run check,在模型调用前拒绝漂移。
贯穿这套流程的是 caveman 的"诚实数字"原则,README 原文与生成模板注释高度一致:
- 本地证据的 basis 永远是
inferred(推断值); - 已验证的节省(verified savings)在真实生产流量通过独立的 rollout 与 ledger 门禁之前保持
$0; - doctor 的人类可读输出中会打印
verified savings: $0——本地机器"不铸造"任何节省声明。
这也解释了为什么初始化器把"eval 未批准""$0""inferred"这些看似琐碎的字符串写进模板:它们不是营销话术,而是框架证据体系的入口约束。
测试如何固化这套行为
packages/create-caveman-agent/tests/initializer.test.mjs 以 node:test 驱动编译后的 dist/index.js,六个用例覆盖了 README 的每条承诺:
| 用例 | 验证的 README 承诺 |
|---|---|
package lifecycle builds executable initializer before packing |
包元数据完整、bin 文件带 shebang(可执行) |
initializer exposes help without credentials or filesystem writes |
--help/-h 零副作用 |
initializer writes one required agent source and deterministic provider choice |
只写一个必需源码文件;provider 选择确定可复现;密钥不出现;run.ts 语义完整 |
exactly one credential selects provider without question |
单凭据静默选择 |
noninteractive ambiguous provider fails without partial directory |
多凭据非交互失败且无残留 |
initializer installs dependencies by default and leaves two-command first-run flow |
默认安装依赖、环境白名单生效、输出收敛为两条命令(cd/npm install 不再需要单独提示,直接 npm run dev) |
initializer rejects unknown options without writing target |
严格参数解析 |
最后一条用例中"two-command first-run flow"值得注意:默认安装成功后,成功输出只剩 created <name> / provider <p> (model) / cd <dir> / npm run dev 四行;--no-install 时插入 npm install 行。初始化器的输出契约本身就是稳定的,可被脚本消费。
适用前提与限制
- Node.js ≥ 22.19:生成项目的
engines、初始化器自身的engines、@caveman-ai/agent三者同要求;run脚本依赖--experimental-strip-types; - 网络与凭据:无需 Caveman 账户,但需要 provider 凭据与网络直连 provider;
@caveman-ai/agent0.1.0 的运行时依赖包含 Pi agent 内核、MCP SDK、TypeBox、zod 等(见 packages/agent/package.json); - observe-only 默认:未安装 Caveman Engine 时,
run/dev以 observe-only 模式直连 provider(无转换、无 gateway 遥测),provider 用量与本地上下文估算仍然可用;要启用压缩优化需另行安装 CLI 并caveman start(见 packages/agent/README.md 的 Quick start 章节); - 本目录的历史定位:packages/create-caveman-agent/CLAUDE.md 注明该目录的 source of truth 在上游 SDK 仓库,本仓库中是"pinned integration"的消费副本——阅读实现时以当前快照为准。
小结
create-caveman-agent 用 300 行零依赖的 TypeScript,把 caveman 框架对"可信 Agent 起点"的定义物化成了 9 个文件:一个显式声明副作用的 agent 定义、一个未批准的 smoke eval、一份严格 tsconfig、一个把 maxUsd/onExhausted 预算语义写成可见代码的 run.ts,以及 provider 选择与构建配置。它的三个工程特色——原子临时目录写入、安装子进程的环境白名单、以及"本地证据恒为 inferred、验证节省恒为 $0"的诚实数字约束——都由 initializer.test.mjs 的用例逐条固化。对需要快速搭建一个带预算上限、Eval 门禁与部署前 check 流程的 TypeScript Agent 项目的读者,这条 npm create 命令就是官方给出的最短路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00