npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@happyvertical/smrt-core

v0.57.42

Published

Core SMRT framework for business objects, persistence, and generated REST, CLI, MCP, and AI interfaces

Readme

@happyvertical/smrt-core

ORM, code generation, AI integration, and the DispatchBus. Everything else in the s-m-r-t framework builds on this.

Use smrt-core when you are defining domain objects, querying collections, generating interfaces, or extending the framework runtime. If you want a full application scaffold with users and tenancy already wired, start with smrt-template-sveltekit.

Installation

pnpm add @happyvertical/smrt-core
pnpm add -D @happyvertical/smrt-cli @happyvertical/smrt-vitest

Requires Node.js 26.0.0 or newer. s-m-r-t projects use the Vite plugin to generate manifests and the CLI to apply schema migrations before runtime.

Usage

Define a class with @smrt()

import {
  foreignKey,
  smrt,
  SmrtCollection,
  SmrtObject,
} from '@happyvertical/smrt-core';

@smrt()
class Category extends SmrtObject {
  name = '';
}

@smrt({ api: true, cli: true, mcp: true })
class Product extends SmrtObject {
  name = '';
  price: number = 0.0;          // DECIMAL (has decimal point)
  quantity: number = 0;          // INTEGER (no decimal point)
  isPublished = false;

  @foreignKey(Category)
  categoryId = '';
}

class ProductCollection extends SmrtCollection<Product> {
  static readonly _itemClass = Product;
}

Basic CRUD

const products = await ProductCollection.create({ db: 'products.db' });

// Create
const product = await products.create({ name: 'Widget', price: 9.99 });

// Query
const results = await products.list({
  where: { isPublished: true, price: { op: '>', value: 5 } },
  orderBy: 'price DESC',
  limit: 20,
});

Run an action exactly once per submission (runOnce)

