The Library

Storyteller writes. The Library keeps. Ask the Librarian. A story is the unit of retrieval: complete, ordered, small enough for a context window. Keep them, and the question why did last night's sync fail? has somewhere to look.

Keep them#

A StoryStore is where stories go and how they come back: append, get, query, children, prune. Two ship with the library and cost nothing: memoryStore(), a bounded map that runs anywhere, and fileStore(path), one JSON-lines file for Node. storeAudience is the one-line bridge from delivery.

keep.ts
import { storeAudience } from "@lovelaces-io/storyteller";
import { fileStore } from "@lovelaces-io/storyteller/store/file";

const kept = fileStore("./stories.jsonl");   // or memoryStore() in a browser, a test, one run
story.audience.add(storeAudience(kept));

Every level is kept by default. A store that only keeps failures can answer what broke but not what happened, and prune() is there for the rest. A rejecting store reports through onAudienceError, like any other audience.

Ask in words#

The same words have to read well aloud and be easy for a model to write. Every clause returns a new question; awaiting one runs it.

ask.ts
import { stories } from "@lovelaces-io/storyteller";

await stories(kept).failing().since("1h");
await stories(kept).about("checkout").from("payment-service").level("oops").since("24h");
await stories(kept).slowerThan("5s").since("7d").oldest().limit(10);
await stories(kept).under(storyId).count();          // its chapters

await kept.prune(new Date(Date.now() - 30 * 86_400_000));   // forget what is older than a month

Clauses: about, from, level, atLeast, failing, succeeding, slowerThan, since, until, under, newest, oldest, limit, skip. Terminals: all(), first(), count(). Durations as people write them: "30s", "5m", "24h", "7d", "2w". A question compiles to a structured StoryQuery, so every store answers the same one and no adapter's syntax ever reaches you.

Ask an agent#

The Librarian is a read-only MCP server over any store. Point it at the file your app writes, and an agent can answer questions from the stories the code told, with no other context.

.mcp.json
// .mcp.json — Claude Code, Cursor, or any MCP client
{
  "mcpServers": {
    "storyteller": {
      "command": "npx",
      "args": ["-y", "@lovelaces-io/storyteller-mcp", "./stories.jsonl"]
    }
  }
}
claude
> why did last night's sync fail?

search_stories { about: "sync", failing: true, since: "24h" }
get_story      { storyId: "7b1e5c2a-…" }

The 02:00 sync (Nightly sync failed, sync-worker) fetched 1,200 rows,
then the upsert failed with deadlock detected on the third batch.
The two runs before it succeeded in under 4s; this one took 31s.

Four tools, every one read-only: search_stories returns summaries sized for a context window; get_story returns one story complete with its chapters; summarize_period gives the shape of a day or a week; find_related pulls on a thread. Results are bounded and say when they were cut. Nothing can write or delete: an agent that can delete your logs is a liability, and nothing about asking a question needs it.

What stays out#

Persisted is the moment a leaked secret stops being a line that scrolled past. Redaction runs at capture and again at the storage boundary: values under secret-named keys, and recognisable secret formats inside any string, error messages and stacks included. Only the secret span is replaced, so the sentence around it survives. auditRedaction() shows what would be removed across a real corpus, so coverage is measured rather than trusted. It is defense in depth, not a guarantee; SECURITY.md says exactly what it does not promise.

Your own store#

The canonical schema is code. Store the columns canonicalRow() derives, and make your query() agree with matchesQuery(). That agreement is the contract; the Librarian and the vocabulary work unchanged on top.

adapter.ts
import { canonicalRow, matchesQuery } from "@lovelaces-io/storyteller";

// Store these columns; answer query() so that it agrees with matchesQuery()
const row = canonicalRow(story);
// { story_id, parent_story_id, timestamp, level, title,
//   origin_who, origin_what, origin_where, duration_ms, error_message,
//   notes, search_text, record }

Full reference: Stores in the API.