API and CLI
Install / build
Requires Node ≥ 20.
npm install npm run build # compiles src/ -> dist/ (tsc) npm test # runs the vitest suite
The package exposes the mdd CLI binary (dist/cli.js via the bin field) and a programmatic API importable from the package root. npm run typecheck runs tsc --noEmit.
The mdd CLI
mdd <command> Commands: init --file <path> --id <name> [--json <path>] [--prefix <str>] [--kind sequence] render --json <path> [--file <path> --id <name>] [--prefix <str>] [--format text|json] validate --json <path> [--format json] check [paths...] [--format text|json] skill install [--dir <path>]
init— scaffolds an emptymdd:begin/mdd:endblock plus an empty sidecar JSON, in Markdown or (comment-prefixed) in a source file.--kind sequencescaffolds a sequence sidecar ({ version: 1, kind: "sequence", participants: [], messages: [] }) instead of a box diagram.render— validates and renders. With--file/--idit splices the ASCII into that file's block — comment-prefixed when the file isn't Markdown — and rewrites the sidecar; without them it prints ASCII to stdout. Idempotent.validate— validation only; printsokor one error per line and exits non-zero on failure.check— sweeps Markdown and source files formddblocks and fails (exit 1) if any sidecar is missing, invalid, or its rendered ASCII is stale. Fenced examples are skipped; zero blocks is a pass — safe as a one-line CI step.skill install— copies the packaged Claude Code skill into.claude/skills/mdd/so coding agents author diagrams with mdd instead of hand-drawing ASCII.
--prefix (on init and render) sets the line-comment prefix used when --file is a source file rather than Markdown: the exact string — indentation included — prepended to every line of the block, so --prefix "// " writes // mdd:begin id=… src=… and comments out each ASCII line the same way. It must contain at least one non-whitespace character and no letters or digits. You rarely need it: known extensions pick their prefix automatically (// for .ts/.go/…, # for .py/.sh/…, -- for .sql/…), and updating an existing block always reuses the prefix already on its marker. Reach for the flag only when scaffolding into an extension mdd doesn't recognize, or to override the default — e.g. --prefix " * " to sit inside a /* */ comment you opened yourself.
Exit codes: 0 success, 1 validation failure, 2 bad usage/args. --format json on render/validate/check emits machine-readable output so an agent can self-correct without scraping text.
Every command is kind-aware: render, validate, and check — and both splice styles, Markdown and comment-prefix — treat a kind: "sequence" sidecar identically to a box diagram. init --kind sequence is the only place a kind is ever chosen; everything downstream dispatches on the sidecar itself.
Worked example
Box diagram
arch.diagram.json:
{
"version": 1,
"boxes": [
{ "id": "lb", "label": "Load Balancer", "row": 0, "style": 2,
"connections": [
{ "to": "api1", "style": 2, "arrow": "to" },
{ "to": "api2", "style": 2, "arrow": "to" }
] },
{ "id": "api1", "label": "API Server A", "row": 1, "style": 2, "connections": [] },
{ "id": "api2", "label": "API Server B", "row": 1, "style": 2, "connections": [] }
]
}mdd render --json arch.diagram.json --file arch.md --id arch
renders (and splices into arch.md):
┌───────────────┐
│ Load Balancer │
└───────┬───────┘
┌─────────┴────────┐
│ │
┌───────▼──────┐ ┌───────▼──────┐
│ API Server A │ │ API Server B │
└──────────────┘ └──────────────┘Sequence diagram
Every sequence feature in one sidecar: an actor participant, a weight-3 header, a labeled call, a self-message, a dashed reply, and a bare label-less arrow. checkout.diagram.json:
{
"version": 1,
"kind": "sequence",
"participants": [
{ "id": "user", "label": "User", "actor": true },
{ "id": "web", "label": "Web App" },
{ "id": "db", "label": "Orders DB", "style": 3 }
],
"messages": [
{ "from": "user", "to": "web", "label": "checkout" },
{ "from": "web", "to": "web", "label": "validate cart" },
{ "from": "web", "to": "db", "label": "INSERT order" },
{ "from": "db", "to": "web", "label": "order id", "reply": true },
{ "from": "web", "to": "user", "reply": true }
]
}mdd render --json checkout.diagram.json --file checkout.md --id checkout
renders (and splices into checkout.md):
o ┌─────────┐ ┏━━━━━━━━━━━┓ /|\ │ Web App │ ┃ Orders DB ┃ / \ └─────────┘ ┗━━━━━━━━━━━┛ User │ │ │ │ │ │ checkout │ │ │─────────▶│ │ │ │ │ │ │ validate cart │ │ │──┐ │ │ │◀─┘ │ │ │ │ │ │ INSERT order │ │ │──────────────▶│ │ │ │ │ │ order id │ │ │◀┄┄┄┄┄┄┄┄┄┄┄┄┄┄│ │ │ │ │◀┄┄┄┄┄┄┄┄┄│ │ │ │ │
Programmatic API
Importable from the package root (mdd-markdown-diagram):
A Diagram is a kind-discriminated union — BoxDiagram | SequenceDiagram — and every function here that takes a diagram accepts either kind, dispatching on kind internally. isSequenceDiagram(d) narrows the union. Sequence renders never produce warnings: columns widen to fit their labels, so nothing can be dropped.
{ text, warnings }. Warnings are ValidationErrors; connection-scoped ones (edge-label-unplaced) also carry the target box id as to. renderDiagram returns only text.kind: "sequence" scaffolds a sequence sidecar instead of a box diagram — the API twin of init --kind sequence.Model mutation ops
Pure functions over an in-memory BoxDiagram (see the note below the table):
{ version: 1, boxes: [] } — a BoxDiagram.{ version: 1, kind: "sequence", participants: [], messages: [] }.Diagram to SequenceDiagram — the discriminator for code that handles both kinds.connect options include style, arrow, and fromRow/toRow row anchors. disconnect(d, from, to) removes all to that target; disconnectAt(d, from, index) removes one by index.Spacing).Spacing).connect, disconnect and disconnectAt above already accept a cluster id as the owner (and a cluster id as the target), so a cluster's own connections list supports parallel edges exactly like a box's. Removing a cluster keeps its member boxes and strips any connection pointing at it.cluster-padding (a Spacing). Passing an empty or whitespace-only label deletes the property, leaving a valid unlabeled cluster — addCluster's label is optional for the same reason.Every other op above takes a BoxDiagram — there are no sequence mutation ops in v1. A sequence sidecar is two flat arrays (participants, messages) edited as plain JSON: array order is the only layout input, so build one with emptySequenceDiagram() and ordinary array pushes.
The browser entry (mdd-markdown-diagram/browser) additionally exports resolveBoxPadding and resolveBoxMargin, which collapse a Spacing (with box + diagram defaults) into a concrete { x, y } — used by the editor's property panel — plus renderCommentBlock and prefixError, which the editor's comment-prefix export uses.
Validation reference
validate() returns a ValidationError[] with these codes. Errors name what they flag: boxId / clusterId on box diagrams, participantId on sequence participants — and for message-scoped errors, connIndex doubles as the message's index in messages.
participants/messages is not, or a participant id is not a non-empty string. → Make the flagged field a JSON array; give every participant a string id.{x,y} of them). → Use ≥ 1 per axis.{x,y} of them). → Use ≥ 0 per axis (its floor is 0, unlike box-padding's 1).{x,y} of them). → Use ≥ 0 per axis."diamond", or is combined with table. → Omit shape, or drop the table.fromRow and toRow to two different rows of the box's table, or remove the connection.kind is present but not "sequence". → Omit kind for a box diagram, or set it to "sequence".from or to doesn't match a participant id. → Point to an existing participant id.boxes, clusters, box-padding, box-margin) appear on a sequence diagram, or sequence-only fields (participants, messages) appear on a box diagram. → Remove the fields that don't belong to that sidecar's kind.