Orrerium
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Uyari
- 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 Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Local-first dashboard over a markdown knowledge vault: interactive graph, projects board, inbox triage, ask-your-brain. Zero dependencies.
Orrerium
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.

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:
- Use your own notes — see
Point it at your own vault. - AI questions over your notes (optional) — see
AI features. - Live agent tracking (optional, Claude Code users) — see
Agents board.
Troubleshooting
vaultPath is not a directory— the vault path you set doesn't exist
(orconfig.jsonpoints somewhere stale). Remember~is not expanded inconfig.json.Port 4321 is already in use— another Orrerium (or something else) owns
the port. Stop it, or setportinconfig.json. If hooks should reach a
non-default port, also setORRERIUM_PORTwhere 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 viafolderTypesinconfig.json. - A root
README.md— sits at the centre of the Rings layout as the vault's
index. - Frontmatter —
name, one-line quoteddescription,type,updated;
project pages carrystatusanddir(the projects board is built from
these);projects: ["[[slug]]"]wikilinks cluster the graph. - An
inbox.mdcapture buffer — feeds the Inbox panel, withcaptureandtriageskills 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 |
projects → project, … |
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.providerinconfig.json:Provider Key env var Default model anthropicANTHROPIC_API_KEYclaude-opus-5openaiOPENAI_API_KEYgpt-5.6-terrageminiGEMINI_API_KEY(orGOOGLE_API_KEY)gemini-3.7-flashgrokXAI_API_KEYgrok-4.5ollamanone — local none: set ai.modelclinone — the Claude Code CLI's own login the CLI's configured model auto(the default) walksanthropic → openai → gemini → grokand picks the
first key it finds, else falls back tocli.GOOGLE_API_KEYcounts only whengeminiis chosen explicitly — it exists on too many machines for unrelated
reasons to trigger auto-detection. Local models: set"provider": "ollama", "model": "llama3"(andbaseUrlfor a non-default
port); any other OpenAI-compatible server works viabaseUrlon 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 headlessclaude -pjobs and always needs the
Claude CLI, whateverai.providersays.
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— plainnode: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.jsis its client twin: one sharedEventSource, panels subscribe by event name.lib/store.js— Orrerium-owned writable state underdata/(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}acrossclaudeScan.roots+ the global dir become agent/command/routine nodes (namespaced ids likeDemoApp.qa-agent, shortlabelfor display), merged onto the graph with onescanedge to the repo's vault project note (matched ondirfrontmatter) or a synthetic repo anchor.lib/vault.js— pure parser: frontmatter (vault dialect) + wikilink/md-link extraction, plus.claude/skills/*/SKILL.mdas 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 fromapplicationTagswith tag edges to every note carrying the tag; routine edges from real markdown links andx.mdmentions in skill bodies.lib/watch.js— debounced recursivefs.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 andrefusalhandling), one OpenAI-compatible Chat Completions adapter covering OpenAI/Gemini/Grok/Ollama (only base URL, key env var and default model differ), or the localclaudeCLI. 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/askrelays 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, whichgraph-view.jspaints.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.jsonwas 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 rendersinbox.mdcaptures and flags the triage threshold. The ask panel is multi-turn — prior Q/A pairs ride along with each request.window.orreriumin 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
notifykey 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 anotifyyou 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 inlib/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 toclaude-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 existingscan 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 onestroke-dashoffset keyframe fits every edge length; the orbit rotates about the
node's local origin via transform-box: view-box; transform-origin: 0 0. Underprefers-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;historyis prior{role, content}turns;modelis the resolved API model or, for the CLI, the model the session reported). Since 0.3,provideris the service name (anthropic,openai,gemini,grok,ollama,cli) — scripts that matched the old"api"value should match"anthropic".POST /api/askwith"stream": true→application/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 itssourcePOST /api/hook-event[?source=<slug>]— agent hook payloads (whitelisted, truncated, logged). Nosourcemeansclaude-code;gemini-cli,codexandcronselect 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 logGET /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+modelGET /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/:idGET /events→ SSE, named events;event: vaultwithdata: {"files": [...]}on every vault change (and after a claude-scan delta);event: agentswith 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.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi