[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7BFVC2K5P3FD79TQRE5RAK3":3,"skill-same:01M7BFVC2K5P3FD79TQRE5RAK3":75},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":43,"repositoryHtmlUrl":44,"repositoryHomepageUrl":6,"ownerHtmlUrl":45,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":46,"install":47,"resources":60},"01M7BFVC2K5P3FD79TQRE5RAK3","tldraw-offline",null,"Drive and script tldraw offline canvases with an agent.","LIJALEN23\u002FBeryl","LIJALEN23","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F141412346?v=4","01M7AQK5G18E0M3NKP1S35F4KE","oneplusnine","01M7AQK5GNQE3MMPFJ9TCCJQCN",1,"MIT","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: tldraw-offline\ndescription: Drive and script tldraw offline canvases with an agent.\nversion: 1.0.0\nauthor: Teknium + Hermes Agent\nlicense: MIT\nplatforms: [linux, macos, windows]\nmetadata:\n  hermes:\n    tags: [tldraw, canvas, whiteboard, document-script, diagramming]\n    category: creative\n    related_skills: []\n---\n\n# tldraw offline Skill\n\nWork with the tldraw offline desktop app (offline.tldraw.com): read the open\ncanvas, make edits, and write **document scripts** — JavaScript embedded in a\n`.tldraw` file that runs on load and gives the file durable behavior. The app\nruns a **local HTTP API** (default `localhost:7236`) that a coding agent drives\nwith plain `curl` from its terminal — this is exactly how the app's own homepage\ndemo (Codex editing a canvas live) works. The agent does NOT use computer-use \u002F\nGUI clicking, and does NOT hand-edit the `.tldraw` file directly. Keep tldraw\noffline open while you work.\n\n## When to Use\n\n- The user has tldraw offline open and asks you to build or modify a canvas\n  (diagrams, wireframes, layouts).\n- You want to add durable behavior to a drawing (reactive shapes, interactive\n  buttons, animation, connection logic) via an embedded document script.\n\nDo NOT hand-place shapes to imitate a drawing — write the code that generates\nthem. Agents are far better at scripting the canvas than at drawing on it.\n\n## Prerequisites\n\n- **tldraw offline installed and running**, with a document open. Releases:\n  https:\u002F\u002Fgithub.com\u002Ftldraw\u002Ftldraw-offline\u002Freleases\u002Flatest (macOS DMG, Windows\n  x64\u002FArm64, Linux `x86_64`\u002F`arm64` AppImage or amd64\u002Farm64 `.deb`).\n- **Agent skills installed in the app**: `Develop → Install Agent Skills`. The\n  app writes its own tldraw skill into `~\u002F.codex\u002Fskills\u002F`, `~\u002F.claude\u002Fskills\u002F`,\n  `~\u002F.cursor\u002Fskills\u002F`, and `~\u002F.gemini\u002Fskills\u002F` — teaching that agent the `curl`\n  recipes below. (This Hermes skill mirrors that guidance for Hermes.)\n- **The local control API.** On launch the app writes `server.json` to its config\n  dir (Linux `~\u002F.config\u002Ftldraw\u002F`, macOS `~\u002FLibrary\u002FApplication Support\u002Ftldraw\u002F`,\n  Windows `%APPDATA%\\tldraw\\`) with `port` (default `7236`), a bearer `token`,\n  `pid`, and `startedAt`. Every request except `GET \u002F` needs\n  `Authorization: Bearer \u003Ctoken>`. A clean quit removes `server.json`; if it's\n  present but the port doesn't answer, the app quit uncleanly — treat as not\n  running.\n- **Re-read port + token on EVERY shell call.** Each terminal call is a fresh\n  shell, so an `export`ed token does not persist — \"export once and reuse\" sends\n  an empty token and 401s. Read both inline at the top of each call:\n  `PORT=$(jq -r .port \u003Cserver.json>); TOKEN=$(jq -r .token \u003Cserver.json>)`.\n- No account or network needed for local editing.\n\n## How to Run\n\nTwo distinct workflows. Pick by whether the change must survive a reload.\n\n**A. One-off canvas edits (`\u002Fexec`)** — layout, generating shapes, cleanup. This\nis a live edit, not saved script:\n\n```bash\nBASE=http:\u002F\u002Flocalhost:7236\nTOKEN=$(python -c \"import json;print(json.load(open('$HOME\u002F.config\u002Ftldraw\u002Fserver.json'))['token'])\")\n# find the focused document id\nDOC=$(curl -s \"$BASE\u002Fapi\u002Fsearch\" -X POST -H 'content-type: application\u002Fjson' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"code\":\"return (await api.getFocusedDoc()).id\"}' | python -c \"import sys,json;print(json.load(sys.stdin)['result'])\")\n# run code with the live `editor` + `helpers` in scope\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fexec\" -X POST -H 'content-type: application\u002Fjson' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -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\"}'\n```\n\n**B. Durable behavior (`script\u002Fmain.js`)** — reactive\u002Finteractive logic that must\nsurvive reload. Edit the file on disk; the app's watcher applies it:\n\n```bash\n# get the live script file path for the doc\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fscript-workspace\" -X POST \\\n  -H \"Authorization: Bearer $TOKEN\"          # -> result.mainJsPath, result.isDefaultScript\n# edit result.mainJsPath with read_file \u002F patch \u002F write_file (see scripts\u002Fmain.js)\n# then confirm the watcher applied it:\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fscript-status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\nThe ready-to-adapt document script is `scripts\u002Fmain.js`.\n\n## Quick Reference\n\nThe document-script contract (verified against the app's bundled\n`script-context.d.ts`):\n\n```js\nimport { createShapeId, toRichText } from 'tldraw'   \u002F\u002F primitives: import, not globals\n\nexport default function ({ editor, helpers, signal }) {\n  editor.run(() => {                                 \u002F\u002F batch = one undo step\n    helpers.createShapeIfMissing({                   \u002F\u002F idempotent furniture\n      id: createShapeId('node-1'), type: 'geo', x: 0, y: 0,\n      props: { geo: 'rectangle', w: 200, h: 100, richText: toRichText('hi') },\n    })\n  })\n\n  const stop = editor.store.listen(() => { \u002F* react *\u002F })  \u002F\u002F fires the tick AFTER a commit\n  signal.addEventListener('abort', () => stop())           \u002F\u002F REQUIRED cleanup on rerun\u002Fclose\n}\n```\n\n- `ctx.editor` — the live `Editor` (`createShape`, `updateShape`, `deleteShapes`,\n  `getCurrentPageShapes`, `getShape`, `getBindingsFromShape`, `zoomToFit`,\n  `on('tick'|'event', fn)`, `run(fn, { history: 'ignore' })`).\n- `ctx.helpers` — `createShapeIfMissing`, `createShapesIfMissing`,\n  `createArrowBetweenShapes(from, to, { arrowheadEnd })`, `translateShapes`,\n  `onShapeTranslate(id, fn, { signal })`, `richTextToPlainText`, `boxShapes`,\n  `getLints`.\n- `ctx.signal` — `AbortSignal`; attach every listener\u002Finterval teardown to it.\n- `config.js` (separate file) registers custom shape\u002Ftool\u002Fcomponent utils and\n  runs before mount; `main.js` runs against the mounted editor and reruns on save.\n\n## Interactive UI (clickable buttons that drive state)\n\nDrawn shapes can behave like a real app — the thing a static whiteboard can't do.\nFull example: `scripts\u002Fcounter.js` (a number display + MINUS\u002FRESET\u002FPLUS buttons).\n\nVerification boundary — read this before claiming interaction works or doesn't.\nThe app's OWN agent playbook says to verify a clickable-UI script with \"one\nsimulated click and one state read\" via `\u002Fexec` (`editor.dispatch` a pointer\nevent, await a tick, read the shape's state) — NOT by driving a real mouse. By\nthat standard the counter is verified: dispatched clicks stepped it\n`0 → 1 → 2 → 1 → 0`. Two caveats worth writing down:\n- **The script only runs once the app's file-watcher applies it.** On Linux that\n  watcher uses inotify; a host with an exhausted `fs.inotify.max_user_instances`\n  logs `inotify_add_watch ... No space left on device`, `script-status` shows\n  `state: \"not-watching\"` \u002F `hasEntry: false`, and the script never executes.\n  This is a host limit, not a script bug — a normal desktop is unaffected.\n- **Driving the canvas with computer-use needs FOREGROUND delivery, not\n  background.** cua-driver's default `background` delivery returns\n  `background_unavailable` against Electron's occluded renderer — but that is the\n  first rung, not a wall. cua-driver returns an `escalation: \"foreground\"` hint;\n  set `delivery_mode: \"foreground\"` (pair with `bring_to_front`) and its X11 XTest\n  path (`x11_xtest_fg`) clicks land — this dismisses the \"Run Script\" consent\n  dialog and clicks the canvas. Verified by the Cua team on this exact v1.11.0\n  AppImage (Linux\u002FX11). Do NOT conclude \"Electron rejects synthetic clicks\" and\n  give up in background mode — climb to foreground. (The real product path is\n  still `\u002Fexec`, not clicks; this note is for computer-use-driven testing.)\n\nThe pattern:\n\n```js\nexport default function ({ editor, helpers, signal }) {\n  \u002F\u002F 1. Build buttons idempotently; tag each with meta so the handler finds them.\n  \u002F\u002F    Give buttons a visible label AND a meta.action.\n  \u002F\u002F 2. Hit-test pointer_down in PAGE coordinates against the button bounds:\n  const inside = (b, p) => p.x >= b.x && p.x \u003C= b.x + b.w && p.y >= b.y && p.y \u003C= b.y + b.h\n  function onEvent(info) {\n    if (!info || info.name !== 'pointer_down') return\n    let p = null\n    try { if (info.point && editor.screenToPage) p = editor.screenToPage(info.point) } catch {}\n    p = p ?? editor.inputs?.currentPagePoint\n    if (!p) return\n    const hit = editor.getCurrentPageShapes().find(\n      (s) => s.meta?.ui === 'button' &&\n        inside({ x: s.x, y: s.y, w: s.props.w, h: s.props.h }, p)\n    )\n    if (hit) runAction(hit.meta.action)   \u002F\u002F mutate state; store it in a shape's meta\n  }\n  editor.on('event', onEvent)\n  signal.addEventListener('abort', () => editor.off('event', onEvent))  \u002F\u002F REQUIRED\n}\n```\n\n- Find buttons by `meta` (or visible label via `helpers.richTextToPlainText`),\n  not by hard-coded coordinates.\n- **One script owns both build and read.** If the shapes are created by one code\n  path (with `meta.action: 'inc'`) and the handler reads another convention\n  (`meta.action === 'PLUS'`), clicks silently do nothing. Ship the buttons built\n  by the same script that handles them, or ship an empty canvas so the script\n  builds them fresh — never pre-bake mismatched shapes into the file's db.\n- Keep app state in a shape's `meta` (e.g. `meta.count`) and render it as that\n  shape's `richText` label, so it survives save and is readable for verification.\n- **Detach the listener on `signal` abort.** Skipping this is not cosmetic: on\n  the next save the old `onEvent` stays attached alongside the new one, so every\n  click fires twice and a counter jumps by 2 instead of 1.\n- For continuous motion use `editor.on('tick', fn)`; for a moving anchor with\n  attached pieces use `helpers.onShapeTranslate(id, fn, { signal })`.\n\n### Shipping a self-running scripted `.tldraw`\n\nA `.tldraw` is a zip of `metadata.json` + `session.json` + `db.sqlite` + `assets\u002F`\n+ `script\u002F` (only those entries are packable). For the script to auto-run without\nthe \"This document contains a script → Run Script\" consent dialog:\n\n- `metadata.json` must carry a `script` manifest: `{ \"sha256\": \"\u003Cdigest>\" }`, where\n  the digest is `sha256` over each sorted `script\u002F` path as `` `${path}\\0${sha256hex(bytes)}\\n` ``.\n  A mismatch is rejected as tampered.\n- Pre-trust the digest by adding it to `~\u002F.tldraw\u002Fscript-trust.json`\n  (`{ \"trusted\": [\"\u003Cdigest>\"] }`, or `$TLDRAW_SCRIPT_TRUST`). The app skips consent\n  when `isScriptTrusted(digest)` is true.\n\n## Procedure\n\n1. Read the current token\u002Fport from `server.json`. Find the target doc with\n   `api.getFocusedDoc()` (or `api.getDocs()`); name it explicitly if several are\n   open.\n2. For layout\u002Fgeneration, use `\u002Fexec`. For durable behavior, edit\n   `script\u002Fmain.js` via `\u002Fscript-workspace`.\n3. Make scripts idempotent: create durable shapes with `helpers.createShapeIfMissing`\n   and stable `createShapeId('name')` ids. Scripts rerun on every load.\n4. Keep script-owned writes out of the user's undo stack:\n   `editor.run(fn, { history: 'ignore' })` (or `helpers.translateShapes`, which\n   already does).\n5. For reactivity, `editor.store.listen(cb)` and tear it down on `signal` abort.\n   For interaction, `editor.on('event', h)` (hit-test `pointer_down` in page\n   coords); for animation, `editor.on('tick', h)`.\n6. For a single moving anchor + attached internals, prefer\n   `helpers.onShapeTranslate(anchorId, fn, { signal })` over a broad store\n   listener — a broad listener can turn your own writes into feedback loops.\n\n## Shape props (validated against tldraw SDK v5 schema)\n\n`editor.createShape` \u002F `createShapeIfMissing` accept partial props (shape utils\nfill defaults). When building **raw records** for a file snapshot, every prop\nbelow is required (run `scripts\u002Fvalidate_shapes.mjs`):\n\n| Shape | Required props |\n|-------|----------------|\n| `note`  | `richText`, `color`, `labelColor`, `size`, `font`, `align`, `verticalAlign`, `growY`, `fontSizeAdjustment`, `url`, `scale`, `textLastEditedBy` |\n| `text`  | `richText`, `color`, `size`, `font`, `textAlign`, `w`, `scale`, `autoSize` |\n| `frame` | `w`, `h`, `name`, `color` |\n| `geo`   | `geo`, `w`, `h`, `color`, `fill`, `richText` (+ dash\u002Fsize\u002Fetc. defaulted) |\n\n`richText` must be `toRichText('...')` — a bare string is rejected. `color` enum:\n`black grey light-violet violet blue light-blue yellow orange green light-green\nlight-red red white`. `font` enum: `draw sans serif mono`.\n\n## Pitfalls\n\n- **`store.listen` fires on the tick AFTER a commit, not synchronously.** If you\n  write a shape and immediately read state expecting the listener to have run, it\n  hasn't. Verified live: an in-turn read shows 0 fires; after one `setTimeout`\n  tick it shows 1. Same reason the app notes `editor.dispatch` is async — await a\n  tick before verifying.\n- **`ctx`, not globals.** The entry is `export default function ({ editor,\n  helpers, signal })`. There is no bare `editor` global in a document script.\n  `createShapeId` \u002F `toRichText` \u002F `Vec` come from `import ... from 'tldraw'`.\n- **`richText`, not `text`.** Text\u002Fnote\u002Fgeo labels use `richText: toRichText(s)`.\n- **Raw records need every prop; `createShape` does not.** In-app pass only the\n  props you care about; a hand-built `.tldraw` snapshot needs the full set (table).\n- **Scripts rerun on every load — be idempotent.** Use `createShapeIfMissing`\n  with stable ids or you duplicate content and clobber user edits.\n- **Clean up on `signal`.** `signal.addEventListener('abort', () => stop())` for\n  every `store.listen` \u002F `editor.on` \u002F `setInterval`; the signal fires before\n  rerun and on close.\n- **Keep script writes out of undo:** `editor.run(fn, { history: 'ignore' })`.\n- **`editor.on('tick')` pauses when the window is hidden** (it is a RAF loop);\n  `setInterval` keeps firing but Electron throttles it to ~1\u002Fs in the background.\n- **The API needs the bearer token** from `server.json`; the port can be non-default\n  (`server.listen(0)` picks one) — always read the file, don't hardcode `7236`.\n- **Only `tldraw` \u002F `react` \u002F `react-dom` import** — not a Node project.\n\n## Verification\n\n- **Shape schema (offline, no app):** `node scripts\u002Fvalidate_shapes.mjs` — builds\n  the real tldraw schema and validates note\u002Ftext\u002Fframe. Passing prints `3\u002F3`.\n- **Live canvas edits:** after `\u002Fexec`, read back with `\u002Fapi\u002Fsearch` →\n  `api.getShapes(docId)` (returns `{ page, viewport, shapes }`) and\n  `api.getBindings(docId)` (array). Confirm expected shapes\u002Fbindings exist. Grab\n  `api.getScreenshot(docId)` (returns `{ filePath, ... }`) and inspect the PNG\u002FJPEG\n  with `vision_analyze`.\n- **Durable script applied:** `GET \u002Fapi\u002Fdoc\u002F:id\u002Fscript-status`. Success is\n  `state: \"applied\"` (`currentDiskDigest === lastAppliedDigest === manifestSha256`,\n  `pendingApply === false`, `lastApplyError === null`). If it stays `\"pending\"`\n  after a short retry, report that instead of claiming success; `\"error\"` means\n  the apply failed — read `errorLogPath`.\n","# tldraw offline Skill\n\nWork with the tldraw offline desktop app (offline.tldraw.com): read the open\ncanvas, make edits, and write **document scripts** — JavaScript embedded in a\n`.tldraw` file that runs on load and gives the file durable behavior. The app\nruns a **local HTTP API** (default `localhost:7236`) that a coding agent drives\nwith plain `curl` from its terminal — this is exactly how the app's own homepage\ndemo (Codex editing a canvas live) works. The agent does NOT use computer-use \u002F\nGUI clicking, and does NOT hand-edit the `.tldraw` file directly. Keep tldraw\noffline open while you work.\n\n## When to Use\n\n- The user has tldraw offline open and asks you to build or modify a canvas\n  (diagrams, wireframes, layouts).\n- You want to add durable behavior to a drawing (reactive shapes, interactive\n  buttons, animation, connection logic) via an embedded document script.\n\nDo NOT hand-place shapes to imitate a drawing — write the code that generates\nthem. Agents are far better at scripting the canvas than at drawing on it.\n\n## Prerequisites\n\n- **tldraw offline installed and running**, with a document open. Releases:\n  https:\u002F\u002Fgithub.com\u002Ftldraw\u002Ftldraw-offline\u002Freleases\u002Flatest (macOS DMG, Windows\n  x64\u002FArm64, Linux `x86_64`\u002F`arm64` AppImage or amd64\u002Farm64 `.deb`).\n- **Agent skills installed in the app**: `Develop → Install Agent Skills`. The\n  app writes its own tldraw skill into `~\u002F.codex\u002Fskills\u002F`, `~\u002F.claude\u002Fskills\u002F`,\n  `~\u002F.cursor\u002Fskills\u002F`, and `~\u002F.gemini\u002Fskills\u002F` — teaching that agent the `curl`\n  recipes below. (This Hermes skill mirrors that guidance for Hermes.)\n- **The local control API.** On launch the app writes `server.json` to its config\n  dir (Linux `~\u002F.config\u002Ftldraw\u002F`, macOS `~\u002FLibrary\u002FApplication Support\u002Ftldraw\u002F`,\n  Windows `%APPDATA%\\tldraw\\`) with `port` (default `7236`), a bearer `token`,\n  `pid`, and `startedAt`. Every request except `GET \u002F` needs\n  `Authorization: Bearer \u003Ctoken>`. A clean quit removes `server.json`; if it's\n  present but the port doesn't answer, the app quit uncleanly — treat as not\n  running.\n- **Re-read port + token on EVERY shell call.** Each terminal call is a fresh\n  shell, so an `export`ed token does not persist — \"export once and reuse\" sends\n  an empty token and 401s. Read both inline at the top of each call:\n  `PORT=$(jq -r .port \u003Cserver.json>); TOKEN=$(jq -r .token \u003Cserver.json>)`.\n- No account or network needed for local editing.\n\n## How to Run\n\nTwo distinct workflows. Pick by whether the change must survive a reload.\n\n**A. One-off canvas edits (`\u002Fexec`)** — layout, generating shapes, cleanup. This\nis a live edit, not saved script:\n\n```bash\nBASE=http:\u002F\u002Flocalhost:7236\nTOKEN=$(python -c \"import json;print(json.load(open('$HOME\u002F.config\u002Ftldraw\u002Fserver.json'))['token'])\")\n# find the focused document id\nDOC=$(curl -s \"$BASE\u002Fapi\u002Fsearch\" -X POST -H 'content-type: application\u002Fjson' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -d '{\"code\":\"return (await api.getFocusedDoc()).id\"}' | python -c \"import sys,json;print(json.load(sys.stdin)['result'])\")\n# run code with the live `editor` + `helpers` in scope\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fexec\" -X POST -H 'content-type: application\u002Fjson' \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -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\"}'\n```\n\n**B. Durable behavior (`script\u002Fmain.js`)** — reactive\u002Finteractive logic that must\nsurvive reload. Edit the file on disk; the app's watcher applies it:\n\n```bash\n# get the live script file path for the doc\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fscript-workspace\" -X POST \\\n  -H \"Authorization: Bearer $TOKEN\"          # -> result.mainJsPath, result.isDefaultScript\n# edit result.mainJsPath with read_file \u002F patch \u002F write_file (see scripts\u002Fmain.js)\n# then confirm the watcher applied it:\ncurl -s \"$BASE\u002Fapi\u002Fdoc\u002F$DOC\u002Fscript-status\" -H \"Authorization: Bearer $TOKEN\"\n```\n\nThe ready-to-adapt document script is `scripts\u002Fmain.js`.\n\n## Quick Reference\n\nThe document-script contract (verified against the app's bundled\n`script-context.d.ts`):\n\n```js\nimport { createShapeId, toRichText } from 'tldraw'   \u002F\u002F primitives: import, not globals\n\nexport default function ({ editor, helpers, signal }) {\n  editor.run(() => {                                 \u002F\u002F batch = one undo step\n    helpers.createShapeIfMissing({                   \u002F\u002F idempotent furniture\n      id: createShapeId('node-1'), type: 'geo', x: 0, y: 0,\n      props: { geo: 'rectangle', w: 200, h: 100, richText: toRichText('hi') },\n    })\n  })\n\n  const stop = editor.store.listen(() => { \u002F* react *\u002F })  \u002F\u002F fires the tick AFTER a commit\n  signal.addEventListener('abort', () => stop())           \u002F\u002F REQUIRED cleanup on rerun\u002Fclose\n}\n```\n\n- `ctx.editor` — the live `Editor` (`createShape`, `updateShape`, `deleteShapes`,\n  `getCurrentPageShapes`, `getShape`, `getBindingsFromShape`, `zoomToFit`,\n  `on('tick'|'event', fn)`, `run(fn, { history: 'ignore' })`).\n- `ctx.helpers` — `createShapeIfMissing`, `createShapesIfMissing`,\n  `createArrowBetweenShapes(from, to, { arrowheadEnd })`, `translateShapes`,\n  `onShapeTranslate(id, fn, { signal })`, `richTextToPlainText`, `boxShapes`,\n  `getLints`.\n- `ctx.signal` — `AbortSignal`; attach every listener\u002Finterval teardown to it.\n- `config.js` (separate file) registers custom shape\u002Ftool\u002Fcomponent utils and\n  runs before mount; `main.js` runs against the mounted editor and reruns on save.\n\n## Interactive UI (clickable buttons that drive state)\n\nDrawn shapes can behave like a real app — the thing a static whiteboard can't do.\nFull example: `scripts\u002Fcounter.js` (a number display + MINUS\u002FRESET\u002FPLUS buttons).\n\nVerification boundary — read this before claiming interaction works or doesn't.\nThe app's OWN agent playbook says to verify a clickable-UI script with \"one\nsimulated click and one state read\" via `\u002Fexec` (`editor.dispatch` a pointer\nevent, await a tick, read the shape's state) — NOT by driving a real mouse. By\nthat standard the counter is verified: dispatched clicks stepped it\n`0 → 1 → 2 → 1 → 0`. Two caveats worth writing down:\n- **The script only runs once the app's file-watcher applies it.** On Linux that\n  watcher uses inotify; a host with an exhausted `fs.inotify.max_user_instances`\n  logs `inotify_add_watch ... No space left on device`, `script-status` shows\n  `state: \"not-watching\"` \u002F `hasEntry: false`, and the script never executes.\n  This is a host limit, not a script bug — a normal desktop is unaffected.\n- **Driving the canvas with computer-use needs FOREGROUND delivery, not\n  background.** cua-driver's default `background` delivery returns\n  `background_unavailable` against Electron's occluded renderer — but that is the\n  first rung, not a wall. cua-driver returns an `escalation: \"foreground\"` hint;\n  set `delivery_mode: \"foreground\"` (pair with `bring_to_front`) and its X11 XTest\n  path (`x11_xtest_fg`) clicks land — this dismisses the \"Run Script\" consent\n  dialog and clicks the canvas. Verified by the Cua team on this exact v1.11.0\n  AppImage (Linux\u002FX11). Do NOT conclude \"Electron rejects synthetic clicks\" and\n  give up in background mode — climb to foreground. (The real product path is\n  still `\u002Fexec`, not clicks; this note is for computer-use-driven testing.)\n\nThe pattern:\n\n```js\nexport default function ({ editor, helpers, signal }) {\n  \u002F\u002F 1. Build buttons idempotently; tag each with meta so the handler finds them.\n  \u002F\u002F    Give buttons a visible label AND a meta.action.\n  \u002F\u002F 2. Hit-test pointer_down in PAGE coordinates against the button bounds:\n  const inside = (b, p) => p.x >= b.x && p.x \u003C= b.x + b.w && p.y >= b.y && p.y \u003C= b.y + b.h\n  function onEvent(info) {\n    if (!info || info.name !== 'pointer_down') return\n    let p = null\n    try { if (info.point && editor.screenToPage) p = editor.screenToPage(info.point) } catch {}\n    p = p ?? editor.inputs?.currentPagePoint\n    if (!p) return\n    const hit = editor.getCurrentPageShapes().find(\n      (s) => s.meta?.ui === 'button' &&\n        inside({ x: s.x, y: s.y, w: s.props.w, h: s.props.h }, p)\n    )\n    if (hit) runAction(hit.meta.action)   \u002F\u002F mutate state; store it in a shape's meta\n  }\n  editor.on('event', onEvent)\n  signal.addEventListener('abort', () => editor.off('event', onEvent))  \u002F\u002F REQUIRED\n}\n```\n\n- Find buttons by `meta` (or visible label via `helpers.richTextToPlainText`),\n  not by hard-coded coordinates.\n- **One script owns both build and read.** If the shapes are created by one code\n  path (with `meta.action: 'inc'`) and the handler reads another convention\n  (`meta.action === 'PLUS'`), clicks silently do nothing. Ship the buttons built\n  by the same script that handles them, or ship an empty canvas so the script\n  builds them fresh — never pre-bake mismatched shapes into the file's db.\n- Keep app state in a shape's `meta` (e.g. `meta.count`) and render it as that\n  shape's `richText` label, so it survives save and is readable for verification.\n- **Detach the listener on `signal` abort.** Skipping this is not cosmetic: on\n  the next save the old `onEvent` stays attached alongside the new one, so every\n  click fires twice and a counter jumps by 2 instead of 1.\n- For continuous motion use `editor.on('tick', fn)`; for a moving anchor with\n  attached pieces use `helpers.onShapeTranslate(id, fn, { signal })`.\n\n### Shipping a self-running scripted `.tldraw`\n\nA `.tldraw` is a zip of `metadata.json` + `session.json` + `db.sqlite` + `assets\u002F`\n+ `script\u002F` (only those entries are packable). For the script to auto-run without\nthe \"This document contains a script → Run Script\" consent dialog:\n\n- `metadata.json` must carry a `script` manifest: `{ \"sha256\": \"\u003Cdigest>\" }`, where\n  the digest is `sha256` over each sorted `script\u002F` path as `` `${path}\\0${sha256hex(bytes)}\\n` ``.\n  A mismatch is rejected as tampered.\n- Pre-trust the digest by adding it to `~\u002F.tldraw\u002Fscript-trust.json`\n  (`{ \"trusted\": [\"\u003Cdigest>\"] }`, or `$TLDRAW_SCRIPT_TRUST`). The app skips consent\n  when `isScriptTrusted(digest)` is true.\n\n## Procedure\n\n1. Read the current token\u002Fport from `server.json`. Find the target doc with\n   `api.getFocusedDoc()` (or `api.getDocs()`); name it explicitly if several are\n   open.\n2. For layout\u002Fgeneration, use `\u002Fexec`. For durable behavior, edit\n   `script\u002Fmain.js` via `\u002Fscript-workspace`.\n3. Make scripts idempotent: create durable shapes with `helpers.createShapeIfMissing`\n   and stable `createShapeId('name')` ids. Scripts rerun on every load.\n4. Keep script-owned writes out of the user's undo stack:\n   `editor.run(fn, { history: 'ignore' })` (or `helpers.translateShapes`, which\n   already does).\n5. For reactivity, `editor.store.listen(cb)` and tear it down on `signal` abort.\n   For interaction, `editor.on('event', h)` (hit-test `pointer_down` in page\n   coords); for animation, `editor.on('tick', h)`.\n6. For a single moving anchor + attached internals, prefer\n   `helpers.onShapeTranslate(anchorId, fn, { signal })` over a broad store\n   listener — a broad listener can turn your own writes into feedback loops.\n\n## Shape props (validated against tldraw SDK v5 schema)\n\n`editor.createShape` \u002F `createShapeIfMissing` accept partial props (shape utils\nfill defaults). When building **raw records** for a file snapshot, every prop\nbelow is required (run `scripts\u002Fvalidate_shapes.mjs`):\n\n| Shape | Required props |\n|-------|----------------|\n| `note`  | `richText`, `color`, `labelColor`, `size`, `font`, `align`, `verticalAlign`, `growY`, `fontSizeAdjustment`, `url`, `scale`, `textLastEditedBy` |\n| `text`  | `richText`, `color`, `size`, `font`, `textAlign`, `w`, `scale`, `autoSize` |\n| `frame` | `w`, `h`, `name`, `color` |\n| `geo`   | `geo`, `w`, `h`, `color`, `fill`, `richText` (+ dash\u002Fsize\u002Fetc. defaulted) |\n\n`richText` must be `toRichText('...')` — a bare string is rejected. `color` enum:\n`black grey light-violet violet blue light-blue yellow orange green light-green\nlight-red red white`. `font` enum: `draw sans serif mono`.\n\n## Pitfalls\n\n- **`store.listen` fires on the tick AFTER a commit, not synchronously.** If you\n  write a shape and immediately read state expecting the listener to have run, it\n  hasn't. Verified live: an in-turn read shows 0 fires; after one `setTimeout`\n  tick it shows 1. Same reason the app notes `editor.dispatch` is async — await a\n  tick before verifying.\n- **`ctx`, not globals.** The entry is `export default function ({ editor,\n  helpers, signal })`. There is no bare `editor` global in a document script.\n  `createShapeId` \u002F `toRichText` \u002F `Vec` come from `import ... from 'tldraw'`.\n- **`richText`, not `text`.** Text\u002Fnote\u002Fgeo labels use `richText: toRichText(s)`.\n- **Raw records need every prop; `createShape` does not.** In-app pass only the\n  props you care about; a hand-built `.tldraw` snapshot needs the full set (table).\n- **Scripts rerun on every load — be idempotent.** Use `createShapeIfMissing`\n  with stable ids or you duplicate content and clobber user edits.\n- **Clean up on `signal`.** `signal.addEventListener('abort', () => stop())` for\n  every `store.listen` \u002F `editor.on` \u002F `setInterval`; the signal fires before\n  rerun and on close.\n- **Keep script writes out of undo:** `editor.run(fn, { history: 'ignore' })`.\n- **`editor.on('tick')` pauses when the window is hidden** (it is a RAF loop);\n  `setInterval` keeps firing but Electron throttles it to ~1\u002Fs in the background.\n- **The API needs the bearer token** from `server.json`; the port can be non-default\n  (`server.listen(0)` picks one) — always read the file, don't hardcode `7236`.\n- **Only `tldraw` \u002F `react` \u002F `react-dom` import** — not a Node project.\n\n## Verification\n\n- **Shape schema (offline, no app):** `node scripts\u002Fvalidate_shapes.mjs` — builds\n  the real tldraw schema and validates note\u002Ftext\u002Fframe. Passing prints `3\u002F3`.\n- **Live canvas edits:** after `\u002Fexec`, read back with `\u002Fapi\u002Fsearch` →\n  `api.getShapes(docId)` (returns `{ page, viewport, shapes }`) and\n  `api.getBindings(docId)` (array). Confirm expected shapes\u002Fbindings exist. Grab\n  `api.getScreenshot(docId)` (returns `{ filePath, ... }`) and inspect the PNG\u002FJPEG\n  with `vision_analyze`.\n- **Durable script applied:** `GET \u002Fapi\u002Fdoc\u002F:id\u002Fscript-status`. Success is\n  `state: \"applied\"` (`currentDiskDigest === lastAppliedDigest === manifestSha256`,\n  `pendingApply === false`, `lastApplyError === null`). If it stays `\"pending\"`\n  after a short retry, report that instead of claiming success; `\"error\"` means\n  the apply failed — read `errorLogPath`.\n",{"name":5,"author":24,"license":15,"version":25,"metadata":26,"platforms":36,"description":7},"Teknium + Hermes Agent","1.0.0",{"hermes":27},{"tags":28,"category":34,"related_skills":35},[29,30,31,32,33],"tldraw","canvas","whiteboard","document-script","diagramming","creative",[],[37,38,39],"linux","macos","windows","master","4a5caebee4b5f9068d3a68c9a7ecd8df12bd4962","local-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline",0,"https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl","https:\u002F\u002Fgithub.com\u002FLIJALEN23","2026-09-01 14:02:08",{"source":8,"skillName":5,"commands":48,"prompt":58,"zipAvailable":59},[49,52,55],{"tool":50,"command":51},"npx","npx skills add LIJALEN23\u002FBeryl --skill tldraw-offline",{"tool":53,"command":54},"bunx","bunx skills add LIJALEN23\u002FBeryl --skill tldraw-offline",{"tool":56,"command":57},"pnpm","pnpm dlx skills add LIJALEN23\u002FBeryl --skill tldraw-offline","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline\u002FSKILL.md",true,[61,67,71],{"path":62,"resourceType":63,"resourceTypeDesc":64,"sizeBytes":65,"sourceHtmlUrl":66},"scripts\u002Fcounter.js","SCRIPT","脚本",4251,"https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline\u002Fscripts\u002Fcounter.js",{"path":68,"resourceType":63,"resourceTypeDesc":64,"sizeBytes":69,"sourceHtmlUrl":70},"scripts\u002Fmain.js",2949,"https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline\u002Fscripts\u002Fmain.js",{"path":72,"resourceType":63,"resourceTypeDesc":64,"sizeBytes":73,"sourceHtmlUrl":74},"scripts\u002Fvalidate_shapes.mjs",2387,"https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Foptional-skills\u002Fcreative\u002Ftldraw-offline\u002Fscripts\u002Fvalidate_shapes.mjs",[76,83,90,97,104,111],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":19},"01M7BGCRH58RARQTF3R8CZ84DB","test-driven-development","TDD: enforce RED-GREEN-REFACTOR, tests before code.","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Ftest-driven-development\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":19},"01M7BGCK0GGNV3K6NWMM0MDN3W","systematic-debugging","4-phase root cause debugging: understand bugs before fixing.","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Fsystematic-debugging\u002FSKILL.md",[],[],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":19},"01M7BGCF0TAZPKSM4WMYA27XGD","spike","Throwaway experiments to validate an idea before build.","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Fspike\u002FSKILL.md",[],[],{"skillNo":98,"name":99,"nameZh":6,"description":100,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":19},"01M7BGCBH01YYYBQWR7ZFRMX27","simplify-code","Parallel 4-agent cleanup of recent code changes.","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Fsimplify-code\u002FSKILL.md",[],[],{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":110,"publishedAt":19},"01M7BGC8MGHS5SRJ3NZYYNAKJY","requesting-code-review","Pre-commit review: security scan, quality gates, auto-fix.","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Frequesting-code-review\u002FSKILL.md",[],[],{"skillNo":112,"name":113,"nameZh":6,"description":114,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":115,"categories":116,"keywords":117,"publishedAt":19},"01M7BGC4HB5ZJP5N19MZTZXKZ1","python-debugpy","Debug Python: pdb REPL + debugpy remote (DAP).","https:\u002F\u002Fgithub.com\u002FLIJALEN23\u002FBeryl\u002Fblob\u002Fmaster\u002Flocal-agent\u002Fskills\u002Fsoftware-development\u002Fpython-debugpy\u002FSKILL.md",[],[]]