Skip to main content
ClaudeWave
Skill240.2k repo starsupdated 2d ago

tldraw-offline

Drive and script tldraw offline canvases with an agent.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/NousResearch/hermes-agent /tmp/tldraw-offline && cp -r /tmp/tldraw-offline/optional-skills/creative/tldraw-offline ~/.claude/skills/tldraw-offline
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# tldraw offline Skill

Work with the tldraw offline desktop app (offline.tldraw.com): read the open
canvas, make edits, and write **document scripts** — JavaScript embedded in a
`.tldraw` file that runs on load and gives the file durable behavior. The app
runs a **local HTTP API** (default `localhost:7236`) that a coding agent drives
with plain `curl` from its terminal — this is exactly how the app's own homepage
demo (Codex editing a canvas live) works. The agent does NOT use computer-use /
GUI clicking, and does NOT hand-edit the `.tldraw` file directly. Keep tldraw
offline open while you work.

## When to Use

- The user has tldraw offline open and asks you to build or modify a canvas
  (diagrams, wireframes, layouts).
- You want to add durable behavior to a drawing (reactive shapes, interactive
  buttons, animation, connection logic) via an embedded document script.

Do NOT hand-place shapes to imitate a drawing — write the code that generates
them. Agents are far better at scripting the canvas than at drawing on it.

## Prerequisites

- **tldraw offline installed and running**, with a document open. Releases:
  https://github.com/tldraw/tldraw-offline/releases/latest (macOS DMG, Windows
  x64/Arm64, Linux `x86_64`/`arm64` AppImage or amd64/arm64 `.deb`).
- **Agent skills installed in the app**: `Develop → Install Agent Skills`. The
  app writes its own tldraw skill into `~/.codex/skills/`, `~/.claude/skills/`,
  `~/.cursor/skills/`, and `~/.gemini/skills/` — teaching that agent the `curl`
  recipes below. (This Hermes skill mirrors that guidance for Hermes.)
- **The local control API.** On launch the app writes `server.json` to its config
  dir (Linux `~/.config/tldraw/`, macOS `~/Library/Application Support/tldraw/`,
  Windows `%APPDATA%\tldraw\`) with `port` (default `7236`), a bearer `token`,
  `pid`, and `startedAt`. Every request except `GET /` needs
  `Authorization: Bearer <token>`. A clean quit removes `server.json`; if it's
  present but the port doesn't answer, the app quit uncleanly — treat as not
  running.
- **Re-read port + token on EVERY shell call.** Each terminal call is a fresh
  shell, so an `export`ed token does not persist — "export once and reuse" sends
  an empty token and 401s. Read both inline at the top of each call:
  `PORT=$(jq -r .port <server.json>); TOKEN=$(jq -r .token <server.json>)`.
- No account or network needed for local editing.

## How to Run

Two distinct workflows. Pick by whether the change must survive a reload.

**A. One-off canvas edits (`/exec`)** — layout, generating shapes, cleanup. This
is a live edit, not saved script:

```bash
BASE=http://localhost:7236
TOKEN=$(python -c "import json;print(json.load(open('$HOME/.config/tldraw/server.json'))['token'])")
# find the focused document id
DOC=$(curl -s "$BASE/api/search" -X POST -H 'content-type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"code":"return (await api.getFocusedDoc()).id"}' | python -c "import sys,json;print(json.load(sys.stdin)['result'])")
# run code with the live `editor` + `helpers` in scope
curl -s "$BASE/api/doc/$DOC/exec" -X POST -H 'content-type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"code":"const {createShapeId,toRichText}=await import(\"tldraw\"); editor.createShape({id:createShapeId(),type:\"geo\",x:0,y:0,props:{geo:\"rectangle\",w:200,h:100,color:\"blue\",fill:\"solid\",richText:toRichText(\"hello\")}}); return editor.getCurrentPageShapes().length"}'
```

**B. Durable behavior (`script/main.js`)** — reactive/interactive logic that must
survive reload. Edit the file on disk; the app's watcher applies it:

```bash
# get the live script file path for the doc
curl -s "$BASE/api/doc/$DOC/script-workspace" -X POST \
  -H "Authorization: Bearer $TOKEN"          # -> result.mainJsPath, result.isDefaultScript
# edit result.mainJsPath with read_file / patch / write_file (see scripts/main.js)
# then confirm the watcher applied it:
curl -s "$BASE/api/doc/$DOC/script-status" -H "Authorization: Bearer $TOKEN"
```

The ready-to-adapt document script is `scripts/main.js`.

## Quick Reference

The document-script contract (verified against the app's bundled
`script-context.d.ts`):

```js
import { createShapeId, toRichText } from 'tldraw'   // primitives: import, not globals

export default function ({ editor, helpers, signal }) {
  editor.run(() => {                                 // batch = one undo step
    helpers.createShapeIfMissing({                   // idempotent furniture
      id: createShapeId('node-1'), type: 'geo', x: 0, y: 0,
      props: { geo: 'rectangle', w: 200, h: 100, richText: toRichText('hi') },
    })
  })

  const stop = editor.store.listen(() => { /* react */ })  // fires the tick AFTER a commit
  signal.addEventListener('abort', () => stop())           // REQUIRED cleanup on rerun/close
}
```

- `ctx.editor` — the live `Editor` (`createShape`, `updateShape`, `deleteShapes`,
  `getCurrentPageShapes`, `getShape`, `getBindingsFromShape`, `zoomToFit`,
  `on('tick'|'event', fn)`, `run(fn, { history: 'ignore' })`).
- `ctx.helpers` — `createShapeIfMissing`, `createShapesIfMissing`,
  `createArrowBetweenShapes(from, to, { arrowheadEnd })`, `translateShapes`,
  `onShapeTranslate(id, fn, { signal })`, `richTextToPlainText`, `boxShapes`,
  `getLints`.
- `ctx.signal` — `AbortSignal`; attach every listener/interval teardown to it.
- `config.js` (separate file) registers custom shape/tool/component utils and
  runs before mount; `main.js` runs against the mounted editor and reruns on save.

## Interactive UI (clickable buttons that drive state)

Drawn shapes can behave like a real app — the thing a static whiteboard can't do.
Full example: `scripts/counter.js` (a number display + MINUS/RESET/PLUS buttons).

Verification boundary — read this before claiming interaction works or doesn't.
The app's OWN agent playbook says to verify a clickable-UI script with "one
simulated click and one state read" via `/exec` (`editor.d