Architecture
OpenKnowledge is a Bun + Turbo monorepo of six packages plus a docs site. The spine that ties them together is a single idea: one markdown document is, at the same time, a file on disk, a CRDT in memory, a git history, and an MCP resource. Every package is a different view onto that one object.
1. The one-object model
┌─────────────────────────────────────────────────────────┐
│ one knowledge document │
├───────────────┬───────────────┬──────────────┬──────────┤
│ file on disk │ CRDT in RAM │ git history │ MCP res. │
│ notes/x.md │ Y.Doc │ refs/wip/… │ exec/edit│
└───────┬───────┴───────┬───────┴──────┬───────┴────┬─────┘
│ │ │ │
external-change Hocuspocus shadow-repo server/mcp
(disk watcher) (y-websocket) (simple-git) (tool handlers)
The four representations are kept in sync by four subsystems, all living in
packages/server:
- CRDT ↔ disk:
external-change.tswatches the filesystem and applies disk edits into the liveY.Doc; persistence flushes theY.Docback to.mdon quiescence. A document carries two Yjs types:Y.Text('source')holds the raw markdown,Y.XmlFragment('default')holds the ProseMirror rich-text state (packages/server/src/external-change.ts:63-64). - CRDT ↔ clients: a Hocuspocus (Yjs WebSocket) server fans document and
awareness updates to every connected editor and every connected agent
(
packages/server/src/server-factory.ts:513-518). - CRDT ↔ git: on quiescence, persistence commits the document tree into a
shadow git repo, one commit per active writer (
packages/server/src/persistence.ts:350-437). - CRDT ↔ agents: the MCP
write/edittools mutate the CRDT layer, so an agent's edit is indistinguishable from a human's once it lands.
Because the markdown source itself is a CRDT field, agents and humans editing
the same file at the same time merge automatically — no lock, no "AI suggestion"
side-channel. That seam is the subject of 06_collaboration_crdt.md.
2. The document schema is pinned
The editor's capabilities (which ProseMirror nodes/marks/attributes exist) are
captured in a schema snapshot dumped by packages/core/scripts/dump-schema.ts:71-119
from @tiptap/core plus the active MDAST plugins. A CI check
(scripts/check-schema-snapshot-clean.sh) fails the build if the schema drifts
without an intentional update.
Why this matters: the markdown ↔ rich-text bridge is lossy if the two sides
disagree about what a node is. Pinning the schema means a .md written by an
old client and re-opened by a new one round-trips deterministically — a
prerequisite for letting agents and humans on different app versions co-edit.
3. Process topology
OpenKnowledge runs as a small constellation of processes, not one server:
ok start
├── collaboration server (HTTP + Hocuspocus WS on :api-port) packages/server
│ ├── persistence extension → git shadow repo
│ ├── liveDerivedIndex extension → backlink / tag indexes
│ ├── principalAuth extension → token + branch validation
│ ├── cc1Broadcaster extension → push file/graph/tag changes
│ └── serverObserver extension → agent/human presence
├── ok ui (static web editor on :39847, detached child) packages/cli/commands/ui.ts
└── ok mcp (stdio MCP server, one per connected agent) packages/cli/mcp/server.ts
runStartCommand→bootStartServerboots the collab server, then auto-spawns the UI as a detached sibling process and prints the local URLs (packages/cli/src/commands/start.ts:560-708, banner at:673-682).- The UI serves the compiled
packages/app/dist/React bundle and upgrades the/collabpath to a Hocuspocus WebSocket (packages/cli/src/commands/ui.ts). - Each agent that loads the OK MCP server gets its own stdio
ok mcpprocess (packages/cli/src/commands/mcp.ts:14-98); that process connects to the collab server as just another Yjs client.
The Hocuspocus extension stack (server-factory.ts:545-699) is the clean part of
the design: persistence, derived indexes, auth, broadcast, and presence are five
orthogonal extensions over one document stream, not five tangled code paths.
4. Entry points
| Entry | File | What it starts |
|---|---|---|
ok CLI |
packages/cli/src/cli.ts (Commander, ~23 commands at :65-168) |
everything below |
ok init |
packages/cli/src/commands/init.ts:587 (runInit) |
scaffold .ok/, wire harnesses, install skills |
ok start |
packages/cli/src/commands/start.ts:560 |
collab server + UI |
ok mcp |
packages/cli/src/commands/mcp.ts:14 |
stdio MCP server for an agent |
| Desktop app | packages/desktop/src/main/index.ts (Electron main) |
windows, terminal, keyring, deep links |
| Web editor | packages/app (React + TipTap) |
the WYSIWYG surface |
5. Data-flow: an agent writes a doc
- Agent (in Claude Code) calls MCP tool
write { path, content }on its stdiook mcpprocess. - The tool handler (
packages/server/src/mcp/tools/) connects to the collab server and applies the change to the document'sY.Text('source')/Y.XmlFragment('default')via@tiptap/y-tiptap(persistence.ts:25). - Hocuspocus broadcasts the Yjs update to every other client — the human's open editor renders the agent's edit live, with the agent's avatar in the presence bar.
- After a debounce of quiescence, persistence builds one git tree and writes one
commit per active writer onto
refs/wip/main/{writerId}in the shadow repo, footers carrying the agent's identity (persistence.ts:350-437,shadow-repo.ts:227-323). - If GitHub sync is on, those commits are pushed to the remote; conflicts freeze
the affected docs for humans and agents (
docs/content/features/github-sync.mdx).
Every later doc expands one of these five steps.