diff --git a/.github/assets/readme/loop-dark.svg b/.github/assets/readme/loop-dark.svg new file mode 100644 index 000000000..124e839fc --- /dev/null +++ b/.github/assets/readme/loop-dark.svg @@ -0,0 +1,52 @@ + +The LoreKit learning loop: learns, recalls, promotes, prunes + +A MONTH IN THE LIFE OF ONE LESSON +Illustrative timeline — every step is a mechanic LoreKit ships today. + +DAY 1 +4 tries +LEARNS +Your agent hits a wall and +works its way out — then +writes the lesson itself. + +memory.write + + + +DAY 4 +1 try +RECALLS +A teammate’s agent fails the +same way. The lesson arrives +with the error, before a retry. + +on the failing call + + + +DAY 12 +0 tries +PROMOTES +Seen three times in three +places, it earns a permanent +rule. Applied before the run. + +seen 3× → rule + + + +DAY 90 +restorable +PRUNES +Lore nobody opens stops +riding along in every session. +Archived, never deleted. + +groom --unseen-days 90 + + + +and round again — every session, every agent + diff --git a/.github/assets/readme/loop-light.svg b/.github/assets/readme/loop-light.svg new file mode 100644 index 000000000..7f7205036 --- /dev/null +++ b/.github/assets/readme/loop-light.svg @@ -0,0 +1,52 @@ + +The LoreKit learning loop: learns, recalls, promotes, prunes + +A MONTH IN THE LIFE OF ONE LESSON +Illustrative timeline — every step is a mechanic LoreKit ships today. + +DAY 1 +4 tries +LEARNS +Your agent hits a wall and +works its way out — then +writes the lesson itself. + +memory.write + + + +DAY 4 +1 try +RECALLS +A teammate’s agent fails the +same way. The lesson arrives +with the error, before a retry. + +on the failing call + + + +DAY 12 +0 tries +PROMOTES +Seen three times in three +places, it earns a permanent +rule. Applied before the run. + +seen 3× → rule + + + +DAY 90 +restorable +PRUNES +Lore nobody opens stops +riding along in every session. +Archived, never deleted. + +groom --unseen-days 90 + + + +and round again — every session, every agent + diff --git a/.github/assets/readme/store-dark.svg b/.github/assets/readme/store-dark.svg new file mode 100644 index 000000000..2b8fe9352 --- /dev/null +++ b/.github/assets/readme/store-dark.svg @@ -0,0 +1,54 @@ + +Every agent reads and writes one LoreKit store, scoped narrow to broad, kept locally, hosted, or self-hosted + +ONE STORE, EVERY AGENT +Connect each agent once. Lessons stay with the repo they’re about. + +Claude Code +plugin · hooks · MCP + +Cursor +rule · stop hook · MCP + +Codex +hooks · experimental + +Any MCP client +endpoint + token + +CI +read-only lk_ro_ token + + + + + + +LoreKit +memory.* +READ ORDER · NARROW → BROAD + + +branch::acme/api::feat/x + + +repo::acme/api + + +project::payments + + +global + +LIVES WHERE YOU CHOOSE + +Local +markdown files · no account + +Hosted +shared across machines + team + +Self-hosted +your Supabase + Vercel · MIT + + diff --git a/.github/assets/readme/store-light.svg b/.github/assets/readme/store-light.svg new file mode 100644 index 000000000..a02d81d69 --- /dev/null +++ b/.github/assets/readme/store-light.svg @@ -0,0 +1,54 @@ + +Every agent reads and writes one LoreKit store, scoped narrow to broad, kept locally, hosted, or self-hosted + +ONE STORE, EVERY AGENT +Connect each agent once. Lessons stay with the repo they’re about. + +Claude Code +plugin · hooks · MCP + +Cursor +rule · stop hook · MCP + +Codex +hooks · experimental + +Any MCP client +endpoint + token + +CI +read-only lk_ro_ token + + + + + + +LoreKit +memory.* +READ ORDER · NARROW → BROAD + + +branch::acme/api::feat/x + + +repo::acme/api + + +project::payments + + +global + +LIVES WHERE YOU CHOOSE + +Local +markdown files · no account + +Hosted +shared across machines + team + +Self-hosted +your Supabase + Vercel · MIT + + diff --git a/README.md b/README.md index 461cf43a1..14c99a3a3 100644 --- a/README.md +++ b/README.md @@ -4,260 +4,298 @@ # [LoreKit](https://lorekit.io) -**Shared, persistent memory for your AI coding agents.** +### Your coding agents forget. LoreKit makes them learn. -Your agent solves something once — a migration gotcha, a flaky-test fix, why the -build breaks only on CI — and **remembers it in every session after, on every -machine, across every tool.** One `npx` command to connect; works with Claude -Code, Cursor, Codex, or any MCP client. +When your agent hits a wall and works its way out, it writes down what it +learned — and the next agent to hit that wall gets the lesson **with the +error**, on any machine, in any tool. What keeps recurring becomes a rule. +What nobody uses gets archived. [![Deploy](https://github.com/mthines/lorekit/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/mthines/lorekit/actions/workflows/deploy.yml) [![npm](https://img.shields.io/npm/v/@lorekit/cli.svg)](https://www.npmjs.com/package/@lorekit/cli) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) +[Website](https://lorekit.io) · [Docs](https://lorekit.io/docs/setup) · [Blog](https://lorekit.io/blog) · [Quick start](#quick-start) · [FAQ](#questions-youre-probably-asking) + ```bash -npx @lorekit/cli install # connect your agent — full setup below +npx @lorekit/cli install ``` -``` - ┌─ your agent, any tool, any machine ─┐ - learns something │ memory.write { scope, key, value } │ - in a session ───→│ │──→ one store, your call: - recalls it next │ memory.list { scope } │←── remote (shared) or local - time it needs it └─────────────────────────────────────┘ -``` +Works with **Claude Code** and **Cursor**, Codex (experimental), and any MCP +client. Runs fully offline with no account, or hosted so your whole team +shares one set of lessons. MIT licensed and self-hostable. -## The problem: your agents forget everything - -Every coding agent starts each session with total amnesia. If you use them -daily, that costs you: - -- **Every session starts from zero.** The agent that just untangled a tricky bug - has no memory of it an hour later. You re-explain the same context, over and - over. -- **Lessons are trapped on one machine.** What your agent learned on your laptop - never reaches your teammate's agent — or the same agent running in CI. -- **Every tool is its own island.** What Claude Code figured out, Cursor doesn't - know. What you learned locally, GitHub Actions can't see. -- **Good context dies quietly.** The `.claude/` notes your agent writes are - wiped on the next CI run and rarely make it back to you. - -The knowledge exists. It just has nowhere to live. - -## What LoreKit gives you - -- **A memory that persists.** Lessons are stored in a database, not a scratch - file — they survive session ends, machine reboots, and CI runs. -- **One brain for every agent.** Any MCP-compatible agent, anywhere, reads and - writes the same memory. Your laptop, your team, your pipeline — one source of - truth. -- **Scoped so it stays relevant.** Memory is partitioned by scope, so an agent - gets the lessons for *this* repo and branch without drowning in noise from - everything else (see [How memory is organized](#how-memory-is-organized)). -- **Learns from your code reviews.** Point a GitHub webhook at LoreKit and PR - review comments become durable lessons automatically — no copy-paste. -- **Works with the tools you already use.** Claude Code, Cursor, Codex, or any - MCP client. One endpoint, one token. -- **Remote or local — your call.** Use the shared remote store for cross-machine - sharing, or keep memory in plain markdown files on your own disk — no account, - no network. Switch between the two whenever you like. - -## Get started - -You don't have to run anything yourself — LoreKit is hosted. Getting your agents -connected takes three steps. - -### 1. Get a token - -Sign in to the dashboard at -[lorekit.io](https://lorekit.io) with GitHub, then -**Lore Explorer → Connect your agent → Generate new token**. - -Pick **Read + Write** for agents that should learn, or **Read only** for -context-injection-only setups like CI. - -Your token is shown once — copy it now. - -### 2. Connect your agent - -The fastest path is the CLI. It scaffolds three companion skills that make your -agent use LoreKit on its own: - -- **`lorekit-memory`** — the runtime loop: reading relevant lessons when it - starts a task, and writing one whenever something goes wrong (a stuck loop, a - repeated failure, a costly wrong assumption). -- **`lorekit-setup`** — the authoring counterpart: wiring a **self-improvement - loop** into one of *your own* skills or workflows — a fast episodic tier that - promotes proven lessons into permanent rules, with the entrenchment guards - that keep a learning loop from reinforcing its own mistakes. -- **`lorekit-groom`** — the maintenance counterpart: running a **grooming pass** - over a store that has grown noisy — finding and merging near-duplicate - lessons, linting out low-quality ones, and setting expiry on time-bound ones — - always analysing read-only and proposing a plan before it changes anything. - For fully automated, server-side cleanup instead of an agent-driven pass, see - [Retention policies](https://lorekit.io/docs/grooming) — saved scoped rules - (`lorekit policy create` / `lorekit groom`) that archive stale lore, reviewed - manually or swept nightly, and never hard-delete. + + + A month in the life of one lesson. Day 1, learns: the agent works its way out of a failure and writes the lesson itself. Day 4, recalls: a teammate's agent fails the same way and the lesson arrives with the error. Day 12, promotes: seen three times, it becomes a permanent rule. Day 90, prunes: lore nobody opens is archived, never deleted. + -```bash -npx @lorekit/cli install \ - --endpoint https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/mcp \ - --token lk_rw_ -``` +## Why not just a CLAUDE.md? -Check that everything is wired up: +Keep it. A rules file holds what you knew to write down **before** anything +broke. LoreKit captures what your agents find out the hard way — and keeps +sorting it. -```bash -npx @lorekit/cli doctor -# → connectivity, token permission, and detected scopes, all green -``` +| | Rules file (`CLAUDE.md`, `.cursorrules`, …) | LoreKit | +|--|--|--| +| **Who writes it** | You, when you remember to | Your agents, the moment they hit friction | +| **What's in it** | What you predicted would matter | What actually broke — and the fix that worked | +| **What gets loaded** | All of it, every session | What matches: at session start, on each prompt, on the failing call | +| **Over time** | Grows until someone prunes it | Promotes what recurs, archives what nobody uses | + +LoreKit is how you find out what has earned a place in your CLAUDE.md. + +### How the loop actually runs + +Each step is shipped behaviour, not a roadmap item: -**Run the CLI often?** Read commands like `list`, `search`, and `tree` are -nicer without the `npx` prefix. Install the binary globally once: +- **Learns.** Lifecycle hooks watch the session for friction — errored tool + calls, a command repeated in a loop — and nudge the agent to write a lesson + before it stops. Clean sessions stay silent. +- **Recalls.** When a tool call fails, the hook searches your lore with the + tool name and error text and injects up to three matching lessons *before* + the agent retries. (Claude Code; Cursor reads lessons through a rule and an + end-of-turn hook.) +- **Promotes.** A lesson re-learned in later sessions accrues a seen-count; + session start ranks the most-recurring first, and the + [Insights](https://lorekit.io/insights) view flags the load-bearing ones to + promote into a permanent rule. +- **Prunes.** Every lesson delivered costs tokens whether it's used or not. + LoreKit tracks how often each lesson is actually opened versus merely + delivered, and retention policies archive what nobody reads — previewed + first, restorable with one command. + +## Quick start ```bash -npm install -g @lorekit/cli +npx @lorekit/cli install ``` -Then drop `npx @lorekit/cli` and call `lorekit` directly: +The installer wires the MCP server, three companion skills, and the lifecycle +hooks into your project, and picks up the repo's scope from its git remote. +It then asks for a token — and that's where you pick a path: + + + + + + + + + + +
Hosted — share across machines and teammatesLocal — no account, nothing leaves your disk
+ +1. Sign in at [lorekit.io](https://lorekit.io) → **Connect your agent** → + **Generate new token** (shown once). +2. Paste it when the installer asks. + +Free plan: 5,000 active memories, 120 requests a minute. + + + +1. Skip the token. +2. Point `.mcp.json` at the CLI's built-in local server and set + `"mode": "local"` in `.lorekit.json` — see the + [offline guide](https://lorekit.io/docs/offline). + +Lessons are markdown files you can grep, diff, and commit. + +
+ +Then check it's wired up: ```bash -lorekit doctor +npx @lorekit/cli doctor # connectivity, token permission, detected scopes ``` -**Tab completion (zsh / fish).** Let `install` wire it, or do it by hand: +From then on, every session starts with the lessons that apply to it. + +
+Claude Code plugin, Claude Code on the web, global install, tab completion + +**Claude Code marketplace plugin** — skill + hooks + MCP in one install: ```bash -lorekit install --completions auto # detect $SHELL and set it up -lorekit completion zsh > ~/.zsh/completions/_lorekit -lorekit completion fish > ~/.config/fish/completions/lorekit.fish +/plugin marketplace add mthines/lorekit +/plugin install lorekit-memory@lorekit ``` -It completes commands, each command's flags, and — from your local store — -scopes and `scope::key` addresses. - -### 3. That's it - -Your agent now remembers. Its lessons survive every session, reach every machine -running the same token, and are there the next time any agent picks up the work. - -> **Prefer a framework plugin?** For memory that fires on host lifecycle events -> — no reliance on the agent choosing to use the skill — install a plugin -> instead. Claude Code has a one-line marketplace install; Cursor and Codex have -> their own bundles. See [plugins/](./plugins/README.md). - -> **Using Claude Code on the web?** Its sessions run in a fresh, ephemeral clone, -> so add `--mcp-json` — `npx @lorekit/cli install --global --mcp-json` also writes -> a committable, secret-free `.mcp.json` (auth via `${LOREKIT_TOKEN}`) the cloud -> session reads directly. `.mcp.json` is usually git-ignored, so you'll need to -> un-ignore and commit it — the command warns if it's still ignored. See the -> [Claude Code on the web guide](https://lorekit.io/docs/claude-code-web). - -## Remote or local — your choice - -LoreKit needs neither an account nor a network. The same `memory.*` tools can -run against **plain markdown files on your machine** instead of the shared -remote store — fully offline, nothing to sign up for. - -| | Remote (shared) | Local | -|--|-----------------|-------| -| Where lessons live | A shared remote store the whole team reaches | Markdown files under `~/.lorekit/` and `/.lorekit/` | -| Best for | Sharing across machines, teammates, and CI | Private, offline, or air-gapped work | -| Setup | A token (above) | No account, no token | -| Sharing | Automatic, everywhere the token is used | Commit `/.lorekit/` to share via git — or gitignore it to keep private | - -Local memory is **two-tier**, mirroring the remote scope model: a per-user -`~/.lorekit/` holds global lessons, and an opt-in `/.lorekit/` holds -repo- and branch-scoped ones. Reads merge both tiers with the closer scope -winning. Every lesson is a human-readable markdown file — greppable and -diffable, not a database you have to query. - -To run local, point your agent's `.mcp.json` at the CLI's built-in local server -(no endpoint, no token needed): - -```jsonc -{ - "mcpServers": { - "lorekit": { "command": "npx", "args": ["-y", "@lorekit/cli", "mcp"] } - } -} +**Claude Code on the web** runs in a fresh clone each session, so add +`--mcp-json` to write a committable, secret-free `.mcp.json` (auth via +`${LOREKIT_TOKEN}`): + +```bash +npx @lorekit/cli install --global --mcp-json ``` -Then select local mode — set `LOREKIT_MODE=local`, or add `{ "mode": "local" }` -to a `.lorekit.json` at your repo root — and create `/.lorekit/` when you -want repo-scoped lessons to persist in the project. +See the [Claude Code on the web guide](https://lorekit.io/docs/claude-code-web). -> **Not committed to one?** Start local and move up to the hosted store later -> with one command — `lorekit migrate --from ~/.lorekit --to remote` previews -> the plan, `--yes` applies it — so lessons are never stranded. `lorekit migrate` also relocates a store between -> the local tiers (`--to home|project`). You can also hard-deny a mode for -> privacy or CI (e.g. `LOREKIT_DENY=remote`). +**Use the CLI a lot?** Install it globally and drop the `npx` prefix: -Full details — the two-tier layout, write routing, the control model, and -migration — are in -[packages/cli/README.md](./packages/cli/README.md#memory-modes--the-control-model). +```bash +npm install -g @lorekit/cli +lorekit search "flaky test" +lorekit install --completions auto # zsh / fish tab completion +``` -## How memory is organized +**The companion skills** the installer adds: -Lessons are partitioned by **scope** — a short string that says how widely a -lesson applies: +- `lorekit-memory` — reads relevant lessons when a task starts, writes one when + something goes wrong. +- `lorekit-setup` — wires a self-improvement loop into *your own* skills: a + fast episodic tier that promotes proven lessons into permanent rules, with + guards that stop a loop reinforcing its own mistakes. +- `lorekit-groom` — tidies a noisy store: merges near-duplicates, lints weak + lessons, sets expiry. Always proposes a plan before changing anything. -``` -global # applies everywhere -project::agent-skills # one project -repo::mthines/gw-tools # one repository -branch::mthines/gw-tools::feat/x # one branch (short-lived) -``` +
-An agent reads from narrow to broad — branch, then repo, then global — and -merges what it finds. So a branch-specific gotcha and a universal convention both -surface, without unrelated repos leaking in. Full spec: -[docs/scope-format.md](./docs/scope-format.md). +## One store, every agent -## Works with your tools + + + Claude Code, Cursor, Codex, any MCP client and CI all connect to one LoreKit store. Lessons are read narrow to broad: branch, repo, project, global. The store lives locally as markdown, hosted, or self-hosted. + + +- **Lessons stay with their repo.** Every lesson lands under a scope — + `branch::`, `repo::`, `project::`, or `global`. Agents read narrowest first, + so one repo's lore never crowds another's. + ([scope format](./docs/scope-format.md)) +- **One shared set for the team.** Bind `repo::acme/*` to your org and every + teammate's agent reads and writes the same lessons. CI reads with a + read-only `lk_ro_` token. Anything unbound stays personal. + ([organizations](https://lorekit.io/docs/organization)) +- **Start local, move up later.** `lorekit migrate --from ~/.lorekit --to remote` + previews the whole plan before it changes anything — lessons are never + stranded. + +| Tool | How it connects | Status | +|------|-----------------|--------| +| **Claude Code** | Marketplace plugin or CLI: skills + 4 lifecycle hooks + MCP | Supported | +| **Cursor** | A rule + `stop` hook + MCP (three copied files) | Supported | +| **Codex** | Feature-flagged hooks with an `AGENTS.md` fallback | Experimental | +| **Any MCP client** | The endpoint and a Bearer token | Works — no hooks | + +All integrations share one zero-dependency hook engine; each host only gets a +thin adapter. See [plugins/](./plugins/README.md). + +## Also in the box + +- **Learns from your code reviews.** Point a GitHub webhook at a repo and + review comments become *candidate* lessons. Noise is filtered out, and a + candidate expires unless it proves useful. +- **Every lesson in plain sight.** The [dashboard](https://lorekit.io) lets you + browse, search, filter, edit, and archive everything your agents have + learned — who wrote it and where it applies. +- **Knows what your memory costs.** Insights estimates what your store costs a + month in tokens and shows the lore that's earning nothing. +- **A real CLI.** `list`, `search`, `show`, `tree`, `diff`, `dedupe`, `lint`, + `groom`, and more — against either store, with `--json` for scripting. + ([CLI reference](./docs/cli.md)) + +

+ LoreKit dashboard: Lore Explorer + LoreKit dashboard: lesson detail +

+ +## Questions you're probably asking + +
+Does it read or upload my code? + +No. LoreKit doesn't index your codebase. It stores exactly what an agent +writes to it through `memory.write` — short lessons — and nothing else. In +local mode nothing leaves your machine at all. + +
+ +
+Who can see my lessons on the hosted store? + +Only you. Every memory is personal by default — visible only to the user or +token that wrote it. Sharing with a team is opt-in, per scope, through an org. +See [private lore](https://lorekit.io/docs/private). + +
+ +
+Won't this bloat my context window? + +It can surface a lesson you didn't need — that's the honest answer. What +keeps it in check: scopes narrow what an agent sees, recall only injects what +matches, LoreKit measures which lessons are actually opened, and grooming +archives what has stopped earning its place. + +
+ +
+What does it cost? + +Nothing. There is no paid tier yet. The hosted free plan holds 5,000 active +memories at 120 requests a minute. Local mode needs no account, and a +self-hosted instance sets its own limits. + +
+ +
+What if the hosted service goes away? + +The whole stack — MCP server, dashboard, database — is in this repo under MIT, +and deploys to your own Supabase and Vercel projects in about five minutes +([self-hosting guide](./docs/install.md)). Or skip the hosted store entirely +and keep lessons as markdown in your repo. + +
-| Tool | How it connects | -|------|-----------------| -| **Claude Code** | Marketplace plugin (skill + lifecycle hooks + MCP), or the CLI above | -| **Cursor** | A rule plus a `stop` hook | -| **Codex** | Feature-flagged hooks with an `AGENTS.md` fallback (experimental) | -| **Any MCP client** | Point it at the endpoint with a Bearer token | - -All the integrations share one engine and differ only in how each host wires it -up. See [plugins/README.md](./plugins/README.md). +
+Does the CLI phone home? + +The human-facing CLI commands (`install`, `doctor`, `list`, `search`, …) send +one usage span per run so the maintainers can see which commands get used: the +command name, a few boolean flags, the CLI/runtime/OS version, and the outcome. +No path, token, endpoint, repo, or scope is ever sent +([details](./packages/cli/README.md#usage-telemetry)). Turn it off with `LOREKIT_TELEMETRY=0` or the cross-vendor +`DO_NOT_TRACK=1`, or set `"telemetry.disabled": true` in `.lorekit.json` for a +whole team. + +
+ +## The fine print, up front + +- **It doesn't make an agent smarter on its own.** If nothing writes a lesson, + there is nothing to read — which is why the plugins wire lifecycle hooks + instead of leaving it to the agent's discretion. +- **Error-matched recall is richest on Claude Code.** Cursor has no + post-failure hook event, so it reads lessons through its rule instead. +- **Codex support is experimental.** Claude Code and Cursor are the supported + paths. +- **It's young.** LoreKit is actively developed and moves fast — check the + [release notes](https://github.com/mthines/lorekit/releases) before + upgrading. ## Documentation | Guide | What it covers | |-------|----------------| -| [docs/scope-format.md](./docs/scope-format.md) | How scopes work and how agents resolve them | +| [lorekit.io/docs](https://lorekit.io/docs/setup) | Setup, offline mode, orgs, grooming, Claude Code on the web | | [docs/mcp-tools.md](./docs/mcp-tools.md) | The `memory.*` tools, with request/response examples | -| [docs/api-tokens.md](./docs/api-tokens.md) | Token types, permissions, and CI usage | -| [docs/limits.md](./docs/limits.md) | Memory caps and rate limits | -| [packages/cli/README.md](./packages/cli/README.md) | Every CLI command and flag | -| [docs/](./docs/README.md) | Everything else — architecture, deployment, observability | - -## Run your own instance - -LoreKit is fully self-hostable — the whole stack (MCP server, dashboard, -database) deploys to your own Supabase and Vercel projects in about five -minutes. See [docs/install.md](./docs/install.md). +| [docs/scope-format.md](./docs/scope-format.md) | How scopes work and how agents resolve them | +| [docs/api-tokens.md](./docs/api-tokens.md) | Token types (`lk_rw_` / `lk_ro_` / `lk_wo_`) and CI usage | +| [docs/cli.md](./docs/cli.md) | Every CLI command and flag | +| [docs/install.md](./docs/install.md) | Run your own instance | +| [docs/](./docs/README.md) | Everything else — architecture, limits, observability | ## Contributing -LoreKit is an NX monorepo. To set it up locally, run the checks, and hack on any -package, see [DEVELOPMENT.md](./DEVELOPMENT.md). - -## Website screenshots - -image - -image +LoreKit is an NX monorepo — MCP server (Deno, Supabase Edge Functions), +dashboard (Next.js), and a zero-dependency CLI. Issues and PRs are welcome; +[DEVELOPMENT.md](./DEVELOPMENT.md) gets you from clone to green checks. +If LoreKit saves your agent a retry or two, a ⭐ helps other developers find it. ## License [MIT](./LICENSE) © LoreKit contributors. - diff --git a/scripts/codegen/readme-graphics.mjs b/scripts/codegen/readme-graphics.mjs new file mode 100644 index 000000000..38846efc6 --- /dev/null +++ b/scripts/codegen/readme-graphics.mjs @@ -0,0 +1,205 @@ +#!/usr/bin/env node +// Renders the root README's illustrations as light + dark SVG pairs into +// .github/assets/readme/. The README swaps them with on GitHub's own +// theme — an SVG's internal prefers-color-scheme follows the OS, not GitHub. +// SVGs loaded through cannot fetch web fonts, so text uses system stacks +// and every line is sized to fit with room for the widest fallback font. +// +// node scripts/codegen/readme-graphics.mjs + +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const OUT = join(dirname(fileURLToPath(import.meta.url)), '../../.github/assets/readme'); + +// Dark mirrors packages/web/src/app/globals.css; light is the same hues +// darkened until text clears WCAG AA on white. +const THEMES = { + dark: { + bg: '#0d0e11', card: '#13151a', border: '#2a2d36', text: '#f0f2f7', muted: '#9aa0ad', + faint: '#5d6370', accent: '#f5a623', accentBg: '#2a2010', + global: '#a78bfa', project: '#34d399', repo: '#60a5fa', branch: '#f59e0b', + }, + light: { + bg: '#ffffff', card: '#f7f7f9', border: '#e2e4e9', text: '#16181d', muted: '#565c69', + faint: '#9aa0ad', accent: '#b86e00', accentBg: '#fff4e0', + global: '#7c3aed', project: '#059669', repo: '#2563eb', branch: '#b45309', + }, +}; + +const SANS = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif`; +const MONO = `ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace`; + +const esc = (s) => s.replace(/&/g, '&').replace(//g, '>'); + +const text = (x, y, s, { size = 14, weight = 400, fill, font = SANS, anchor = 'start', spacing } = {}) => + `${esc(s)}`; + +const svg = (w, h, t, body, title) => + ` +${esc(title)} + +${body} + +`; + +// ── 1. The learning loop ──────────────────────────────────────────────────── + +const BEATS = [ + { + day: 'DAY 1', verb: 'LEARNS', tries: '4 tries', + lines: ['Your agent hits a wall and', 'works its way out — then', 'writes the lesson itself.'], + code: 'memory.write', + }, + { + day: 'DAY 4', verb: 'RECALLS', tries: '1 try', + lines: ['A teammate’s agent fails the', 'same way. The lesson arrives', 'with the error, before a retry.'], + code: 'on the failing call', + }, + { + day: 'DAY 12', verb: 'PROMOTES', tries: '0 tries', + lines: ['Seen three times in three', 'places, it earns a permanent', 'rule. Applied before the run.'], + code: 'seen 3× → rule', + }, + { + day: 'DAY 90', verb: 'PRUNES', tries: 'restorable', + lines: ['Lore nobody opens stops', 'riding along in every session.', 'Archived, never deleted.'], + code: 'groom --unseen-days 90', + }, +]; + +function loop(t) { + const W = 1000, H = 400, cardW = 214, gap = 30, x0 = 34, cardY = 92, cardH = 236; + const parts = [ + text(x0, 44, 'A MONTH IN THE LIFE OF ONE LESSON', { size: 12, weight: 600, fill: t.accent, font: MONO, spacing: 2 }), + text(x0, 68, 'Illustrative timeline — every step is a mechanic LoreKit ships today.', { size: 14, fill: t.muted }), + ]; + BEATS.forEach((b, i) => { + const x = x0 + i * (cardW + gap); + const hot = i === 0; + parts.push( + ``, + text(x + 18, cardY + 30, b.day, { size: 11, weight: 600, fill: t.muted, font: MONO, spacing: 1.5 }), + text(x + cardW - 18, cardY + 30, b.tries, { size: 11, weight: 600, fill: t.accent, font: MONO, anchor: 'end' }), + text(x + 18, cardY + 62, b.verb, { size: 22, weight: 700, fill: t.text, spacing: 0.5 }), + ...b.lines.map((l, j) => text(x + 18, cardY + 96 + j * 21, l, { size: 13.5, fill: t.muted })), + ``, + text(x + 26, cardY + cardH - 30, b.code, { size: 12, fill: t.text, font: MONO }), + ); + if (i < BEATS.length - 1) { + const ax = x + cardW + 5, ay = cardY + cardH / 2; + parts.push( + ``, + ``, + ); + } + }); + // The return path: from the last card back under the row to the first. + const yB = cardY + cardH, yL = H - 34; + const xs = x0 + cardW / 2, xe = x0 + 3 * (cardW + gap) + cardW / 2; + parts.push( + ``, + ``, + ``, + text(W / 2, yL + 4, 'and round again — every session, every agent', { size: 13, fill: t.muted, anchor: 'middle' }), + ); + return svg(W, H, t, parts.join('\n'), 'The LoreKit learning loop: learns, recalls, promotes, prunes'); +} + +// ── 2. One store, every agent ─────────────────────────────────────────────── + +const AGENTS = [ + ['Claude Code', 'plugin · hooks · MCP'], + ['Cursor', 'rule · stop hook · MCP'], + ['Codex', 'hooks · experimental'], + ['Any MCP client', 'endpoint + token'], + ['CI', 'read-only lk_ro_ token'], +]; + +const SCOPES = [ + ['branch::acme/api::feat/x', 'branch'], + ['repo::acme/api', 'repo'], + ['project::payments', 'project'], + ['global', 'global'], +]; + +const STORES = [ + ['Local', 'markdown files · no account'], + ['Hosted', 'shared across machines + team'], + ['Self-hosted', 'your Supabase + Vercel · MIT'], +]; + +function store(t) { + const W = 1000, H = 392; + const parts = [ + text(34, 44, 'ONE STORE, EVERY AGENT', { size: 12, weight: 600, fill: t.accent, font: MONO, spacing: 2 }), + text(34, 68, 'Connect each agent once. Lessons stay with the repo they’re about.', { size: 14, fill: t.muted }), + ]; + + // Left: the agents. + const ax = 34, aw = 250, ah = 46, ay0 = 96, ag = 10; + AGENTS.forEach(([name, how], i) => { + const y = ay0 + i * (ah + ag); + parts.push( + ``, + text(ax + 16, y + 20, name, { size: 14, weight: 600, fill: t.text }), + text(ax + 16, y + 37, how, { size: 11.5, fill: t.muted, font: MONO }), + ); + }); + + // Centre: the store and its read order. + const sx = 344, sw = 340, sy = 96, sh = 270; + const midY = sy + sh / 2; + AGENTS.forEach((_, i) => { + const y = ay0 + i * (ah + ag) + ah / 2; + parts.push( + ``, + ); + }); + parts.push( + ``, + text(sx + 20, sy + 32, 'LoreKit', { size: 18, weight: 700, fill: t.text }), + text(sx + sw - 20, sy + 32, 'memory.*', { size: 12, fill: t.accent, font: MONO, anchor: 'end' }), + text(sx + 20, sy + 60, 'READ ORDER · NARROW → BROAD', { size: 10.5, weight: 600, fill: t.muted, font: MONO, spacing: 1.2 }), + ); + SCOPES.forEach(([label, kind], i) => { + const y = sy + 76 + i * 44; + const w = (sw - 40) * (0.76 + i * 0.08); + parts.push( + ``, + ``, + text(sx + 36, y + 22, label, { size: 12.5, fill: t[kind], font: MONO }), + ); + }); + + // Right: where it lives. + const rx = 744, rw = 222; + parts.push( + ``, + text(rx, sy + 14, 'LIVES WHERE YOU CHOOSE', { size: 10.5, weight: 600, fill: t.muted, font: MONO, spacing: 1.2 }), + ); + STORES.forEach(([name, how], i) => { + const y = sy + 30 + i * 80; + const hot = i === 0; + parts.push( + ``, + text(rx + 16, y + 27, name, { size: 15, weight: 600, fill: t.text }), + text(rx + 16, y + 47, how, { size: 11, fill: t.muted, font: MONO }), + ); + }); + // Bracket joining the three options to the store's connector. + parts.push( + ``, + ); + return svg(W, H, t, parts.join('\n'), 'Every agent reads and writes one LoreKit store, scoped narrow to broad, kept locally, hosted, or self-hosted'); +} + +mkdirSync(OUT, { recursive: true }); +for (const [name, theme] of Object.entries(THEMES)) { + writeFileSync(join(OUT, `loop-${name}.svg`), loop(theme)); + writeFileSync(join(OUT, `store-${name}.svg`), store(theme)); +} +console.log(`wrote 4 SVGs to ${OUT}`);