diff --git a/packages/cli/package.json b/packages/cli/package.json index eddf0f1..190c1d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -17,6 +17,7 @@ "scripts": { "prepublishOnly": "bash ../../scripts/prepublish-check.sh", "build": "rslib build", + "pretest": "pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-daemon run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/cli/src/__tests__/create-sense.test.ts b/packages/cli/src/__tests__/create-sense.test.ts index 5a3e68d..5c4c425 100644 --- a/packages/cli/src/__tests__/create-sense.test.ts +++ b/packages/cli/src/__tests__/create-sense.test.ts @@ -7,7 +7,6 @@ import { describe, expect, it } from "vitest"; import { buildSenseIndexTs, buildSenseMigrationSql, - buildSensePackageJson, buildSenseSchemaTs, validateResourceName, } from "../commands/create.js"; @@ -46,20 +45,11 @@ describe("buildSenseMigrationSql", () => { }); }); -describe("buildSensePackageJson", () => { - it("includes esbuild script and sense name", () => { - const pkg = JSON.parse(buildSensePackageJson("my-sense")); - expect(pkg.name).toBe("nerve-sense-my-sense"); - expect(pkg.scripts.build).toContain("esbuild"); - expect(pkg.scripts.build).toContain("src/index.ts"); - expect(pkg.devDependencies.esbuild).toBeTruthy(); - }); -}); - describe("buildSenseIndexTs", () => { it("embeds sense id in stub with TypeScript types", () => { const ts = buildSenseIndexTs("my-sense"); expect(ts).toContain("my-sense"); + expect(ts).toContain("export { mySense as table }"); expect(ts).toContain("export async function compute"); expect(ts).toContain("LibSQLDatabase"); expect(ts).toContain("Promise"); diff --git a/packages/cli/src/__tests__/create-workflow.test.ts b/packages/cli/src/__tests__/create-workflow.test.ts index 212c654..e64fc07 100644 --- a/packages/cli/src/__tests__/create-workflow.test.ts +++ b/packages/cli/src/__tests__/create-workflow.test.ts @@ -9,7 +9,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { buildWorkflowPackageJson, buildWorkflowScaffold } from "../commands/create.js"; +import { buildWorkflowScaffold } from "../commands/create.js"; let tmpDir: string; @@ -81,21 +81,6 @@ describe("buildWorkflowScaffold", () => { const { roleMainPromptMd } = buildWorkflowScaffold("my-flow"); expect(roleMainPromptMd).toContain("# my-flow — main role"); }); - - it("package.json defines esbuild bundling to dist/", () => { - const pkg = JSON.parse(buildWorkflowPackageJson("my-flow")) as { - scripts: { build: string }; - devDependencies: { esbuild: string }; - }; - expect(pkg.scripts.build).toContain("esbuild"); - expect(pkg.scripts.build).toContain("--outdir=dist"); - expect(pkg.devDependencies.esbuild).toBeTruthy(); - }); - - it("buildWorkflowScaffold includes package.json body", () => { - const { packageJson } = buildWorkflowScaffold("wf"); - expect(JSON.parse(packageJson).scripts.build).toContain("esbuild"); - }); }); describe("workflow scaffold file writing (simulated)", () => { diff --git a/packages/cli/src/__tests__/e2e-create.test.ts b/packages/cli/src/__tests__/e2e-create.test.ts index 92044e3..6553351 100644 --- a/packages/cli/src/__tests__/e2e-create.test.ts +++ b/packages/cli/src/__tests__/e2e-create.test.ts @@ -122,54 +122,49 @@ describe("e2e create", () => { }); it( - "create workflow scaffolds sources and package.json with esbuild build", - { timeout: 10_000 }, + "create workflow scaffolds sources and root build emits dist/workflows//index.js", + { timeout: 120_000 }, async () => { fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-")); const nerveRoot = join(fakeHome, ".uncaged-nerve"); - await runTestCli(fakeHome, ["init", "--force", "--skip-install"]); + await runTestCli(fakeHome, ["init", "--force"]); const wf = await runTestCli(fakeHome, ["create", "workflow", "e2e-flow"]); expect(wf.exitCode).toBe(0); expect(wf.stdout).toContain("✅"); - const pkgPath = join(nerveRoot, "workflows", "e2e-flow", "package.json"); - const indexPath = join(nerveRoot, "workflows", "e2e-flow", "index.ts"); - const mainRolePath = join(nerveRoot, "workflows", "e2e-flow", "roles", "main", "index.ts"); - expect(existsSync(pkgPath)).toBe(true); - expect(JSON.parse(readFileSync(pkgPath, "utf8")).scripts.build).toContain("esbuild"); + const wfDir = join(nerveRoot, "workflows", "e2e-flow"); + const indexPath = join(wfDir, "index.ts"); + const mainRolePath = join(wfDir, "roles", "main", "index.ts"); + expect(existsSync(join(wfDir, "package.json"))).toBe(false); expect(existsSync(indexPath)).toBe(true); expect(existsSync(mainRolePath)).toBe(true); expect(readFileSync(indexPath, "utf8")).toContain('name: "e2e-flow"'); expect(readFileSync(mainRolePath, "utf8")).toContain("e2e-flow started"); + expect(existsSync(join(nerveRoot, "dist", "workflows", "e2e-flow", "index.js"))).toBe(true); }, ); it( - "create sense scaffolds src/index.ts, src/schema.ts, package.json and migration", - { timeout: 60_000 }, + "create sense scaffolds src/, migration, and root build emits dist/senses//index.js", + { timeout: 120_000 }, async () => { fakeHome = mkdtempSync(join(tmpdir(), "nerve-create-e2e-")); const nerveRoot = join(fakeHome, ".uncaged-nerve"); - await runTestCli(fakeHome, ["init", "--force", "--skip-install"]); + await runTestCli(fakeHome, ["init", "--force"]); const sense = await runTestCli(fakeHome, ["create", "sense", "e2e-sense"]); expect(sense.exitCode).toBe(0); expect(sense.stdout).toContain("✅"); const base = join(nerveRoot, "senses", "e2e-sense"); - expect(existsSync(join(base, "package.json"))).toBe(true); + expect(existsSync(join(base, "package.json"))).toBe(false); expect(existsSync(join(base, "src", "index.ts"))).toBe(true); expect(existsSync(join(base, "src", "schema.ts"))).toBe(true); expect(existsSync(join(base, "migrations", "0001_init.sql"))).toBe(true); - - const pkg = JSON.parse(readFileSync(join(base, "package.json"), "utf8")); - expect(pkg.scripts.build).toContain("esbuild"); - - // pnpm install + build should produce index.js - expect(existsSync(join(base, "index.js"))).toBe(true); + expect(existsSync(join(nerveRoot, "dist", "senses", "e2e-sense", "index.js"))).toBe(true); }, ); diff --git a/packages/cli/src/__tests__/e2e-harness.ts b/packages/cli/src/__tests__/e2e-harness.ts index bffc4ce..58e3536 100644 --- a/packages/cli/src/__tests__/e2e-harness.ts +++ b/packages/cli/src/__tests__/e2e-harness.ts @@ -37,7 +37,15 @@ * ``` */ -import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; @@ -61,6 +69,27 @@ const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.j const senseWorkerScript = join(nerveDaemonRoot, "dist", "sense-worker.js"); const workflowWorkerScript = join(nerveDaemonRoot, "dist", "workflow-worker.js"); +function resolveDrizzleOrmPackageRoot(): string { + const requireFromDaemon = createRequire(join(nerveDaemonRoot, "package.json")); + const entry = requireFromDaemon.resolve("drizzle-orm"); + let dir = dirname(entry); + for (let i = 0; i < 12; i += 1) { + const pkgPath = join(dir, "package.json"); + if (existsSync(pkgPath)) { + try { + const name = (JSON.parse(readFileSync(pkgPath, "utf8")) as { name: string }).name; + if (name === "drizzle-orm") return dir; + } catch { + // keep walking + } + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error("Could not resolve drizzle-orm package root for e2e harness"); +} + const nerveYamlTemplate = `senses: counter: group: e2e @@ -88,9 +117,9 @@ const echoWorkflowIndexJs = `const END = "__end__"; export default { name: "echo", roles: { - echo: async (start, _messages) => { + echo: async (ctx) => { await new Promise((r) => setTimeout(r, 350)); - const p = typeof start.content === "string" ? start.content : ""; + const p = typeof ctx.start.content === "string" ? ctx.start.content : ""; return { content: p.length > 0 ? "echo:" + p : "echo:empty", meta: {}, @@ -121,17 +150,30 @@ api: host: 127.0.0.1 `; -/** Empty migration — counter sense uses only `_signals` (auto-created by daemon). */ -const counterMigration = `-- no-op migration for e2e counter sense -SELECT 1; +/** Schema for sense signal rows persisted via \`db.insert(table)\` (see sense-runtime). */ +const counterMigration = `CREATE TABLE IF NOT EXISTS counter_signals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + count INTEGER, + launched INTEGER, + idle INTEGER +); `; /** * Minimal counter sense — each compute returns an incrementing count. - * Does NOT touch the DB directly; signal persistence is handled by the daemon - * (`runtime.persistSignal`) which writes to `_signals` automatically. + * Does NOT touch the DB directly in compute(); the daemon inserts into \`table\` + * and persistSignal handles \`_signals\`. */ -const counterIndexJs = `let _count = 0; +const counterIndexJs = `import { integer, sqliteTable } from "drizzle-orm/sqlite-core"; + +export const table = sqliteTable("counter_signals", { + id: integer("id").primaryKey({ autoIncrement: true }), + count: integer("count"), + launched: integer("launched"), + idle: integer("idle"), +}); + +let _count = 0; export async function compute(_db, _peers, _options) { _count += 1; return { signal: { count: _count }, workflow: null }; @@ -139,12 +181,21 @@ export async function compute(_db, _peers, _options) { `; /** First trigger launches local noop workflow; later triggers emit a plain signal. */ -const counterIndexJsWithNoopWorkflow = `let _launched = false; +const counterIndexJsWithNoopWorkflow = `import { integer, sqliteTable } from "drizzle-orm/sqlite-core"; + +export const table = sqliteTable("counter_signals", { + id: integer("id").primaryKey({ autoIncrement: true }), + count: integer("count"), + launched: integer("launched"), + idle: integer("idle"), +}); + +let _launched = false; export async function compute(_db, _peers, _options) { if (!_launched) { _launched = true; return { - signal: { launched: true }, + signal: { launched: 1 }, workflow: { name: "noop", maxRounds: 3, @@ -153,7 +204,7 @@ export async function compute(_db, _peers, _options) { }, }; } - return { signal: { idle: true }, workflow: null }; + return { signal: { idle: 1 }, workflow: null }; } `; @@ -209,7 +260,8 @@ function writeWorkspaceLayout(nerveRoot: string, withNoopWorkflow: boolean): voi mkdirSync(join(nerveRoot, "data", "senses"), { recursive: true }); mkdirSync(join(nerveRoot, "data", "blobs"), { recursive: true }); mkdirSync(join(nerveRoot, "senses", "counter", "migrations"), { recursive: true }); - mkdirSync(join(nerveRoot, "workflows", "echo", "dist"), { recursive: true }); + mkdirSync(join(nerveRoot, "dist", "senses", "counter"), { recursive: true }); + mkdirSync(join(nerveRoot, "dist", "workflows", "echo"), { recursive: true }); writeFileSync( join(nerveRoot, "nerve.yaml"), withNoopWorkflow ? nerveYamlWithNoopWorkflow : nerveYamlTemplate, @@ -221,20 +273,19 @@ function writeWorkspaceLayout(nerveRoot: string, withNoopWorkflow: boolean): voi "utf8", ); writeFileSync( - join(nerveRoot, "senses", "counter", "index.js"), + join(nerveRoot, "dist", "senses", "counter", "index.js"), withNoopWorkflow ? counterIndexJsWithNoopWorkflow : counterIndexJs, "utf8", ); writeFileSync( - join(nerveRoot, "workflows", "echo", "dist", "index.js"), + join(nerveRoot, "dist", "workflows", "echo", "index.js"), echoWorkflowIndexJs, "utf8", ); if (withNoopWorkflow) { - mkdirSync(join(nerveRoot, "workflows", "noop", "dist"), { recursive: true }); - mkdirSync(join(nerveRoot, "workflows", "noop", "migrations"), { recursive: true }); + mkdirSync(join(nerveRoot, "dist", "workflows", "noop"), { recursive: true }); writeFileSync( - join(nerveRoot, "workflows", "noop", "dist", "index.js"), + join(nerveRoot, "dist", "workflows", "noop", "index.js"), noopWorkflowIndexJs, "utf8", ); @@ -267,11 +318,17 @@ function useNoopWorkflow(opts: StartTestDaemonOpts): boolean { */ export function linkWorkspaceDaemonIntoNerveRoot(nerveRoot: string): void { const daemonPkgRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.json")); - const linkDir = join(nerveRoot, "node_modules", "@uncaged"); - const linkPath = join(linkDir, "nerve-daemon"); + const nm = join(nerveRoot, "node_modules"); + mkdirSync(nm, { recursive: true }); + + const linkDir = join(nm, "@uncaged"); mkdirSync(linkDir, { recursive: true }); - if (existsSync(linkPath)) return; - symlinkSync(daemonPkgRoot, linkPath); + const linkPath = join(linkDir, "nerve-daemon"); + if (!existsSync(linkPath)) symlinkSync(daemonPkgRoot, linkPath); + + const drizzlePkgRoot = resolveDrizzleOrmPackageRoot(); + const drizzleLink = join(nm, "drizzle-orm"); + if (!existsSync(drizzleLink)) symlinkSync(drizzlePkgRoot, drizzleLink); } /** diff --git a/packages/cli/src/__tests__/e2e-validate-init.test.ts b/packages/cli/src/__tests__/e2e-validate-init.test.ts index dbf8429..0abeb6f 100644 --- a/packages/cli/src/__tests__/e2e-validate-init.test.ts +++ b/packages/cli/src/__tests__/e2e-validate-init.test.ts @@ -202,10 +202,9 @@ describe("e2e init", () => { // Verify key files exist expect(existsSync(join(nerveRoot, "nerve.yaml"))).toBe(true); expect(existsSync(join(nerveRoot, "package.json"))).toBe(true); - expect(existsSync(join(nerveRoot, "pnpm-workspace.yaml"))).toBe(true); + expect(existsSync(join(nerveRoot, "scripts", "build.mjs"))).toBe(true); expect(existsSync(join(nerveRoot, "biome.json"))).toBe(true); expect(existsSync(join(nerveRoot, ".gitignore"))).toBe(true); - expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "package.json"))).toBe(true); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "src", "index.ts"))).toBe(true); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "src", "schema.ts"))).toBe(true); expect(existsSync(join(nerveRoot, "senses", "cpu-usage", "migrations", "0001_init.sql"))).toBe( @@ -214,19 +213,14 @@ describe("e2e init", () => { expect(existsSync(join(nerveRoot, ".cursor", "rules", "nerve-skills.mdc"))).toBe(true); const pkgJson = readFileSync(join(nerveRoot, "package.json"), "utf8"); - expect(pkgJson).toContain('"@uncaged/nerve-skills": "latest"'); - expect(pkgJson).toContain('"build": "pnpm -r build"'); + expect(pkgJson).not.toContain("nerve-skills"); + expect(pkgJson).toContain('"build": "node scripts/build.mjs"'); + expect(pkgJson).toContain('"esbuild": "^0.27.0"'); - const workspaceYaml = readFileSync(join(nerveRoot, "pnpm-workspace.yaml"), "utf8"); - expect(workspaceYaml).toContain("workflows/*"); - expect(workspaceYaml).toContain("senses/*"); - - const sensePkgJson = readFileSync( - join(nerveRoot, "senses", "cpu-usage", "package.json"), - "utf8", - ); - expect(sensePkgJson).toContain("nerve-sense-cpu-usage"); - expect(sensePkgJson).toContain("esbuild"); + const buildScript = readFileSync(join(nerveRoot, "scripts", "build.mjs"), "utf8"); + expect(buildScript).toContain('path.join(root, "senses")'); + expect(buildScript).toContain('path.join(root, "workflows")'); + expect(buildScript).toContain("dist"); }); it("generated nerve.yaml passes validate", { timeout: 10_000 }, async () => { diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index 0db440e..6480331 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -20,34 +20,13 @@ export type WorkflowScaffoldFiles = { indexTs: string; roleMainIndexTs: string; roleMainPromptMd: string; - packageJson: string; }; -export function buildWorkflowPackageJson(name: string): string { - return `${JSON.stringify( - { - name: `nerve-workflow-${name}`, - private: true, - type: "module", - scripts: { - build: - "esbuild index.ts --bundle --platform=node --format=esm --outdir=dist --packages=external", - }, - devDependencies: { - esbuild: "^0.27.0", - }, - }, - null, - 2, - )}\n`; -} - export function buildWorkflowScaffold(name: string): WorkflowScaffoldFiles { return { indexTs: buildWorkflowIndexTs(name), roleMainIndexTs: buildWorkflowMainRoleIndexTs(name), roleMainPromptMd: buildWorkflowMainRolePromptMd(name), - packageJson: buildWorkflowPackageJson(name), }; } @@ -132,32 +111,14 @@ export const ${exportName} = sqliteTable("${table}", { `; } -export function buildSensePackageJson(name: string): string { - return `${JSON.stringify( - { - name: `nerve-sense-${name}`, - private: true, - type: "module", - scripts: { - build: - "esbuild src/index.ts --bundle --platform=node --format=esm --outdir=. --out-extension:.js=.js --packages=external", - }, - devDependencies: { - esbuild: "^0.27.0", - "drizzle-orm": "*", - }, - }, - null, - 2, - )}\n`; -} - export function buildSenseIndexTs(senseId: string): string { const exportName = senseIdToSchemaExportName(senseId); return `import type { LibSQLDatabase } from "drizzle-orm/libsql"; import { ${exportName} } from "./schema.js"; +export { ${exportName} as table } from "./schema.js"; + type SenseResult = { signal: { label: string; ts: number }; workflow: null; @@ -245,30 +206,39 @@ const createWorkflowCommand = defineCommand({ mkdirSync(workflowDir, { recursive: true }); const scaffold = buildWorkflowScaffold(args.name); - writeFile(join(workflowDir, "package.json"), scaffold.packageJson); writeFile(join(workflowDir, "index.ts"), scaffold.indexTs); writeFile(join(workflowDir, "roles", "main", "index.ts"), scaffold.roleMainIndexTs); writeFile(join(workflowDir, "roles", "main", "prompt.md"), scaffold.roleMainPromptMd); process.stdout.write("✅ Workflow scaffolded:\n"); - process.stdout.write(` ${join(workflowDir, "package.json")}\n`); process.stdout.write(` ${join(workflowDir, "index.ts")}\n`); process.stdout.write(` ${join(workflowDir, "roles", "main", "index.ts")}\n`); process.stdout.write(` ${join(workflowDir, "roles", "main", "prompt.md")}\n`); + + process.stdout.write("\nBuilding workspace (workflows + senses)…\n"); + try { + await spawnAsync("pnpm", ["run", "build"], nerveRoot); + process.stdout.write( + `✅ Build complete — ${join("dist", "workflows", args.name, "index.js")} ready.\n`, + ); + } catch { + process.stdout.write(`⚠️ Build failed. Run manually:\n cd ${nerveRoot} && pnpm run build\n`); + } + process.stdout.write("\n💡 Next steps:\n"); - process.stdout.write( - ` 1. In ${workflowDir}, run \`npm install\` then \`npm run build\` (bundles to dist/index.js).\n`, - ); - process.stdout.write(" 2. Add to nerve.yaml:\n"); + process.stdout.write(" 1. Add to nerve.yaml:\n"); process.stdout.write(" workflows:\n"); process.stdout.write(` ${args.name}:\n`); process.stdout.write(" concurrency: 1\n"); process.stdout.write(" overflow: drop\n"); process.stdout.write( - ` 3. Edit ${join(workflowDir, "roles", "main", "index.ts")} (and optional prompt.md).\n`, + ` 2. Edit ${join(workflowDir, "roles", "main", "index.ts")} (and optional prompt.md).\n`, ); process.stdout.write( - ` 4. Adjust moderator routing in ${join(workflowDir, "index.ts")} if you add roles.\n`, + ` 3. Adjust moderator routing in ${join(workflowDir, "index.ts")} if you add roles.\n`, + ); + process.stdout.write( + ` 4. After edits, run \`pnpm run build\` from the workspace root (${nerveRoot}); output is dist/workflows//index.js.\n`, ); process.stdout.write(" 5. Run `nerve start` to launch the daemon.\n"); }, @@ -309,26 +279,23 @@ const createSenseCommand = defineCommand({ mkdirSync(join(senseDir, "src"), { recursive: true }); mkdirSync(join(senseDir, "migrations"), { recursive: true }); - writeFile(join(senseDir, "package.json"), buildSensePackageJson(args.name)); writeFile(join(senseDir, "src", "index.ts"), buildSenseIndexTs(args.name)); writeFile(join(senseDir, "src", "schema.ts"), buildSenseSchemaTs(args.name)); writeFile(join(senseDir, "migrations", "0001_init.sql"), buildSenseMigrationSql(args.name)); process.stdout.write("✅ Sense scaffolded:\n"); - process.stdout.write(` ${join(senseDir, "package.json")}\n`); process.stdout.write(` ${join(senseDir, "src", "index.ts")}\n`); process.stdout.write(` ${join(senseDir, "src", "schema.ts")}\n`); process.stdout.write(` ${join(senseDir, "migrations", "0001_init.sql")}\n`); - process.stdout.write("\nInstalling sense dependencies and building…\n"); + process.stdout.write("\nBuilding workspace (senses + workflows)…\n"); try { - await spawnAsync("pnpm", ["install", "--no-cache", "--ignore-workspace"], senseDir); - await spawnAsync("pnpm", ["run", "build"], senseDir); - process.stdout.write("✅ Build complete — index.js ready.\n"); - } catch { + await spawnAsync("pnpm", ["run", "build"], nerveRoot); process.stdout.write( - `⚠️ Build failed. Run manually:\n cd ${senseDir} && pnpm install --no-cache --ignore-workspace && pnpm run build\n`, + `✅ Build complete — ${join("dist", "senses", args.name, "index.js")} ready.\n`, ); + } catch { + process.stdout.write(`⚠️ Build failed. Run manually:\n cd ${nerveRoot} && pnpm run build\n`); } process.stdout.write("\n💡 Next steps:\n"); @@ -341,7 +308,9 @@ const createSenseCommand = defineCommand({ process.stdout.write( ` 2. Edit ${join(senseDir, "src", "index.ts")} to implement ${args.name}.\n`, ); - process.stdout.write(` 3. Re-run \`pnpm run build\` in ${senseDir} after edits.\n`); + process.stdout.write( + ` 3. Re-run \`pnpm run build\` from the workspace root (${nerveRoot}) after edits.\n`, + ); process.stdout.write(" 4. Run `nerve start` to launch the daemon.\n"); }, }); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e77caf3..b6f54ff 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -17,11 +17,6 @@ senses: interval: 10s `; -const PNPM_WORKSPACE_YAML = `packages: - - 'workflows/*' - - 'senses/*' -`; - const BIOME_JSON = `{ "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json", "formatter": { @@ -54,17 +49,20 @@ const PACKAGE_JSON = `${JSON.stringify( private: true, type: "module", scripts: { - build: "pnpm -r build", + build: "node scripts/build.mjs", }, dependencies: { "@uncaged/nerve-core": "latest", "@uncaged/nerve-daemon": "latest", - "@uncaged/nerve-skills": "latest", "drizzle-orm": "latest", + zod: "^4.3.6", }, devDependencies: { "@biomejs/biome": "latest", + "@types/node": "^22.0.0", "drizzle-kit": "latest", + esbuild: "^0.27.0", + typescript: "^5.7.0", }, pnpm: { onlyBuiltDependencies: ["esbuild"], @@ -74,6 +72,54 @@ const PACKAGE_JSON = `${JSON.stringify( 2, )}\n`; +const BUILD_MJS = `import * as esbuild from "esbuild"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const dist = path.join(root, "dist"); + +const opts = { + bundle: true, + platform: "node", + format: "esm", + packages: "external", +}; + +function listDirs(dir) { + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir) + .filter((name) => !name.startsWith(".") && !name.startsWith("_")) + .map((name) => ({ name, full: path.join(dir, name) })) + .filter(({ full }) => fs.statSync(full).isDirectory()); +} + +async function main() { + // Clean dist/ + fs.rmSync(dist, { recursive: true, force: true }); + + for (const { name, full } of listDirs(path.join(root, "senses"))) { + const entry = path.join(full, "src", "index.ts"); + if (!fs.existsSync(entry)) continue; + const outfile = path.join(dist, "senses", name, "index.js"); + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + await esbuild.build({ ...opts, entryPoints: [entry], outfile }); + } + + for (const { name, full } of listDirs(path.join(root, "workflows"))) { + const entry = path.join(full, "index.ts"); + if (!fs.existsSync(entry)) continue; + const outfile = path.join(dist, "workflows", name, "index.js"); + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + await esbuild.build({ ...opts, entryPoints: [entry], outfile }); + } +} + +await main(); +`; + const GITIGNORE = `data/ logs/ nerve.pid @@ -83,31 +129,26 @@ knowledge.db const NERVE_SKILLS_MDC = `--- description: >- - Nerve skills package — where bundled Agent Skills live in this workspace and how to use them + Where Agent Skills live in this Nerve workspace and how to use them with Cursor alwaysApply: true --- -# Nerve skills (\`@uncaged/nerve-skills\`) +# Nerve Agent Skills -This workspace lists **@uncaged/nerve-skills** in \`package.json\`. It ships **Agent Skills** (one directory per skill, each with a \`SKILL.md\`) for Nerve development and related tasks. +**Agent Skills** are directories that contain a \`SKILL.md\` (with YAML frontmatter). Cursor loads them from **Project Skills** paths (for example \`.cursor/skills/\` or your global skills directory). -## After install +## Getting Nerve-oriented skills -Run your package manager in this workspace (e.g. \`pnpm install\`, \`npm install\` — whatever \`nerve init\` used). Then skills are on disk at: +There is no separate npm package for skills in the default workspace. To align with Nerve CLI, daemon, and monorepo conventions: -- \`node_modules/@uncaged/nerve-skills//SKILL.md\` - -Example (current catalog): - -- **nerve-dev** — Nerve architecture, CLI, sense/workflow patterns, \`nerve.yaml\`, and conventions: read \`node_modules/@uncaged/nerve-skills/nerve-dev/SKILL.md\`. +1. Copy or symlink skill folders from the **Nerve** repository (e.g. \`packages/skills/*/\`) into \`.cursor/skills/\`, **or** +2. Follow project documentation and \`CLAUDE.md\` / \`.cursor/rules/\` in this repo. ## How to use in an agent -1. For tasks that match a skill’s **description** (in the \`SKILL.md\` frontmatter), open that \`SKILL.md\` and follow its structure and checklists. -2. Prefer the skill as the **source of truth** for Nerve-specific conventions over generic assumptions. -3. If the catalog grows, new skills appear as new sibling directories under \`node_modules/@uncaged/nerve-skills/\`. - -Do not commit \`node_modules\`; the dependency is the supported way to get and update skills to match \`@uncaged/nerve-skills\` on npm. +1. When a task matches a skill’s **description** (in \`SKILL.md\` frontmatter), open that file and follow its steps. +2. Prefer those conventions for sense/workflow layout, \`nerve.yaml\`, and tooling over generic guesses. +3. Keep skills versioned with your dotfiles or project; update them when you upgrade Nerve. `; const execFileAsync = promisify(execFile); @@ -124,6 +165,8 @@ export const cpuUsage = sqliteTable("cpu_usage", { const CPU_INDEX_TS = `import { cpus } from "node:os"; +export { cpuUsage as table } from "./schema.js"; + type SenseResult = { signal: { model: string; loadPercent: number; ts: number }; workflow: null; @@ -154,24 +197,6 @@ export async function compute(): Promise { } `; -const CPU_SENSE_PACKAGE_JSON = `${JSON.stringify( - { - name: "nerve-sense-cpu-usage", - private: true, - type: "module", - scripts: { - build: - "esbuild src/index.ts --bundle --platform=node --format=esm --outdir=. --out-extension:.js=.js --packages=external", - }, - devDependencies: { - esbuild: "^0.27.0", - "drizzle-orm": "*", - }, - }, - null, - 2, -)}\n`; - const CPU_MIGRATION_SQL = `CREATE TABLE IF NOT EXISTS cpu_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, @@ -334,10 +359,9 @@ async function runInitWorkspace(force: boolean, skipInstall = false): Promise/index.js ← compiled compute + * dist/senses//index.js ← bundled compute (esbuild) * senses//migrations/ ← SQL migration files * data/senses/.db ← SQLite data file * nerve.yaml ← config @@ -19,7 +19,7 @@ import { readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { parseNerveConfig } from "@uncaged/nerve-core"; -import type { NerveConfig, WorkflowTrigger } from "@uncaged/nerve-core"; +import type { NerveConfig } from "@uncaged/nerve-core"; import type { WorkerToParentMessage } from "./ipc.js"; import { parseParentMessage } from "./ipc.js"; @@ -49,10 +49,6 @@ function sendError(sense: string, error: string): void { send({ type: "error", sense, error }); } -function sendWorkflowTrigger(sense: string, workflow: WorkflowTrigger): void { - send({ type: "sense-workflow-trigger", sense, workflow }); -} - // --------------------------------------------------------------------------- // Initialisation helpers // --------------------------------------------------------------------------- @@ -154,10 +150,8 @@ async function runCompute( } clearGracePeriodTimer(senseName); if (result.value != null) { - sendSignal(senseName, result.value.signal); - if (result.value.workflow !== null) { - sendWorkflowTrigger(senseName, result.value.workflow); - } + // Single IPC message: kernel uses routeSenseComputeOutput(payload) for signal + optional workflow. + sendSignal(senseName, result.value); } } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : String(e);