Orrerium

skill
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 6 GitHub stars
Code Warn
  • process.env — Environment variable access in hooks/emit.js
  • network request — Outbound network request in hooks/emit.js
  • process.env — Environment variable access in lib/ask.js
  • network request — Outbound network request in lib/ask.js
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

Local-first dashboard over a markdown knowledge vault: interactive graph, projects board, inbox triage, ask-your-brain. Zero dependencies.

README.md

Orrerium

CI

Local-first dashboard over a markdown knowledge vault: no build step, zero npm
dependencies, all state in files that both humans and AI agents can read.

Three core panels: Brain (interactive graph of the vault with note reader,
search, ask-your-brain, and live reload), Projects (a board built from
project-page frontmatter), and Inbox (the capture buffer with a
ready-for-triage indicator). Panels deep-link via the URL hash (#/brain,
#/projects, #/inbox).

If you use Claude Code, the graph can also map
your whole machine's agent setup: .claude/{skills,agents,commands} across every
repo under the configured roots plus the global ~/.claude land on an AGENTS
orbit, and live sessions light up the graph in real time (see
Agents board). None of that is required — the vault dashboard
works on its own.

The Brain graph over an example vault: notes on the inner rings, the machine's skills, agents and commands on the outer ones, and two projects lit up cyan while their subagents work

Above, animated and live: an example vault — 23 lessons, 9 project pages,
7 machine notes — plus a scanned Claude Code setup across eight repos. The cyan
orbit rings mark nodes that are running right now, and each travelling dot is one
working subagent's traffic: six on a WebGL game repo, four on a kids' game, and a
third session quietly triaging the vault's own inbox.

Quick start

git clone https://github.com/cfirz/Orrerium
cd Orrerium
node server.js

Then open http://127.0.0.1:4321. Requires Node 20+ (uses fs.watch recursive
and node:test). There is nothing to install — with no configuration, Orrerium
boots against the bundled demo vault in starter-vault/.

Prefer a double-click? orrerium.bat (Windows) and
orrerium.sh (macOS/Linux) start the server, open the browser once
it answers, and reuse an already-running instance instead of starting a second
one. The server runs in that window — Ctrl+C or closing it stops the server.

First-time setup

Three steps, all offline — no accounts, no API keys, nothing to install.

1. Install Node.js — version 20 or newer, from
nodejs.org. To check what you have:

node -v      # should print v20.x or higher

2. Download Orrerium — on the
GitHub page, click Code → Download ZIP
and unzip it anywhere. Or, if you use git:

git clone https://github.com/cfirz/Orrerium

3. Open it — double-click orrerium.bat (Windows) or run
orrerium.sh (macOS/Linux):

cd Orrerium
./orrerium.sh     # macOS / Linux — Windows: double-click orrerium.bat

Your browser opens on a small demo vault — click any node to read its note.

That's it. Whenever you're ready:

Troubleshooting

  • vaultPath is not a directory — the vault path you set doesn't exist
    (or config.json points somewhere stale). Remember ~ is not expanded in
    config.json.
  • Port 4321 is already in use — another Orrerium (or something else) owns
    the port. Stop it, or set port in config.json. If hooks should reach a
    non-default port, also set ORRERIUM_PORT where the hooks run.
  • Errors on startup — almost always Node older than 20; check node -v.
  • Agents / Flows / Icons panels are empty — expected until the
    Agents board hooks are installed (node hooks/install.js,
    optionally --tool gemini / --tool codex) and an agent session runs.
  • Graph is static (no spark animations) — your OS has reduced motion
    switched on and the topbar motion toggle is set to Auto; switch it back
    to On (the default). On Windows, turning "animation effects" on in
    Settings also brings the motion back.

Point it at your own vault

Any folder of markdown files works, and Orrerium treats it as strictly
read-only
— it never touches your notes. Either set ORRERIUM_VAULT when
starting the server:

ORRERIUM_VAULT=/path/to/your/vault node server.js     # macOS / Linux
set ORRERIUM_VAULT=C:\path\to\your\vault && node server.js    # Windows cmd
$env:ORRERIUM_VAULT="C:\path\to\your\vault"; node server.js   # PowerShell

