# nib CLI and API reference

> Full reference for humans and agents operating on a `nib` doc from the CLI
> or HTTP API.
>
> `nib` is intentionally agent-first: anything a human can do in the
> editor, an agent can do over HTTP or via the `nib` CLI. There is
> no separate "agent surface" — these are the same primitives the
> browser editor uses.

The short agent onboarding contract lives in [`agent-quickstart.md`](agent-quickstart.md)
and is served from `GET /agents.md`. The `nib` server serves this reference
from `GET /reference.md`.

---

## TL;DR

You will typically be handed one of:

- A **share URL** like `https://nib.so/d/<slug>` or
  `http://localhost:4000/d/<slug>#me=<handle>` — open
  it in a browser to operate as a human, or use the slug directly via the
  CLI / HTTP. A share URL plus the copied agent invite/token is enough for
  hosted agent collaboration; no local file mirror is required.
- A **local Markdown file path** that is mirrored to a slug — changes you
  make to the file are watched and safe non-conflicting local edits auto-push
  upward into the cloud doc, including inserted blocks like new paragraphs,
  headings, list items, table rows, and cells. Use `nib sync` for review states:
  it merges safe non-overlapping local/cloud edits and stops only for same-block conflicts.
  This is for the owner or for collaborators who explicitly choose to create
  their own mirror.

Opening a share URL never auto-downloads a `.md` file or writes to the
collaborator's filesystem. Work from the URL by default. Create a local
mirror only when the human explicitly asks for one.

Browser humans may toggle the footer between rendered **Preview** and raw
**Markdown** source. That split is only a browser editing affordance: agents
should continue to read clean markdown with `nib read`, `/snapshot`, or
`/state`, and should mutate with `/edit/v2` or comment endpoints.

For a hosted invite prompt with a secret one-time invite token, claim the token
first and use the returned bearer; that bearer is your identity for API calls.
For localhost or no-token flows, the fastest path for an agent is the CLI:

```bash
# One-time identity setup (saved to ~/.config/nib/credentials.json)
nib identity set --handle owner-claude --kind agent

# Check local/cloud sync health
nib status <file.md|slug|url>

# Safely merge non-overlapping local/cloud changes
nib sync <file.md|slug|url>

# Read a doc
nib read <file.md|slug|url>

# See open comments + replies
nib comments <file.md|slug|url> --json

# Generate model-agnostic prompt/data packets for any LLM
nib review  <file.md|slug|url> --raw
nib resolve <file.md|slug|url> --plan approved-plan.md --raw

# Inspect history for summarization prompts
nib activity <file.md|slug|url> --since 7d --by owner-claude --json

# Drop a comment, reply to one, resolve a thread
nib comment <file.md|slug|url> --text "this paragraph is unclear" --quote "fuzzy sentence"
nib reply   <file.md|slug|url> --mark <markId> --text "agreed — rewriting"
nib resolve <file.md|slug|url> --mark <markId>

# Stream the activity log in real time (Ctrl-C to stop)
nib activity <file.md|slug|url> --tail
```

Once the identity is set, every subsequent CLI call sends that principal's
identity headers — no `--by` per command needed. Per-call `--by <handle>`
selects that principal's cached bearer token when available; it does not
override server-side attribution without a matching credential.

`nib whoami` prints the resolved identity at any point.

---

## URL-only agent workflow

If a human pastes an agent invite prompt or share URL into your context, do
not assume you need a local file. The short prompt is intentionally tiny:

```text
Join this nib doc as an agent.
Approve once: `nib agent *`
First run: nib agent connect https://... --invite-token <invite-token>
```

If you are reading this from a non-agentic web chat, stop here. You can discuss
the doc with the human, but you cannot safely claim the invite, install the CLI,
or keep the returned doc credential local. Ask the human to paste the invite into
Claude Code, Codex, Cursor, or another coding-agent environment with shell
access.

Permission contract for shell-based agents:

- Ask for one persistent shell approval rule for `nib agent *` before the first
  command. In Codex-style sandboxes this is the `["nib", "agent"]` prefix.
- Your first shell command should be the exact `nib agent connect ...` command
  from the pasted prompt. After that, use only `nib agent read`,
  `nib agent comments`, `nib agent comment`, `nib agent reply`,
  `nib agent edit-block`, `nib agent activity`, `nib agent revert`,
  `nib agent guide`, `nib agent sync`, `nib agent urls`, or `nib agent do`.
  Most product commands accept the same `nib agent <command> ...` spelling;
  server-side roles and tokens still decide whether the action is allowed.
- Do not spend extra approvals on discovery commands such as `which nib`,
  `nib --version`, `nib agent ... --help`, `head`, shell pipes, or bare
  `nib ...` commands. If `nib agent connect` fails because `nib` is missing,
  ask once for the install command, then return to the `nib agent *` rule.
- Do not mutate the document during the connect/orientation step. After
  connect succeeds, tell the human you are in the doc and ready.

Read this guide, then treat the pasted one-time invite token as a claim
credential, not a working credential. Run `nib agent connect ...` once with the
doc URL and token from the pasted prompt. This is the one-time-token to
bearer redemption path: it claims the invite, sends your agent platform, stores
the returned bearer under this doc in `~/.config/nib/credentials.json`,
announces active agent presence for the browser facepile, fetches the snapshot
and open comments, and prints a short ready summary.

If the `nib` command is not available, install it from the same origin that
served the invite prompt:

```bash
curl -fsSL https://nib.so/install | sh
```

```bash
nib agent connect https://nib.so/d/<slug> --invite-token <secret>
```

