caveman 的 create-caveman-agent:一个零运行时依赖的原子化 npm 初始化器设计剖析
本文以 packages/create-caveman-agent/CLAUDE.md 为骨架,逐条落实其中对 @caveman-ai/create-agent(CLI 名 create-caveman-agent)的每一项行为约定——参数解析、临时目录脚手架、默认安装依赖、原子重命名、密钥不落盘、歧义选择失败——并用 src/index.ts 的源码与 tests/initializer.test.mjs 的测试断言给出佐证。读完后你可以掌握该初始化器的完整命令行用法、provider 选择策略、原子化目录生成的实现原理,以及生成项目的文件结构与验收测试方式。
组件定位:为 @caveman-ai/agent 提供零运行时依赖的脚手架
CLAUDE.md 给出的核心定位是:这是 @caveman-ai/agent 的 Zero-runtime-dependency npm initializer——解析 provider/install 标志,把脚手架写入临时目录,默认安装依赖,然后原子化地重命名到目标路径。它同时列出了四条必须遵守的不变量:
- 生成的项目只保留一个必需的源文件;
- 永远不打印、不持久化 provider 密钥;
- 非交互场景下 provider 选择歧义时必须失败,且不留下部分生成的目标目录;
--no-install让调用方可以自行管理依赖。
这些约定都可以与源码核对。从 package.json 可以看到:包内 devDependencies 仅有 typescript 与 @types/node,没有任何 dependencies 字段,印证了"零运行时依赖"的说法;bin 入口指向 ./dist/index.js,engines 要求 node >=22.19.0。该目录在 pnpm workspace 中注册为 packages/create-caveman-agent(见 pnpm-workspace.yaml)。
CLAUDE.md 开头还有一段仓库路由说明:初始化器产品工作的 source of truth 位于 JuliusBrussee/caveman-agent-sdk,本目录是"historical consumer copy",仅在 pinned 集成、迁移/移除或显式跨仓库同步时才编辑。可以推断这是该 monorepo 对该子包的维护边界约定:读代码没问题,但改动需谨慎。
命令行接口:完整用法与标志语义
初始化的标准用法(与 README.md 一致):
npm create @caveman-ai/agent@latest my-agent
cd my-agent
npm run doctor
npm run dev
--help / -h 时输出的 usage 行由 src/index.ts 中的 USAGE 常量定义:
usage: npm create @caveman-ai/agent@latest <project> [--provider anthropic|openai|google] [--no-install]
两个标志的解析逻辑在 parseArgs:
--provider必须跟随一个值;若值缺失或以下一个--开头,抛出--provider requires a value;--no-install将install置为false(默认为true,即默认安装依赖);- 任何未识别的
--xxx选项都会抛出unknown option <value>,且此时尚未写入任何目标文件; - 位置参数必须恰好一个,否则直接回显 usage 报错。
provider 的取值由 parseProvider 做归一化(trim + 小写),只接受 anthropic、openai、google 三个值,其他值报 unsupported provider。
Provider 选择:凭据探测、静默选择与非交互失败
CLAUDE.md 约定"Ambiguous noninteractive provider selection fails without partial target",README 进一步描述为"Exactly one detected provider credential selects silently. Zero or multiple credentials prompt once"。对应实现是 chooseProvider,决策顺序如下:
- 显式
--provider优先:直接解析,不做凭据探测; - 凭据探测:依次检查
ANTHROPIC_API_KEY、OPENAI_API_KEY、GEMINI_API_KEY(或GOOGLE_API_KEY),命中的 provider 收集进detected列表; - 恰好命中一个 → 静默选择,不打扰用户;
- 命中 0 个或多个:
- 若 stdin/stdout 都不是 TTY(如 CI、管道),直接抛错——0 个报
no provider credential detected; pass --provider,多个报multiple provider credentials detected; pass --provider; - 若是交互终端,则用 readline 提问一次
Provider (anthropic/openai/google):。
- 若 stdin/stdout 都不是 TTY(如 CI、管道),直接抛错——0 个报
每种 provider 在生成项目中写入的默认模型由 MODELS 固定:
| Provider | 默认模型(写入 .caveman/provider.json) |
探测的环境变量 |
|---|---|---|
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 或 GOOGLE_API_KEY |
注意选定的 provider 与模型只是写进 .caveman/provider.json 的 { provider, model }(见 projectFiles),从不写入任何密钥本身——这正是"Never print or persist provider secrets"约定的落点之一。
原子化脚手架流程:临时目录、wx 独占写、失败即清理
CLAUDE.md 中最关键的一句是"writes scaffold into temporary directory ... then atomically renames into target"。main 函数 的完整流程:
resolve(targetArg)解析目标路径,safeName(basename(target))生成项目名(下文详述);- assertAbsent:若目标路径已存在(
stat不返回ENOENT)立即报target already exists,在任何写入之前拦截; - 选择 provider;
- 构造临时目录:
.<name>.<pid>.<uuid>.tmp(src/index.ts#L29),与目标同目录、带 PID 和crypto.randomUUID(),保证并发运行互不冲突; - 在临时目录内创建
src/、evals/、.caveman/三个子目录,然后逐文件写入projectFiles(name, provider)返回的文件——每个writeFile都使用flag: "wx", mode: 0o600:wx要求独占创建(文件已存在即失败),0o600限定属主读写权限; - 若未指定
--no-install,调用installDependencies(temporary); rename(temporary, target)完成原子替换——用户在成功前永远看不到半成品目录;- 任何一步抛错都会
rm(temporary, { recursive: true, force: true })清理临时目录后再抛出。
这个"先写临时目录、rename 原子落地、失败全量清理"的设计,正是 CLAUDE.md 所说"fails without partial target"的机制基础:即使非交互场景因 provider 歧义失败(该失败发生在步骤 3,更早于任何写入),磁盘上也不会有残留目标。
成功后 stdout 输出的引导信息(src/index.ts#L44-L52)会列出 created <name>、provider <p> (<model>)、cd <target>,并在跳过安装时补一行 npm install,最后固定提示:eval starts unapproved; inspect it, set approved: true, then npm run build。
依赖安装:npm_execpath 复用、Windows 适配与环境变量白名单
installDependencies 对安装方式做了三层适配:
- 若
process.env.npm_execpath存在(npm create启动时通常会有),直接用当前 Node 解释器执行该npm-cli.js,避免 PATH 中 npm 版本不一致的问题; - Windows 且无
npm_execpath时,回退到ComSpec(默认cmd.exe)执行/d /s /c npm install ...; - 其他环境直接
npm install。
三者的安装参数统一为 install --no-audit --no-fund --ignore-scripts:跳过审计与基金提示、且不执行依赖包的安装脚本,降低脚手架阶段的不确定性。
更值得注意的是 dependencyInstallEnv:安装子进程的环境变量不是透传 process.env,而是白名单式重建——只放行 PATH、HOME、TEMP 系列、代理变量、NPM_CONFIG_*/npm_config_* 等约 28 个与安装相关的键。这意味着运行 npm create 时环境里的 ANTHROPIC_API_KEY、OPENAI_API_KEY 等 provider 凭据(以及其他任何无关密钥)根本不会进入 npm install 子进程。CLAUDE.md 的"Never print or persist provider secrets"在这里得到了最严格的一条实现证据:测试 initializer.test.mjs#L164-L192 注入 OPENAI_API_KEY、CAVE_API_KEY、AWS_SECRET_ACCESS_KEY 三个假密钥,用伪造的 npm_execpath 捕获子进程环境后断言它们均不存在,而 PATH 仍然可用。
--no-install 则把安装步骤整体跳过,对应 CLAUDE.md 中"supports callers that manage dependencies"的场景;此时成功输出会补一行 npm install 提示,README 中的非交互示例即:
npm create @caveman-ai/agent@latest my-agent -- --provider openai --no-install
生成的项目文件:一个必需源文件与显式的预算拨盘
CLAUDE.md 要求"Keep generated project at one required source file"——即 src/agent.ts 是唯一的必需 agent 定义文件。projectFiles 实际生成 9 个文件,各自角色如下:
| 文件 | 作用 |
|---|---|
package.json |
定义 doctor/dev/run/build/check/typecheck 六个脚本,依赖 @caveman-ai/agent ^0.1.0,engines.node >=22.19.0 |
tsconfig.json |
ES2024 + NodeNext + strict: true + noEmit,include 覆盖 src/**、evals/**、caveman.config.ts |
src/agent.ts |
唯一必需源文件:定义一个 readiness 工具与一个 agent() 导出 |
src/run.ts |
演示 run() 调用与预算配置(预算拨盘) |
evals/smoke.eval.ts |
起步 eval,初始 approved: false |
caveman.config.ts |
构建配置:entry、evals glob、maxSearchCostUsd: 2 |
.caveman/provider.json |
{ provider, model },无密钥 |
.gitignore |
忽略 node_modules/、.caveman/traces/、.env* |
README.md |
按 provider 生成对应的凭据环境变量名(如 OPENAI_API_KEY)与运行指引 |
生成的 src/agent.ts 是一个最小但完整的 agent:
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: "<name>",
instructions: "Call readiness once. Then reply with exactly: ready",
model: auto(),
tools: [readiness],
});
src/run.ts 则刻意把"预算拨盘"写出来,源码注释解释了每个字段的语义:
const result = await run(definition, "Reply with exactly: ready", {
rootDir,
entryPath: "src/agent.ts",
budget: {
// 调用前准入上限:每次调用按估算最坏情况预留,
// 若 provider 最终用量超过预留,receipt 记录 capBreached/overspent 并停止。
maxUsd: 0.5,
// 预留余量逼近上限时的行为:"compact"(默认)重写上下文继续,
// "stop" 直接钳制输出并停止。
onExhausted: "compact",
},
});
生成的 README 中还包含一段关于诚实性口径的说明:provider 后来报告超过估算的用量时,receipt 会如实记录 capBreached 与 overspent 后运行停止;本地结果标记为 inferred,"Verified savings remain $0 until active real traffic passes existing Caveman rollout and ledger gates"。这套表述与测试断言一一对应——initializer.test.mjs#L79-L85 明确检查生成 README 与 src/run.ts 不得出现"never spend past"这类营销式措辞,而必须包含 capBreached/overspent 说明。
起步 eval(evals/smoke.eval.ts)默认 approved: false,质量检查为 exact_match: "ready" 加 tool_called: ["readiness"]。CLAUDE.md 与 README 一致要求:先审查预期行为、手动把 approved 改为 true,再执行 npm run build 与 npm run check。
项目名归一化与错误出口
main 末尾的错误处理 将所有失败收敛为 stderr 一行 create-caveman-agent: <message> 加退出码 1;safeName 的归一化规则是:小写、非 [a-z0-9_-] 字符替换为 -、去掉首尾连字符、空名报 project name must contain a letter or number、截断到 96 字符。由于项目名同时是生成的 package.json 的 name 与 agent 的 id,这套限制保证了脚手架产物可直接 npm install。
测试证据:每条约定都有断言
CLAUDE.md 指定运行 pnpm --dir public/create-caveman-agent test;文档中的 public/ 路径与当前仓库实际位置不一致——workspace 中注册的是 packages/create-caveman-agent,等价命令应为 pnpm --dir packages/create-caveman-agent test。该命令先 tsc 编译出 dist/,再用 node --test --test-force-exit 跑 tests/initializer.test.mjs。各测试与 CLAUDE.md 约定的对应关系:
- 打包生命周期:
prepack必须等于npm run build,bin首行必须是#!/usr/bin/env node(#L27-L42); --help无副作用:退出码 0、stdout 以 usage 开头、stderr 为空、无凭据要求;- 单一必需源文件 + 确定性 provider:用
--provider openai --no-install生成后,校验src/agent.ts存在且含agent(、.caveman/provider.json的 model 为openai/gpt-5.4-mini、各 npm 脚本与 TypeScript 版本精确匹配;随后用 mock 的@caveman-ai/agent包实际执行src/run.ts,断言run()收到的rootDir等于项目根、entryPath为src/agent.ts(#L53-L134); - 密钥不落盘不打印:注入
OPENAI_API_KEY: "secret-never-print",断言 stdout+stderr 中不出现该字符串; - 恰好一个凭据静默选择:仅设
ANTHROPIC_API_KEY时不问任何交互问题,provider.json 得到anthropic; - 非交互歧义失败无残留:同时设
ANTHROPIC_API_KEY与OPENAI_API_KEY,断言退出码 1、stderr 含multiple provider credentials,且目标目录stat返回ENOENT; - 默认安装行为:伪造
npm_execpath,断言安装的 argv 恰为install --no-audit --no-fund --ignore-scripts,且三个注入的密钥均未进入子进程环境、PATH仍在;同时断言成功输出不含裸npm install行但含npm run dev行; - 未知选项拒绝:
--wat导致退出码 1、unknown option --wat,且目标目录不存在。
小结:如何验收与使用该初始化器
- 本地开发/验收:
pnpm --dir packages/create-caveman-agent test(先编译再跑 node:test),package.json 中的lint为tsc --noEmit; - 生产使用:
npm create @caveman-ai/agent@latest <project>,CI 中务必显式传--provider,由他方管理依赖时加--no-install; - 生成项目的第一条路径是
npm run doctor(doctor 不调用 provider;如报缺少签名运行时产物,按其唯一提示执行caveman setup --install后重跑); - 起步 eval 在人工确认前保持
approved: false,锁定构建后用npm run check复核。
从源码结构看,这个初始化器把"零依赖"做成了硬约束:没有第三方运行时库,全部能力来自 node:child_process、node:fs/promises、node:readline/promises 等内置模块;而"原子性""无密钥泄漏""歧义即失败"三条不变量各有对应的测试断言守护。若需要理解生成项目里 run()/build/eval 的更完整语义,可继续阅读 packages/agent/README.md。
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