pi-tldraw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/bridge/app-bridge-entry.js +6 -0
  4. package/mcp-app/LICENSE.md +9 -0
  5. package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
  6. package/mcp-app/README.md +129 -0
  7. package/mcp-app/dev-tunnel.sh +51 -0
  8. package/mcp-app/dist/editor-api.json +8493 -0
  9. package/mcp-app/dist/mcp-app.html +643 -0
  10. package/mcp-app/dist/method-map.json +915 -0
  11. package/mcp-app/package.json +42 -0
  12. package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
  13. package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
  14. package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
  15. package/mcp-app/scripts/extract-editor-api.ts +1374 -0
  16. package/mcp-app/server.json +21 -0
  17. package/mcp-app/src/logger.ts +45 -0
  18. package/mcp-app/src/register-tools.ts +368 -0
  19. package/mcp-app/src/shared/generated-data.ts +160 -0
  20. package/mcp-app/src/shared/pending-requests.ts +69 -0
  21. package/mcp-app/src/shared/types.ts +76 -0
  22. package/mcp-app/src/shared/utils.ts +132 -0
  23. package/mcp-app/src/tools/exec.ts +120 -0
  24. package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
  25. package/mcp-app/src/tools/search.ts +150 -0
  26. package/mcp-app/src/widget/app-context.tsx +29 -0
  27. package/mcp-app/src/widget/dev-log.tsx +70 -0
  28. package/mcp-app/src/widget/exec-helpers.ts +232 -0
  29. package/mcp-app/src/widget/export-tldr.ts +35 -0
  30. package/mcp-app/src/widget/focused/defaults.ts +141 -0
  31. package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
  32. package/mcp-app/src/widget/focused/format.ts +366 -0
  33. package/mcp-app/src/widget/focused/to-focused.ts +258 -0
  34. package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
  35. package/mcp-app/src/widget/image-guard.tsx +106 -0
  36. package/mcp-app/src/widget/index.html +33 -0
  37. package/mcp-app/src/widget/mcp-app.css +113 -0
  38. package/mcp-app/src/widget/mcp-app.tsx +857 -0
  39. package/mcp-app/src/widget/persistence.ts +337 -0
  40. package/mcp-app/src/widget/snapshot.ts +157 -0
  41. package/mcp-app/src/worker.ts +305 -0
  42. package/mcp-app/tsconfig.json +23 -0
  43. package/mcp-app/vite.config.ts +13 -0
  44. package/mcp-app/wrangler.toml +45 -0
  45. package/mcp-app-source.json +36 -0
  46. package/package.json +90 -0
  47. package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
  48. package/scripts/assemble-mcp-app.mjs +193 -0
  49. package/scripts/build-bridge.mjs +74 -0
  50. package/scripts/e2e-mcp.mjs +69 -0
  51. package/scripts/e2e-packaged-mcp-app.mjs +79 -0
  52. package/scripts/run-mcp-app-dev.mjs +44 -0
  53. package/scripts/verify-bundle.mjs +41 -0
  54. package/scripts/verify-mcp-app-source.mjs +51 -0
  55. package/scripts/verify-mcp-app.mjs +38 -0
  56. package/scripts/verify-package-files.mjs +50 -0
  57. package/src/canvas/export.ts +164 -0
  58. package/src/canvas/state.ts +117 -0
  59. package/src/canvas/workflow.ts +105 -0
  60. package/src/commands/tldraw-command.ts +48 -0
  61. package/src/diagram/guidance.ts +44 -0
  62. package/src/host/local-host.ts +289 -0
  63. package/src/index.ts +762 -0
  64. package/src/mcp/client.ts +126 -0
  65. package/src/mcp/response.ts +74 -0
  66. package/src/semantic/layer.ts +309 -0
  67. package/src/server/server-manager.ts +153 -0
  68. package/src/store/export-store.ts +33 -0
  69. package/src/store/project-store.ts +251 -0
  70. package/src/ui/tldraw-status.ts +88 -0
  71. package/static/app-bridge-bundle.js +18114 -0
  72. package/static/app-bridge-bundle.meta.json +164 -0
  73. package/static/host.html +390 -0
  74. package/tsconfig.json +13 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * ES Proxy that wraps the tldraw Editor to auto-convert between focused format