If your environment reports a host allowlist or proxy error for `nib.so`, the
fix is network access to `nib.so` from that agent environment. Do not ask the
human to paste a bearer token into chat; a bearer still needs the same network
path and is a longer-lived secret.

During redemption, `nib agent connect` auto-detects your platform slug
(`claude`, `codex`, `cursor`, or similar). The server combines the inviter's
handle prefix with that platform to mint your named agent principal, such as
`kmiller-codex`. If platform detection is wrong or unavailable, rerun the
connect command with `--force --platform codex`, `--force --platform claude`,
or `--force --platform cursor`.

If your shell permission model supports persistent command approvals, connect
is the right moment to request one narrow approval for `nib agent *` (or the
`["nib", "agent"]` prefix in Codex-style sandboxes). That lets later
`nib agent read`, `nib agent comments`, `nib agent comment`,
`nib agent reply`, `nib agent edit-block`, `nib agent activity`,
`nib agent revert`, `nib agent guide`, `nib agent urls`, `nib agent token`,
`nib agent invite`, and other `nib agent <command> ...` product actions run
without repeated prompts. Do not request broader `nib *` or shell-wide access.

Do not print the invite token or returned bearer. Keep literal tokens out of
chat, visible status/thinking text, summaries, and final replies. After
`nib agent connect` succeeds, prefer `nib agent read`,
`nib agent comments`, `nib agent comment`, `nib agent reply`, and
`nib agent edit-block`, `nib agent activity`, `nib agent revert`,
`nib agent guide`, and other `nib agent <command> ...` aliases; these keep
follow-up actions under the same approval prefix while the CLI loads the stored
doc-scoped bearer automatically where the command supports it.
If the prompt has no token and points at localhost, use the suggested handle
with `nib agent connect <doc-url> --handle <agent-handle> --platform <tool>`.

Agent handles should stay tied to the human who invited you. When `nib` knows
the human email, the invite prefix is the email local part before `@`, with any
`+tag` removed: `kmiller@dropbox.com` becomes `kmiller`. When `nib` only knows
a display name, multi-word names use first initial plus last word
(`Kyle Miller` becomes `kmiller`) and one-word names use the normalized word
(`Dropbox` becomes `dropbox`). Claim the invite with your platform slug and
`nib` appends it after a dash, so Codex joining for Kyle Miller becomes
`kmiller-codex`.

After connecting, tell the human you are in the doc and ready for instructions.
Do not edit, comment, resolve, or otherwise mutate the doc until the human asks
you to.
The token's role and expiry are part of the server-side credential. Obey the
capabilities exposed by `/state` or failed mutation responses: viewer tokens
read only, commenter tokens mutate comments only, and editor tokens may use
`/edit/v2` after human approval.

Use `/api/agent/:slug/edit/v2` for direct edits and the comment endpoints for
anchored comments / replies. Your resolved identity is what lets the browser
show your agent avatar and attribute your work.

The low-call read-first path is: fetch `/snapshot`, fetch open `/comments`,
then tell the human you are in the doc and ready for instructions. Before
you propose specific edits or comment actions, refresh `/snapshot` and
`/comments` again so your plan is based on the current doc, not the orientation
copy. Once the human approves updates, fetch a fresh mutation base one more
time, then send one batched `/edit/v2` mutation and any needed
comment/reply/resolve mutation. Use `/state` when you need full markdown/marks
or stale-base retry details; do not fetch every endpoint by default.

Treat any bearer token as a secret. Prefer `nib agent connect` so the bearer
is stored in nib's credentials cache and normal commands can reuse it without
environment variables. Do not print raw tokens in command echoes, visible
status/thinking text, summaries, or final replies.

On hosted pilot servers, browser humans use Google OAuth through
`/auth/login`. In invite-only mode, allowlisted, domain-allowed, and
admin-approved users receive sessions immediately; other verified Google
accounts land in the private admin queue. Humans with site access can open
active shared slugs directly. Email-bound doc invites are stricter per-doc
session links, not site-access grants. Agents should use
explicit doc-scoped bearer/share tokens when provided; those API credentials
remain valid without a browser session and do not enter the human approval
queue. Do not try to act as a human by spoofing `X-Nib-Me` on a hosted pilot
server.

For v1, agents use the caller's principal email local-part plus a tool tag.
For example, `owner-claude` or `owner-cursor`. When self-declaring, use the
same value for `X-Nib-Me` and `X-Nib-Display` unless the human gives you a
different display label.

Only create a local `.md` mirror when the human explicitly asks you to.

Rules of engagement:

- Read comments before editing around highlighted text.
- Refresh `/snapshot` and `/comments` before proposing changes and again before
  mutating; stale plans are worse than slow plans.
- Preserve anchored quotes unless your edit intentionally addresses or removes
  that exact issue.
- Write clean Markdown only: no HTML, `data-nib` spans, or provenance sidecars
  inside block markdown.
- After any approved mutation, tell the human exactly what document edits and
  comment actions you made.
- If your token is `viewer`, do not mutate the doc. If it is `commenter`, do not
  edit document text. If you need more access, ask the human for a new token.

---

## Identity model

Every edit, comment, and reply is attributed to a **principal** (a named
human or agent). Identity is established via two HTTP headers:

| Header | Required | What it does |
|---|---|---|
| `X-Nib-Me`      | yes (for self-declared) | Your principal handle, e.g. `owner-claude`. Immutable per-principal. |
| `X-Nib-Display` | optional | Friendly display name. Defaults to the handle. |
| `X-Nib-Kind`    | optional | `"agent"` or `"human"`. Defaults to `"human"`. |

