hermes-agent/optional-skills/creative/tldraw-offline/scripts/main.js
teknium1 e321b83392 feat(skills): add tldraw-offline agent scripting skill
Optional skill for driving the tldraw offline desktop app via its local
HTTP control API (the same curl-based path the app's own agent skills use
for Codex/Claude Code/Cursor/Gemini) — read the canvas, make live edits,
and write embedded document scripts.

Grounded in the app's bundled script-context.d.ts and agent playbook, and
in the real tldraw SDK v5 shape schema:
- document-script contract: export default function ({ editor, helpers, signal })
- HTTP API: /api/search, /api/doc/:id/exec, /api/doc/:id/script-workspace,
  /api/doc/:id/script-status (bearer token from server.json, re-read per call)
- shape schema table validated against @tldraw/tlschema (scripts/validate_shapes.mjs, 3/3)
- interactive-UI example (scripts/counter.js) + diagram-generation (scripts/main.js)
- honest verification boundary: click->state logic verified via /exec dispatch
  (0->1->2->1->0), with documented host caveats (inotify watcher, Electron
  background-click rejection)

Tests: tests/skills/test_tldraw_offline_skill.py (15 passing).
2026-07-23 08:07:00 -07:00

78 lines
2.9 KiB
JavaScript

// tldraw offline — document script (script/main.js)
//
// A document script's default export receives a ctx object and runs whenever the
// document loads (and reruns when you save the script). Contract, verified against
// the app's bundled script-context.d.ts:
//
// export default function ({ editor, helpers, signal }) { ... }
//
// editor — the live tldraw Editor for this document
// helpers — editor-bound conveniences: createShapeIfMissing, createShapesIfMissing,
// createArrowBetweenShapes, translateShapes, onShapeTranslate,
// richTextToPlainText, boxShapes, getLints
// signal — an AbortSignal fired before the script reruns and when the board
// closes. Register ALL cleanup on it (this is how you avoid leaks).
//
// Pure tldraw primitives (createShapeId, toRichText, Vec, ...) are imported from
// the `tldraw` app module — NOT globals. react / react-dom are also importable.
// It is not a Node project; only those modules are available.
import { createShapeId, toRichText } from 'tldraw'
export default function ({ editor, helpers, signal }) {
const { createShapeIfMissing, createArrowBetweenShapes } = helpers
// --- 1. Build durable "furniture" idempotently (stable ids, create-if-missing).
// Re-running the script must NOT duplicate or clobber user edits.
const nodes = [
{ id: createShapeId('node-ui'), x: 0, y: 0, color: 'blue', label: 'CLI / Gateway' },
{ id: createShapeId('node-core'), x: 280, y: 0, color: 'violet', label: 'Agent Core' },
{ id: createShapeId('node-tools'), x: 560, y: 0, color: 'green', label: 'Tools' },
]
editor.run(() => {
for (const n of nodes) {
createShapeIfMissing({
id: n.id,
type: 'geo',
x: n.x,
y: n.y,
props: {
geo: 'rectangle',
w: 220,
h: 110,
color: n.color,
fill: 'solid',
richText: toRichText(n.label),
},
})
}
})
// Connect them (arrows bind to the shapes, so they follow when moved).
createArrowBetweenShapes(nodes[0].id, nodes[1].id, { arrowheadEnd: 'arrow' })
createArrowBetweenShapes(nodes[1].id, nodes[2].id, { arrowheadEnd: 'arrow' })
// --- 2. Add reactive behavior: recolor the last node based on arrow count.
// store.listen fires on the tick AFTER a commit — never read state you just
// wrote synchronously and expect the listener to have run yet.
const targetId = nodes[2].id
function update() {
const hasArrows = editor.getCurrentPageShapes().some((s) => s.type === 'arrow')
editor.run(
() =>
editor.updateShape({
id: targetId,
type: 'geo',
props: { fill: hasArrows ? 'solid' : 'none' },
}),
{ history: 'ignore' } // keep script-owned writes out of the user's undo stack
)
}
const stop = editor.store.listen(update)
signal.addEventListener('abort', () => stop()) // <-- the one required cleanup
update() // run once on load
editor.zoomToFit({ animation: { duration: 200 } })
}