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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bridge/app-bridge-entry.js +6 -0
- package/mcp-app/LICENSE.md +9 -0
- package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
- package/mcp-app/README.md +129 -0
- package/mcp-app/dev-tunnel.sh +51 -0
- package/mcp-app/dist/editor-api.json +8493 -0
- package/mcp-app/dist/mcp-app.html +643 -0
- package/mcp-app/dist/method-map.json +915 -0
- package/mcp-app/package.json +42 -0
- package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
- package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
- package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
- package/mcp-app/scripts/extract-editor-api.ts +1374 -0
- package/mcp-app/server.json +21 -0
- package/mcp-app/src/logger.ts +45 -0
- package/mcp-app/src/register-tools.ts +368 -0
- package/mcp-app/src/shared/generated-data.ts +160 -0
- package/mcp-app/src/shared/pending-requests.ts +69 -0
- package/mcp-app/src/shared/types.ts +76 -0
- package/mcp-app/src/shared/utils.ts +132 -0
- package/mcp-app/src/tools/exec.ts +120 -0
- package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
- package/mcp-app/src/tools/search.ts +150 -0
- package/mcp-app/src/widget/app-context.tsx +29 -0
- package/mcp-app/src/widget/dev-log.tsx +70 -0
- package/mcp-app/src/widget/exec-helpers.ts +232 -0
- package/mcp-app/src/widget/export-tldr.ts +35 -0
- package/mcp-app/src/widget/focused/defaults.ts +141 -0
- package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
- package/mcp-app/src/widget/focused/format.ts +366 -0
- package/mcp-app/src/widget/focused/to-focused.ts +258 -0
- package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
- package/mcp-app/src/widget/image-guard.tsx +106 -0
- package/mcp-app/src/widget/index.html +33 -0
- package/mcp-app/src/widget/mcp-app.css +113 -0
- package/mcp-app/src/widget/mcp-app.tsx +857 -0
- package/mcp-app/src/widget/persistence.ts +337 -0
- package/mcp-app/src/widget/snapshot.ts +157 -0
- package/mcp-app/src/worker.ts +305 -0
- package/mcp-app/tsconfig.json +23 -0
- package/mcp-app/vite.config.ts +13 -0
- package/mcp-app/wrangler.toml +45 -0
- package/mcp-app-source.json +36 -0
- package/package.json +90 -0
- package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
- package/scripts/assemble-mcp-app.mjs +193 -0
- package/scripts/build-bridge.mjs +74 -0
- package/scripts/e2e-mcp.mjs +69 -0
- package/scripts/e2e-packaged-mcp-app.mjs +79 -0
- package/scripts/run-mcp-app-dev.mjs +44 -0
- package/scripts/verify-bundle.mjs +41 -0
- package/scripts/verify-mcp-app-source.mjs +51 -0
- package/scripts/verify-mcp-app.mjs +38 -0
- package/scripts/verify-package-files.mjs +50 -0
- package/src/canvas/export.ts +164 -0
- package/src/canvas/state.ts +117 -0
- package/src/canvas/workflow.ts +105 -0
- package/src/commands/tldraw-command.ts +48 -0
- package/src/diagram/guidance.ts +44 -0
- package/src/host/local-host.ts +289 -0
- package/src/index.ts +762 -0
- package/src/mcp/client.ts +126 -0
- package/src/mcp/response.ts +74 -0
- package/src/semantic/layer.ts +309 -0
- package/src/server/server-manager.ts +153 -0
- package/src/store/export-store.ts +33 -0
- package/src/store/project-store.ts +251 -0
- package/src/ui/tldraw-status.ts +88 -0
- package/static/app-bridge-bundle.js +18114 -0
- package/static/app-bridge-bundle.meta.json +164 -0
- package/static/host.html +390 -0
- 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
|
+
}
|