On first sight of an `X-Nib-Me` header, the server **auto-registers**
the principal and gives them an `editor` role on the document. This is
trust-on-first-use — fine for the localhost-only setup. Hosted pilot browser
traffic uses the verified Google session instead; agent API callers should
prefer a stable bearer token when one is available. See "Bearer-token identity
(advanced)" below.

The CLI handles all of this for you when `nib identity set` has
been run. Outbound HTTP requests are a thin layer; if you need to call
the API by hand, just send the headers above.

---

## CLI surface

### Document lifecycle

| Command | What it does |
|---|---|
| `nib share <file.md>` | Idempotent. Creates a new cloud doc for the file on first run, prints the share URL on every subsequent run, establishes the local/cloud baseline, and registers persistent watch. On hosted servers it starts `nib auth pair` automatically when the CLI session is missing or expired. Pass `--open` to open it, `--copy` to copy the human share link, or `--copy-agent-invite` to copy a paste-ready agent prompt. Agent platform naming happens when the agent redeems the one-time invite with `nib agent connect`. |
| `nib invite mint --doc <slug> --handle <email> --role viewer\|commenter\|editor [--expires 7d]` | Advanced stricter path for a single-use human browser invite. On hosted nib, the browser must already pass normal site access through allowlists or admin approval, then pass Google OAuth as the named email before the invite is consumed. A successful claim receives a per-doc session cookie and redirects to the clean `/d/<slug>` URL. Unclaimed invites expire after 7 days by default. |
| `nib open <file.md\|slug>` | Print or open the personal share URL for an existing mirror. |
| `nib sync <file.md\|slug\|url> [--json]` | Smartly merge safe non-overlapping local/cloud changes in both directions. This is the predominant path when a user says "sync this doc." Stops with a conflict when both sides changed the same block. |
| `nib pull <file.md\|slug\|url> [--json]` | Pull the cloud/browser copy into the registered local `.md` file. Also accepts `nib pull <url\|slug> --path <file.md>` to create a new local mirror from a URL or slug. |
| `nib push <file.md\|slug\|url> [--json]` | Push the registered local `.md` file into the cloud/browser copy. `nib push <file.md> --replace` remains the lower-level destructive overwrite path; most creation flows want `share` instead. |
| `nib pull <url\|slug> --path <file.md> [--force]` | Explicitly create a local `.md` mirror from a nib URL or slug. Mirror tracking starts after pull. Refuses to overwrite a non-empty file without `--force`. |
| `nib agent connect <url\|slug> --invite-token <token> [--platform auto\|claude\|codex\|cursor]` | One-shot hosted agent join. Claims a single-use agent invite, stores the returned doc-scoped bearer locally, announces active agent presence for the browser facepile, fetches the snapshot and open comments, and prints a ready summary. Existing bearers can use `--token <bearer> --handle <agent-handle>`. |
| `nib agent read\|review\|comments\|comment\|reply\|edit-block\|activity\|revert\|do ...` | Agent-prefixed aliases for the normal doc commands. Use these after connecting so shell-approval tools can keep one narrow persistent approval rule for `nib agent *`. |
| `nib agent <command> ...` | Agent-prefixed spelling for most product commands, including share/open/urls/list/status/sync/pull/push/doctor/watch/conflicts/activity/revert/read/review/comments/comment/reply/comment-edit/comment-delete/resolve/edit-block/unshare/unlink/delete/restore/token/invite/whoami/identity/auth. Shell approval is broad; app authorization is still enforced by server roles, doc tokens, owner secrets, and command capabilities. |
| `nib agent guide [--json]` | Print the LLM-facing map from plain-language requests to safe nib commands. Invited agents should read this when a user says "share via nib", "sync this doc", "respond to comments", etc. |
| `nib do "<plain request>" [file.md\|slug\|url] [--dry-run]` | Conservative plain-language helper for obvious intents: share, sync, pull, push, review/respond-to-comments, comments, comment, and reply. If it cannot map a request safely, it fails closed and tells the agent to use an explicit command. |
| `nib list [--json]` | List every locally-mirrored doc on this instance (slug, title, path, share URL). |
| `nib urls [--mine\|--all] [--include-deleted] [--json]` | List nib URLs visible to the CLI identity. With `nib identity set`, defaults to docs you own or have joined; without identity, lists all local docs on the current server. |
| `nib urls open <slug-or-title>` | Open a URL from `nib urls` by slug or a unique title fragment. |
| `nib info <slug>` | Print mirror metadata for a specific slug. |
| `nib unlink <file.md\|slug\|url>` | Drop the local mirror/watch relationship. The local `.md` file and cloud doc remain. |
| `nib doctor <file.md\|slug\|url> [--json]` | Diagnose whether canonical SQLite, the projection row, active live-collab state, and any local mirror agree. Prints the active ownership mode, contract verdict, and the layer that owns content, selection, command ingress, and persistence. Use this before trusting a suspicious `Synced` state, reload surprise, or agent/local mismatch. |
| `nib auth pair\|status\|logout` | Hosted CLI auth. `pair` opens the browser approval flow, signs in with Google if needed, waits for approval, stores the default server, and caches a scoped CLI session token. `nib share <file.md>` runs this automatically when needed. |
| `nib version [--check] [--json]` | Print the running CLI version, protocol, build SHA, script path, and default server. With `--check`, compare against the server and print the update command when the local CLI is stale or from a different build. |
| `nib watch status [file.md] [--all]` | Show persistent local watcher state registered by `nib share`. `--all` lists cached watches across every server in `~/.config/nib/credentials.json`, including old localhost/Railway origins. `nib watch start\|stop\|restart <file.md>` exists for debugging, but the normal flow should not require it. |
| `nib status [file.md\|slug\|url] [--json]` | Show local/cloud sync state: `synced`, `local_push_pending`, `cloud_pull_pending`, `diverged`, `conflict`, or `watcher_error`. JSON output includes the server-built `snapshot` with UI copy, hashes, gutter signatures, and recent smart-sync runs. |
| `nib sync <sync\|push\|pull> <file.md\|slug\|url> [--json]` | Compatibility aliases for the older nested sync surface. Prefer top-level `nib sync`, `nib push`, and `nib pull` in new docs/prompts. |
| `nib sync diff <file.md\|slug\|url> [--json]` | Show the registered local `.md` file versus the cloud/browser copy. Also works for a cached foreground remote-watch file. |
| `nib sync watch <file.md> [doc-url\|slug] [--doc <url\|slug>] [--token <bearer>]` | Foreground hosted-doc helper for debugging or agent environments without a background watcher. Creates or reuses a cloud doc for the file, pushes safe local-only edits, and reports cloud-ahead/diverged states without writing cloud changes to disk. Existing hosted docs can use `--token` or `NIB_AGENT_TOKEN`. |
| `nib review <file.md\|slug\|url> [--json] [--raw] [--output <file.md>]` | Generate a model-agnostic review prompt plus raw packet data from the doc, comments, replies, activity, participants, and block context. Paste it into Claude, Cursor, Codex, or any other LLM before planning v2 changes. |
| `nib resolve <file.md\|slug\|url> [--plan <file.md>] [--json] [--raw] [--output <file.md>]` | Generate a read-only v2 execution prompt plus raw doc/comment/activity data. Include an approved plan file when available; paste into any LLM to produce the patch, reply drafts, and final resolve commands. |
| `nib conflicts list [--doc <slug>] [--json]` | List same-block local/cloud conflict snapshots captured under `.local/conflicts/`. |
| `nib conflicts show <id> [--json]` | Show one conflict snapshot. Use `--json` for the full local/cloud/base markdown bodies. Resolve only by choosing an explicit direction (`nib push` to keep local, `nib pull` to keep cloud) or by running `nib sync` after the human approves a safe merge. |
| `nib unshare <slug>` | Revoke share access. The doc stays in the database, but share URLs and minted bearer tokens stop authenticating. Owner secret is auto-loaded from the credentials cache. |
| `nib delete <file.md\|slug\|url> [--soft\|--hard]` | Soft-delete/archive a nib doc by default. The local file stays on disk; history, comments, and activity remain available for restore. The CLI stops/removes cached local watcher entries by default. With `--hard`, permanently remove the server doc, principals, comments, history, tokens, and collab state; it cannot be restored. |
| `nib restore <slug\|url> [--path <file.md>] [--open]` | Restore a soft-deleted doc at the same `/d/<slug>` URL and optionally re-register a local mirror path. |

