# link.md agent guide

A terse, agent-facing reference for collaborating on a single link.md share.
This is the canonical entry point for LLM agents — fetch it directly when handed an agent invitation.

## Reference clients

Pick the path that matches your runtime. All three speak the same protocol; the rest of this document covers the wire-level details.

- **link-md skill** (shell + outbound network — e.g. Claude Code, Cursor, Aider). A Bash CLI that wraps every endpoint below in one command:

  ```
  publish.sh <file.md> --share <share_id> --agent-token <token>
  ```

  Source and install instructions: https://link.md/skill

- **link-md MCP server** (sandboxed runtimes — e.g. ChatGPT, Claude Desktop, MCP-capable IDEs). A Node stdio server that exposes the endpoints below as MCP tools, so the host (not your sandbox) makes the network calls. Configure your host with the env vars `LINKMD_AGENT_TOKEN` and `LINKMD_SHARE_ID` (or `LINKMD_API_KEY`). Source: https://link.md/skill/mcp

- **Raw HTTP** (always works). The rest of this document. Use this if you have `fetch`/`curl`/equivalent and nothing fancier.

## Identity & auth

You've been issued a Bearer token scoped to one share. Send these on every request:

```
Authorization: Bearer <token>
X-Agent-Id: <agent_name>
```

The `X-Agent-Id` header is informational; the server derives your identity from the token. Tokens are revocable at any time by the share owner.

## Endpoints

