Real-time, git-native collaboration for Markdown: a CLI, multiplayer editing, and a structured agent API, all local-first with no hosted service.
Think of it as "Google Docs for .md files" that lives in your repo. Humans and
AI agents work the same document together, live, with comments, suggestions,
presence, and authorship/provenance, all stored inside the Markdown file and
versioned by plain git.
Most "Markdown for agents" tools stop at letting an agent edit a file on disk.
mddocs goes further: agents are first-class collaborators over a structured HTTP
API. They read document state, post comments and suggestions a human can accept
or reject, rewrite prose, announce presence, and poll a live event stream of what
humans and other agents are doing, each bound to its own token identity
(ai:<model>). That is collaboration, not just file access.
The editor with an AI reviewer's mark live in the document: a comment on the
opening line asking for a version and release date. Open the comment to see the
thread, attributed to ai:claude-opus-4-8:
Prefer the terminal? The same human-plus-agent review, end to end:
Above: a human and an AI reviewer agent on the same notes.md. The agent reads
the live document, leaves a comment, and proposes a fix over the agent API; the
human accepts it from the CLI; and every change is an ordinary git commit with
authorship recorded in the file. Reproduce it with
examples/demo.tape (vhs examples/demo.tape).
mddocs is a thin, self-hostable layer built on the MIT-licensed
proof-sdk. It reuses Proof's marks
model and browser editor, and adds a local-first, git-backed workflow, a
command-line interface, a real-time collaboration server, and an agent HTTP API,
all keeping the .md file plus git as the single source of truth.
Install from npm:
npm install -g @devyrpauli/mddocs
mddocs --helpThe npm package is published under the @devyrpauli scope; the command it
installs is mddocs. You can also run from source (see
Install from source).
Proof is a good collaborative Markdown editor, and proof-sdk is open-source,
but the usual way to self-host collaboration is to stand up a database-backed
server. mddocs takes a different approach:
- The file is the database. Comments, suggestions, and provenance are serialized
into a
<!-- PROOF ... -->JSON footer inside the same.mdfile. Open the file anywhere and the collaboration state travels with it. - git is the history and async-sync layer. Edits are ordinary commits. Async
collaboration is just branches and merges; a conflicted footer is union-resolved
by
mddocs resolve. - Live multiplayer is a relay, not a new source of truth.
mddocs servehosts a real-time session (Yjs over WebSocket); every settled change is written straight back to the.mdand auto-committed. The database never takes over. - The CLI and HTTP API are clients too. Add a comment, file a suggestion, or read state without a browser, which is useful for scripts and AI agents.
| Capability | How |
|---|---|
| Browser editor (comments, suggestions, provenance) | mddocs open <file> (single-user) or mddocs serve <file> (multiplayer) |
| Real-time multiplayer with presence | mddocs serve <file>: everyone on the URL co-edits live; edits persist to the file plus git |
| Role-based share links (editor / commenter / viewer) | serve prints a link per role; roles enforced server-side (viewers read-only, commenters cannot edit prose) |
| Agent HTTP API | AI tools read state, post comments/suggestions, rewrite prose, announce presence, and poll document events live, attributed to ai:<model> |
| Comments and suggestions from the terminal | mddocs comment ..., mddocs suggest ..., mddocs accept/reject |
| Repo-wide review inbox | mddocs status lists every open comment and pending suggestion across all managed docs (--all includes resolved/accepted/rejected) |
| History and diff | mddocs log <file>, mddocs diff <file> [rev] (plain git underneath) |
| Async multiplayer and conflict resolution | edit on branches; mddocs resolve <file> unions a conflicted PROOF footer |
| Authorship and provenance | every mark records by (human:<user> or ai:<model>) and at |
| Re-anchoring | marks re-attach to their quoted text after external edits; unmatched marks are flagged orphaned, never dropped |
- Node 20+ (developed on v24)
- git on your PATH (for history and multiplayer)
For development, or to run the latest from the repo:
git clone https://github.com/devYRPauli/mddocs.git
cd mddocs
npm install
npm run build # builds the browser editor bundle into dist/ (needed for open/serve)npm run build only needs to be re-run if you change the editor or @proof/*
sources. The CLI itself runs straight from source via tsx.
To run the CLI from a source checkout without installing it globally, go
through tsx:
alias mddocs='npx tsx "$(pwd)/packages/mddocs-cli/src/bin.ts"'The examples below use mddocs as if it were installed on your PATH.
# In a git repo holding your markdown:
git init # if it isn't one already
mddocs init # mark .md files as mddocs-managed (.gitattributes)
# Single-user editing in the browser (comments/suggestions persist to the file):
mddocs open notes.md
# A live multiplayer session you can share on your LAN:
mddocs serve notes.md # prints role links and an agent API block; Ctrl-C to stop
# Or collaborate straight from the terminal:
mddocs comment add notes.md --quote "the API is fast" --text "cite a benchmark?"
mddocs suggest add notes.md --quote "teh" --replace "the"
mddocs suggest ls notes.md # list suggestions and their ids/status
mddocs accept <suggestion-id> --file notes.md
# See everything that needs attention across every managed doc:
mddocs status # open comments + pending suggestions (--all for the rest)
# History is just git:
mddocs log notes.md
mddocs diff notes.mdmddocs serve notes.md [--port <n>] [--host <ip>] [--no-autocommit]Hosts a live editing session on one port: the browser editor, a Yjs/WebSocket
collaboration channel, and the agent API. Everyone who opens the URL co-edits the
same document in real time; every settled change is serialized back to notes.md
and auto-committed to git. Use --host 0.0.0.0 to share on your LAN.
serve prints three role links. Share the one matching the access you want to
grant:
| Link | Role | Can do |
|---|---|---|
| edit (you) | editor | read, comment, edit |
| comment link | commenter | read, comment |
| view link | viewer | read only |
- An absent or unknown token gets the least privilege (viewer), so a leaked bare URL cannot edit.
- Roles are enforced server-side, not just in the editor UI. A viewer's WebSocket connection is read-only, so a crafted client cannot write at all. A commenter may write comments (a comment is a write to the marks map) but cannot edit the prose: any prose change from a commenter connection is reverted server-side before it persists or reaches other clients. Editors can do both.
A live serve session also exposes an HTTP API so AI agents can read the
document, post comments/suggestions, and edit the prose directly. Everything
appears in every connected editor in real time and persists to git, attributed to
ai:<model>. serve prints the base URL and an agent token; send it as the
x-share-token header.
GET /api/agent/:slug/state -> { content, marks, presence }
POST /api/agent/:slug/comment { quote, text, model? } -> { id }
POST /api/agent/:slug/reply { id, text, model? } -> { id, replies }
POST /api/agent/:slug/suggest { quote, replace|insert|delete, model? } -> { id, kind }
POST /api/agent/:slug/rewrite { markdown, quote?, model? } -> { chars, by, markId? }
POST /api/agent/:slug/presence { name?, status?, details? } -> { presence }
POST /api/agent/:slug/presence/disconnect -> { disconnected }
GET /api/agent/:slug/events/pending?after=<id>&limit=<n> -> { events, cursor }
GET /api/agent/:slug/events/stream?after=<id> (SSE) -> text/event-stream
POST /api/agent/:slug/events/ack { upToId } -> { acked }
reply appends to an existing comment thread (the same threads the CLI's
comment reply writes to); id is the comment mark id from state or a prior
comment call. suggest proposes a change a human accepts; rewrite edits the prose directly.
With a quote, rewrite replaces that span; without one it replaces the whole
body. The change is applied to the live document and recorded as an authored mark.
presence lets an agent announce it is active on the document (with a status
like reviewing and a free-text details); it shows up in every state read's
presence array and as an agent.presence event. presence/disconnect removes
the caller's own presence (and emits agent.disconnected). The presence identity
and every event actor are bound to the agent token (ai:<name>, from the
token's --agent name) and cannot be set from the request, so one agent cannot
impersonate or disconnect another. Run serve --agent <name> (repeatable) to
give each agent its own token and identity.
events/pending is how an agent reacts to what humans and other agents did,
without holding a WebSocket. Both browser edits and agent mutations land on the
same live document, so all activity surfaces uniformly:
mark.added/mark.updated/mark.removed- a comment, suggestion, accept/reject, reply, or provenance mark changed (data.markId,data.kind,data.by, anddata.statusfor suggestions).document.changed- the prose changed (coalesced while someone is typing).agent.presence/agent.disconnected- presence came or went.
Poll with ?after=<cursor> to receive only events newer than the last cursor
you saw; each event carries a monotonic id and an actor (ai:<model>,
human:<name>, or unknown). Call events/ack with upToId once you have
handled them. Events are kept in memory for the life of the serve session.
For push instead of poll, events/stream delivers the same events over
Server-Sent Events. Each frame is id: <n>, event: <type>, data: <event JSON>
(the same shape events/pending returns), so a client reacts the instant a human
comments or another agent edits. On (re)connect it replays the in-memory backlog
from ?after=<id> or the standard Last-Event-ID header, then streams live; a
20-second heartbeat comment keeps the connection alive. It does not ack: track
the last id you saw and reconnect with it. events/pending remains the simple
poll alternative for clients that cannot hold a connection.
By default serve issues one shared agent token. Pass --agent <name> (repeatable)
to register named agents, each with its own token; serve then prints a token per
agent. A request that omits model is attributed to the token's agent name
(ai:<name>). Per-agent rate limits are available through the engine API
(serveShare({ agents: [{ name, rateLimit: { maxRequests, windowMs } }] })),
returning HTTP 429 when exceeded. When a limit is configured, every agent API
response carries X-RateLimit-Limit, X-RateLimit-Remaining, and
X-RateLimit-Reset (unix seconds), and a 429 adds Retry-After, so an agent can
self-throttle instead of being surprised by the 429.
# Read the live document:
curl -H "x-share-token: $TOKEN" http://127.0.0.1:<port>/api/agent/notes.md/state
# Post a comment as an agent:
curl -X POST -H "x-share-token: $TOKEN" -H 'content-type: application/json' \
-d '{"quote":"The latency is acceptable.","text":"Quantify, p50 or p99?","model":"claude-opus-4-8"}' \
http://127.0.0.1:<port>/api/agent/notes.md/comment
# Stream events live (SSE):
curl -N -H "x-share-token: $TOKEN" \
http://127.0.0.1:<port>/api/agent/notes.md/events/streamSee examples/agent-reviewer.mjs for a runnable
reviewer agent and a human-plus-agent walkthrough, and
examples/agent-watcher.mjs for a runnable SSE
consumer that prints document events live (with ?after/Last-Event-ID replay).
When two people edit a document on different branches and you git merge, the
prose merges normally but the <!-- PROOF --> footer can conflict. Union both
sides' marks:
git merge other-branch # may leave a conflicted footer
mddocs resolve notes.md # unions both sides' marks; on id collision, latest `at` wins
git add notes.md && git commitmddocs open <file> [--port <n>] [--no-autocommit] single-user browser editor (loopback)
mddocs serve <file> [--port <n>] [--host <ip>] [--no-autocommit] [--agent <name>]
live multiplayer, role links, agent API
mddocs init mark the repo as mddocs-managed
mddocs resolve <file> union a git-conflicted PROOF footer
mddocs comment add <file> --quote <q> --text <t> add a comment anchored to <q>
mddocs comment ls <file> [--open|--resolved|--orphaned]
mddocs comment reply <id> --text <t> [--file <f>] reply in a comment thread
mddocs comment resolve <id> [--file <f>] resolve a comment thread
mddocs suggest add <file> --quote <q> (--replace <c> | --insert <c> | --delete)
mddocs suggest ls <file> [--pending|--accepted|--rejected|--orphaned]
mddocs accept <id> [--file <f>] apply a suggestion to the prose
mddocs reject <id> [--file <f>] mark a suggestion rejected
mddocs log <file> commit history for a document
mddocs diff <file> [rev] changes vs working tree or a revision
Notes. Id-only commands (reply, resolve, accept, reject) find their
document automatically by scanning the managed .md files for the mark; pass
--file <path> to skip the scan or disambiguate. accept applies the suggested
change to the prose (replace, insert, or delete, anchored by the suggestion's
quote) and keeps the suggestion as an accepted record, preserving who proposed it
(by); reject records the decision on the mark and leaves the prose unchanged.
Both decisions stay in the file as provenance.
Auto-commit. In a git repo, the mutating commands (comment add/reply/resolve,
suggest add, accept, reject) commit just the changed file with an action- and
actor-attributed message, so mddocs log reflects terminal edits the way a live
serve session already auto-commits, for example:
mddocs: comment by human:sam on notes.md
mddocs: accept suggestion (proposed by ai:claude-opus-4-8) in notes.md
The git author stays whoever runs the command (you own the repo); the agent/human
that originated the change is recorded in the message and in the mark's by. Pass
--no-commit to leave the edit in the working tree, or run outside a git repo to
skip committing entirely.
mddocs/ (fork of the proof-sdk monorepo)
packages/doc-core @proof/core reused marks model, embed/extract, anchoring
packages/doc-editor @proof/editor reused browser editor (served from dist/)
packages/mddocs-local mddocs-local new engine
doc.ts loadDoc / saveDoc (atomic, embed/extract)
reanchor.ts re-resolve marks against current text
footer.ts detect and union-resolve a conflicted PROOF footer
git.ts history / diff / commit (simple-git)
serve.ts single-user editor host (HTTP file API)
collab.ts file-backed Hocuspocus server (live relay)
serialize.ts prosemirror-fragment to/from markdown (headless Milkdown boundary)
share.ts multiplayer host: role links, bootstrap, WS, agent routes
agent.ts agent operations over a live Hocuspocus DirectConnection
packages/mddocs-cli mddocs-cli new commander CLI over the engine
Live multiplayer reuses upstream's Yjs and Hocuspocus stack as an in-memory
concurrency layer only; the resolved markdown and marks are persisted through the
same engine path as the CLI (saveDoc, reanchor, debounced git commit). The
editor's canonical content is the prosemirror Y.XmlFragment, which serialize.ts
converts to and from markdown using upstream's headless Milkdown serializer.
Every call into @proof/core goes through one adapter
(packages/mddocs-local/src/proof.ts).
A managed document is just Markdown with a trailing footer:
# My document
The body everyone reads and diffs normally.
<!-- PROOF
{"version":2,"marks":{"<id>":{"kind":"comment","by":"human:alice","at":"...","quote":"...","data":{"text":"...","resolved":false}}}}
-->loadDoc/saveDoc split and rejoin this footer; the prose above it stays clean,
diffable text.
npm test -w mddocs-local # engine unit and integration tests (incl. headless collab/agent)
npm test -w mddocs-cli # CLI integration tests against real temp git repos
npm run typecheck -w mddocs-local
npm run typecheck -w mddocs-cliThe live collaboration and agent paths are covered headlessly (real
HocuspocusProvider clients driven in-process, no browser needed); the
browser-interactive path is verified manually.
- M1: local-first editor and CLI (comments, suggestions, provenance, git history). Done.
- M2: live collaboration server (real-time multiplayer, file plus git canonical). Done.
- M2.5: share links and roles (editor/commenter/viewer, server-side role enforcement: viewers read-only, commenters cannot edit prose). Done.
- M3: agent HTTP API (read state, comment, suggest, and rewrite prose live). Done.
- M3.5: agent presence and events (announce activity; poll mark/prose/presence changes from humans and other agents with an after-cursor and ack). Done.
Contributions welcome. Next on the list:
- Upstream the
@proof/coreTS2308 fix (proof-sdk#57) and drop the local fork patch once merged.
Built on proof-sdk (MIT, Every Inc).
The packages/doc-*, src/, and server/ trees originate from proof-sdk and
retain its license; see LICENSE, NOTICE.md, and
TRADEMARKS.md. The original upstream README is preserved as
README.proof-sdk.md. "Proof" is a trademark of Every
Inc.; this project is not affiliated with or endorsed by them.
Local modifications to the vendored SDK are tracked in
FORK_PATCHES.md.