`local_push_pending` means the local file has changes that are not yet in the
cloud/browser copy; nib will try to auto-sync safe block-level local edits up
to the cloud. `cloud_pull_pending` means the cloud/browser copy has changes
that are not in the local file; local disk writes from cloud remain explicit.
`diverged` means both sides changed. Use `nib sync` first when a status
stays pending or needs review. It merges safe non-overlapping changes and stops
for review only when both sides changed the same block. Use `nib sync diff`,
`nib push`, or `nib pull` when you explicitly want to inspect or
replace one side.

If a human asks you to "pull these changes" or "push these changes" and
`nib status <file.md|slug|url> --json` shows a registered mirror path or
`"source": "remote_watch"`, treat that wording as potentially ambiguous.
Ask whether they want smart sync instead of a directional replacement. The
safe default is `nib sync <file.md|slug|url> --json`; use `nib pull` only
after they explicitly choose cloud-to-local, and `nib push` only after they
explicitly choose local-to-cloud.

`nib sync`, the foreground watcher, and the browser status modal's Sync action
read the saved file on disk. They cannot see unsaved editor buffers in Cursor,
Obsidian, Vim, or another local editor. If the human may have unsaved local
edits, ask them to save, reload, or stash those edits before syncing.

For a hosted cloud doc, the server cannot watch your laptop's disk by itself,
so the local CLI owns disk observation. Run `nib share <file.md>` from the local
machine: if needed it starts browser auth, creates or reuses the hosted doc,
establishes the local/cloud baseline, and registers a persistent local watcher.
The watcher pushes safe local-only edits to the cloud, including newly inserted
blocks, while cloud-ahead or diverged states are reported and never pulled into
the local file silently. Local mirror and hosted remote-watch paths record a
green sync state and advance the baseline only after canonical cloud readback
and, for cloud-to-local writes, a raw local file readback prove the intended
content landed. Normalized Markdown comparison still suppresses formatting-only
drift before a write is needed. Use `nib watch status` for visibility. In the
browser, a linked local
doc must surface watcher stoppage or errors through the main status icon's error
state and restart instructions instead of a green synced icon. Use
`nib sync <file.md>` or the
browser status modal's Sync action to pull safe cloud-only changes down through
the watcher; use `nib sync diff <file.md>`, `nib pull <file.md>`, or
`nib push <file.md>` only when you need to inspect or make an explicit
directional choice. `nib sync watch` remains available as a foreground/debug
helper for agents or environments where a background watcher is not installed.

### Reading