A double-tapped submit, a retried request, or two concurrent requests should never write the same record twice. runOnce() (#3080) is the shared idempotency seam: an insert-only claim, written in the SAME transaction as the work it guards, so a retry either runs the work once or replays the first attempt's stored result.

import { runOnce, RunOnceClaimError } from '@happyvertical/smrt-core';

const purchaseOrderId = await runOnce(
  {
    db,
    tenantId: session.tenantId,
    actor: session.userId,
    token: formData.get('submissionToken') as string, // minted once per rendered form
    content: { supplierId, lines }, // only its digest is persisted, never the raw value
  },
  async (tx) => {
    const po = await createPurchaseOrder(tx, { supplierId, lines });
    return po.id; // must be JSON-serializable — this is what a replay returns
  },
);
  • Key derivation is (tenantId, actor, token, contentDigest) — never the token alone. A token-only key collapses two genuinely different submissions that happen to reuse one form's token. contentDigest is a sha256 of the sorted-key-JSON encoding of content, so key order never changes it.
  • The claim lives in the hand-migrated _smrt_run_once_claims system table (never an application table you migrate yourself) and only ever stores digests plus the JSON-serialized work() result — never the raw token or raw content.
  • A claim whose work is still in flight, or whose outcome cannot be resolved, rejects with RunOnceClaimError (code: 'RUN_ONCE_IN_FLIGHT' or 'RUN_ONCE_OUTCOME_UNKNOWN') instead of silently re-running or guessing — mirroring the refused-vs-unknown-outcome distinction from #2990. Retry with the SAME token and content; runOnce() never retries on its own.
  • If work() throws, the transaction that inserted the claim rolls it back too — a failed attempt never strands a claim, and a retry with the same key runs again.

See agents/run-once.md for the full contract. The browser half — minting and keeping the submission key per tab and form, refusing a concurrent submit, keeping typed values across a failed submit, and opt-in restore after a reload — is createFormRetry() in @happyvertical/smrt-ui/form-retry; the form retry guide shows both halves together.

Bounded multi-collection reads

When one request needs several independent collections, use a keyed read plan instead of an unbounded Promise.all. Every entry still uses the normal collection list() path, but only the requested number of operations run at once:

import {
  executeCollectionReadPlan,
  type SmrtCollectionReadPlanEntry,
} from '@happyvertical/smrt-core';

const categories: SmrtCollectionReadPlanEntry<Category> = {
  className: 'Category',
  options: { orderBy: 'name ASC' },
};
const products: SmrtCollectionReadPlanEntry<Product> = {
  className: 'Product',
  options: { where: { isPublished: true }, orderBy: 'name ASC' },
};
const records = await executeCollectionReadPlan(
  {
    categories,
    products,
  },
  {
    collectionOptions: { db: 'file:products.db' },
    maxConcurrency: 2,
  },
);

maxConcurrency is required and must be a positive integer. If an entry fails, the executor starts no further queued entries, waits for already-running entries to settle, and rethrows the first error. Read plans do not compose SQL, cache whole-plan results, or change database pool defaults.

Generate metadata and migrate

The Vite integrations support Vite 5–8. Install Vite in the consuming build project. s-m-r-t's plugin declarations use a structural contract independent of Vite's bundler types; build-time Vite loading resolves from the project root. Core keeps Vite development-only, so server consumers need no build tool and different Vite peer contexts cannot duplicate the s-m-r-t runtime registry. node scripts/verify-packed-vite-peer.mjs verifies the packed consumer contract.

Configure Vite 8's Oxc decorator transform and point smrtPlugin() at the directory containing your objects:

// vite.config.ts
import { smrtPlugin } from '@happyvertical/smrt-core/vite-plugin';
import { defineConfig } from 'vite';

export default defineConfig({
  oxc: {
    decorator: { legacy: true, emitDecoratorMetadata: true },
  },
  plugins: [
    smrtPlugin({
      include: ['src/objects/**/*.ts'],
      exclude: ['**/*.test.ts'],
    }),
  ],
});

Vite 7 and large browser workers

Apps using Vite 7 with Planner/browser AI workers can add smrtAssetUrlCompatibility() from @happyvertical/smrt-core/vite-plugin to their root plugins array. It also composes the existing worker plugin factory, including async plugin options, so no separate worker configuration is required. It replaces only Vite 7's exact known backtracking asset URL prefilter with a literal substring filter. A bounded iterative walk of the bundler's parsed AST skips comment/string-only import.meta.url matches, which can otherwise overflow Vite's tokenizer on large escaped bundles. Every real import.meta expression, parse failure, or exhausted walk budget delegates to Vite's original handler with its original context and arguments. Vite's asset parsing, ID exclusions, resolution and rewriting remain unchanged. Vite 8 and unknown filter shapes are left untouched. This compatibility helper can be removed after moving the consuming app to a Vite release with a safe prefilter. The version is loaded lazily during configuration; the resolved hook changes filters synchronously before subsequent build hooks can observe them. Vite 7's unchanged tokenizer may also need Node's --regexp-interpret-all for very large escaped literals; use that flag on the Vite build process where required.

One-call preset for SvelteKit apps

smrt() applies the Oxc decorator config and composes smrtConsumer() and smrtPlugin() with the SvelteKit conventions (src/lib/objects, src/lib/types/smrt-generated, src/routes/api, src/lib/server/smrt.ts):

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { smrt } from '@happyvertical/smrt-core/vite';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [sveltekit(), smrt()] });
// smrt.config.ts: the declared, deterministic list of consumed packages.
// Tooling and UI packages are never inferred from package.json.
export default { consumer: { packages: ['@happyvertical/smrt-users'] } };

smrt({ packages }) overrides the config list; [] declares that none are consumed. A missing or malformed list fails the build. objectsDir, typesDir, routesDir, configPath, configFileName, include, exclude and projectRoot override the conventions; decorators: false omits the decorator config (values you set under oxc.decorator are never overwritten). smrt() returns a promise, which Vite accepts in plugins. The plugin also writes the virtual-modules.d.ts ambient declarations (into typesDir) whenever it runs a build or whenever Vite resolves the config, including svelte-kit sync (SvelteKit 2.69.3+ resolves the Vite config there), not only under a dev server. svelte-kit sync && tsc therefore passes on a clean clone with no prior build and no copied shim; the file is rewritten only when its contents change. The two plugins remain public for custom setups.