All paths are under `/api/v1/shares/<share_id>/`. Replace `<share_id>` with the ID from your invitation.

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/state` | Read current `{ version, content, title, awareness }` |
| `POST` | `/ops` | Submit an edit `{ version, content, title }` |
| `POST` | `/presence` | Heartbeat `{ cursor?: { anchor, head } | null }` |
| `GET` | `/files/<path>` | Read a sibling file (image, sub-markdown, etc.) |
| `PUT` | `/files/<path>` | Upload or replace a file (raw body, set `Content-Type`) |
| `DELETE` | `/files/<path>` | Delete a file (the entry file cannot be deleted) |

The agent token only works for the share it was issued for. Calls against any other share return `403`. Storage quota and rate limits for file writes are charged to the inviting user (not the agent identity).

## Edit protocol

1. `GET /state` — capture `version` and `content`.
2. Compute the new content (full document, not a diff).
3. `POST /ops` with `{ "version": <captured>, "content": "<new>", "title": "<new>" }`.
4. On `200`, the response is `{ "version": <next> }` — your edit was accepted and broadcast to live editors.
5. On `409`, the response is `{ "error": "conflict", "version": <current>, "content": <current>, "title": <current> }` — someone else edited first. Rebase your change against the returned state and retry.

Edits are full-document replacements at the wire level. The server handles broadcast diffs and cursor preservation for live human editors automatically.

## Presence

`POST /presence` registers (or refreshes) your cursor in the share's awareness list. Suggested cadence: every 60 seconds. TTL: 90 seconds — missed pings drop your cursor from peers' editors.

Request body:

```json
{ "cursor": { "anchor": 17, "head": 17 } }
```

- `cursor: null` clears your cursor visibly but keeps you in the presence list.
- `{}` is a pure liveness ping (no cursor update).

Your presence appears in `state.awareness` as `[<your-name>, { "agent": { "name": "<your-name>" }, "cursor": ... }]`.

## Attribution

Edits you broadcast include `by: { "agent": "<your-name>" }` on the wire so human editors can label them. Human edits include `by: { "user": "<id>" }` (anonymous edits omit `by`).

## Error codes

| Status | Meaning |
|--------|---------|
| `401` | Token invalid or revoked |
| `403` | Token doesn't match this share, or you are a `viewer` role attempting to write |
| `409` | Version conflict on `/ops` — body includes current state, rebase and retry |
| `410` | Share has expired |

## Adding images and sibling files

To add an image and reference it from the markdown:

```
PUT /api/v1/shares/<share_id>/files/diagram.png
Content-Type: image/png
<raw bytes>
```

Then reference it in the document content (via `/ops`):

```markdown
![Architecture diagram](diagram.png)
```

Supported uploads: PNG, JPEG, GIF, WebP, SVG (sanitized), PDF, and any `text/*` MIME type (including additional `.md` files). Filename must match `[a-zA-Z0-9_.-]+`. Maximum file size and per-share total are governed by the inviter's account tier — exceeding either returns `413`.

You can also upload sibling markdown files to attach related notes:

```
PUT /api/v1/shares/<share_id>/files/notes.md
Content-Type: text/markdown
<raw markdown>
```

The entry file (the doc your `/ops` edits target) cannot be deleted via `DELETE /files/...`.

## Supported markdown

The document is parsed as CommonMark with GFM (`gfm: true`, `breaks: true`) plus link.md-specific extensions:

- **Task lists** — `- [ ] todo`, `- [x] done`, and any single character inside the brackets (e.g. `- [-] cancelled`, `- [>] forwarded`) renders as a checkbox with the raw character preserved on `data-task` for theming.
- **Math** — inline `$E = mc^2$` and block `$$ ... $$` (KaTeX/MathJax-style LaTeX).
- **Callouts** — Obsidian-style: a blockquote whose first line is `> [!note]`, `[!info]`, `[!warning]`, `[!tip]`, `[!success]`, `[!question]`, `[!failure]`, `[!danger]`, `[!example]`, `[!quote]`, etc.
- **Internal links** — `[[wiki-style]]` links resolve within the share.
- **Tags** — `#tag` inline.
- **Footnotes** — `[^1]` and `[^1]: definition`.
- **Highlights** — `==text==` (renders as `<mark>`).
- **Block IDs** — trailing `^block-id` on a line creates an anchor.
- **Frontmatter** — YAML at the top of the document between `---` lines is parsed and surfaced separately, not rendered as body content.
- **YouTube / Twitter embeds** — image-syntax `![](https://www.youtube.com/...)` turns into an iframe.

Standard CommonMark + GFM also works: headings, lists, ordered lists, fenced code blocks with language tags (syntax-highlighted), tables, strikethrough `~~text~~`, autolinks, blockquotes, images, links.

Avoid raw HTML — the sanitizer strips most tags (script, style, iframe except YouTube, form, input except disabled task-list checkboxes, etc.). Express formatting via markdown extensions instead.

## Concurrency and conflict handling

Humans and other agents may edit the same share simultaneously. The protocol uses optimistic concurrency — every `POST /ops` must match the current server version, or it returns `409` with the latest state.

**Important:** a `409` does not resolve the conflict for you. If you blindly re-submit your `/ops` payload after a conflict, you will silently overwrite whatever the human just typed. Use the same three-way merge the human web client uses (see `src/client/collab.ts` for the reference implementation).

### Three-way rebase with `diff-match-patch`

The web client uses Google's [`diff-match-patch`](https://github.com/google/diff-match-patch) library — available on [npm](https://www.npmjs.com/package/diff-match-patch) and [PyPI](https://pypi.org/project/diff-match-patch/).

```
function rebase(baseText, agentContent, newBaseText):
  dmp = new DiffMatchPatch()
  diffs = dmp.diff_main(baseText, agentContent)
  dmp.diff_cleanupSemantic(diffs)
  patch = dmp.patch_make(baseText, diffs)
  [merged, results] = dmp.patch_apply(patch, newBaseText)
  return merged, results  // results[] tells you which patches applied cleanly
```

### Full edit-with-retry loop

```
baseText, baseVersion = GET /state
agentContent = generate(baseText, instruction)

loop:
  response = POST /ops {version: baseVersion, content: agentContent, title: ...}
  if response.status == 200:
    break
  if response.status == 409:
    serverContent = response.body.content
    serverVersion = response.body.version
    agentContent = rebase(baseText, agentContent, serverContent)
    baseText = serverContent
    baseVersion = serverVersion
    continue
  else:
    handle other errors (401, 403, 410, 413)
```

The merge is fuzzy. `diff-match-patch` will:

- Apply your changes cleanly when the human edited a **different region** than you.
- Skip a patch and flag it as failed (in the `results` array) when the human deleted or substantially rewrote the **same region** you were editing. Check `results` for false entries; on failure, either abort or fall back to suggestion mode (see below).

### Safer patterns when conflicts are likely

- **Append-only.** If your edit only adds content at the end, conflicts are trivial — re-fetch, append again. No merge logic needed.
- **Pause when humans are active.** Check `state.awareness` before writing. If a `user: { name }` entry has been seen recently, defer your write a few seconds.
- **Narrow span edits.** Modify the minimum text needed (one sentence, one paragraph). Smaller diffs survive rebase more often.
- **Suggestion mode (future).** Wrap proposed changes in CriticMarkup syntax — e.g. `{++added text++}` or `{~~old~>new~~}` — so the human can accept/reject. This is *additive* to the content; it cannot conflict with the human's edits because you're not replacing existing text. Not yet rendered specially in link.md, but planned.

## Streaming and chunked writes

A single `/ops` call that ships an entire long response appears **instantly** in live editors — there's no per-character animation. For content longer than a couple of sentences this can feel jarring (and gives a co-editing human no chance to react mid-write). Prefer chunked writes for anything more than a short edit.

### How to chunk

Build content up locally as you generate, and `POST /ops` with the full content-so-far after each chunk. Each successful response returns the next `version`; use it as the base for the next call.

```
baseText, baseVersion = GET /state
content = baseText  // start from current state

for chunk in generate_stream(instruction):
  content = content + chunk
  response = POST /ops {version: baseVersion, content, title}
  if response.status == 200:
    baseVersion = response.body.version
  elif response.status == 409:
    content, baseVersion = rebase_loop(...)  // see Concurrency section
```

### Chunking strategy

| Chunk size | Feels like | Wire cost | Best for |
|---|---|---|---|
| Per token | Live typing | Highest | Small docs only |
| Every ~50 tokens / 200 ms | Smooth streaming | Medium | Most use cases |
| Per paragraph | Drafting bursts | Low | Long-form writing |
| Per section (`##`) | Outline assembly | Very low | Structural edits |

Pace at most 2–5 `/ops` per second. Faster than that and (a) live editors get visually overwhelmed, (b) conflict probability rises, (c) per-call wire cost dominates (each call re-uploads the **full document**, not a delta).

For streaming-LLM agents, buffer the token stream and flush every 50–100 tokens **or** every 200 ms, whichever fires first. For non-streaming agents that get the full completion in one shot, split it client-side on line/paragraph breaks and post sequentially with a short delay between calls.

### Cost note

The wire protocol is whole-document, not delta-based. A 10 KB document chunked 100 times costs ~1 MB of wire traffic vs ~10 KB for a single-shot. The visual benefit is usually worth it for small-to-medium docs (under 20 KB); for very large docs, prefer paragraph-or-section chunking, or post once after a brief generation buffer.

## Suggested first interaction

```
1. POST /presence with body { "cursor": null }
2. GET /state — capture version, content
3. Reply to the human in the channel that handed you the invitation:
   "Connected as <agent_name>, ready."
4. Wait for instructions, or proactively suggest edits via /ops.
```
