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,232 @@
1
+ import {
2
+ Box,
3
+ type Editor,
4
+ Mat,
5
+ TLShapeId,
6
+ Vec,
7
+ clamp,
8
+ createBindingId,
9
+ createShapeId,
10
+ degreesToRadians,
11
+ fitFrameToContent,
12
+ getArrowBindings,
13
+ radiansToDegrees,
14
+ toRichText,
15
+ } from 'tldraw'
16
+ import { getRequiredEmbeddedMethodMap } from '../shared/generated-data'
17
+ import { createFocusedEditorProxy } from './focused/focused-editor-proxy'
18
+
19
+ function ensureTldrawShapeId(id: string): TLShapeId {
20
+ if (id.startsWith('shape:')) return id as TLShapeId
21
+ return ('shape:' + id) as TLShapeId
22
+ }
23
+
24
+ function createArrowBetweenShapesFn(editor: Editor) {
25
+ /**
26
+ * Create an arrow shape that connects two existing shapes by their IDs.
27
+ *
28
+ * @param fromId - The shape ID to connect the arrow start to.
29
+ * @param toId - The shape ID to connect the arrow end to.
30
+ * @param opts - Optional arrow properties: a signed bend amount for the curve and a text label.
31
+ *
32
+ *
33
+ * @example
34
+ * createArrowBetweenShapes('box1', 'box2', { text: 'next', bend: 50 })
35
+ */
36
+ return (fromId: string, toId: string, opts?: { bend?: number; text?: string }) => {
37
+ const arrowId = createShapeId()
38
+ const resolvedFromId = ensureTldrawShapeId(fromId)
39
+ const resolvedToId = ensureTldrawShapeId(toId)
40
+ editor.createShape({
41
+ id: arrowId,
42
+ type: 'arrow',
43
+ props: {
44
+ ...(opts?.text ? { richText: toRichText(opts.text) } : {}),
45
+ ...(opts?.bend != null ? { bend: opts.bend } : {}),
46
+ },
47
+ })
48
+ editor.createBindings([
49
+ {
50
+ id: createBindingId(),
51
+ type: 'arrow',
52
+ fromId: arrowId,
53
+ toId: resolvedFromId,
54
+ props: {
55
+ terminal: 'start',
56
+ isPrecise: false,
57
+ isExact: false,
58
+ normalizedAnchor: { x: 0.5, y: 0.5 },
59
+ },
60
+ },
61
+ {
62
+ id: createBindingId(),
63
+ type: 'arrow',
64
+ fromId: arrowId,
65
+ toId: resolvedToId,
66
+ props: {
67
+ terminal: 'end',
68
+ isPrecise: false,
69
+ isExact: false,
70
+ normalizedAnchor: { x: 0.5, y: 0.5 },
71
+ },
72
+ },
73
+ ])
74
+ return editor
75
+ }
76
+ }
77
+
78
+ const BOX_SHAPES_MARGIN = 40
79
+
80
+ function boxShapesFn(editor: Editor) {
81
+ /**
82
+ * Create a rectangle shape around a group of existing shapes with a margin. Also groups the shapes together.
83
+ *
84
+ * @param shapesOrIds - Array of shape IDs or shape objects to box around.
85
+ * @param opts - Optional properties: shapeId, color, fill, text, note.
86
+ *
87
+ * @example
88
+ * boxShapes(['box1', 'box2'], { text: 'Group A', color: 'blue' })
89
+ */
90
+ return (
91
+ shapesOrIds: (string | { shapeId: string })[],
92
+ opts?: { shapeId?: string; color?: string; fill?: string; text?: string; note?: string }
93
+ ) => {
94
+ const ids = shapesOrIds.map((s) =>
95
+ typeof s === 'string' ? ensureTldrawShapeId(s) : ensureTldrawShapeId(s.shapeId)
96
+ )
97
+
98
+ const bounds = editor.getShapesPageBounds(ids)
99
+ if (!bounds) return editor
100
+
101
+ const boxId = opts?.shapeId ? ensureTldrawShapeId(opts.shapeId) : createShapeId()
102
+
103
+ editor.createShape({
104
+ id: boxId,
105
+ type: 'geo',
106
+ x: bounds.x - BOX_SHAPES_MARGIN,
107
+ y: bounds.y - BOX_SHAPES_MARGIN,
108
+ props: {
109
+ geo: 'rectangle',
110
+ w: bounds.w + BOX_SHAPES_MARGIN * 2,
111
+ h: bounds.h + BOX_SHAPES_MARGIN * 2,
112
+ color: (opts?.color ?? 'black') as any,
113
+ fill: 'none' as any,
114
+ align: 'start' as any,
115
+ verticalAlign: 'start' as any,
116
+ ...(opts?.text ? { richText: toRichText(opts.text) } : {}),
117
+ },
118
+ meta: { note: opts?.note ?? '' },
119
+ })
120
+
121
+ editor.sendToBack([boxId])
122
+ editor.groupShapes([...ids, boxId])
123
+
124
+ return editor
125
+ }
126
+ }
127
+
128
+ export function createExecHelpers(editor: Editor) {
129
+ const helpers = {
130
+ createShapeId,
131
+ createBindingId,
132
+ Box,
133
+ Vec,
134
+ Mat,
135
+ clamp,
136
+ degreesToRadians,
137
+ radiansToDegrees,
138
+ getArrowBindings,
139
+ fitFrameToContent,
140
+ createArrowBetweenShapes: createArrowBetweenShapesFn(editor),
141
+ boxShapes: boxShapesFn(editor),
142
+ }
143
+
144
+ return helpers
145
+ }
146
+
147
+ type ExecHelpers = ReturnType<typeof createExecHelpers>
148
+
149
+ const EXEC_TIMEOUT_MS = 10_000
150
+
151
+ function serializeResult(result: unknown) {
152
+ try {
153
+ return JSON.parse(JSON.stringify(result))
154
+ } catch {
155
+ return result != null ? String(result) : undefined
156
+ }
157
+ }
158
+
159
+ async function loadExecModule(code: string) {
160
+ const moduleSource = `export default async function runExec({ editor, helpers }) {
161
+ const {
162
+ createShapeId,
163
+ createBindingId,
164
+ Box,
165
+ Vec,
166
+ Mat,
167
+ clamp,
168
+ degreesToRadians,
169
+ radiansToDegrees,
170
+ getArrowBindings,
171
+ fitFrameToContent,
172
+ createArrowBetweenShapes,
173
+ boxShapes,
174
+ } = helpers
175
+
176
+ return await (async () => {
177
+ ${code}
178
+ })()
179
+ }`
180
+
181
+ const moduleUrl = URL.createObjectURL(new Blob([moduleSource], { type: 'text/javascript' }))
182
+ try {
183
+ return (await import(/* @vite-ignore */ moduleUrl)).default as (args: {
184
+ editor: Editor
185
+ helpers: ExecHelpers
186
+ }) => Promise<unknown>
187
+ } finally {
188
+ URL.revokeObjectURL(moduleUrl)
189
+ }
190
+ }
191
+
192
+ export async function executeCode(
193
+ editor: Editor,
194
+ code: string
195
+ ): Promise<{ success: boolean; result?: unknown; error?: string }> {
196
+ const focusedEditor = createFocusedEditorProxy(editor, getRequiredEmbeddedMethodMap())
197
+ const helpers = createExecHelpers(editor)
198
+
199
+ const originalFetch = window.fetch
200
+ const originalXHR = window.XMLHttpRequest
201
+ const originalSetInterval = window.setInterval
202
+ const originalSetTimeout = window.setTimeout
203
+
204
+ try {
205
+ // Disable fetch, XMLHttpRequest, and timers while the exec code runs
206
+ ;(window as any).fetch = undefined
207
+ ;(window as any).XMLHttpRequest = undefined
208
+ ;(window as any).setInterval = undefined
209
+ ;(window as any).setTimeout = undefined
210
+
211
+ const runExec = await loadExecModule(code)
212
+ const result = await Promise.race([
213
+ runExec({ editor: focusedEditor, helpers }),
214
+ new Promise((_, reject) =>
215
+ originalSetTimeout(
216
+ () => reject(new Error(`Execution timed out after ${EXEC_TIMEOUT_MS}ms`)),
217
+ EXEC_TIMEOUT_MS
218
+ )
219
+ ),
220
+ ])
221
+
222
+ return { success: true, result: serializeResult(result) }
223
+ } catch (err) {
224
+ const message = err instanceof Error ? err.message : String(err)
225
+ return { success: false, error: message }
226
+ } finally {
227
+ window.fetch = originalFetch
228
+ window.XMLHttpRequest = originalXHR
229
+ window.setInterval = originalSetInterval
230
+ window.setTimeout = originalSetTimeout
231
+ }
232
+ }
@@ -0,0 +1,35 @@
1
+ import type { App } from '@modelcontextprotocol/ext-apps/react'
2
+ import { Editor, serializeTldrawJson, serializeTldrawJsonBlob } from 'tldraw'
3
+
4
+ export async function exportTldr(editor: Editor, app?: App) {
5
+ const json = await serializeTldrawJson(editor)
6
+
7
+ // Copy to clipboard
8
+ navigator.clipboard.writeText(json).catch(() => {
9
+ // Clipboard may be unavailable in some contexts.
10
+ })
11
+
12
+ // Download file
13
+ if (app?.getHostCapabilities()?.downloadFile) {
14
+ await app.downloadFile({
15
+ contents: [
16
+ {
17
+ type: 'resource',
18
+ resource: {
19
+ uri: 'file:///diagram.tldr',
20
+ mimeType: 'application/json',
21
+ text: json,
22
+ },
23
+ },
24
+ ],
25
+ })
26
+ } else {
27
+ const blob = await serializeTldrawJsonBlob(editor)
28
+ const url = URL.createObjectURL(blob)
29
+ const a = document.createElement('a')
30
+ a.href = url
31
+ a.download = 'diagram.tldr'
32
+ a.click()
33
+ URL.revokeObjectURL(url)
34
+ }
35
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Default shape templates for creating new shapes from focused format.
3
+ * Extracted from tldraw-internal desktop app handlers.ts.
4
+ */
5
+ import { createShapeId, IndexKey, TLShape, toRichText } from 'tldraw'
6
+ import { FOCUSED_TO_GEO_TYPES, type FocusedShape } from './format'
7
+
8
+ export function getDefaultShape(shapeType: FocusedShape['_type']): Partial<TLShape> {
9
+ const isGeo = shapeType in FOCUSED_TO_GEO_TYPES
10
+ if (isGeo) {
11
+ return {
12
+ isLocked: false,
13
+ opacity: 1,
14
+ rotation: 0,
15
+ meta: {},
16
+ id: createShapeId(),
17
+ props: {
18
+ align: 'middle',
19
+ color: 'black',
20
+ dash: 'draw',
21
+ fill: 'none',
22
+ font: 'draw',
23
+ geo: 'rectangle',
24
+ growY: 0,
25
+ h: 200,
26
+ labelColor: 'black',
27
+ richText: toRichText(''),
28
+ scale: 1,
29
+ size: 'm',
30
+ url: '',
31
+ verticalAlign: 'middle',
32
+ w: 200,
33
+ },
34
+ } as any
35
+ }
36
+
37
+ switch (shapeType) {
38
+ case 'text':
39
+ return {
40
+ isLocked: false,
41
+ opacity: 1,
42
+ rotation: 0,
43
+ meta: {},
44
+ id: createShapeId(),
45
+ props: {
46
+ autoSize: true,
47
+ color: 'black',
48
+ font: 'draw',
49
+ richText: toRichText(''),
50
+ scale: 1,
51
+ size: 'm',
52
+ textAlign: 'start',
53
+ w: 100,
54
+ },
55
+ } as any
56
+ case 'line':
57
+ return {
58
+ isLocked: false,
59
+ opacity: 1,
60
+ rotation: 0,
61
+ meta: {},
62
+ id: createShapeId(),
63
+ props: {
64
+ size: 'm',
65
+ color: 'black',
66
+ dash: 'draw',
67
+ points: {
68
+ a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
69
+ a2: { id: 'a2', index: 'a2' as IndexKey, x: 100, y: 0 },
70
+ },
71
+ scale: 1,
72
+ spline: 'line',
73
+ },
74
+ } as any
75
+ case 'arrow':
76
+ return {
77
+ isLocked: false,
78
+ opacity: 1,
79
+ rotation: 0,
80
+ meta: {},
81
+ id: createShapeId(),
82
+ props: {
83
+ arrowheadEnd: 'arrow',
84
+ arrowheadStart: 'none',
85
+ bend: 0,
86
+ color: 'black',
87
+ dash: 'draw',
88
+ elbowMidPoint: 0.5,
89
+ end: { x: 100, y: 0 },
90
+ fill: 'none',
91
+ font: 'draw',
92
+ kind: 'arc',
93
+ labelColor: 'black',
94
+ labelPosition: 0.5,
95
+ richText: toRichText(''),
96
+ scale: 1,
97
+ size: 'm',
98
+ start: { x: 0, y: 0 },
99
+ },
100
+ } as any
101
+ case 'note':
102
+ return {
103
+ isLocked: false,
104
+ opacity: 1,
105
+ rotation: 0,
106
+ meta: {},
107
+ id: createShapeId(),
108
+ props: {
109
+ color: 'black',
110
+ richText: toRichText(''),
111
+ size: 'm',
112
+ align: 'middle',
113
+ font: 'draw',
114
+ fontSizeAdjustment: 0,
115
+ growY: 0,
116
+ labelColor: 'black',
117
+ scale: 1,
118
+ url: '',
119
+ verticalAlign: 'middle',
120
+ },
121
+ } as any
122
+ case 'draw':
123
+ return {
124
+ isLocked: false,
125
+ opacity: 1,
126
+ rotation: 0,
127
+ meta: {},
128
+ id: createShapeId(),
129
+ props: {},
130
+ } as any
131
+ default:
132
+ return {
133
+ isLocked: false,
134
+ opacity: 1,
135
+ rotation: 0,
136
+ meta: {},
137
+ id: createShapeId(),
138
+ props: {},
139
+ } as any
140
+ }
141
+ }