pnpm vite build
pnpm smrt db:migrate

Runtime verifies application tables but does not create them. Rebuild the manifest and rerun the migration after changing persisted object fields.

Reuse a verified generation snapshot in CI

Independent Vite invocations can reuse one generation snapshot prepared by an earlier job without rescanning or rewriting it. First run one normal generation invocation with smrtPlugin() and smrtConsumer() enabled. After both plugins finish, .smrt/manifest.json contains the project and dependency views. Wrap that aggregate once:

import { readFileSync, writeFileSync } from 'node:fs';
import {
  serializeSmrtGenerationSnapshot,
  sha256SmrtGenerationSnapshot,
} from '@happyvertical/smrt-core/vite-plugin';

const provenance = process.env.GITHUB_SHA!;
const sourceRoot = process.env.GITHUB_WORKSPACE ?? process.cwd();
const manifest = JSON.parse(readFileSync('.smrt/manifest.json', 'utf8'));
const bytes = serializeSmrtGenerationSnapshot(manifest, provenance, {
  sourceRoot,
});
writeFileSync('.ci/smrt-generation-snapshot.json', bytes);
console.log(sha256SmrtGenerationSnapshot(bytes));

Transport the exact bytes and digest together, then configure the consumers with caller-trusted provenance (normally the checked-out commit or tree). Both plugins use the same snapshot; each selects its own manifest view. sourceRoot is the current checkout root, so normalized source paths remain portable across workers:

import { smrtConsumer } from '@happyvertical/smrt-core/consumer-plugin';
import { smrtPlugin } from '@happyvertical/smrt-core/vite-plugin';

const provenance = process.env.GITHUB_SHA!;

const generationSnapshot = {
  path: '.ci/smrt-generation-snapshot.json',
  sha256: process.env.SMRT_GENERATION_SNAPSHOT_SHA256!,
  provenance,
  sourceRoot: process.env.GITHUB_WORKSPACE ?? process.cwd(),
};

smrtPlugin({ generationSnapshot });
smrtConsumer({ generationSnapshot });

Both plugins fail closed when the snapshot is missing, malformed, has different bytes, declares different provenance, cannot resolve its portable paths, or the current source-file digests differ from the prepared inputs. Reuse mode still generates routes, types, registration, and virtual modules, but it disables source/package scans, watch rescans, and manifest writes. Omit generationSnapshot for normal local development.

Where consumer manifests are resolved

smrtConsumer() resolves each consumed package's manifest through that package's own package.json#exports map first — the ./manifest.json entry (or its ./manifest alias), which every s-m-r-t model package is required to publish — and only then falls back to the conventional dist/manifest/static-manifest.js, dist/manifest.json, and manifest.json paths. A package whose build emits elsewhere (for example a triple-purpose package that maps "./manifest.json": "./dist/lib/manifest.json") therefore loads normally.

Every declared target is a candidate, not a single choice: an export array is a fallback list and each recognized condition contributes its own target, so a missing first entry does not hide a valid later one. Candidates are probed in order and the first one that loads wins, so a candidate that exists but cannot be read, parsed, or imported is reported and skipped rather than ending the search — the conventional paths stay reachable behind a stale or malformed export target. Targets that resolve outside the package directory are dropped, both lexically and after following symlinks; the package root is itself resolved through symlinks first, so pnpm and workspace installs are unaffected.

A package that yields no usable manifest is never skipped silently. An entry in an explicit packages list is an assertion, so it fails the build by name; a package found by dependency-name discovery only warns by name and contributes no objects.

Consumer web collection definitions

smrtConsumer() also resolves @smrt/web for browser collection consumers:

