diff --git a/packages/cli/skills/claude/CLAUDE.md b/packages/cli/skills/claude/CLAUDE.md new file mode 100644 index 0000000..e5cdf76 --- /dev/null +++ b/packages/cli/skills/claude/CLAUDE.md @@ -0,0 +1,553 @@ +# Nerve — AI Agent 观测引擎 + +Nerve 是一个轻量级观测引擎守护进程。它持续观测外部状态,通过声明式规则响应变化,编排多步骤工作流。 + +## 核心架构 + +``` +External World → Sense(state) → { newState, workflow? } → Workflow → Log +``` + +| 概念 | 说明 | +|------|------| +| **Sense** | 有状态的 `compute(state)` 函数。返回新状态和可选的 workflow trigger。状态以 JSON 文件持久化。调度由 nerve.yaml 配置。 | +| **Workflow** | 有状态的多步骤执行。包含 Role(有副作用的执行者)和 Moderator(纯路由器)。每个实例是一个 Thread,有唯一 runId。 | +| **Log** | 不可变审计日志。记录执行、状态转换、错误。不能触发 Sense(防止反馈循环)。 | +| **Engine** | 内核,持有 Process Manager、Workflow Manager、Sense Scheduler。不直接加载用户代码。 | +| **Daemon** | 引擎运行时,作为后台进程运行。 | + +**关键规则:** +- 因果链单向:External → Sense(state) → Workflow(触发时) + Log +- 进程隔离:每个 Sense group 一个 worker(长期),每个 Workflow 类型一个 worker(按需) +- 两个扩展点:Sense(观测什么 + 何时)、Workflow(做什么) + +## 工作区结构 + +由 `nerve init` 生成的工作区根目录(默认 `~/.uncaged-nerve/`)包含 **`AGENT.md`**。实现 sense/workflow 前先阅读该文件。 + +``` +~/.uncaged-nerve/ +├── AGENT.md # 人类 / Agent 可读的工作区约定(init 生成) +├── nerve.yaml # 核心配置 +├── package.json # 单一根包(sense/workflow 下不再有独立 package) +├── scripts/build.mjs # 根目录 esbuild;通过 npm/pnpm 的 build 脚本调用 +├── senses/ +│ └── / +│ └── src/index.ts # exports compute(state) + initialState +├── workflows/ +│ └── / +│ ├── index.ts # default export:WorkflowDefinition +│ ├── moderator.ts # 可选:抽出 moderator,由 index 导入 +│ ├── build.ts # 可选:共享常量 / 纯函数(避免 index 臃肿;非 esbuild 入口) +│ └── roles/ +│ └── .ts # 每角色单文件(推荐平铺,而非 roles//index.ts) +└── data/ + ├── senses/ # sense 状态 JSON 文件(自动生成) + └── logs.db # 日志存储(自动生成) +``` + +### 命名约定 + +- **Workflow**:动词开头的 kebab-case(例如 `review-pull-request`、`deploy-staging`)。避免单独名词式命名(如 `notifications`)。 +- **Sense**:描述性名词 kebab-case(例如 `cpu-usage`)。 + +--- + +## CLI 完整参考 + +全局选项:`--host `(连接远程 daemon)、`--api-token `(Bearer 认证) + +### 初始化与脚手架 + +```bash +nerve init # 初始化工作区 +nerve init --from # 从 git 仓库克隆工作区 +nerve init workspace # 只初始化工作区结构 + +nerve create sense # 创建 sense 脚手架 +nerve create sense --force # 覆盖已有 +nerve create workflow # 创建 workflow 脚手架 +nerve create workflow --force + +nerve validate # 验证 nerve.yaml 配置 +``` + +### Daemon 管理 + +```bash +nerve daemon start # 启动后台 daemon +nerve daemon start --port 3000 # 指定 HTTP API 端口 +nerve daemon stop # 停止 daemon +nerve daemon restart # 重启 +nerve daemon status # 查看状态 +nerve daemon logs # 查看日志 +nerve daemon logs --follow # 实时日志 +nerve daemon logs --n 50 # 最近 50 行 + +nerve dev # 前台开发模式(不 fork daemon) +nerve dev --port 3000 # 指定端口 +``` + +### Sense 操作 + +```bash +nerve sense list # 列出所有注册的 sense +nerve sense trigger # 手动触发 sense 计算 +``` + +### Workflow 操作 + +```bash +nerve workflow list # 列出 nerve.yaml 中定义的 workflow +nerve workflow status # 查看运行中的 workflow 状态 +nerve workflow trigger # 触发 workflow +nerve workflow trigger --prompt "检查生产环境" +nerve workflow trigger --maxRounds 50 +nerve workflow trigger --dryRun # 干跑模式 +``` + +### Thread(Workflow 执行记录) + +```bash +nerve thread list # 列出最近的 workflow 执行 +nerve thread list --all # 包含已完成/失败的 +nerve thread list --workflow # 按 workflow 过滤 +nerve thread list --limit 50 # 最多 50 条 + +nerve thread show # 查看 role 对话轮次 +nerve thread show --budget 16000 # 增大输出预算(默认 8000 字符) + +nerve thread inspect # 查看详情和事件 + +nerve thread kill # 终止运行中/排队中的 thread +``` + +### Store(日志归档) + +```bash +nerve store archive # 导出旧日志到 JSONL 归档 +nerve store archive --vacuum # 归档后 VACUUM 数据库 +``` + +### Knowledge(知识库) + +```bash +nerve knowledge sync # 从 knowledge.yaml 重建索引 +nerve knowledge query "搜索内容" # 搜索知识库 +nerve knowledge query "内容" --limit 5 +nerve knowledge query "内容" -g # 搜索所有注册仓库 +``` + +### Remote(远程 daemon) + +```bash +nerve remote add --token +nerve remote list +nerve remote show +nerve remote set-url +nerve remote set-token +nerve remote remove +nerve remote default # 设为默认远程 +``` + +### Agent(向 AI Agent 注入 nerve skill) + +```bash +nerve agent status # CLI 版本与各注入目录中的 skill 版本 +nerve agent inject hermes # 安装到 ~/.hermes/skills/nerve +nerve agent inject hermes --profile # 写入 ~/.hermes/profiles//skills/nerve +nerve agent inject cursor # 注入到当前项目 .cursorrules +nerve agent inject claude # 注入到 ~/.claude/CLAUDE.md +nerve agent update # 将所有已注入目录更新到当前 CLI 对应版本 +nerve agent remove hermes # 移除 Hermes 注入 +nerve agent remove cursor # 移除 Cursor 注入 +nerve agent remove claude # 移除 Claude Code 注入 +``` + +--- + +## nerve.yaml 配置参考 + +```yaml +# 引擎全局配置 +max_rounds: 100 # moderator 最大轮次(默认 100) + +# Sense 配置 +senses: + cpu-usage: + group: system # 必填,同 group 的 sense 共享 worker + interval: 10s # 轮询间隔(duration: 5s, 10m, 1h) + throttle: 5s # 最小计算间隔 + timeout: 10s # compute 超时 + grace_period: null # 超时后优雅等待 + + system-health: + group: derived + on: [cpu-usage, disk-usage] # 响应式:被列出的 sense 完成 compute 时触发 + throttle: null + timeout: null + +# Workflow 配置 +workflows: + my-workflow: + concurrency: 1 # 必填,并发数 + overflow: drop # 必填,超并发时处理:drop | queue + max_queue: 100 # overflow=queue 时的队列上限(默认 100) + +# HTTP API +api: + port: 3000 # null = 不启用 HTTP + host: "127.0.0.1" # 监听地址 + token: null # 非 loopback 时必填 + +# LLM Extract(可选) +extract: + provider: anthropic + model: claude-sonnet-4-20250514 +``` + +--- + +## Sense 开发指南 + +### compute 函数签名 + +Sense 的 `compute` 接收当前状态,返回新状态和可选的 workflow trigger。状态以 JSON 文件持久化在 `data/senses/.json`。 + +```typescript +import type { SenseComputeFn, WorkflowTrigger } from "@uncaged/nerve-core"; + +type MyState = { + lastRun: number | null; + count: number; +}; + +export const initialState: MyState = { lastRun: null, count: 0 }; + +export async function compute(state: MyState): Promise<{ + state: MyState; + workflow: WorkflowTrigger | null; +}> { + return { + state: { lastRun: Date.now(), count: state.count + 1 }, + workflow: null, + }; +} +``` + +### 状态持久化 + +| 时机 | 行为 | +|------|------| +| Worker 启动 | 读取 `data/senses/.json`;不存在或损坏则使用 `initialState` | +| Compute 成功 | 原子写入新状态(temp + rename),然后更新内存 | +| Compute 失败 | 状态不变(磁盘和内存都不变) | +| Daemon 重启 | 从上次成功写入恢复 | + +### 返回值 + +```typescript +// workflow: null → 不触发 workflow +// workflow: WorkflowTrigger → 触发 workflow + +type WorkflowTrigger = { + name: string; // workflow 名称(对应 nerve.yaml 中的 key) + maxRounds: number; // moderator 最大轮次 + prompt: string; // 初始 prompt + dryRun: boolean; // 干跑模式 +}; +``` + +### Sense 模块导出 + +每个 sense 的 `src/index.ts` 必须导出两个东西: + +```typescript +// senses//src/index.ts + +// 1. 初始状态 +export const initialState: MyState = { ... }; + +// 2. compute 函数 +export async function compute(state: MyState): Promise<{ + state: MyState; + workflow: WorkflowTrigger | null; +}> { + // ... +} +``` + +### 调度方式 + +1. **interval 轮询**:`interval: 10s` — 每 10 秒执行一次 +2. **响应式触发**:`on: [cpu-usage]` — 当 cpu-usage 完成 compute 后触发 +3. 两者可以组合 + +### 调试 + +```bash +nerve dev # 前台运行,看实时输出 +nerve sense trigger # 手动触发一次 +``` + +### 完整示例:CPU 监控 + +```typescript +// senses/cpu-usage/src/index.ts +import { loadavg } from "node:os"; + +type CpuState = { + samples: Array<{ ts: number; value: number }>; +}; + +export const initialState: CpuState = { samples: [] }; + +export async function compute(state: CpuState): Promise<{ + state: CpuState; + workflow: null; +}> { + const [oneMin] = loadavg(); + const value = typeof oneMin === "number" && !Number.isNaN(oneMin) ? oneMin : 0; + const newSamples = [...state.samples.slice(-99), { ts: Date.now(), value }]; + return { state: { samples: newSamples }, workflow: null }; +} +``` + +nerve.yaml: +```yaml +senses: + cpu-usage: + group: system + interval: 10s + throttle: 5s + timeout: 10s +``` + +--- + +## Workflow 开发指南 + +### 核心类型 + +```typescript +import type { + WorkflowDefinition, + RoleResult, + ThreadContext, + RoleMeta, + Moderator, +} from "@uncaged/nerve-core"; +import { END } from "@uncaged/nerve-core"; + +// Role — (ctx: ThreadContext) => Promise> +// RoleResult — { content: string; meta: Meta } +// ThreadContext — threadId, start(__start__ 帧), steps(各 role 轮次) +// Moderator — (ctx) => 下一个 role 名 | END +// WorkflowDefinition — name, roles, moderator +``` + +### createRole 四元组(接入 LLM 时推荐) + +工作区根目录需安装 **`@uncaged/nerve-workflow-utils`**(及所选 agent 适配器包)。默认 `nerve init` 的 `package.json` 不含该依赖时,在 `~/.uncaged-nerve` 下执行 `pnpm add @uncaged/nerve-workflow-utils`(或 npm 等价命令)。 + +使用 **`createRole`**,按固定顺序传入四件事: + +1. **adapter** — `AgentFn`,`(ctx, systemPrompt) => Promise`(原始模型输出文本)。 +2. **prompt** — `string`,或 `async (ctx: ThreadContext) => string`。 +3. **meta** — `z.ZodType`,供 moderator 路由的结构化 meta。 +4. **extract** — `{ provider: LlmProvider; dryRun: boolean | null }`,声明从回复中抽取 meta 时用的 LLM(OpenAI 兼容)及是否 dry-run。 + +```typescript +import { createLlmAdapter, createRole } from "@uncaged/nerve-workflow-utils"; +import type { ThreadContext } from "@uncaged/nerve-core"; +import { z } from "zod"; + +const provider = { + baseUrl: "https://api.example.com/v1", + apiKey: process.env.EXAMPLE_API_KEY!, + model: "gpt-4o-mini", +}; + +const planMeta = z.object({ next: z.enum(["execute", "stop"]) }); + +export const planner = createRole( + createLlmAdapter(provider), + async (ctx: ThreadContext) => `规划任务:${ctx.start.content}`, + planMeta, + { provider, dryRun: null }, +); +``` + +`createLlmAdapter` 仅位于 **`@uncaged/nerve-workflow-utils`**:用 `LlmProvider` 生成 `AgentFn`,单轮对话里 **system** 来自 `createRole` 解析后的 prompt 字符串,**user** 为线程起点 `ctx.start.content`。 + +### 基本 Workflow 示例(平铺 `roles/.ts`) + +```typescript +// workflows/example/roles/main.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function main(ctx: ThreadContext): Promise> { + const prompt = ctx.start.content; + return { + content: `处理完成: ${prompt}`, + meta: { round: ctx.steps.length }, + }; +} +``` + +```typescript +// workflows/example/index.ts +import type { ThreadContext, WorkflowDefinition } from "@uncaged/nerve-core"; +import { END } from "@uncaged/nerve-core"; + +import { main } from "./roles/main.js"; + +type Meta = Record<"main", { round: number }>; + +const workflow: WorkflowDefinition = { + name: "example", + roles: { main }, + moderator(ctx: ThreadContext) { + return ctx.steps.length === 0 ? "main" : END; + }, +}; + +export default workflow; +``` + +可选:将 `moderator` 挪到 `moderator.ts` 再 `import { route } from "./moderator.js"`,保持 `index.ts` 只负责组装 `WorkflowDefinition`。 + +### 多 Role Workflow 示例 + +```typescript +// workflows/plan-execute-review/roles/planner.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function planner(ctx: ThreadContext): Promise> { + void ctx; + return { content: "计划: ...", meta: { status: "planned" } }; +} +``` + +```typescript +// workflows/plan-execute-review/roles/executor.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function executor(ctx: ThreadContext): Promise> { + void ctx; + return { content: "执行: ...", meta: { status: "executed" } }; +} +``` + +```typescript +// workflows/plan-execute-review/roles/reviewer.ts +import type { RoleResult, ThreadContext } from "@uncaged/nerve-core"; + +export async function reviewer(ctx: ThreadContext): Promise> { + void ctx; + return { content: "审核通过", meta: { status: "approved" } }; +} +``` + +```typescript +// workflows/plan-execute-review/index.ts +import type { WorkflowDefinition, ThreadContext } from "@uncaged/nerve-core"; +import { END } from "@uncaged/nerve-core"; + +import { executor } from "./roles/executor.js"; +import { planner } from "./roles/planner.js"; +import { reviewer } from "./roles/reviewer.js"; + +type Roles = Record<"planner" | "executor" | "reviewer", { status: string }>; + +const workflow: WorkflowDefinition = { + name: "plan-execute-review", + roles: { planner, executor, reviewer }, + moderator(ctx: ThreadContext) { + if (ctx.steps.length === 0) return "planner"; + const last = ctx.steps[ctx.steps.length - 1]; + if (last.role === "planner") return "executor"; + if (last.role === "executor") return "reviewer"; + return END; + }, +}; + +export default workflow; +``` + +### Agent 适配器 + +Workflow role 可以集成 AI agent。已知适配器 **ID**:`echo`、`cursor`、`hermes`、`codex`。 + +```typescript +type AgentFn = (ctx: ThreadContext, systemPrompt: string) => Promise; +``` + +没有现成 agent 包时,用 **`createLlmAdapter`(`@uncaged/nerve-workflow-utils`)** 从 OpenAI 兼容的 `LlmProvider` 构造 `AgentFn`,再交给 **`createRole`** 的四元组。 + +### Workflow 运行状态 + +`queued` → `started` → `completed` | `failed` | `crashed` | `killed` | `interrupted` | `dropped` + +--- + +## 日常操作 Pattern + +### 查看系统整体状态 + +```bash +nerve daemon status # daemon 是否在运行 +nerve sense list # 所有 sense 及其调度配置 +nerve workflow status # 运行中的 workflow +nerve thread list # 最近的 workflow 执行记录 +``` + +### 手动触发 workflow + +```bash +nerve workflow trigger my-workflow --prompt "手动检查" +nerve thread list --workflow my-workflow # 查看执行状态 +nerve thread show # 查看对话详情 +``` + +### 排查 sense 报错 + +```bash +nerve daemon logs --follow # 查看实时日志 +nerve sense trigger # 手动触发看报错 +nerve dev # 前台模式,更详细的输出 +``` + +### 开发新 sense + +```bash +nerve create sense my-sensor # 脚手架 +# 编辑 senses/my-sensor/src/index.ts — 实现 compute(state) + initialState +nerve validate # 验证配置 +nerve dev # 前台测试 +nerve sense trigger my-sensor # 单次触发验证 +``` + +### 开发新 workflow + +```bash +nerve create workflow my-flow # 脚手架 +# 推荐对齐 AGENT.md:workflows/my-flow/index.ts + roles/.ts(平铺),moderator 可拆到 moderator.ts +nerve validate # 验证配置 +cd ~/.uncaged-nerve && npm run build # 工作区根目录构建;勿在单个 workflow 子目录单独跑 build +nerve workflow trigger my-flow --prompt "测试" --dryRun # 干跑 +nerve thread show # 查看执行轨迹 +``` + +--- + +## Pitfalls + +- **Sense 状态**:`compute(state)` 必须返回 `{ state, workflow }` 对象。不要返回 null 或 undefined。 +- **initialState**:每个 sense 必须导出 `initialState`,否则加载失败。 +- **状态持久化**:daemon 自动管理状态读写,业务代码不要自行读写 state.json。 +- **no optional properties**:nerve 代码规范禁止 `?:`,用 `T | null` 代替。 +- **函数式风格**:用 `function` + `type`,不用 `class` + `interface`。 +- **workflow 用 default export**:工作区里通常只有 `workflows//index.ts` 使用 default export(daemon 加载约定)。 +- **concurrency + overflow**:workflow 必须配置并发策略,否则验证失败。 +- **moderator 是同步函数**:不要加 async,moderator 是纯路由逻辑,不能有副作用。 diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index b29b599..33ed228 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -65,6 +65,10 @@ function writeVersionFile(skillDir: string, version: string): void { const CURSOR_VERSION_MARKER_RE = //; +const CLAUDE_BLOCK_START_RE = //; +const CLAUDE_BLOCK_END = ""; +const CLAUDE_BLOCK_RE = /[\s\S]*?/; + function resolveCursorProjectDir(pathArg: string | null): string { const raw = pathArg !== null && pathArg !== "" ? pathArg : process.cwd(); return resolvePath(raw); @@ -164,6 +168,82 @@ function removeHermes(profile: string | null): void { process.stdout.write(`✅ Removed Hermes nerve skill${loc}\n`); } +// --- Claude Code --- + +function getClaudeGlobalFile(): string { + return join(homedir(), ".claude", "CLAUDE.md"); +} + +function readClaudeBlockVersion(): string | null { + const filePath = getClaudeGlobalFile(); + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, "utf8"); + const match = content.match(CLAUDE_BLOCK_START_RE); + return match !== null ? match[1].trim() : null; +} + +function injectClaude(): void { + const templatePath = join(getSkillSourceDir(), "claude", "CLAUDE.md"); + if (!existsSync(templatePath)) { + throw new Error("Cannot locate claude/CLAUDE.md template. Is the CLI package intact?"); + } + const version = cliVersion(); + const existingVer = readClaudeBlockVersion(); + + if (existingVer === version) { + process.stdout.write(`✅ Claude Code nerve skill is already up to date (v${version})\n`); + return; + } + + const body = readFileSync(templatePath, "utf8"); + const block = `\n${body.trim()}\n${CLAUDE_BLOCK_END}`; + + const filePath = getClaudeGlobalFile(); + mkdirSync(dirname(filePath), { recursive: true }); + + if (existsSync(filePath)) { + const existing = readFileSync(filePath, "utf8"); + if (CLAUDE_BLOCK_RE.test(existing)) { + // Replace existing block + const updated = existing.replace(CLAUDE_BLOCK_RE, block); + writeFileSync(filePath, updated, "utf8"); + } else { + // Append + const sep = existing.endsWith("\n") ? "\n" : "\n\n"; + writeFileSync(filePath, `${existing}${sep}${block}\n`, "utf8"); + } + } else { + writeFileSync(filePath, `${block}\n`, "utf8"); + } + + const action = existingVer !== null ? "Updated" : "Installed"; + process.stdout.write(`✅ ${action} Claude Code nerve skill v${version}\n`); + process.stdout.write(` → ${filePath}\n`); +} + +function removeClaude(): void { + const filePath = getClaudeGlobalFile(); + if (!existsSync(filePath)) { + process.stdout.write("ℹ️ Claude Code nerve skill is not installed.\n"); + return; + } + const content = readFileSync(filePath, "utf8"); + if (!CLAUDE_BLOCK_RE.test(content)) { + process.stdout.write("ℹ️ Claude Code nerve skill is not installed.\n"); + return; + } + const cleaned = content + .replace(CLAUDE_BLOCK_RE, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); + if (cleaned.length === 0) { + rmSync(filePath, { force: true }); + } else { + writeFileSync(filePath, `${cleaned}\n`, "utf8"); + } + process.stdout.write("✅ Removed Claude Code nerve skill\n"); +} + function printCursorStatusLine(projectDir: string): void { const rulesPath = join(projectDir, ".cursorrules"); const label = `Cursor (${projectDir})`; @@ -193,6 +273,11 @@ function printStatus(): void { printCursorStatusLine(process.cwd()); process.stdout.write("\n"); + // Claude Code + const claudeVer = readClaudeBlockVersion(); + printAgentLine("Claude Code", claudeVer); + process.stdout.write("\n"); + // Default profile const defaultDir = getHermesSkillDir(null); const defaultVer = readVersionFile(defaultDir); @@ -237,7 +322,7 @@ const injectCommand = defineCommand({ args: { target: { type: "positional", - description: "Agent target: hermes | cursor", + description: "Agent target: hermes | cursor | claude", }, profile: { type: "string", @@ -267,8 +352,12 @@ const injectCommand = defineCommand({ injectCursor(resolveCursorProjectDir(pathArg)); return; } + if (target === "claude") { + injectClaude(); + return; + } process.stderr.write(`❌ Unknown agent target: ${target}\n`); - process.stderr.write(" Supported targets: hermes, cursor\n"); + process.stderr.write(" Supported targets: hermes, cursor, claude\n"); process.exit(1); }, }); @@ -307,6 +396,12 @@ const updateCommand = defineCommand({ if (updated === 0) { process.stdout.write("ℹ️ No injected skills found. Run `nerve agent inject hermes` first.\n"); } + + // Claude Code (always check, no profiles) + const claudeVer = readClaudeBlockVersion(); + if (claudeVer !== null) { + injectClaude(); + } }, }); @@ -348,8 +443,12 @@ const removeCommand = defineCommand({ removeCursor(resolveCursorProjectDir(pathArg)); return; } + if (target === "claude") { + removeClaude(); + return; + } process.stderr.write(`❌ Unknown agent target: ${target}\n`); - process.stderr.write(" Supported targets: hermes, cursor\n"); + process.stderr.write(" Supported targets: hermes, cursor, claude\n"); process.exit(1); }, });