or, for a permanent setting, copy
config.example.json to config.json (gitignored) and
set vaultPath. Use absolute paths in config.json~ is not expanded.

You get the most out of the dashboard when the vault follows the conventions the
UI understands (the bundled starter-vault/ demonstrates all of
them, and its CONVENTIONS.md explains the why):

  • Type folders — notes live in projects/, lessons/, machine/, ideas/,
    templates/; the folder gives the node its type, colour, and ring. Notes in
    other folders still render, just untyped. A vault with different folder names
    (PARA, Zettelkasten, …) can map them onto the same five types via
    folderTypes in config.json.
  • A root README.md — sits at the centre of the Rings layout as the vault's
    index.
  • Frontmattername, one-line quoted description, type, updated;
    project pages carry status and dir (the projects board is built from
    these); projects: ["[[slug]]"] wikilinks cluster the graph.
  • An inbox.md capture buffer — feeds the Inbox panel, with capture and
    triage skills under .claude/skills/ (rendered as ROUTINE nodes).

To adopt the whole system, copy starter-vault/ somewhere, delete the sample
notes, and start capturing.

Configuration reference

Key Default Meaning
vaultPath starter-vault Markdown vault to visualize (relative paths resolve from the repo root; ORRERIUM_VAULT overrides)
host / port 127.0.0.1 / 4321 Where the dashboard listens — keep it on loopback, see SECURITY.md
excludeDirs .obsidian, .claude, .git Folders never scanned (skills are scanned explicitly)
applicationTags unity, python, … Tags that become APPLICATION nodes on the outer ring
folderTypes projectsproject, … Folder name → note type for vaults with different conventions; values must be one of the five built-in types, and a provided map replaces the default (start from a copy of it)
ai.provider auto anthropic, openai, gemini, grok, ollama, cli (local claude CLI / Claude Code login), or auto — the first provider whose API key is set, else cli. (api is the pre-0.3 alias for anthropic)
ai.model per provider Overrides the provider's default model (see AI features); required for ollama (the CLI uses its own configured model)
ai.baseUrl per provider Overrides the endpoint base — an Ollama port, LM Studio, vLLM, a proxy: any OpenAI-compatible server
ai.keyEnv per provider Name of the env var holding the API key, if not the provider's usual one (the key value itself never goes in config)
ai.maxContextTokens 120000 Ask context budget: vaults estimated under it ship whole (and prompt-cache); over it a local BM25 + link-graph retrieval step selects the notes that fit
claudeScan.roots [] Dirs whose children are scanned (one level) for .claude/{skills,agents,commands} — absolute paths, e.g. ["C:/code"] or ["/Users/you/code"]
claudeScan.globalDir ~/.claude The global Claude dir, scanned the same way
claudeScan.settingsPath ~/.claude/settings.json Read for skillOverrides so disabled skills render dormant
claudeScan.rescanMs 300000 External repos are re-scanned on this cadence (no watchers)

AI features (optional)