import { collectionDefinitions, getCollectionDefinition } from '@smrt/web';

The module projects the consumer's aggregated dependency manifest through the same web-definition generator as smrtPlugin(). Each definition retains its provider-qualified objectRef (for example, @acme/widgets:Widget) and its manifest-derived fields, actions, relationships, WebMCP descriptors, and manifestHash. Consumer snapshots select the dependency view, so local project objects are not included. During declaration generation the matching smrt-web.d.ts module is written alongside the other consumer virtual modules.

For SvelteKit projects, default consumer declaration generation runs during the Vite configuration lifecycle as well as the normal build lifecycle. With SvelteKit 2.69.3 or later, svelte-kit sync && svelte-check loads that Vite configuration, so a clean checkout receives the physical @smrt/manifest, @smrt/client, and @smrt/web declarations before type checking. Set generateTypes: false only when another generation step owns those files.

Generated SvelteKit routes

Enable SvelteKit route generation with svelteKit: { enabled: true }. Its default output directory is src/routes/api; set svelteKit.routesDir when your application uses a different route root.

Generated routes do not need a hand-written getCollection. Each route embeds a prelude that imports the generated smrt-register module and the app's config module (svelteKit.configPath/configFileName, $lib/server/smrt by default), then resolves collections through that module's exported runtime (createSmrtSvelteKitRuntime() from @happyvertical/smrt-app-runtime/sveltekit, whose getCollection() is request-scoped). A getCollection/getSmrtConfig export there still takes precedence for one release, with a deprecation warning. The plugin also prepends the registration import to that config module, and generation refuses a configFileName that names the registration module (smrt-register.*).

A consumer can host selected dependency models with the same generator. This is an explicit HTTP boundary that is separate from the broader packages registration inventory. Name exact provider-qualified object references; a simple class name, an unknown object, or an empty list fails the build before any generated route is replaced:

smrtConsumer({
  packages: runtimeProviderPackages,
  svelteKit: {
    objects: ['@acme/widgets:Widget'],
    routesDir: 'src/routes/api',
  },
});

projectRoot selects one artifact root for consumer route generation, registration, manifests, and types; it defaults to the current working directory. It remains the consumer artifact root when a Vite config declares a different root; pass projectRoot explicitly when consumer artifacts should share that Vite root or belong elsewhere.

Only the listed objects are passed to route generation, so another API model in the same provider remains absent until it is named. api: false and an empty api.include still suppress handlers, and generated auth, writable-field, and tenant safeguards are unchanged. svelteKit: true remains a legacy consumer integration flag and does not host dependency CRUD routes. Consumer-hosted _changes, _events, and _resources routes are disabled unless their existing option is explicitly enabled; sync/apply is generated when a selected model exposes a mutating API action.

When smrtPlugin() and smrtConsumer() generate routes in one SvelteKit application, they may use the same canonical routesDir and share one generated surface, or use disjoint directories. Nested directories are rejected before files are changed because a parent generated-root cleanup would otherwise own and remove the child surface. Canonical ownership resolves symlink aliases through the nearest existing directory, so paths that name the same generated directory share one surface. When separate artifact roots resolve to that same route directory, their resolved object and config outputs must also agree; incompatible settings fail before generated handlers or managed ownership records change.

The consumer keeps a private managed-root inventory under .smrt after a successful explicit hosting plan is validated. Changing its routesDir, setting svelteKit: false, returning to legacy svelteKit: true, or omitting the option reconciles only consumer-managed generated handlers before SvelteKit inventories routes. Handwritten handlers and current producer-owned routes are preserved.

The plugin records only the concrete +server.ts files it generated in a bounded .gitignore block. Handwritten handlers below routesDir remain visible to Git, including routes that live beside generated resource handlers. Generation refreshes that block, so stale generated paths stop being ignored when the generator no longer owns them.

Migration note: generation replaces the legacy block below with the bounded exact-path block:

# SMRT auto-generated routes (from Vite plugin)
src/routes/api/**/+server.ts
!src/routes/api/v1/**/+server.ts

Migration takes the recognized # SMRT auto-generated routes (from Vite plugin) header — matched as a whole line — plus the contiguous run of recursive +server.ts wildcards directly beneath it, negations included. The run is matched by shape rather than against your current routesDir, so a project that moved routesDir after adopting the plugin still migrates, and migration still runs once the bounded block exists — leaving a stale negation in place would silently re-include whatever the bounded block stops listing.

The first line that is not a recursive +server.ts wildcard ends the run. A broad rule you added or moved yourself is left unchanged, including an identical pattern elsewhere in the file; remove or narrow it manually if it hides a handwritten route.

Generated routes are build output and are not meant to be committed: they are regenerated on every dev-server start and are not formatted to your Biome or Prettier configuration, so tracking them makes a lint job fail on output no one edits. Track the handwritten handlers beside them — the bounded block lists only generator-owned paths, so they stay visible to Git.

AI operations

With an AI provider configured, every object can use the inherited is() and do() operations:

const isExpensive = await product.is('costs more than the average product');
const description = await product.do('Write a short marketing description');

Opt-In Read Cache

SSR apps that re-query read-heavy / write-rare collections on every request can memoize list() and get() results with an opt-in TTL. Defaults are off — nothing is cached unless a call or model opts in.

// Per call
const published = await resumes.list({
  where: { status: 'published' },
  cache: { ttl: 60_000 },
});

// Per model — list()/get() on this collection cache by default
@smrt({ cache: { ttl: 60_000 } })
class Resume extends SmrtObject {}

// Force a fresh read on hot paths that must read through
const latest = await resumes.list({ cache: false });

Because s-m-r-t owns every mutation path (save(), delete(), collection.create(), getOrUpsert(), junction attach/detach), any write automatically invalidates the affected table's cached entries in-process — including STI siblings sharing the table. Cached values are raw rows: hydration and read interceptors (tenancy, audit) still run on every call. Concurrent identical misses coalesce only on the same concrete database interface. Distinct connection or transaction interfaces never share pending reads or completed rows, even with the same URL; invalidation still clears all interfaces for that URL and table.

Caches are per-process. For multi-replica deployments, add crossProcess: true to broadcast invalidations over the database adapter's notification capability (e.g. Postgres LISTEN/NOTIFY) so peers drop their entries immediately instead of waiting out the TTL:

@smrt({ cache: { ttl: 60_000, crossProcess: true } })
class Resume extends SmrtObject {}

Model-level config is the reliable cross-process opt-in: every process writing the model knows to broadcast, and STI children writing the shared table broadcast even if they opted out of caching their own reads. As a per-call option (list({ cache: { ttl, crossProcess: true } })), writes broadcast only from processes that have already performed such a read — typical for homogeneous replicas running the same routes, but a write-only process never learns about a call-site opt-in.

Writes that bypass s-m-r-t (raw SQL, external processes without crossProcess) are only bounded by the TTL — pick one that matches how stale the data may be.

Bundled Runtimes And External Manifests

Long-lived bundled runtimes such as SvelteKit servers, background workers, and CLI entrypoints should ship two things together:

  1. The generated smrt-register runtime entrypoint for local class registration.
  2. The manifest.json exports for any installed external @happyvertical/smrt-* packages the runtime needs to hydrate at query time.

For an SSR build, smrtConsumer() leaves its consumed providers and their transitive server-side @happyvertical/smrt-* runtime dependencies external. Node therefore resolves native-backed provider dependencies from their installed pnpm package context, while core and tenancy retain one server-side registry and async context. Mixed packages are split by export: server entries stay external, while Svelte and CSS entries remain in Vite's compiler graph. The generated consumer registration entrypoint loads transitive-only object modules through the configured provider's ESM resolution context. This avoids unresolvable app-root imports while preserving the exact model and collection constructors used by the provider. The Vite preset retains its narrower app-runtime split so SvelteKit control-flow classes keep app identity. This applies only to SSR; browser and Svelte component builds retain their normal bundled resolution.

