Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started: the baseline inside your project

You are building sm-erp (or any app). You want users to be able to say “add a discount field to invoices and a tool that finds overdue ones” and have it happen — safely, live, persistently. This is how.

1. Install

The baseline is private and distributed as git tags with committed dist/. Pick the umbrella plus the drivers for your stack (every package is a separate git+path dependency; they all pin the same tag):

TAG=v0.2.0; REPO=github:<owner>/sms-baseline
pnpm add "$REPO#$TAG&path:/packages/baseline" \
         "$REPO#$TAG&path:/packages/drivers/store/sqlite" \
         "$REPO#$TAG&path:/packages/drivers/executor/quickjs" \
         "$REPO#$TAG&path:/packages/drivers/host/node" \
         "$REPO#$TAG&path:/packages/drivers/model/claude"   # or model-gpt
NeedPackage
alwayspackages/baseline
a storestore-sqlite (Node ≥ 22.13, node:sqlite) · store-postgres (pg or PGlite) · store-do (Durable Objects)
a hosthost-node · host-fastify · host-cloudflare
an executorexecutor-quickjs (recommended, WASM boundary) · executor-node (worker threads, dev only)
a modelmodel-claude (your claude login) · model-gpt (your codex login) · model-claude-api / model-gpt-api / model-openrouter (API keys; Worker-safe) · model-gpt-oauth (your codex login tokens, Worker-safe, opt-in with unofficial: true; readCodexAuth() from @sms/model-gpt-oauth/node) · or implement ModelService from @sms/contracts yourself
object storage (optional)objects-r2 · objects-s3 — the baseline defines the system-only objects key (OBJECTS_KEY); a system plugin of yours provides it (examples/cloudflare-worker)

Requirements: Node ≥ 22.13 (or Workers with nodejs_compat), pnpm, ESM ("type": "module"). TypeScript types ship in dist/.

2. Compose a kernel

// src/app.ts — your composition root
import { createBaseline } from '@sms/baseline'
import { QuickJSExecutor } from '@sms/executor-quickjs'
import { createClaudeModel } from '@sms/model-claude'
import { openSqliteStore } from '@sms/store-sqlite'
import { invoicesPlugin } from './plugins/invoices.ts'

export async function createApp() {
  return createBaseline({
    name: 'sm-erp',
    store: await openSqliteStore(process.env.SMS_DB ?? 'erp.db'),
    executor: new QuickJSExecutor(),
    model: createClaudeModel({ model: process.env.SMS_MODEL }), // or createGptModel() from @sms/model-gpt
    plugins: [invoicesPlugin],                       // your modules (docs/writing-plugins.md)
    identity: 'You are the assistant inside sm-erp. You can extend the ERP itself.',
    adminToken: process.env.SMS_ADMIN_TOKEN,         // mutating /api routes: this token, or an admin session
    auth: { bootstrapAdmin: { login: 'root', password: process.env.SMS_ROOT_PASSWORD! } }, // users + rbac + orm guard; `false` = dev only
    // what needs a human; default: scope 'net' and the points in DEFAULT_SENSITIVE_POINTS
    // ('route', 'schedule', 'model-method', 'model-hook', 'automation') — your list replaces it
    judge: { sensitiveScopes: ['net'], sensitivePoints: ['route', 'schedule'] },
    // rpc: true, meta: true — /api/rpc and the view/action/menu points (docs/building-a-ui.md); off by default
  })
}

createBaseline installs, in order: the database key → httpcollectionsschedulerauthrbacorm (+ the rbac→orm guard) → meta / rpc when enabled → judgemodelagent-loopadmin-api → your plugins; then settle()s and restore()s agent plugins from the log. It throws if a system plugin is not active. You get { kernel, store, http, handle, tick, close }tick() runs due schedules; the host decides the cadence.

3. Put it on the wire

// src/main.ts
import { createNodeHost } from '@sms/host-node'
import { createApp } from './app.ts'
const app = await createApp()
const server = await createNodeHost({ port: 8787 }).listen(app.handle)
console.log(server.url)

Fastify (your existing app keeps its routes): createFastifyHost({ app: fastify, prefix: '/sms' }).listen(app.handle). Cloudflare: see tenancy.md and examples/cloudflare-worker.

4. Talk to it

curl -s localhost:8787/api/state | jq '.plugins[].id'
curl -s -H "authorization: Bearer $SMS_ADMIN_TOKEN" -H 'content-type: application/json' \
     -d '{"text":"Add a discount field to invoices and a tool that lists overdue ones"}' localhost:8787/api/chat
curl -s localhost:8787/api/state | jq '.pending'          # anything waiting for a human?
curl -s -X POST -H "authorization: Bearer $SMS_ADMIN_TOKEN" localhost:8787/api/approvals/agent.invoice-discount/approve

Your UI does exactly this — building-a-ui.md.

5. Multi-tenant

Replace createBaseline with createTenantPool (one kernel per tenant, each on its own store) — see tenancy.md. examples/erp-fastify-postgres is the full picture on Postgres.

Layout of a downstream repo

sm-erp/
  package.json            @sms/* git deps pinned to one tag
  src/app.ts              composition root (createBaseline / createTenantPool)
  src/main.ts             host driver → listen
  src/plugins/*.ts        your modules (SystemPlugin objects)
  ui/                     your frontend, talks to /api/*
  test/                   boot createApp with createScriptedModel([...]) and assert

Testing tip: createTestBaseline({ plugins, turns }) from @sms/baseline/testkit (or createScriptedModel(turns) from @sms/baseline) replays canned model turns so the whole prompt → judge → mount → use → undo loop runs offline (see packages/baseline/test/baseline.test.ts, examples/reference-node-sqlite/src/demo.ts and writing-plugins.md → Testing).

Upgrading

Bump the tag in every @sms/* specifier (they must match) and pnpm install. Release notes live on the release tags; the event log format is append-only and versions fold forward.