Two features talk to an LLM; everything else is fully offline.

  • Ask-your-brain (#/brain, ask box) works with any one of these — set the
    key in the environment (never in a file) and, if you want a specific one,
    ai.provider in config.json:

    Provider Key env var Default model
    anthropic ANTHROPIC_API_KEY claude-opus-5
    openai OPENAI_API_KEY gpt-5.6-terra
    gemini GEMINI_API_KEY (or GOOGLE_API_KEY) gemini-3.7-flash
    grok XAI_API_KEY grok-4.5
    ollama none — local none: set ai.model
    cli none — the Claude Code CLI's own login the CLI's configured model

    auto (the default) walks anthropic → openai → gemini → grok and picks the
    first key it finds, else falls back to cli. GOOGLE_API_KEY counts only when
    gemini is chosen explicitly — it exists on too many machines for unrelated
    reasons to trigger auto-detection. Local models: set
    "provider": "ollama", "model": "llama3" (and baseUrl for a non-default
    port); any other OpenAI-compatible server works via baseUrl on any of the
    openai-dialect providers. Answers stream in as they generate; the stop button
    cancels mid-answer (the provider call is aborted server-side, and a stopped
    turn is not kept in the conversation).

  • Crons (#/crons) runs headless claude -p jobs and always needs the
    Claude CLI, whatever ai.provider says.

Privacy note: for vaults that fit the context budget
(ai.maxContextTokens, default 120k tokens ≈ 480 KB of markdown), every
question ships your whole vault's text to whichever provider is
configured
(Anthropic, OpenAI, Google, xAI, your local Ollama, or whatever
the claude CLI is logged into). Bigger vaults go through a local retrieval
step that selects only the notes relevant to the question — but those are
still vault text going to the provider. Don't point Orrerium at a vault you
wouldn't send there. Cost note: the Anthropic path caches the vault
context explicitly, so repeat questions are cheap; the other cloud providers
cache implicitly and partially, so a large vault may re-bill more often. For
local Ollama models with small context windows, lower ai.maxContextTokens
to match.

Architecture (10 lines)

  • server.js — plain node:http: static files (Cache-Control: no-cache — live edits, no stale modules), JSON routes, one SSE route.
  • lib/sse.js — the SSE channel: named events (broadcast(event, payload)), one client set, keepalive pings. public/js/bus.js is its client twin: one shared EventSource, panels subscribe by event name.
  • lib/store.js — Orrerium-owned writable state under data/ (gitignored): atomic JSON writes (tmp+rename) and NDJSON append logs. The vault stays read-only.
  • lib/claude-scan.js — cross-repo scanner: .claude/{skills,agents,commands} across claudeScan.roots + the global dir become agent/command/routine nodes (namespaced ids like DemoApp.qa-agent, short label for display), merged onto the graph with one scan edge to the repo's vault project note (matched on dir frontmatter) or a synthetic repo anchor.
  • lib/vault.js — pure parser: frontmatter (vault dialect) + wikilink/md-link extraction, plus .claude/skills/*/SKILL.md as routine nodes. Importable by agents and CLIs; no http/fs.watch in it.
  • lib/graph.js — pure: notes → {nodes, edges, warnings}; undirected dedupe, ghost nodes for unresolved wikilinks, degree; application nodes derived from applicationTags with tag edges to every note carrying the tag; routine edges from real markdown links and x.md mentions in skill bodies.
  • lib/watch.js — debounced recursive fs.watch; every change triggers a full re-parse (the vault is small; incremental bookkeeping is not worth bugs).
  • lib/ask.js — ask-your-brain: whole-vault context + question to an LLM. Zero-dependency providers behind one registry: raw HTTP to the Claude API (with prompt caching and refusal handling), one OpenAI-compatible Chat Completions adapter covering OpenAI/Gemini/Grok/Ollama (only base URL, key env var and default model differ), or the local claude CLI. All stream — the APIs via SSE, the CLI via --output-format stream-json (with a buffered retry for CLIs too old for the flags) — and /api/ask relays the deltas as NDJSON when the client asks to stream; a dropped connection cancels the provider call. A missing API key fails before any request, naming the env var. Answers cite notes as [[wikilinks]], which the UI renders as graph navigation.
  • public/js/graph-view.js — SVG graph with two layouts: Rings (default — concentric orbits with README at the core, then root docs, PROJECTS, LESSONS, MACHINE, IDEAS, TEMPLATES, ROUTINES, and hexagonal APPLICATIONS outermost; notes are angularly sorted toward the projects they link to) and Force (Obsidian-style d3-force; position cache keeps live reloads from re-exploding the layout).
  • public/js/agent-activity.js — live agent traffic on the graph, in both layouts (see "Live agent activity"). Pure derivation: the agents SSE snapshot reduces to a set of live nodes and live edges, which graph-view.js paints.
  • public/js/note-panel.js — marked with a wikilink tokenizer; every in-vault link navigates the graph.
  • public/js/search.js — substring filter over id/description/tags.
  • public/vendor/ — committed single-file builds of d3 and marked (versions and licenses).
  • The vault's .obsidian/graph.json was the design reference for colours/forces; it is never read or written at runtime (Obsidian clobbers it).
  • public/js/panels.js + projects-panel.js + inbox-panel.js — the dashboard shell: a topbar nav switches panels; the projects board renders from graph-node frontmatter (status, dir, tags, degree) and clicks through to the graph; the inbox view renders inbox.md captures and flags the triage threshold. The ask panel is multi-turn — prior Q/A pairs ride along with each request.
  • window.orrerium in the browser console exposes the view, graph data and simulation for debugging.

Agents board

This whole section is entirely optional — without a hook setup below, the
Agents, Flows and Icons panels are simply empty.

#/agents shows agent sessions on the machine live: the orchestrator's current
activity, spawned subagents and their status, tool and error counts. Claude Code
gets the full picture; Gemini CLI and Codex CLI can report too (with the
fidelity their hook systems allow), and anything else can feed the board through
a small generic event shape. Every event carries a source, each card wears a
source badge, and a source filter row appears once a second tool reports.

Events reach the board by POSTing to /api/hook-event through
hooks/emit.js — a fire-and-forget relay that always exits 0 and
swallows every failure, so sessions never notice when Orrerium is down (a
Claude Code PreToolUse hook that exits non-zero would block the tool call).

To enable, run the installer once from your clone, per tool:

node hooks/install.js
node hooks/install.js --tool gemini
node hooks/install.js --tool codex
  • Claude Code (the default) merges hook entries for all seven events into
    ~/.claude/settings.json — full live activity: prompts, tools, subagents.
  • Gemini CLI merges its six hook events (SessionStart, BeforeAgent,
    BeforeTool, AfterTool, AfterAgent, SessionEnd) into
    ~/.gemini/settings.json, tagged --source=gemini-cli; the server maps that
    vocabulary onto the canonical one, so cards show prompts and tool activity.
  • Codex CLI sets the single top-level notify key in ~/.codex/config.toml.
    Codex only notifies when a turn completes, so its cards are honest but
    coarse — one card per turn with the prompt, no live tool activity; that is all
    Codex exposes. The installer refuses to clobber a notify you already have
    (Codex supports exactly one) and prints manual instructions instead.

Every install shows the plan and asks before writing, and backs the file up
beside itself first — every other setting and every existing hook stays
untouched. --dry-run previews without writing, --uninstall takes the hooks
back out, --print emits the config snippet for wiring by hand, and re-running
after moving the clone fixes up the stale paths. Already-running sessions pick
the hooks up on their next restart.

Any other tool (Grok CLI, CI jobs, your own scripts) can feed the board by
POSTing events in the normalized shape to
/api/hook-event?source=<its-slug> (slug: lowercase [a-z0-9-], max 32):

{ "ts": 1755500000000, "event": "PreToolUse", "sessionId": "run-42",
  "cwd": "/repo", "tool": "compile", "prompt": "...", "input": { "command": "..." } }

event is one of SessionStart, UserPromptSubmit, PreToolUse,
PostToolUse, SubagentStop, Stop, SessionEnd; everything else is
optional and truncated server-side. hooks/emit.js --source=<slug> relays
stdin (or a single JSON argument) there, or POST directly.

Prefer to wire Claude Code by hand? Add hook entries for all seven events —
SessionStart, UserPromptSubmit, PreToolUse (matcher *), PostToolUse
(matcher *), SubagentStop, Stop, SessionEnd — each running the same
command (replace the path with your clone's absolute path; --print generates
exactly this for every tool):

"hooks": {
  "SessionStart": [{ "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "UserPromptSubmit": [{ "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "PreToolUse": [{ "matcher": "*", "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "PostToolUse": [{ "matcher": "*", "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "SubagentStop": [{ "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "Stop": [{ "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}],
  "SessionEnd": [{ "hooks": [
    { "type": "command", "command": "node \"/path/to/Orrerium/hooks/emit.js\"", "timeout": 5 }
  ]}]
}

If Orrerium listens on a non-default port, set ORRERIUM_PORT in the environment
the hooks run in.

Events are translated per source, whitelisted and truncated server-side
(lib/agents.js — one adapter per wire dialect, everything downstream speaks
the canonical shape), appended to data/agent-events/YYYY-MM-DD.ndjson, and
replayed on server start so a restart never blanks the board (pre-0.3 log lines
without a source replay as claude-code). Foreign session ids are normalized
to URL-safe form at ingest. Subagent spawns are detected as PreToolUse of
the Agent tool (older Claude Code builds called it Task, and log days from
back then still hold those events, so both names count — isSubagentTool in
lib/agents.js). Only SubagentStop closes the oldest working subagent:
PostToolUse is not a completion, because a backgrounded agent returns its
handle within milliseconds and keeps running.

The Claude desktop app emits hook events of its own, so every session in the
snapshot carries a kind (classifySession in lib/agents.js): work for
real sessions, startup for the throwaway sessions the app opens and closes on
launch (Start+End pair, no prompt, no tools — one per recent project plus one
rooted at the home dir), and housekeeping for the bare-SessionEnd bursts
its periodic tick emits while finalizing old sessions (~every 2h at :28 past
the hour while the app is open). Those two heuristics apply only to
claude-code sessions — another tool's promptless ended session stays work.
The board's filter chips (All · Live · Idle · Ended · System) keep the two
system kinds out of every view except System, which shows them labeled for what
they are. Cron runs are never misclassified: the runner posts a synthetic
prompt event (tagged source: cron).

Flows (#/flows) replays the same log as a timeline: subagent spans stack
above the orchestrator lane (parallel agents get parallel lanes, packed
greedily by lib/flows.js), tool ticks and prompt diamonds ride lane 0, and a
scrubber replays the run (setInterval-driven — rAF pauses in hidden panes).
Live sessions redraw as events arrive.

Live agent activity paints the same snapshot onto the graph itself, in both
layouts. A live session resolves to its project node by matching cwd against
node dir (walking up parent directories, and filtered to project/repo nodes —
agent and command nodes carry their repo's dir too, so an unfiltered index
resolves a session to a random command). Each of its working subagents
resolves to that repo's agent node — same repo first, then a global agent, then
nothing, because lighting another repo's qa-agent would be a lie. The existing
scan edge between them then carries star dots from the project out to the
agent, a ring of sparks orbits every live node, and one ripple fires per spawn
or burst of tool calls. A session with no working subagent — the common case —
has no agent edge to carry traffic, so it radiates along its own strongest links
instead (frontmatter before body before tag, capped at four per node and picked
deterministically, or the d3 join churns and restarts every animation). Those
ambient sparks are thinner, dimmer and slower so attributable agent traffic still
reads as the stronger signal, they travel away from the live node whichever end
of the edge it sits on, and they deliberately do not light the far end: those
neighbours are context, not running work. Sessions decay hot → warm → dark on the
client (20s / 90s, the second matching the server's stale threshold), because a
quiet session produces no new snapshot to react to.

All of that motion is CSS keyframes, not a JS loop — rAF pauses in hidden
panes, so a per-frame animation would silently freeze. Travelling dots are a
near-zero dash on a copy of the edge with pathLength="100", so one
stroke-dashoffset keyframe fits every edge length; the orbit rotates about the
node's local origin via transform-box: view-box; transform-origin: 0 0. Under
prefers-reduced-motion: reduce the decorative traffic can withdraw entirely,
with live nodes still reading as live through their static styling — but that
branch is opt-in: motion defaults to On and only an explicit Auto on the
topbar motion toggle hands the decision back to the OS. (Windows users:
switching the OS "animation effects" off makes Chrome report reduce for every
page, which is exactly why the sparks — the live-activity read-out itself — do
not disappear by default.)

Icons (#/icons) assigns hand-authored pixel faces
(public/js/icons.js, 12×12 grids as data — no asset
pipeline) to scanned agents. Assignments live in data/icon-assignments.json
(GET /api/icons, POST /api/icons/assign) and every consumer — board rows,
graph agent nodes — repaints over the icons SSE event.

Crons

#/crons schedules headless claude -p runs. The scheduler is in-process
(a deliberate substrate choice over the OS scheduler: every definition
and run log stays a file an agent can read; nothing to reconcile with OS
state). Definitions live in data/crons.json — never in the vault — with a
small schedule grammar parsed by lib/cron-parse.js: every 30m,
daily@07:30, weekly@mon 09:00 (local wall-clock, DST-safe). Jobs fire only
while the server runs; a per-job catchUp flag runs a missed occurrence
once at startup (lib/crons.js). Runs never overlap themselves;
output is captured to data/cron-runs/<id>/<ts>.log with a tail in the run
record (data/cron-runs/<id>.ndjson). Each run also emits synthetic session
events, so scheduled runs appear on the Agents board and in Flows. The panel
has the job list (enable/run-now/delete), a week calendar (● past runs
green/red, ○ upcoming), and per-run output.

Stats

#/stats rolls up everything Orrerium logs into one page: stat tiles for the
last fortnight (sessions split interactive/cron, tool calls, errors, subagent
spawns, active time), a per-day bar chart of tool calls with errors overlaid,
the top tools, per-job cron health, and ask usage by model. Fully offline —
it reads the same data/ logs the Agents board and Crons panel write, so it
is empty until those features have something to log.

API (what an agent can use)

  • GET /api/graph{ generatedAt, vaultPath, nodes, edges, warnings }
  • GET /api/note/:slug{ id, path, folder, type, frontmatter, markdown } (also serves scanned claude assets, e.g. DemoApp.qa-agent)
  • POST /api/ask { question, history? }{ answer, provider, model } (markdown with [[wikilink]] citations; history is prior {role, content} turns; model is the resolved API model or, for the CLI, the model the session reported). Since 0.3, provider is the service name (anthropic, openai, gemini, grok, ollama, cli) — scripts that matched the old "api" value should match "anthropic".
  • POST /api/ask with "stream": trueapplication/x-ndjson: one {"type":"meta","provider"} line, {"type":"delta","text"} lines as the answer generates, closed by {"type":"done", answer, provider, model, conversationId} (authoritative full text) or {"type":"error", error}. The ask panel uses this; the plain JSON shape above stays for scripts.
  • GET /api/agents{ generatedAt, sessions: [...] } — the live board snapshot; each session carries its source
  • POST /api/hook-event[?source=<slug>] — agent hook payloads (whitelisted, truncated, logged). No source means claude-code; gemini-cli, codex and cron select their dialect adapters; any other slug is read as the normalized generic shape (see Agents board)
  • GET /api/flows{ sessions: [...] } — replayable sessions from the last fortnight's log
  • GET /api/stats{ generatedAt, days, totals, topTools, crons, ask } — per-day usage rollups over the same fortnight (sessions split interactive/cron, tool calls, errors, subagent spawns, active time), cron run health per job, and ask usage by provider+model
  • GET /api/flows/:sessionId{ sessionId, flow: { start, end, lanes, spans, ticks, prompts } }
  • GET /api/icons / POST /api/icons/assign { agent, icon|null }{ assignments }
  • GET /api/crons{ jobs } · POST /api/crons (upsert def) · POST /api/crons/:id/run · GET /api/crons/:id/runs · DELETE /api/crons/:id
  • GET /events → SSE, named events; event: vault with data: {"files": [...]} on every vault change (and after a claude-scan delta); event: agents with the board snapshot on every hook event

Test

node --test

Fixture mini-vault under test/fixtures/vault/ plus a smoke test against the
bundled starter vault.

Contributing

Bug reports, fixes, and macOS/Linux portability patches are welcome — please open an
issue before a PR so scope is agreed first. CONTRIBUTING.md has the
details, including the three design constraints (zero dependencies, never write the
vault, keep the parser modules pure) that a change must not break.

Security issues go through private reporting rather than a public issue — see
SECURITY.md, which also covers what to keep in mind when running it.

License

MIT. Vendored libraries keep their own licenses —
see public/vendor/README.md.

Roadmap

Everything from the original roadmap has shipped: streaming ask answers, a
retrieval step for oversized vaults, configurable folder→type mapping, and the
stats panel. New ideas are tracked as GitHub issues.

Reviews (0)

No results found