ObjectRegistry.ensureManifestLoaded() can now auto-load installed external classes by either simple name ('EventType') or qualified name ('@happyvertical/smrt-events:EventType'). SmrtCollection.list() and SmrtCollection.get() use that path when they encounter STI discriminators from external packages, so bundled runtimes do not need to import every child class eagerly just to hydrate rows correctly.

If you want to avoid on-demand manifest discovery in a long-lived process, call ObjectRegistry.loadAllManifests() during startup after your local registration file has run.

API

Entry points

| Import | Purpose | | --- | --- | | @happyvertical/smrt-core | Objects, collections, decorators, registry, configuration | | @happyvertical/smrt-core/vite-plugin | Manifest, route, client, and knowledge generation | | @happyvertical/smrt-core/vite | One-call preset: decorators, consumer, and producer plugins | | @happyvertical/smrt-core/consumer-plugin | Consume manifests from installed s-m-r-t packages | | @happyvertical/smrt-core/generators | REST, OpenAPI, CLI, and MCP generator APIs | | @happyvertical/smrt-core/manifest | Runtime manifest loading and inspection | | @happyvertical/smrt-core/testing | Database and registry test helpers |

Core Classes

| Export | Description | |--------|------------| | SmrtClass | Base class with DB, AI, and filesystem access | | SmrtObject | Persistent object — save, delete, is(), do() | | SmrtCollection | CRUD collection — list, get, create, upsert | | ObjectRegistry | Global singleton for class/field metadata | | smrt (decorator) | Registers a class for code generation and AI |

Field Decorators

| Export | Description | |--------|------------| | field | General-purpose field decorator | | foreignKey | Foreign key relationship | | oneToMany | One-to-many relationship | | manyToMany | Many-to-many relationship | | meta | STI child field stored in _meta_data JSONB |

Dispatch (Inter-Agent Messaging)

| Export | Description | |--------|------------| | DispatchBus | Persistent message bus with wildcard subscriptions | | createDispatchBus | Factory function for DispatchBus; retries a transient connection failure at startup (startupRetry, default 5 attempts from 500 ms, false to disable) | | Dispatch | Dispatch record model | | DispatchSubscription | Persistent subscription model |

Code Generators

| Export | Description | |--------|------------| | APIGenerator | Generates OpenAPI-compliant REST endpoints | | MCPGenerator | Generates Model Context Protocol tool servers | | createRestServer | Creates an Express REST server from config | | generateOpenAPISpec | Generates OpenAPI/Swagger spec |

Embeddings (Semantic Search)

| Export | Description | |--------|------------| | EmbeddingProvider | Generates embeddings via AI provider | | EmbeddingStorage | Persists/queries embedding vectors | | CosineSimilarity | Ranks results by vector similarity | | ContentHasher | Content hashing for change detection |

Context Memory

Every SmrtObject/SmrtCollection can persist learned knowledge via remember() / recall() / recallAll() / forget() (system table _smrt_contexts) — confidence-scored and versioned, with opt-in hierarchical scope fallback (recall({ includeAncestors: true })) and a stored expiresAt the caller filters on (recall() does not auto-drop expired entries). Pairs with semantic search (above) for retrieval; contributor-level behavior and invariants live in AGENTS.md.

Signals (Observability)

| Export | Description | |--------|------------| | SignalBus | Universal method-tracking event bus | | SignalSanitizer | Sanitizes sensitive data in signals | | MetricsAdapter | Prometheus-compatible metrics adapter | | PubSubAdapter | Broadcast signals to subscribers |

Manifest (Build-Time Metadata)

| Export | Description | |--------|------------| | ManifestManager | Reads, writes, and generates manifests | | ManifestBuilder | Orchestrates scanning to manifest | | ManifestGenerator | Converts scan results to manifest format | | getManifest | Async getter for static manifest data |