| Command | What it does |
|---|---|
| `nib read <file.md\|slug\|url>` | Print the canonical markdown to stdout. This is clean user-facing `.md`; comments and authorship are returned through marks/comments/activity APIs rather than embedded markup. |
| `nib read <file.md\|slug\|url> --blocks` | Print the block-id snapshot (`ref`, `id`, `type`, `markdown`, `level`). These `ref`s are what `edit-block` operates on. |

### Comments

| Command | What it does |
|---|---|
| `nib comments <file.md\|slug\|url> [--include-resolved] [--by <handle>] [--json]` | List comment threads on a doc. This is the fast primitive for prompts like "draft replies to every open collaborator comment." |
| `nib comment <file.md\|slug\|url> --text "<body>" --quote "<anchor text>"` | Add a comment anchored to `<quote>` (must appear verbatim in the doc). |
| `nib reply <file.md\|slug\|url> --mark <markId> --text "<body>"` | Post a reply to an existing thread. |
| `nib comment-edit <file.md\|slug\|url> --mark <markId> --text "<body>"` | Edit the root text of an existing comment. |
| `nib comment-delete <file.md\|slug\|url> --mark <markId>` | Delete an existing comment thread. |
| `nib resolve <file.md\|slug\|url> --mark <markId> [--unresolve]` | Mark one thread resolved / unresolved after the author approves the disposition. |

### Editing

| Command | What it does |
|---|---|
| Edit the **mirrored file** directly | The watcher notices the file changed and auto-pushes safe non-conflicting local edits into the cloud copy, including newly inserted paragraphs, headings, list items, table rows, and cells. Same-block conflicts and cloud-only changes remain visible as sync status. Run `nib sync` to review/merge pending states, or use `nib push` when you explicitly want local to replace cloud. |
| `nib edit-block <file.md\|slug\|url> --ops <file.json>` | Apply a block-op JSON file (an array of `/edit/v2`-shaped ops) to a doc. Surgical: blocks not referenced are untouched, so anchored comments survive. |

For URL-only collaborators and invited agents, `edit-block` or the HTTP
`/edit/v2` endpoint is the normal edit path. Direct local-file edits are only
available after a human has explicitly created a local mirror.

### Activity + history

| Command | What it does |
|---|---|
| `nib activity <file.md\|slug\|url> [--since 1h\|2026-04-21] [--by <handle>] [--type <kind[,kind]>] [--limit 200] [--json]` | Print filtered history for prompts like "what changed since Monday?" or "what did a collaborator's agent edit recently?" |
| `nib activity <file.md\|slug\|url> --tail` | Long-poll for new entries every 2s and stream them to stdout. Use this to "watch" a doc in real time. |
| `nib revert <file.md\|slug\|url> <event-id>` | Revert the doc to the state captured at `<event-id>`. |

### Identity

| Command | What it does |
|---|---|
| `nib identity set --handle <email-or-agent-handle> [--display-name "..."] [--kind human\|agent]` | Save a default identity to `~/.config/nib/credentials.json`. Humans use email handles; agents may use short handles like `owner-claude`. Subsequent CLI calls auto-stamp `X-Nib-Me` / `X-Nib-Display` / `X-Nib-Kind`. |
| `nib whoami [--json]` | Print the current resolved identity. |
| `nib identity clear` | Forget the saved identity. |

### Bearer-token identity (advanced)

Humans usually use the clean share URL from **Copy human invite** in the browser:
anyone who already has Nib site access through allowlists or admin approval can
open `/d/<slug>`, sign in with Google if needed, and join the doc. For a stricter
email-bound per-doc browser invite, owners can still mint a single-use link:

```bash
nib invite mint --doc <slug> --handle collaborator@example.com --role editor
```

Most flows don't need the email-bound link. If an agent needs a stable, scoped bearer
token (e.g. to call the HTTP API from a sandbox without filesystem access
to the credentials cache), provision one with:

```bash
nib token mint --doc <slug> --handle owner-codex --kind agent --role editor --expires 30d
nib token list --doc <slug>
nib token revoke --doc <slug> --handle owner-codex
```

Then pass `Authorization: Bearer <token>` (or `--token <token>` to the CLI).
Bearer tokens identify the principal by themselves; do not also send `X-Nib-*`
identity headers. Agent tokens expire after 30 days by default; use
`--expires 7d`, `90d`, or `never` for a shorter, longer, or explicit
non-expiring credential.

Treat bearer tokens as secrets. If you are using visible shell commands, store
the token in `NIB_AGENT_TOKEN` through your tool's secret/env mechanism and use
`Authorization: Bearer ${NIB_AGENT_TOKEN}` in curl examples. Do not paste the
raw token into repeated commands, command echoes, visible status/thinking text,
summaries, or final replies.

---

## HTTP surface

The CLI is a thin wrapper around these. Use them directly when you need
to integrate `nib` into another tool.

### Read

