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
| Need | Package |
|---|---|
| always | packages/baseline |
| a store | store-sqlite (Node ≥ 22.13, node:sqlite) · store-postgres (pg or PGlite) · store-do (Durable Objects) |
| a host | host-node · host-fastify · host-cloudflare |
| an executor | executor-quickjs (recommended, WASM boundary) · executor-node (worker threads, dev only) |
| a model | model-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 → http → collections → scheduler → auth → rbac
→ orm (+ the rbac→orm guard) → meta / rpc when enabled → judge → model → agent-loop → admin-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.