3
+ * and tldraw's internal format at method boundaries.
4
+ *
5
+ * AI agents interact entirely in focused format — simple string IDs, flat shape
6
+ * objects with `_type` — and the proxy silently translates to/from tldraw's
7
+ * `TLShape`, `TLShapeId`, etc.
8
+ */
9
+ import {
10
+ Editor,
11
+ TLBindingId,
12
+ TLCreateShapePartial,
13
+ TLShape,
14
+ TLShapeId,
15
+ TLShapePartial,
16
+ } from 'tldraw'
17
+ import type { MethodMap, RetKind } from '../../shared/generated-data'
18
+ import { getDefaultShape } from './defaults'
19
+ import { convertSimpleIdToTldrawId, convertTldrawIdToSimpleId, type FocusedShape } from './format'
20
+ import { convertTldrawShapeToFocusedShape } from './to-focused'
21
+ import { convertFocusedShapeToTldrawShape } from './to-tldraw'
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Input conversion helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Convert a single value that might be a focused ID or focused shape into tldraw format. */
28
+ function convertIdOrShape(val: string | TLShapeId | TLShape | FocusedShape | null | undefined) {
29
+ if (val === null || val === undefined) return val
30
+ if (typeof val === 'string') return ensureTldrawId(val)
31
+ if ('_type' in val) {
32
+ return ensureTldrawId((val as FocusedShape).shapeId)
33
+ }
34
+ return val
35
+ }
36
+
37
+ /** Convert an array where each element might be a focused ID or focused shape. */
38
+ function convertIdsOrShapes(
39
+ arr: Array<string | TLShapeId | TLShape | FocusedShape> | TLShapeId[] | TLShape[]
40
+ ) {
41
+ return arr.map(convertIdOrShape)
42
+ }
43
+
44
+ /** Ensure a string is a valid TLShapeId. Passthrough if already prefixed. */
45
+ function ensureTldrawId(id: string): TLShapeId {
46
+ if (id.startsWith('shape:')) return id as TLShapeId
47
+ return convertSimpleIdToTldrawId(id)
48
+ }
49
+
50
+ /** Detect whether a value is a focused shape (has `_type` field). */
51
+ function isFocusedShape(
52
+ val: FocusedShape | TLShapePartial | TLCreateShapePartial
53
+ ): val is FocusedShape {
54
+ return '_type' in val
55
+ }
56
+
57
+ /** Detect update payloads that use focused `shapeId` but omit `_type`. */
58
+ function hasFocusedShapeId(
59
+ val: FocusedShape | TLShapePartial | TLCreateShapePartial
60
+ ): val is TLShapePartial & { shapeId: string } {
61
+ return (
62
+ typeof val === 'object' &&
63
+ val !== null &&
64
+ 'shapeId' in val &&
65
+ typeof (val as { shapeId?: unknown }).shapeId === 'string'
66
+ )
67
+ }
68
+
69
+ /** Normalize raw tldraw shape partial IDs when models omit the `shape:` prefix. */
70
+ function normalizeRawShapePartialId<T extends TLShapePartial>(partial: T): T {
71
+ const rawId = partial.id
72
+ if (typeof rawId !== 'string') return partial
73
+ if (rawId.startsWith('shape:')) return partial
74
+ return { ...partial, id: ensureTldrawId(rawId) } as T
75
+ }
76
+
77
+ /**
78
+ * Normalize an incoming partial into focused shape format when possible.
79
+ * - If `_type` is already present, returns as-is.
80
+ * - If `shapeId` is present, infers `_type` from the existing canvas shape.
81
+ * - Otherwise returns null, meaning callers should treat it as raw tldraw input.
82
+ */
83
+ function toFocusedShapeIfPossible(
84
+ editor: Editor,
85
+ partial: FocusedShape | TLShapePartial | TLCreateShapePartial
86
+ ): FocusedShape | null {
87
+ if (isFocusedShape(partial)) return partial
88
+ if (!hasFocusedShapeId(partial)) return null
89
+
90
+ const shapeId = ensureTldrawId(partial.shapeId)
91
+ const existingShape = editor.getShape(shapeId)
92
+ if (!existingShape) return null
93
+
94
+ const focusedExisting = convertTldrawShapeToFocusedShape(editor, existingShape)
95
+ const merged = {
96
+ ...focusedExisting,
97
+ ...partial,
98
+ _type: focusedExisting._type,
99
+ shapeId: focusedExisting.shapeId,
100
+ }
101
+ // `partial` may be a broad TLShapePartial union; runtime merge is safe here
102
+ // because we anchor on a valid focused shape from the existing record.
103
+ return merged as unknown as FocusedShape
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Output conversion helpers
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function convertOutputShape(editor: Editor, shape: TLShape): FocusedShape {
111
+ try {
112
+ return convertTldrawShapeToFocusedShape(editor, shape)
113
+ } catch {
114
+ return {
115
+ _type: 'unknown',
116
+ shapeId: convertTldrawIdToSimpleId(shape.id),
117
+ subType: shape.type,
118
+ note: '',
119
+ x: shape.x,
120
+ y: shape.y,
121
+ }
122
+ }
123
+ }
124
+
125
+ function isTLShape(val: TLShape | FocusedShape | string | null | undefined): val is TLShape {
126
+ if (val === null || val === undefined || typeof val === 'string') return false
127
+ return 'typeName' in val && val.typeName === 'shape'
128
+ }
129
+
130
+ /**
131
+ * Convert a return value from tldraw format to focused format based on the method spec.
132
+ * Uses targeted casts because the Proxy handler is inherently dynamic dispatch —
133
+ * the `result` type varies per method and can't be statically narrowed from the spec string.
134
+ */
135
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
136
+ type ProxyResult =
137
+ | TLShape
138
+ | TLShape[]
139
+ | TLShapeId
140
+ | TLShapeId[]
141
+ | Set<TLShapeId>
142
+ | Editor
143
+ | null
144
+ | undefined
145
+ | void
146
+
147
+ function convertReturnValue(editor: Editor, proxy: Editor, spec: RetKind, result: ProxyResult) {
148
+ switch (spec) {
149
+ case 'this':
150
+ return proxy
151
+ case 'shape': {
152
+ const shape = result as TLShape | undefined
153
+ return shape && isTLShape(shape) ? convertOutputShape(editor, shape) : result
154
+ }
155
+ case 'shape-or-null': {
156
+ if (result === null || result === undefined) return result
157
+ const shape = result as TLShape
158
+ return isTLShape(shape) ? convertOutputShape(editor, shape) : result
159
+ }
160
+ case 'shapes': {
161
+ const shapes = result as TLShape[]
162
+ if (!Array.isArray(shapes)) return result
163
+ return shapes.map((s) => (isTLShape(s) ? convertOutputShape(editor, s) : s))
164
+ }
165
+ case 'id': {
166
+ const id = result as TLShapeId
167
+ return typeof id === 'string' ? convertTldrawIdToSimpleId(id) : result
168
+ }
169
+ case 'id-or-null': {
170
+ if (result === null || result === undefined) return result
171
+ const id = result as TLShapeId
172
+ return typeof id === 'string' ? convertTldrawIdToSimpleId(id) : result
173
+ }
174
+ case 'ids': {
175
+ const ids = result as TLShapeId[]
176
+ if (!Array.isArray(ids)) return result
177
+ return ids.map((id) => (typeof id === 'string' ? convertTldrawIdToSimpleId(id) : id))
178
+ }
179
+ case 'id-set': {
180
+ const idSet = result as Set<TLShapeId>
181
+ if (!(idSet instanceof Set)) return result
182
+ const out = new Set<string>()
183
+ for (const id of idSet) {
184
+ out.add(convertTldrawIdToSimpleId(id))
185
+ }
186
+ return out
187
+ }
188
+ }
189
+ }
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Special-case handlers for create/update (arrow bindings)
193
+ // ---------------------------------------------------------------------------
194
+
195
+ type CreateEditorMethod = (...args: Parameters<Editor['createShape']>) => Editor
196
+ type UpdateEditorMethod = (...args: Parameters<Editor['updateShape']>) => Editor
197
+
198
+ function handleCreateShape(
199
+ editor: Editor,
200
+ proxy: Editor,
201
+ partial: FocusedShape | TLCreateShapePartial,
202
+ realMethod: CreateEditorMethod
203
+ ): Editor {
204
+ if (!isFocusedShape(partial)) {
205
+ realMethod.call(editor, partial)
206
+ return proxy
207
+ }
208
+
209
+ const result = convertFocusedShapeToTldrawShape(editor, partial, {
210
+ defaultShape: getDefaultShape(partial._type),
211
+ })
212
+
213
+ editor.createShape(result.shape)
214
+
215
+ if (result.bindings) {
216
+ for (const binding of result.bindings) {
217
+ editor.createBinding({
218
+ type: binding.type,
219
+ fromId: binding.fromId,
220
+ toId: binding.toId,
221
+ props: binding.props,
222
+ meta: binding.meta,
223
+ })
224
+ }
225
+ }
226
+
227
+ return proxy
228
+ }
229
+
230
+ function handleCreateShapes(
231
+ editor: Editor,
232
+ proxy: Editor,
233
+ partials: Array<FocusedShape | TLCreateShapePartial>,
234
+ realMethod: CreateEditorMethod
235
+ ): Editor {
236
+ if (!Array.isArray(partials)) {
237
+ realMethod.call(editor, partials)
238
+ return proxy
239
+ }
240
+
241
+ for (const partial of partials) {
242
+ handleCreateShape(editor, proxy, partial, realMethod)
243
+ }
244
+ return proxy
245
+ }
246
+
247
+ function handleUpdateShape(
248
+ editor: Editor,
249
+ proxy: Editor,
250
+ partial: FocusedShape | TLShapePartial,
251
+ realMethod: UpdateEditorMethod
252
+ ): Editor {
253
+ const focusedPartial = toFocusedShapeIfPossible(editor, partial)
254
+ if (!focusedPartial) {
255
+ realMethod.call(editor, normalizeRawShapePartialId(partial as TLShapePartial))
256
+ return proxy
257
+ }
258
+
259
+ const shapeId = ensureTldrawId(focusedPartial.shapeId)
260
+ const existingShape = editor.getShape(shapeId)
261
+ if (!existingShape) {
262
+ return proxy
263
+ }
264
+
265
+ const result = convertFocusedShapeToTldrawShape(editor, focusedPartial, {
266
+ defaultShape: existingShape,
267
+ })
268
+
269
+ editor.updateShape(result.shape)
270
+
271
+ if (result.bindings) {
272
+ const existingBindings = editor.getBindingsFromShape(shapeId, 'arrow')
273
+ for (const binding of existingBindings) {
274
+ editor.deleteBinding(binding.id as TLBindingId)
275
+ }
276
+ for (const binding of result.bindings) {
277
+ editor.createBinding({
278
+ type: binding.type,
279
+ fromId: binding.fromId,
280
+ toId: binding.toId,
281
+ props: binding.props,
282
+ meta: binding.meta,
283
+ })
284
+ }
285
+ }
286
+
287
+ return proxy
288
+ }
289
+
290
+ function handleUpdateShapes(
291
+ editor: Editor,
292
+ proxy: Editor,
293
+ partials: Array<FocusedShape | TLShapePartial>,
294
+ realMethod: UpdateEditorMethod
295
+ ): Editor {
296
+ if (!Array.isArray(partials)) {
297
+ realMethod.call(editor, partials)
298
+ return proxy
299
+ }
300
+
301
+ for (const partial of partials) {
302
+ handleUpdateShape(editor, proxy, partial, realMethod)
303
+ }
304
+ return proxy
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------
308
+ // The Proxy factory
309
+ // ---------------------------------------------------------------------------
310
+
311
+ export function createFocusedEditorProxy(editor: Editor, methodMap: MethodMap): Editor {
312
+ const proxy: Editor = new Proxy(editor, {
313
+ get(target, prop, receiver) {
314
+ const value = Reflect.get(target, prop, receiver)
315
+
316
+ // Only intercept function calls on string-named properties
317
+ if (typeof prop !== 'string' || typeof value !== 'function') {
318
+ return value
319
+ }
320
+
321
+ const spec = methodMap[prop]
322
+
323
+ // --- Special-case: create/update need binding handling ---
324
+ if (prop === 'createShape') {
325
+ return (partial: FocusedShape | TLCreateShapePartial) =>
326
+ handleCreateShape(target, proxy, partial, value as CreateEditorMethod)
327
+ }
328
+ if (prop === 'createShapes') {
329
+ return (partials: Array<FocusedShape | TLCreateShapePartial>) =>
330
+ handleCreateShapes(target, proxy, partials, value as CreateEditorMethod)
331
+ }
332
+ if (prop === 'updateShape') {
333
+ return (partial: FocusedShape | TLShapePartial) =>
334
+ handleUpdateShape(target, proxy, partial, value as UpdateEditorMethod)
335
+ }
336
+ if (prop === 'updateShapes') {
337
+ return (partials: Array<FocusedShape | TLShapePartial>) =>
338
+ handleUpdateShapes(target, proxy, partials, value as UpdateEditorMethod)
339
+ }
340
+ if (prop === 'animateShape') {
341
+ return (partial: FocusedShape | TLShapePartial, ...rest: [Record<string, number>?]) => {
342
+ const focusedPartial = toFocusedShapeIfPossible(target, partial)
343
+ if (focusedPartial) {
344
+ const shapeId = ensureTldrawId(focusedPartial.shapeId)
345
+ const existing = target.getShape(shapeId)
346
+ if (existing) {
347
+ const converted = convertFocusedShapeToTldrawShape(target, focusedPartial, {
348
+ defaultShape: existing,
349
+ })
350
+ value.call(target, converted.shape, ...rest)
351
+ return proxy
352
+ }
353
+ }
354
+ value.call(target, normalizeRawShapePartialId(partial as TLShapePartial), ...rest)
355
+ return proxy
356
+ }
357
+ }
358
+ if (prop === 'animateShapes') {
359
+ return (
360
+ partials: Array<FocusedShape | TLShapePartial>,
361
+ ...rest: [Record<string, number>?]
362
+ ) => {
363
+ if (Array.isArray(partials)) {
364
+ const converted = partials.map((p) => {
365
+ const focusedPartial = toFocusedShapeIfPossible(target, p)
366
+ if (focusedPartial) {
367
+ const shapeId = ensureTldrawId(focusedPartial.shapeId)
368
+ const existing = target.getShape(shapeId)
369
+ if (existing) {
370
+ return convertFocusedShapeToTldrawShape(target, focusedPartial, {
371
+ defaultShape: existing,
372
+ }).shape
373
+ }
374
+ }
375
+ return normalizeRawShapePartialId(p as TLShapePartial)
376
+ })
377
+ value.call(target, converted, ...rest)
378
+ } else {
379
+ value.call(target, partials, ...rest)
380
+ }
381
+ return proxy
382
+ }
383
+ }
384
+
385
+ // --- No spec: pass through, but still catch `this` returns ---
386
+ if (!spec) {
387
+ return (...args: Parameters<typeof value>) => {
388
+ const result = value.apply(target, args)
389
+ return result === target ? proxy : result
390
+ }
391
+ }
392
+
393
+ // --- Generic handler for mapped methods ---
394
+ // The proxy handler is dynamic dispatch: args vary per intercepted method.
395
+ // We use targeted casts inside the switch rather than a single static signature.
396
+ return (...args: Parameters<typeof value>) => {
397
+ const convertedArgs: Parameters<typeof value> = [...args]
398
+ for (let i = 0; i < spec.args.length && i < convertedArgs.length; i++) {
399
+ const kind = spec.args[i]
400
+ const arg = convertedArgs[i]
401
+ switch (kind) {
402
+ case 'id':
403
+ if (typeof arg === 'string') {
404
+ convertedArgs[i] = ensureTldrawId(arg)
405
+ }
406
+ break
407
+ case 'id-or-shape':
408
+ convertedArgs[i] = convertIdOrShape(
409
+ arg as string | TLShapeId | TLShape | FocusedShape
410
+ )
411
+ break
412
+ case 'ids-or-shapes':
413
+ convertedArgs[i] = convertIdsOrShapes(
414
+ arg as Array<string | TLShapeId | TLShape | FocusedShape>
415
+ )
416
+ break
417
+ case 'spread-ids':
418
+ for (let j = i; j < convertedArgs.length; j++) {
419
+ convertedArgs[j] = convertIdOrShape(
420
+ convertedArgs[j] as string | TLShapeId | TLShape | FocusedShape
421
+ )
422
+ }
423
+ break
424
+ }
425
+ }
426
+
427
+ const result = value.apply(target, convertedArgs)
428
+ return convertReturnValue(target, proxy, spec.ret, result)
429
+ }
430
+ },
431
+ }) as Editor
432
+
433
+ return proxy
434
+ }