| Method | Path | Returns |
|---|---|---|
| `GET` | `/api/documents?scope=mine\|all&includeDeleted=0\|1` | `{ count, documents: [{ slug, title, shareState, relation, role, shareUrl }] }` — URL catalog for the declared `X-Nib-Me` identity, or all local docs with `scope=all`. |
| `GET` | `/api/local/list` | `{ count, mirrors: [{ slug, path, title, exists, revision, shareState, url }] }` |
| `GET` | `/api/local/mirror/:slug` | `{ slug, path, ... }` or `null` |
| `GET` | `/api/local/lookup?path=<abs-path>` | The slug mirrored to `<abs-path>`, or `null`. |
| `GET` | `/api/local/sync/status` | `{ count, statuses: [...] }` — local mirror sync status for every registered mirror. Each status includes `snapshot` and `liveHealth`; clients must not show a green synced state when `liveHealth.status` is `stale_repairable`, `active_owner_mismatch`, or `active_owner_missing`. |
| `GET` | `/api/local/sync/status/:slug` | `{ slug, path, status, reason, lastConflict, snapshot, liveHealth, ... }` — local mirror sync status for one doc. `snapshot` is the authoritative local/cloud relation for browser UI and agents: direction, hashes, change counts, gutter signatures/colors, UI copy, and recent smart-sync run traces. `liveHealth` is the shared live-room/canonical contract used by the status icon and sync doctor. |
| `GET` | `/api/local/sync/:slug/diff` | `{ localMarkdown, cloudMarkdown, baselineMarkdown, localHash, cloudHash, newer, ahead, aligned }` — inspect local/cloud differences. `ahead` is hash/baseline-derived (`local`, `cloud`, `both`, `none`, `unknown`); `newer` is timestamp context only. |
| `GET` | `/api/local/conflicts?slug=<slug>` | `{ count, conflicts: [...] }` — local conflict snapshots. |
| `GET` | `/api/local/conflicts/:id` | One local conflict snapshot; includes local, current cloud, and selected-base markdown. |
| `GET` | `/api/documents/:slug/sync-doctor` | `{ verdict, contractVerdict, reasons, ownership, canonical, projection, live, localMirror }` — sync invariant report. `live.status` uses the same `liveHealth` classifier exposed on local/remote-watch status. `ownership` names the active mode, contract verdict, and the layer that owns content, selection, command ingress, and persistence. `healthy` means canonical Markdown/marks, projection, live room when loaded, and linked local mirror status agree; `pending` means a known review/sync action is still open; `mismatch` means one layer claims a state that does not match canonical. |
| `GET` | `/api/documents/:slug/principals` | List visible principals and revocation capabilities for the caller. Owners may pass `?includeRevoked=1` and also receive token summaries. |
| `GET` | `/api/agent/:slug/state` | `{ markdown, marks, ... }` — full state including marks. |
| `GET` | `/api/agent/:slug/snapshot` | `{ revision, blocks: [{ ref, id, type, markdown, level }] }` — the block snapshot needed for `/edit/v2`. |
| `GET` | `/api/agent/:slug/comments?includeResolved=0\|1` | `{ count, comments: [{ markId, kind, by, text, quote, createdAt, resolved, replies }] }` |
| `GET` | `/api/documents/:slug/activity?since=<iso>&limit=<n>&afterId=<id>` | `{ entries: [...] }` — incremental tail of the activity log. |
| `GET` | `/api/documents/:slug/activity/:eventId/revert/preview` | `{ currentMarkdown, targetMarkdown, changed, entry }` — non-mutating restore preview. Use this to render a red/green diff before confirming. |

### Write

All write endpoints accept JSON bodies. Identity must be present (header
or bearer). All edits emit activity events with attribution derived from
the authenticated principal, document token, owner secret, paired session,
or localhost identity headers. Request-body `by` / `actor` fields are
legacy compatibility inputs and are not authority boundaries.

The `/api/local/*` mirror endpoints are local-runtime conveniences. They
are disabled by default in hosted environments unless the server is
explicitly started with `NIB_ENABLE_LOCAL_MIRROR_ROUTES=1`.

| Method | Path | Body |
|---|---|---|
| `POST` | `/api/documents/:slug/me` | `{ handle?, displayName?, kind? }` — explicitly self-register the principal identity. The CLI calls this for you on first contact; you only need it if you're driving the API by hand. |
| `POST` | `/api/documents/:slug/invites` | Owner-only advanced human invite. Body may include `{ handle, displayName, role, expires }`; response includes a single-use `inviteToken`, `shareUrl`, and expiry. Hosted browsers must already have normal Nib site access and then pass Google OAuth as the invited email before the per-doc invite is consumed. |
| `DELETE` | `/api/documents/:slug/principals/:handle` | Revoke a principal. Owners can revoke anyone; editors can revoke agents they invited. |
| `POST` | `/api/local/push` | `{ path: "<abs-path>", title?, replace? }` — register a new mirror, or (with `replace: true`) overwrite an existing mirror's content. |
| `POST` | `/api/local/sync/:slug/sync` | Smart sync. Merges safe non-overlapping local/cloud edits and stops for same-block conflicts. |
| `POST` | `/api/local/sync/:slug/push-to-cloud` | Explicitly keep local as source and update cloud to match. Also repairs stale app projections before returning synced. |
| `POST` | `/api/local/sync/:slug/pull-from-cloud` | Explicitly keep cloud as source and update the local mirror file to match. |
| `POST` | `/api/local/conflicts/:id/resolve` | `{ source: "local" \| "browser" }` — resolve a saved same-block conflict by keeping the local file copy or the cloud/browser copy. Writes both sides to the chosen clean markdown and clears the conflict status. |
| `DELETE` | `/api/local/mirror/:slug` | Drop the local mirror/watch relationship; never deletes the `.md` file or the cloud doc. |
| `DELETE` | `/api/documents/:slug` | Soft-delete/archive the doc. Owner-only; makes the URL unavailable, preserves history, and returns `{ success, shareState: "DELETED" }`. Pass `?hard=1` only for permanent cleanup of test docs; it removes server state and returns `{ success, hardDeleted: true }`. |
| `POST` | `/api/documents/:slug/restore` | Owner-only restore for a soft-deleted doc. Re-enables the same `/d/<slug>` URL and returns `{ success, shareState: "ACTIVE" }`. |
| `POST` | `/api/documents/:slug/agent-invite` | Mint a single-use agent invite for the owner or an editor principal. Body may include `{ role, expires, handlePrefix }`; response includes `inviteToken`, `handlePrefix`, and `expiresAt`. Agents invited by an editor are tagged to that inviter for revocation. |
| `POST` | `/api/documents/:slug/agent-invite/claim` | Token-only agent claim. Send `Authorization: Bearer <inviteToken>` with `{ platform }`; response includes the working bearer `token`. |
| `POST` | `/api/agent/:slug/edit/v2` | `{ baseRevision | baseToken, operations: [{ op, ref, ... }] }` — surgical block-level edits. **This is the canonical edit endpoint.** |
| `POST` | `/api/agent/:slug/marks/comment` | `{ quote, text }` — create a comment anchored to `quote`. |
| `POST` | `/api/agent/:slug/marks/reply` | `{ markId, text }` — reply to an existing thread. |
| `POST` | `/api/agent/:slug/marks/edit` | `{ markId, text }` — edit a comment's root text. |
| `POST` | `/api/agent/:slug/marks/delete` | `{ markId }` — delete a comment thread. |
| `POST` | `/api/agent/:slug/marks/resolve` | `{ markId }` — resolve a thread. |
| `POST` | `/api/agent/:slug/marks/unresolve` | `{ markId }` — re-open a thread. |
| `POST` | `/api/documents/:slug/activity/:eventId/revert` | `{}` — restore the document to the state before that activity entry and add a new revert activity entry. The response includes the credential-derived `actor`; request-body attribution is ignored. |

