noggin

Quickstart: JavaScript / Node

Embed noggin's engine — the Noggin interface, the verb dispatch, the file/memory providers, the YAML/JSON serializers — directly in your own Node program. Same engine the CLI, MCP server, VS Code extension, and desktop app run on; nothing host-specific.

Caveat: not on npm yet

The engine lives in this repo as @noggin/engine, a workspace package marked "private": true. It is not currently published to npm. To consume it today you have two options:

The TypeScript declarations in engine/noggin-api.d.mts give your editor IntelliSense once the workspace dep is wired up.

If you want this published as @noggin/engine on npm, please open an issue — the API itself is stable; only the publication path is deferred.

1. Add the workspace dep

In your project's package.json:

{
  "dependencies": {
    "@noggin/engine": "file:../noggin/engine"
  }
}

Then npm install.

2. Open a noggin

// Side-effect import: registers the `file://` provider.
import '@noggin/engine/providers/file';
import { openNoggin } from '@noggin/engine';

const noggin = await openNoggin('file:///path/to/.noggin.yaml', { watch: true });

openNoggin is async because the first load happens before the instance is returned. Subsequent reads come from an in-memory snapshot that the file watcher keeps fresh. The provider is selected by URI scheme — file://, memory://, localstorage://, https://. If you have a raw filesystem path (a CLI flag, a file-open dialog result), use the file provider's direct factory:

import { openFileNoggin } from '@noggin/engine/providers/file';
const noggin = await openFileNoggin('/path/to/.noggin.yaml');

3. Call verbs

The returned Noggin exposes a bound method for every verb: push, add, move, goto, done, pop, edit, show, note, delete. All return Promise. Per-instance calls are serialised through an in-process queue; concurrent processes are protected by an advisory file lock the provider manages.

const view = await noggin.push({ title: 'ship the redesign' });
await noggin.add({ title: 'write the spec' });
await noggin.add({ title: 'wire up tests' });
await noggin.note({ text: 'using node:test, not jest' });

console.log(noggin.active?.title);   // 'ship the redesign'
console.log(noggin.items.length);    // 3

view is a CurrentTreeView — the same shape returned by the CLI's --json output.

Free-function alternative

If you'd rather call verbs as free functions (the in-tree CLI and MCP server do, since they take a noggin in from above), import verbs instead:

import { verbs } from '@noggin/engine';

await verbs.push(noggin, { title: 'ship the redesign' });

Both forms run the same engine code; bound methods are sugar over the free functions. The bound form is preferable when you only have one noggin in scope; the free form is preferable when you accept a noggin as a parameter and want to keep verb dispatch obviously parametric (the engine's own tests use this form).

4. React to changes

noggin.onDidChange((changes) => render(noggin.items, changes));

Fires after every verb call in this process, and whenever the watcher detects an external write (the CLI in another terminal, the extension saving in another window, etc.). The changes payload is a ChangeEvent — an ItemChange[] describing what shifted between the previous and current documents (added, removed, moved, updated, activeChanged).

5. Pure document operations

If you need to manipulate a noggin without a provider — composing, running scenarios in tests, batch-transforming — use applyOps over a NogginDocument directly:

import { applyOps, SCHEMA_VERSION } from '@noggin/engine';
import { fromYaml, toYaml } from '@noggin/engine/serializers/yaml';

let doc = fromYaml(text);
// Atomic ops compose every mutation the verbs make. The verb
// modules ship the high-level combinators; for one-off batches
// you can hand-build ops yourself.
doc = applyOps(doc, [
  { type: 'add', item: { /* ... */ }, parentKey: null, position: 'end' },
]);
const out = toYaml(doc);

applyOps mutates the passed-in document in place AND returns it (convenience for chaining). No I/O, no events — just data → data.

6. Clean shutdown

await noggin.dispose();

Releases the file watcher, the in-process queue, and (when present) the advisory lock. After dispose the noggin is unusable.

What you've learned

Next