modules/home/programs/cli-agents/shared/skills/cli-tools/tldraw-offline/SKILL.md
Operate the user's tldraw offline canvas app, including open .tldraw or .tldr files. Use whenever a task involves inspecting, editing, arranging, connecting, linting, or scripting a tldraw Desktop canvas.
npx skillsauth add not-matthias/dotfiles-nix tldraw-offlineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill for tasks involving open tldraw Desktop files. The desktop app exposes a local HTTP server that can list documents, inspect canvas state, capture screenshots, execute JavaScript against a live editor, and expose live script files for durable behavior.
The default server is http://localhost:7236. If that port is not active, read the port from $HOME/.config/tldraw/server.json.
A clean quit removes server.json; the next launch rewrites it. It also records pid and startedAt, so if the file is present but requests to its port fail, treat it as stale (the app quit uncleanly) — the app is not running.
Every request except GET / and /readme needs the per-launch token from that same server.json, sent as -H "authorization: Bearer <token>".
If the server's base URL and bearer token are already in your context — an optional SubagentStart hook (inject-server-context.sh, shipped alongside this file) can pre-inject them; it is not auto-wired, so by default assume neither is in hand — use those literal values directly, or just call the tq helper (below). The rest of this section is the fallback for when neither is in hand.
Each Bash tool call runs in a fresh shell — exported env vars do NOT persist between calls. A TLDRAW_TOKEN you export in one call is empty in the next, so the request sends authorization: Bearer with no token and 401s. "Export once and reuse" does not work here — re-establish the port and token on every call. Read them together at the top of each call (both stay fixed for the app's lifetime, so re-reading is cheap):
PORT=$(jq -r .port "$HOME/.config/tldraw/server.json"); TOKEN=$(jq -r .token "$HOME/.config/tldraw/server.json")
# use as: http://localhost:$PORT/... -H "authorization: Bearer $TOKEN"
tqA ready-made helper ships next to this SKILL.md as tq. Resolve its path relative to this skill directory (the directory containing this file) — the install root differs per agent (~/.claude/skills/cli-tools/tldraw-offline, ~/.codex/skills/cli-tools/tldraw-offline, ~/.amp/skills/..., etc.):
SKILL_DIR=/path/to/this/skill # the directory containing this SKILL.md
sh "$SKILL_DIR/tq" POST /api/search '{"code":"return await api.getDocs()"}'
sh "$SKILL_DIR/tq" POST /api/doc/DOC_ID/exec 'return editor.getCurrentPageShapes().length'
sh "$SKILL_DIR/tq" GET /api/doc/DOC_ID/script-status
tq re-reads the port and token from server.json itself on every call, so you never handle the token or the fresh-shell env problem. A body starting with { is sent as JSON; anything else as raw text/plain. If tq is missing, fall back to raw curl with the PORT/TOKEN reads shown above. The raw-curl examples below stay in explicit form so each request is visible; translate any to sh "$SKILL_DIR/tq" <METHOD> <path> [body].
curl -s http://localhost:7236/readme
POST /api/search: run JavaScript with an api object. Use this to discover docs, read shapes and bindings, capture screenshots, and query the editor API reference.POST /api/doc/:id/exec: run JavaScript with a live tldraw editor scoped to one document. Use this for saved canvas edits.POST /api/doc/:id/script-workspace: expose live script paths for direct durable document-script and asset edits.GET /api/doc/:id/script-status: inspect watcher state for script/** edits and find errorLogPath.The code-taking POST endpoints accept raw JavaScript as the request body (content-type: text/plain) or a JSON body {"code": "..."}, and wrap the code in an async function so top-level await works. Prefer raw bodies for shell use.
Most tasks do not require searching api.members. Start with these calls and search the full Editor API only if a snippet fails or you truly need an unknown method. The object is api, not spec. Each block below is shown as raw curl so the request is visible; sh "$SKILL_DIR/tq" <METHOD> <path> [body] is the shorter equivalent that handles the port and token for you.
# Fresh shell per call: re-read port + token first (or use the values already in your context).
PORT=$(jq -r .port "$HOME/.config/tldraw/server.json"); TOKEN=$(jq -r .token "$HOME/.config/tldraw/server.json")
# Pick the target doc by focused window or filename.
curl -s -X POST http://localhost:$PORT/api/search \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"return await api.getDocs({ name: \"NAME\" })"}'
# Read the current page's shapes with ids, bounds, text, and metadata.
curl -s -X POST http://localhost:$PORT/api/search \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"const doc = await api.getFocusedDoc(); const page = doc ? await api.getShapes(doc.id) : null; return { doc, shapes: page?.shapes.map(s => ({ id: s.id, type: s.type, x: s.x, y: s.y, props: s.props, meta: s.meta })) ?? [] }"}'
# Read bindings only for connection-dependent behavior.
curl -s -X POST http://localhost:$PORT/api/search \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"const doc = await api.getFocusedDoc(); return doc ? await api.getBindings(doc.id) : []"}'
api.recipes (via /api/search) is an object keyed by recipe id; read one in full with api.recipes['<id>']. Query it when a task matches one of the worked recipes:
stack-existing-boxes — Stack existing boxesadd-durable-behavior-with-a-document-script — Add durable behavior with a document scripteditable-furniture-with-anchored-internals — Editable furniture with anchored internalsclickable-card-or-button-ui — Clickable card or button UIconnection-dependent-behavior — Connection-dependent behavioranimation-simulation-loop — Animation / simulation loopcustom-shape-config-js — Custom shape (config.js)custom-overlay-config-js — Custom overlay (config.js)Fetch /readme when an endpoint fails or you need API details not covered here.
For durable UI behavior, open /script-workspace, write script/main.js, check script-status, then verify behavior once. script-status returns a derived state field — treat state: "applied" as success; "pending" means the watcher has not caught up yet (retry once), and "error" means the apply failed (read lastApplyError / errorLogPath). Branch on state rather than comparing the raw digests yourself. The /script-workspace response reports isDefaultScript (true while script/main.js is still the untouched starter template, pre-created when absent) — when isDefaultScript is false there is a preexisting script to extend, not clobber. Read mainJsPath to see the current contents before editing (and read it once first if your file tools refuse to write a file they have not read). Do not spend the run searching for pointer/click APIs — read the clickable-UI recipe from api.recipes first.
api.getShapes(), /exec, and document scripts all use raw tldraw SDK records. Create shapes with normal tldraw partials. Prefer importing primitives from 'tldraw' when the host import map is active — in an /exec snippet use await import('tldraw') (a snippet can't use a static import); a document script can use a top-level import { createShapeId } from 'tldraw'. The helpers bag carries only editor-bound conveniences (not SDK primitives) — import primitives from 'tldraw' directly. Read api.imports (from /api/search) for the full list of importable symbols:
const { createShapeId, toRichText } = await import('tldraw')
editor.createShape({
id: createShapeId('box1'),
type: 'geo',
x: 100,
y: 100,
props: { geo: 'rectangle', w: 300, h: 200, richText: toRichText('Label') },
})
Use api.getShapes(doc.id) to inspect existing raw shape records before mutating them.
api.getScreenshot(docId, opts?) captures a JPEG to a temp file and returns { filePath, width, height, pageName, viewport, bounds, captureMode } — a path, not image data, so open the file yourself to look at it. opts.size is 'small' | 'medium' | 'large' | 'full' (default 'small'). opts.mode is 'canvas' (default — just the shapes, framed to their bounds) or 'window' (the whole app window: canvas plus UI chrome); use 'window' to see UI a script's components override draws outside the canvas. opts.bounds ({ x, y, w, h } in page coordinates) applies to 'canvas' mode only. Prefer reading records with api.getShapes(); screenshot only when visual placement is uncertain or the user asks for visual proof.
helpers.createArrowBetweenShapes(fromId, toId, options) so both endpoints have real bindings.helpers.getLints() before reporting a diagram complete and address every actionable result. Fetch /readme for the helper recipe and the opt-out for intentional decorative arrows./exec./script-workspace and direct filesystem edits under script/**. Read the worked recipes from api.recipes (via /api/search) before building durable behavior.api.getShapes(), api.getBindings(), api.getScriptStatus(), or a screenshot when visual placement is uncertain.Never edit .tldraw archive files directly while they are open, and never edit db.sqlite, db.sqlite-wal, db.sqlite-shm, metadata.json, .lock, or .script-workspace/**.
Use this when a document script draws a board that users should rearrange or restyle while script-owned animation/game pieces still follow it.
helpers.createShapeIfMissing / helpers.createShapesIfMissing; never delete and redraw it on rerun.helpers.onShapeTranslate(anchorId, ({ dx, dy }) => ... , { signal }) to respond only to that anchor.helpers.translateShapes(..., dx, dy) (it runs without recording undo history); wrap other script-owned writes in editor.run(fn, { history: 'ignore' }).store.listen / afterChange layout handlers that react to every shape; they can treat the script's own writes as new user edits and recurse.config.js)Custom shape types, tools, overlays, or UI components need a script/config.js next to main.js (create it through /script-workspace, same as main.js) — a main.js-only script cannot register them. Its default export runs BEFORE the editor mounts, receives { config } (the app's default TldrawConfig), and returns it after mutating or spreading it. The passed config carries shapeUtils, bindingUtils, assetUtils, overlayUtils, tools (arrays of constructors), components (a TLComponents map), and options; optional getShapeVisibility(shape, editor), assetUrls, and initialState. Push your constructors onto the arrays — a util/tool whose static type/id matches a stock one replaces it. Custom shapes subclass ShapeUtil and custom overlays subclass OverlayUtil (both from 'tldraw'); define them in a sibling file and import them, since config.js and main.js are separate module graphs.
Read the worked custom-shape and custom-overlay recipes from api.recipes for the full ShapeUtil / OverlayUtil skeletons before writing either. Saving config.js (or a file it imports) rebuilds the store and editor — document, camera, and selection are preserved but undo history resets — whereas saving main.js never remounts. Keep run-on-mount logic in main.js; config.js only builds the config. Types live in .script-workspace/script-context.d.ts (ConfigScriptContext, TldrawConfig).
Shown as raw curl; sh "$SKILL_DIR/tq" <METHOD> <path> [body] is the shorter equivalent that handles the port and token for you.
# Fresh shell per call: re-read port + token (or use the values already in your context).
PORT=$(jq -r .port "$HOME/.config/tldraw/server.json"); TOKEN=$(jq -r .token "$HOME/.config/tldraw/server.json")
# Discover docs.
curl -s -X POST http://localhost:$PORT/api/search \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"return await api.getDocs()"}'
# Read shapes for a doc.
curl -s -X POST http://localhost:$PORT/api/search \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"const [doc] = await api.getDocs(); return await api.getShapes(doc.id)"}'
# Mutate with /exec, then verify once with api.getShapes().
curl -s -X POST http://localhost:$PORT/api/doc/DOC_ID/exec \
-H 'content-type: application/json' \
-H "authorization: Bearer $TOKEN" \
-d '{"code":"const { createShapeId, toRichText } = await import(\"tldraw\"); const id = createShapeId(\"r1\"); editor.createShape({ id, type: \"geo\", x: 100, y: 100, props: { geo: \"rectangle\", w: 200, h: 100, richText: toRichText(\"Hello\") } }); return { created: [id] }"}'
Keep summaries tight. Include the doc id/name, changed shape ids or script path, and the one verification result. If something fails, quote the server error, digest mismatch, or the relevant .script-workspace/error.log line.
documentation
Save notes, journal entries, and research to the personal-notes Obsidian vault (personal-vault-v2). Use when the user asks to 'save note', 'save to notes', 'write to personal notes', 'save to daily notes', 'note this down', or wants to persist findings/analysis to their personal vault.
documentation
Use whenever the user asks to address, fix, resolve, review, or respond to pull-request comments or review feedback.
development
Apply Not Matthias's Rust-first personal coding style. Use whenever the user explicitly asks to apply or review their code style, make Rust match their preferences, perform a style pass, or simplify/refactor according to their conventions. Inspect only task-touched code, honor local project conventions first, and make only safe opt-out style edits.
development
Guide for writing ast-grep rules to perform structural code search and analysis. Use when users need to search codebases using Abstract Syntax Tree (AST) patterns, find specific code structures, or perform complex code queries that go beyond simple text search. This skill should be used when users ask to search for code patterns, find specific language constructs, or locate code with particular structural characteristics.