Runtime

| Export | Description | |--------|------------| | createMCPServer / SmrtMCPServer | MCP server runtime | | createSmrtServer | REST server runtime | | createSmrtClient | API client runtime |

Schema & Migrations

| Export | Description | |--------|------------| | SchemaComparer | Compares current vs. desired schema | | generateSchemaDiff | Produces a diff between two schemas | | getSQLFromDiff | Converts schema diff to SQL statements |

Interceptors

| Export | Description | |--------|------------| | GlobalInterceptors | Plugin hooks for beforeList/Get/Save/Delete | | createInterceptorContext | Creates context for interceptor execution |

Errors

| Export | Description | |--------|------------| | SmrtError | Abstract base error class | | DatabaseError | Database operation failures | | AIError | AI provider failures | | ValidationError | Field/object validation failures | | RuntimeError | General runtime failures | | RunOnceClaimError | runOnce() claim in flight or unresolvable (RUN_ONCE_IN_FLIGHT / RUN_ONCE_OUTCOME_UNKNOWN) | | ErrorUtils | Retry policy (withRetry, isRetryable) plus sanitization helpers |

Database error classification

Driver errors reach the model layer wrapped by @happyvertical/sql, which stringifies the driver text into context.originalError — so the constraint wording never appears on error.message. Classify through the cause chain instead of matching messages.

| Export | Description | |--------|------------| | classifyDatabaseError | Walks the cause chain and returns the kind plus deterministic / retryable | | classifyDialectMessage | Matches a single raw dialect message (the DuckDB fallback) | | isUniqueViolationError | Unique or primary-key violation anywhere in the chain | | isNotNullViolationError | NOT NULL violation anywhere in the chain | | isAbortedTransactionError | Statement issued inside an aborted PostgreSQL transaction (25P02) | | isDeterministicDatabaseError | A retry cannot change the outcome | | isTransientDatabaseError | Contention or availability; a retry may succeed | | isTransientConnectionError | The connection itself dropped, was refused, or the server could not accept it yet (class 08, 57P01–57P03, 53300, ECONNREFUSED, ECONNRESET, ETIMEDOUT, EAI_AGAIN); an unresolvable or unreachable host is not retried | | DatabaseErrorKind | Union of classification kinds | | DatabaseErrorClassification | Structured result of classifyDatabaseError |

Tools (AI Function Calling)

| Export | Description | |--------|------------| | generateToolManifest | Generates AI tool definitions from methods | | executeToolCall / executeToolCalls | Executes AI tool calls |

Utilities

| Export | Description | |--------|------------| | config | Global s-m-r-t configuration function | | resolveDatabase | Resolves DB config to a DatabaseInterface | | smrtPlugin | Vite plugin for auto-service generation | | generateSvelteKitRoutes | SvelteKit route generator | | resetVerifiedTables | Resets table verification cache (testing) | | getTestDatabase | Creates isolated test databases | | parse / stringify / clone | JSON utilities with optional SIMD | | createQualifiedName / parseQualifiedName | STI qualified name helpers |

Code Generation

The @smrt() decorator controls what gets generated. Set api, cli, or mcp to true or { include: [...] }:

@smrt({
  api: { include: ['list', 'get', 'create'] },
  mcp: true,
  cli: true,
})
class Product extends SmrtObject { /* ... */ }

Generators produce OpenAPI REST endpoints, Commander CLI commands, and MCP server tools respectively. The Vite plugin (smrtPlugin) generates virtual modules at dev time for routes, clients, and manifests.

Durable MCP tasks

Long-running item actions may opt into the experimental io.modelcontextprotocol/tasks extension. Tasks are disabled by default; list the action names explicitly, and — if the class restricts what the job runner may dispatch — include the action in that allowlist:

import { backgroundEligible } from '@happyvertical/smrt-jobs';

@smrt({
  mcp: { include: ['generateReport'], tasks: ['generateReport'] },
})
class Report extends SmrtObject {
  @backgroundEligible()
  async generateReport(): Promise<ReportResult> { /* ... */ }
}