### Error shape

Errors come back as flat JSON with a stable `code` agents can match on:

```json
{ "error": "human-readable message", "code": "STABLE_CODE", "hint": "optional next step", "slug": "..." }
```

Common codes:

| Code | Meaning |
|---|---|
| `INVALID_SLUG` | Slug param is missing or malformed. |
| `NOT_FOUND` | The doc doesn't exist on this server. |
| `DOC_DELETED` | The doc was previously deleted. |
| `OWNER_SECRET_REQUIRED` | This action needs the owner secret (e.g. `nib token mint`). |
| `IDENTITY_REQUIRED` | The request needs an identity (header or bearer) and didn't have one. |
| `INVALID_HANDLE` | Human handles must be email addresses. Agent handles may be email addresses or lowercase slugs like `owner-claude`. |
| `INVALID_EVENT_ID` | `nib revert` got a bad event id. |
| `MISSING_PATH` | `/api/local/lookup` called without `?path=`. |
| `ALREADY_MIRRORED` | The lower-level overwrite path ran on a file that's already mirrored. Use `nib share` to re-print the URL, or `nib push <file.md> --replace` to force-overwrite. |
| `STALE_REVISION` | `/edit/v2` got a `baseRevision` older than the current revision. The response includes the latest snapshot for retry. |
| `STALE_BASE` | `/edit/v2` or another mutation got a stale `baseToken`. Fetch fresh state/snapshot and retry. |
| `LIVE_DOC_BUSY` | A browser live-editing session currently owns the doc. Wait for `retryAfterMs` (or the `Retry-After` header), fetch fresh state/snapshot, then retry with the new base token. |
| `LIVE_DOC_UNAVAILABLE` | The server could not load the collaborative document. Fetch fresh state/snapshot and retry; if it repeats, ask the owner to refresh the browser tab. |
| `LOCAL_MIRROR_DISABLED` | A `/api/local/*` route was called on a hosted runtime where local mirror routes are disabled. |

When in doubt, treat any non-2xx as retryable only if the `code` is one
of `STALE_REVISION`, `STALE_BASE`, `LIVE_DOC_BUSY`, `LIVE_DOC_UNAVAILABLE`, or you're
handling network errors. Everything else is a real failure that needs
intervention.

---

## Edit V2: surgical block-level edits

`POST /api/agent/:slug/edit/v2` is the canonical edit endpoint. It uses
stable block refs (`b1`, `b2`, …) so you can target specific blocks
without rewriting the whole document, and applies edits inside a Y.Doc
transaction so anchored comments survive any edit that doesn't touch
their target text.

The `block.markdown` strings in edit operations must be pure user-facing
Markdown. Do not include HTML wrappers, `data-nib` attributes, hidden provenance
comments, or any other sidecar metadata.

Table cells are still Markdown table row text, not nested block documents.
When you need multiple visual lines inside a table cell, encode them with
literal `<br>` separators inside that cell. Lists inside cells are supported
when each item stays in the same cell and is separated by `<br>`, for example
`| Notes | - First item<br>- Second item |` or
`| Steps | 1. Draft<br>2. Review |`. Do not insert real blank lines, raw
multi-line Markdown list blocks, or extra table pipes inside a cell; that
breaks the table row. Outside table cells, prefer normal Markdown lists and
paragraphs instead of `<br>` for spacing. When editing a table through
`/edit/v2`, replace the table block or contiguous table-row range while
preserving the column count and pipe delimiters.

Before editing, read comments as part of the mutation context:

```bash
curl -s http://localhost:4000/api/agent/<slug>/comments?includeResolved=0 | jq
```

Comment anchors are tied to quoted text. If your edit leaves the quoted text
intact, the comment should move with it. If your edit rewrites surrounding
language but keeps part of the original quoted text, nib may preserve the
comment as a partial anchor. If you delete or fully replace the quoted text,
the comment is considered retired and can disappear rather than slide onto
unrelated characters. Plan edits accordingly: avoid touching an anchored quote
unless the edit is intentionally addressing or removing that exact issue.

Anchor-safe edit checklist:

- Read open comments before editing any nearby block.
- For new material, prefer `insert_before` or `insert_after` so existing
  commented blocks are untouched.
- For edits to an existing commented block, keep the exact quoted text in the
  replacement block unless the human explicitly asked you to address or remove
  that comment.
- For table edits, preserve the quoted text and the table shape in any row or
  table block you replace; do not rebuild the whole table if a row-level or
  nearby insert is enough.
- If you must delete or substantially rewrite anchored text, reply to the
  thread with what changed, and resolve it only when the human's intent is
  clear.

When an edit addresses a comment, leave the human a short paper trail: reply to
the thread with what changed, or resolve the thread only when the disposition is
obvious from the task. When you are unsure, keep the comment open and add a
reply instead of resolving it silently.

### Step 1 — get the snapshot

The examples below use localhost URLs. On hosted `nib.so`, use
`https://nib.so` as the origin and send
`Authorization: Bearer ${NIB_AGENT_TOKEN}` instead of self-declared
`X-Nib-*` headers.

```bash
curl -s http://localhost:4000/api/agent/<slug>/snapshot | jq
```

Returns `{ revision, blocks: [{ ref, id, type, markdown, level }] }`.

### Step 2 — apply ops

```bash
curl -X POST http://localhost:4000/api/agent/<slug>/edit/v2 \
  -H "Content-Type: application/json" \
  -H "X-Nib-Me: owner-claude" \
  -H "X-Nib-Kind: agent" \
  -d '{
    "baseRevision": 128,
    "operations": [
      { "op": "replace_block", "ref": "b3", "block": { "markdown": "Updated paragraph." } },
      { "op": "insert_after",  "ref": "b3", "blocks": [{ "markdown": "## New section" }] }
    ]
  }'
```

Op shapes:

- `{ op: "replace_block", ref, block: { markdown } }` — swap a block's content.
- `{ op: "insert_after", ref, blocks: [{ markdown }, …] }` — insert after a ref.
- `{ op: "insert_before", ref, blocks: [{ markdown }, …] }` — insert before a ref.
- `{ op: "delete_block", ref }` — drop a block.

You may use either `baseRevision` from `/snapshot` or the preferred
`baseToken` from `/state.mutationBase.token`; do not send both. In active
live-collab rooms, prefer `baseToken`: token-based surgical edits are applied
softly into the room instead of evicting connected browsers. If your base is
stale, you'll get `STALE_REVISION` or `STALE_BASE` plus fresh state details.
Re-resolve refs against the new snapshot and retry. A `202` response is
pending, not a user-visible success: live-room verification has not proved that
connected browsers received the edit. Fetch fresh state and retry idempotently,
or report the pending state instead of claiming the document is synced.

### Edit V2 retry loop

When `/edit/v2` returns a retryable error, do not repeat the same stale request
blindly.

- `STALE_REVISION` or `STALE_BASE`: fetch fresh `/state` or `/snapshot`,
  re-resolve block refs against the latest `blocks[]`, rebuild the intended
  operation, then retry with the new `baseToken` (preferred) or `baseRevision`.
- `LIVE_DOC_BUSY`: a browser live-editing session is currently ahead of the
  agent. Wait for `retryAfterMs` or the `Retry-After` header, fetch fresh
  `/state` or `/snapshot`, re-resolve block refs, then retry with the new
  `baseToken`.
- `LIVE_DOC_UNAVAILABLE`: fetch fresh state once and retry. If it repeats, stop
  and ask the human to refresh the browser tab or try again later.

A safe default is three attempts with a 2-3 second wait for `LIVE_DOC_BUSY`.
After a few failed retries, stop and report the latest `code` and `message`
rather than looping forever.

Additional op shapes:

- `{ op: "replace_range", fromRef, toRef, blocks: [{ markdown }, ...] }` — replace a contiguous block range.
- `{ op: "find_replace_in_block", ref, find, replace, occurrence?: "first" | "all" }` — replace text inside one block.

### When to skip Edit V2

If you want to push a whole file's contents through the normal sync path,
edit the locally-mirrored `.md` file directly. The mirror computes a
block-level diff and auto-pushes safe local changes into the cloud copy. It
only asks for review when local and cloud changed the same block, or when a
cloud change is waiting for an explicit local pull. Use `nib sync` for
that review path, `nib push` for an explicit local-to-cloud replacement,
and `nib push <file.md> --replace` only for a full overwrite that may drop anchored
comments.

---

## Activity tail (real-time)

`nib` doesn't push events to non-browser agents — there's no SSE or
WebSocket out-channel today. Instead, `nib activity --tail`
long-polls every 2s and prints new entries to stdout (one per line in
`--json` mode), so you can pipe it to another process or watch a doc
from a script:

```bash
nib activity <slug> --tail --json | while read -r line; do
  echo "$line" | jq -r '"\(.createdAt): \(.by) \(.kind)"'
done
```

The underlying endpoint is `GET /api/documents/:slug/activity?afterId=<id>`,
so you can do the same thing without the CLI if you prefer.

---

## What this doc deliberately leaves out

- **Track-changes / suggestions**: not a thing in `nib`. Edits are
  always direct. The local-edit-then-push loop handles "agent proposes
  a change for human review" entirely on the local side, and the
  activity log + `nib revert` give you the safety net.
- **Streaming token-append API**: deferred. Use `/edit/v2` with full
  block content per op; the activity tail closes the "see edits land
  in real time" gap.
- **MCP server**: skipped for now. The CLI + HTTP API cover every
  primitive without a third API surface to keep in sync. Revisit if
  remote / non-Cursor / non-Claude agents need to plug in.