@backgroundEligible() is owned and enforced by @happyvertical/smrt-jobs, not by this package. It is restrictive: a class with no marked methods lets TaskRunner dispatch any of its methods, and the first marked method turns the set into an exhaustive allowlist that excludes every other method on the class. Use it to narrow the reachable surface, and mark every method you dispatch.

The generated MCP server advertises the extension only when at least one task action is enabled. A task-aware client can request the action as a durable job, then use tasks/get, tasks/update, and tasks/cancel to observe or control it. Generated stdio servers run an mcp-tasks worker automatically.

Dependencies

  • @happyvertical/ai -- AI client (is/do operations, embeddings)
  • @happyvertical/sql -- Database interface (SQLite, PostgreSQL)
  • @happyvertical/files -- Filesystem adapter
  • @happyvertical/logger -- Structured logging
  • @happyvertical/smrt-types -- Shared type definitions

Contributor guide

See AGENTS.md for package architecture, invariants, validation, and contributor guidance.

Consumer runtime restrictions

After importing/registering package models, close their generated surface without subclassing or creating another registry key:

import { ObjectRegistry } from '@happyvertical/smrt-core';
import '@happyvertical/smrt-timesheets';
import '@happyvertical/smrt-expenses';

for (const name of [
  '@happyvertical/smrt-timesheets:ServiceTimeEntry',
  '@happyvertical/smrt-expenses:Expense',
]) {
  ObjectRegistry.registerOverride(name, {
    api: false,
    mcp: false,
    cli: false,
    tenancy: { mode: 'required' },
  });
}

Call this at startup after registration (runtime decorators or manifest stubs) and before generating/constructing transports. It closes CRUD and custom actions; { include: [] } is equivalent to false. Omitted keys preserve existing policy. Only complete closures and optional-to-required tenancy are supported: reopening, partial allowlists, unknown options, and unqualified or unregistered class names throw before any policy is installed. Existing tenancy is required; this API does not introduce a tenant column or change the database schema. Enable the tenancy interceptor with enableTenancy() as usual. Required mode also takes precedence over an optional direct registerTenantScopedClass selector; existing explicit system/super-admin bypass semantics remain applicable.

Restrictions are process-local, accumulate monotonically, survive decorator and manifest re-registration/HMR, and reset with ObjectRegistry.clear(). getRuntimeOverride(qualifiedName) returns the immutable installed policy. Already emitted static route files must be regenerated with closed build-time configuration; a runtime override does not rewrite deployed route modules.

Same-named subclasses that share their parent's table replace that parent, even across explicitly named packages and across more than two levels. Runtime registration uses constructor ancestry; an isolated child manifest supplies its own fields, methods, schema, and policy while preserving inherited runtime restrictions. Manifests should qualify cross-package extends names. The deepest subtype wins, including parent replay and late intermediate manifests. Unrelated classes and sibling subtypes do not choose a winner by registration order: sharing their table still raises a collision.

Consumer integration fixtures

Consumer integration tests create disposable applications in src/consumer-plugin/__test-consumer-*. These applications have their own compiler configuration and aliases. Core's production, test typecheck, and declaration builds exclude these directories, including leftovers from an interrupted test run; the authored consumer integration tests remain included.

External agent measurements

ExternalAgentUsageRecorder persists validated provider counters on the existing AI usage store, with trusted tenant/project/task/session ownership bindings, immutable replay-safe sequences and nullable unknown counters. summarizeExternalAgentUsage returns per-session projections separately from hosted AI billing. See the contract and validation design.

The initial contract supports one task, provider, model, and counter semantic per real native harness session. Rebinding an existing session fails closed. A new task requires a real fresh session or future explicit attribution-span support; never invent session IDs or reset a cumulative baseline to count prior work twice. Only adapter-proven exclusive structured measurements are supported. Call ensureSystemTables(db) before entering a caller-owned transaction.