agentic-canvas 0.0.0-stage → 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.
@@ -0,0 +1,932 @@
1
+ import {
2
+ FileStoreError
3
+ } from "./chunk-I3EJ5JB7.js";
4
+
5
+ // src/version.ts
6
+ import { createRequire } from "module";
7
+ var VERSION = true ? "0.1.0" : readPackageVersion();
8
+
9
+ // src/brand.ts
10
+ var BRAND = {
11
+ command: "agentic-canvas",
12
+ description: "A shared whiteboard for diagrams, drawn by people and agents.",
13
+ version: VERSION
14
+ };
15
+
16
+ // src/core/principal.ts
17
+ import { PRINCIPAL_KINDS, isMachineMade } from "agentic-identity";
18
+ import { z } from "zod";
19
+ var principalSchema = z.object({ kind: z.enum(PRINCIPAL_KINDS), id: z.string().min(1).max(200) }).strict();
20
+
21
+ // src/core/board.ts
22
+ import { randomUUID } from "crypto";
23
+ import * as Y from "yjs";
24
+ import { z as z2 } from "zod";
25
+ var CanvasError = class extends Error {
26
+ constructor(code, message) {
27
+ super(message);
28
+ this.code = code;
29
+ this.name = "CanvasError";
30
+ }
31
+ code;
32
+ };
33
+ var SHAPE_TYPES = ["rect", "ellipse", "diamond", "text", "sticky", "frame", "image"];
34
+ var MAX_ELEMENTS = 1e4;
35
+ var MAX_TEXT = 1e4;
36
+ var id = z2.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
37
+ var coord = z2.number().finite().min(-1e7).max(1e7);
38
+ var size = z2.number().finite().gt(0).max(1e7);
39
+ var text = z2.string().max(MAX_TEXT);
40
+ var COLOR_PATTERN = /^(#[0-9a-fA-F]{3,8}|[a-z]{3,20})$/;
41
+ var IMAGE_PATTERN = /^(data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+=*|blob:[A-Za-z0-9._-]{1,200})$/;
42
+ var color = z2.string().regex(COLOR_PATTERN);
43
+ var imageSrc = z2.string().max(2e6).regex(IMAGE_PATTERN);
44
+ var endpoint = z2.union([z2.object({ id }).strict(), z2.object({ x: coord, y: coord }).strict()]);
45
+ var shapeInput = z2.object({
46
+ id: id.optional(),
47
+ type: z2.enum(SHAPE_TYPES),
48
+ x: coord,
49
+ y: coord,
50
+ w: size.default(120),
51
+ h: size.default(60),
52
+ text: text.default(""),
53
+ fill: color.optional(),
54
+ stroke: color.optional(),
55
+ src: imageSrc.optional()
56
+ }).strict().refine((s) => s.type === "image" === (s.src !== void 0), {
57
+ message: "src is required for image shapes and allowed on no other type"
58
+ });
59
+ var connectorInput = z2.object({
60
+ id: id.optional(),
61
+ from: endpoint,
62
+ to: endpoint,
63
+ text: text.default(""),
64
+ startArrow: z2.boolean().default(false),
65
+ endArrow: z2.boolean().default(true),
66
+ stroke: color.optional()
67
+ }).strict();
68
+ var shapePatch = z2.object({ x: coord, y: coord, w: size, h: size, text, fill: color, stroke: color, src: imageSrc }).partial().strict();
69
+ var connectorPatch = z2.object({ from: endpoint, to: endpoint, text, startArrow: z2.boolean(), endArrow: z2.boolean(), stroke: color }).partial().strict();
70
+ var delta = z2.number().finite().min(-2e7).max(2e7);
71
+ var MAX_Z = 1e9;
72
+ var stampFields = {
73
+ id,
74
+ z: z2.number().finite().min(-MAX_Z).max(MAX_Z),
75
+ groupId: id.optional(),
76
+ author: principalSchema,
77
+ editedBy: principalSchema
78
+ };
79
+ var storedShape = z2.object({ ...stampFields, type: z2.enum(SHAPE_TYPES), x: coord, y: coord, w: size, h: size, text, fill: color.optional(), stroke: color.optional(), src: imageSrc.optional() }).refine((s) => s.type === "image" === (s.src !== void 0), { message: "src is required for image shapes and allowed on no other type" });
80
+ var storedConnector = z2.object({
81
+ ...stampFields,
82
+ type: z2.literal("connector"),
83
+ from: endpoint,
84
+ to: endpoint,
85
+ text,
86
+ startArrow: z2.boolean(),
87
+ endArrow: z2.boolean(),
88
+ stroke: color.optional()
89
+ });
90
+ var storedGroup = z2.object({ ...stampFields, type: z2.literal("group") });
91
+ var storedElement = z2.union([storedShape, storedConnector, storedGroup]);
92
+ var titleSchema = z2.string().max(200);
93
+ function parse(schema, value) {
94
+ const result = schema.safeParse(value);
95
+ if (!result.success) {
96
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`);
97
+ throw new CanvasError("INVALID", issues.join("; "));
98
+ }
99
+ return result.data;
100
+ }
101
+ var isBindable = (type) => typeof type === "string" && type !== "connector" && type !== "group";
102
+ var isValidZ = (value) => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAX_Z;
103
+ var clampZ = (value) => Math.min(MAX_Z, Math.max(-MAX_Z, value));
104
+ var hasId = (e) => typeof e === "object" && e !== null && typeof e.id === "string";
105
+ var Board = class _Board {
106
+ doc;
107
+ elements;
108
+ meta;
109
+ maxElements;
110
+ constructor(doc = new Y.Doc(), options = {}) {
111
+ this.maxElements = Math.min(options.maxElements ?? MAX_ELEMENTS, MAX_ELEMENTS);
112
+ this.doc = doc;
113
+ this.elements = doc.getMap("elements");
114
+ this.meta = doc.getMap("meta");
115
+ }
116
+ /** Load a saved board with no element checks. Only the owner repair path uses it. */
117
+ static open(update) {
118
+ const doc = new Y.Doc();
119
+ Y.applyUpdate(doc, update);
120
+ return new _Board(doc);
121
+ }
122
+ /**
123
+ * Load a saved board. Refuses a doc past the element cap. Then it deletes every entry that fails
124
+ * the element checks, and repairs what concurrent writers left broken.
125
+ */
126
+ static decode(update) {
127
+ const board2 = _Board.open(update);
128
+ if (board2.elements.size > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
129
+ board2.repair();
130
+ return board2;
131
+ }
132
+ encode() {
133
+ return Y.encodeStateAsUpdate(this.doc);
134
+ }
135
+ /** An independent copy. Edits to it leave this board alone. */
136
+ clone() {
137
+ const copy = _Board.decode(this.encode());
138
+ copy.maxElements = this.maxElements;
139
+ return copy;
140
+ }
141
+ get title() {
142
+ const value = titleSchema.safeParse(this.meta.get("title"));
143
+ return value.success ? value.data : "";
144
+ }
145
+ get titleBy() {
146
+ const value = principalSchema.safeParse(this.meta.get("titleBy"));
147
+ return value.success ? value.data : void 0;
148
+ }
149
+ setTitle(title, actor) {
150
+ const t = parse(z2.string().max(200), title);
151
+ const by = parse(principalSchema, actor);
152
+ this.doc.transact(() => {
153
+ this.meta.set("title", t);
154
+ this.meta.set("titleBy", by);
155
+ });
156
+ }
157
+ /** One element. An invalid entry, or a connector with a missing bound end, is hidden. */
158
+ get(elementId) {
159
+ const el = this.stored(elementId);
160
+ return el && !this.isDetached(el, (endId) => this.stored(endId)) ? el : void 0;
161
+ }
162
+ /** All visible elements from back to front. */
163
+ list() {
164
+ const all = this.storedList();
165
+ const byId = new Map(all.map((e) => [e.id, e]));
166
+ return all.filter((e) => !this.isDetached(e, (endId) => byId.get(endId)));
167
+ }
168
+ /**
169
+ * Delete what peers can leave broken: entries that fail the element checks, connectors whose
170
+ * bound end is gone, and members that name a deleted group. Idempotent, so every peer may run it.
171
+ * `drop` names elements the owner removes too, with what they hold: a shape takes its connectors,
172
+ * a group takes its members. Refuses a result still past the cap, and then changes nothing.
173
+ * Returns the ids fixed.
174
+ */
175
+ repair(drop = []) {
176
+ const all = this.allStored();
177
+ const byId = new Map(all.map((e) => [e.id, e]));
178
+ const doomed = new Set([...this.elements.keys()].filter((key) => !byId.has(key)));
179
+ for (const name of drop) {
180
+ if (byId.has(name)) this.subtreeIn(all, name).forEach((d) => doomed.add(d));
181
+ else if (!this.elements.has(name)) throw new CanvasError("NOT_FOUND", `no element "${name}"`);
182
+ }
183
+ const alive = all.filter((e) => !doomed.has(e.id));
184
+ const aliveById = new Map(alive.map((e) => [e.id, e]));
185
+ const detached = new Set(alive.filter((e) => this.isDetached(e, (endId) => aliveById.get(endId))).map((e) => e.id));
186
+ const kept = alive.filter((e) => !detached.has(e.id));
187
+ if (kept.length > this.maxElements) throw new CanvasError("LIMIT", `a board holds at most ${this.maxElements} elements`);
188
+ const loose = kept.filter((e) => e.groupId !== void 0 && aliveById.get(e.groupId)?.type !== "group").map((e) => e.id);
189
+ this.doc.transact(() => {
190
+ [...doomed, ...detached].forEach((d) => this.elements.delete(d));
191
+ loose.forEach((l) => this.elements.get(l)?.delete("groupId"));
192
+ });
193
+ return [.../* @__PURE__ */ new Set([...doomed, ...detached, ...loose])].sort();
194
+ }
195
+ addShape(input, actor) {
196
+ const { id: given, ...fields } = parse(shapeInput, input);
197
+ return this.create({ ...fields, id: given }, actor);
198
+ }
199
+ addConnector(input, actor) {
200
+ const { id: given, ...fields } = parse(connectorInput, input);
201
+ this.checkEnd(fields.from);
202
+ this.checkEnd(fields.to);
203
+ return this.create({ ...fields, type: "connector", id: given }, actor);
204
+ }
205
+ update(elementId, patch, actor) {
206
+ const by = parse(principalSchema, actor);
207
+ const el = this.require(elementId);
208
+ if (el.type === "group") throw new CanvasError("INVALID", "a group has no fields to update");
209
+ const fields = parse(el.type === "connector" ? connectorPatch : shapePatch, patch);
210
+ if (Object.keys(fields).length === 0) throw new CanvasError("INVALID", "patch is empty");
211
+ for (const [key, value] of Object.entries(fields)) {
212
+ if (value === void 0 && key !== "fill" && key !== "stroke") throw new CanvasError("INVALID", `${key} cannot be cleared`);
213
+ }
214
+ if ("src" in fields && el.type !== "image") throw new CanvasError("INVALID", "only an image has a src");
215
+ if (el.type === "connector") {
216
+ this.checkEnd(fields.from);
217
+ this.checkEnd(fields.to);
218
+ }
219
+ this.write([[elementId, fields]], by);
220
+ return this.get(elementId);
221
+ }
222
+ /** Shift an element, or a whole group. Returns the ids that moved. Bound connector ends follow their shapes. */
223
+ move(elementId, dx, dy, actor) {
224
+ const by = parse(principalSchema, actor);
225
+ const [mx, my] = parse(z2.tuple([delta, delta]), [dx, dy]);
226
+ this.require(elementId);
227
+ const free = (e) => hasId(e) ? e : { x: e.x + mx, y: e.y + my };
228
+ const plan = [];
229
+ for (const memberId of this.subtree(elementId)) {
230
+ const el = this.get(memberId);
231
+ if (!el) continue;
232
+ if (el.type === "connector") {
233
+ const patch = parse(connectorPatch, { from: free(el.from), to: free(el.to) });
234
+ if (!hasId(el.from) || !hasId(el.to)) plan.push([memberId, patch]);
235
+ } else if (el.type !== "group") {
236
+ plan.push([memberId, parse(shapePatch, { x: el.x + mx, y: el.y + my })]);
237
+ }
238
+ }
239
+ this.write(plan, by);
240
+ return plan.map(([memberId]) => memberId);
241
+ }
242
+ /** Delete an element. A shape takes its connectors and a group takes its members. Returns the ids removed. */
243
+ remove(elementId) {
244
+ this.require(elementId);
245
+ const doomed = new Set(this.subtree(elementId));
246
+ for (const el of this.storedList()) {
247
+ if (el.type === "connector" && [el.from, el.to].some((e) => hasId(e) && doomed.has(e.id))) doomed.add(el.id);
248
+ }
249
+ this.doc.transact(() => doomed.forEach((d) => this.elements.delete(d)));
250
+ return [...doomed].sort();
251
+ }
252
+ group(ids, actor, groupId) {
253
+ const by = parse(principalSchema, actor);
254
+ const members = parse(z2.array(id).min(2).max(MAX_ELEMENTS), ids);
255
+ const gid = parse(id.optional(), groupId);
256
+ if (new Set(members).size !== members.length) throw new CanvasError("INVALID", "member ids repeat");
257
+ const parents = new Set(members.map((m) => this.require(m).groupId));
258
+ if (parents.size > 1) throw new CanvasError("BAD_REFERENCE", "members must share one parent group");
259
+ const [parent] = parents;
260
+ const group = this.create({ type: "group", id: gid, groupId: parent }, by);
261
+ this.write(members.map((m) => [m, { groupId: group.id }]), by);
262
+ return group;
263
+ }
264
+ /** Remove a group and hand its members to the parent group. */
265
+ ungroup(groupId, actor) {
266
+ const by = parse(principalSchema, actor);
267
+ const group = this.require(groupId);
268
+ if (group.type !== "group") throw new CanvasError("INVALID", `"${groupId}" is not a group`);
269
+ const members = this.list().filter((e) => e.groupId === groupId);
270
+ this.doc.transact(() => {
271
+ this.write(members.map((m) => [m.id, { groupId: group.groupId }]), by);
272
+ this.elements.delete(groupId);
273
+ });
274
+ }
275
+ /** Put an element, or a group with its inner order kept, in front of or behind every other element. */
276
+ reorder(elementId, to, actor) {
277
+ const by = parse(principalSchema, actor);
278
+ this.require(elementId);
279
+ const inner = new Set(this.subtree(elementId));
280
+ const members = this.list().filter((e) => inner.has(e.id) && e.type !== "group");
281
+ const start = to === "front" ? this.edgeZ(1) : this.edgeZ(-1) - (members.length - 1);
282
+ if (start < -MAX_Z || start + members.length - 1 > MAX_Z) throw new CanvasError("LIMIT", `z order is full: values must stay within ${MAX_Z}`);
283
+ this.write(members.map((m, i) => [m.id, { z: start + i }]), by);
284
+ }
285
+ /** One stored element that passes the checks and whose id matches its key, or undefined. */
286
+ stored(key) {
287
+ const entry = this.elements.get(key);
288
+ if (!(entry instanceof Y.Map)) return void 0;
289
+ const parsed = storedElement.safeParse(entry.toJSON());
290
+ return parsed.success && parsed.data.id === key ? parsed.data : void 0;
291
+ }
292
+ /** Every stored element that passes the checks, from back to front. Refuses a doc past the cap. */
293
+ storedList() {
294
+ if (this.elements.size > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
295
+ return this.allStored();
296
+ }
297
+ /** Like storedList, with no cap check. Repair needs every entry, even on a board past the cap. */
298
+ allStored() {
299
+ const all = [];
300
+ for (const key of this.elements.keys()) {
301
+ const el = this.stored(key);
302
+ if (el) all.push(el);
303
+ }
304
+ return all.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
305
+ }
306
+ /** A connector is detached when a bound end does not name a stored shape. */
307
+ isDetached(el, lookup) {
308
+ if (el.type !== "connector") return false;
309
+ return [el.from, el.to].some((e) => hasId(e) && !isBindable(lookup(e.id)?.type));
310
+ }
311
+ require(elementId) {
312
+ const el = this.get(elementId);
313
+ if (!el) throw new CanvasError("NOT_FOUND", `no element "${elementId}"`);
314
+ return el;
315
+ }
316
+ checkEnd(end) {
317
+ if (!end || !hasId(end)) return;
318
+ if (!isBindable(this.stored(end.id)?.type)) {
319
+ throw new CanvasError("BAD_REFERENCE", `"${end.id}" is not a shape a connector can bind to`);
320
+ }
321
+ }
322
+ /** The z value just past the top or the bottom. Not clamped: the caller checks the range. */
323
+ edgeZ(direction) {
324
+ let z4 = 0;
325
+ for (const m of this.elements.values()) {
326
+ const value = m instanceof Y.Map ? m.get("z") : void 0;
327
+ if (isValidZ(value)) z4 = direction === 1 ? Math.max(z4, value) : Math.min(z4, value);
328
+ }
329
+ return z4 + direction;
330
+ }
331
+ /** The element and, for a group, everything below it. Cycle-safe. */
332
+ subtree(elementId) {
333
+ return this.subtreeIn(this.storedList(), elementId);
334
+ }
335
+ subtreeIn(all, elementId) {
336
+ const children = /* @__PURE__ */ new Map();
337
+ for (const { id: childId, groupId: parent } of all) {
338
+ if (parent !== void 0) children.set(parent, [...children.get(parent) ?? [], childId]);
339
+ }
340
+ const seen = /* @__PURE__ */ new Set([elementId]);
341
+ for (const next of seen) for (const child of children.get(next) ?? []) seen.add(child);
342
+ return [...seen];
343
+ }
344
+ create(fields, actor) {
345
+ const by = parse(principalSchema, actor);
346
+ const elementId = fields.id ?? randomUUID();
347
+ if (this.elements.has(elementId)) throw new CanvasError("DUPLICATE_ID", `element "${elementId}" already exists`);
348
+ if (this.elements.size >= this.maxElements) throw new CanvasError("LIMIT", `a board holds at most ${this.maxElements} elements`);
349
+ const record = { ...fields, id: elementId, z: clampZ(this.edgeZ(1)), author: by, editedBy: by };
350
+ const entries = Object.entries(record).filter(([, v]) => v !== void 0);
351
+ this.doc.transact(() => this.elements.set(elementId, new Y.Map(entries)));
352
+ return this.get(elementId);
353
+ }
354
+ /** Apply field changes and stamp the editor, all in one transaction. An undefined value clears the field. */
355
+ write(plan, actor) {
356
+ this.doc.transact(() => {
357
+ for (const [elementId, fields] of plan) {
358
+ const m = this.elements.get(elementId);
359
+ for (const [key, value] of Object.entries(fields)) {
360
+ if (value === void 0) m.delete(key);
361
+ else m.set(key, value);
362
+ }
363
+ m.set("editedBy", actor);
364
+ }
365
+ });
366
+ }
367
+ };
368
+
369
+ // src/actor.ts
370
+ var ActorConfigError = class extends Error {
371
+ };
372
+ function operatorActor(env = process.env) {
373
+ const raw = env.CANVAS_ACTOR;
374
+ if (!raw) throw new ActorConfigError("the operator must set CANVAS_ACTOR=<kind>:<id>");
375
+ const at = raw.indexOf(":");
376
+ const parsed = principalSchema.safeParse({ kind: raw.slice(0, at), id: raw.slice(at + 1) });
377
+ if (at < 0 || !parsed.success) {
378
+ throw new ActorConfigError(`bad CANVAS_ACTOR "${raw}": use <kind>:<id>, kind is one of ${PRINCIPAL_KINDS.join(", ")}`);
379
+ }
380
+ return parsed.data;
381
+ }
382
+
383
+ // src/mcp.ts
384
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
385
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
386
+ import { z as z3 } from "zod";
387
+ var json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
388
+ function failure(error) {
389
+ if (!(error instanceof CanvasError)) console.error(error instanceof Error ? error.message : String(error));
390
+ const body = error instanceof CanvasError ? { code: error.code, message: error.message } : { code: "INTERNAL", message: "the board operation failed" };
391
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(body) }] };
392
+ }
393
+ function tool(run) {
394
+ return async (args) => {
395
+ try {
396
+ return await run(args);
397
+ } catch (error) {
398
+ return failure(error);
399
+ }
400
+ };
401
+ }
402
+ var board = z3.string().describe("Board id.");
403
+ var num = z3.number();
404
+ function createMcpServer(service, actor) {
405
+ const server = new McpServer({ name: BRAND.command, version: BRAND.version });
406
+ const read = { readOnlyHint: true };
407
+ server.registerTool(
408
+ "create_board",
409
+ { description: "Make a board.", inputSchema: { id: z3.string().optional(), title: z3.string().optional() } },
410
+ tool(async (a) => json(await service.create(actor, a)))
411
+ );
412
+ server.registerTool(
413
+ "read_board",
414
+ { description: "Read a board as data: nodes, edges, groups, text, and who made what.", inputSchema: { board }, annotations: read },
415
+ tool(async (a) => json(await service.read(a.board)))
416
+ );
417
+ server.registerTool(
418
+ "add_shape",
419
+ {
420
+ description: "Add a shape. Image shapes need src.",
421
+ inputSchema: {
422
+ board,
423
+ id: z3.string().optional(),
424
+ type: z3.enum(SHAPE_TYPES),
425
+ x: num,
426
+ y: num,
427
+ w: num.optional(),
428
+ h: num.optional(),
429
+ text: z3.string().optional(),
430
+ fill: z3.string().optional(),
431
+ stroke: z3.string().optional(),
432
+ src: z3.string().optional()
433
+ }
434
+ },
435
+ tool(async ({ board: b, ...shape }) => json(await service.add(b, shape, actor)))
436
+ );
437
+ server.registerTool(
438
+ "connect",
439
+ { description: "Join two shapes with an arrow.", inputSchema: { board, from: z3.string(), to: z3.string(), id: z3.string().optional(), text: z3.string().optional() } },
440
+ tool(async (a) => json(await service.connect(a.board, { id: a.id, from: { id: a.from }, to: { id: a.to }, text: a.text }, actor)))
441
+ );
442
+ server.registerTool(
443
+ "move",
444
+ { description: "Move an element, or a whole group.", inputSchema: { board, element: z3.string(), dx: num, dy: num } },
445
+ tool(async (a) => json(await service.move(a.board, a.element, a.dx, a.dy, actor)))
446
+ );
447
+ server.registerTool(
448
+ "remove",
449
+ { description: "Delete an element. A shape takes its connectors, a group its members.", inputSchema: { board, element: z3.string() }, annotations: { destructiveHint: true } },
450
+ tool(async (a) => json(await service.remove(a.board, a.element)))
451
+ );
452
+ server.registerTool(
453
+ "draw_flow",
454
+ {
455
+ description: 'Draw a flowchart from text, one statement per line: "A[Box] -> B{Choice}: label". [] box, () ellipse, {} diamond.',
456
+ inputSchema: { board, text: z3.string(), idPrefix: z3.string().optional() }
457
+ },
458
+ tool(async (a) => json(await service.flow(a.board, a.text, actor, { idPrefix: a.idPrefix })))
459
+ );
460
+ server.registerTool(
461
+ "export_svg",
462
+ { description: "Export the board as SVG text.", inputSchema: { board }, annotations: read },
463
+ tool(async (a) => ({ content: [{ type: "text", text: await service.exportSvg(a.board) }] }))
464
+ );
465
+ server.registerTool(
466
+ "export_png",
467
+ { description: "Export the board as a PNG image. Needs the optional playwright-core and a Chromium.", inputSchema: { board }, annotations: read },
468
+ tool(async (a) => ({ content: [{ type: "image", data: Buffer.from(await service.exportPng(a.board)).toString("base64"), mimeType: "image/png" }] }))
469
+ );
470
+ return server;
471
+ }
472
+ async function runMcp(service, actor) {
473
+ await createMcpServer(service, actor).connect(new StdioServerTransport());
474
+ }
475
+
476
+ // src/core/flow.ts
477
+ import { randomUUID as randomUUID2 } from "crypto";
478
+ import dagre from "@dagrejs/dagre";
479
+ import * as Y2 from "yjs";
480
+ var MAX_FLOW_NODES = 500;
481
+ var MAX_FLOW_EDGES = 2e3;
482
+ var MAX_FLOW_CHARS = 2e5;
483
+ var NODE = String.raw`([A-Za-z0-9_-]{1,48})(?:\[([^\]]*)\]|\(([^)]*)\)|\{([^}]*)\})?`;
484
+ var EDGE = new RegExp(String.raw`^${NODE}\s*->\s*${NODE}\s*(?::(.*))?$`);
485
+ var ALONE = new RegExp(String.raw`^${NODE}$`);
486
+ var SHAPES = ["rect", "ellipse", "diamond"];
487
+ function parseFlow(source) {
488
+ if (source.length > MAX_FLOW_CHARS) throw new CanvasError("LIMIT", `flow text is over ${MAX_FLOW_CHARS} characters`);
489
+ const graph = { direction: "TB", nodes: [], edges: [] };
490
+ const byId = /* @__PURE__ */ new Map();
491
+ const fail = (line, message) => {
492
+ throw new CanvasError("INVALID", `line ${line}: ${message}`);
493
+ };
494
+ const node = (line, m, at) => {
495
+ const id2 = m[at];
496
+ if (/^edge_\d+$/.test(id2)) fail(line, `"${id2}" is reserved for connectors. Pick another node id.`);
497
+ const brackets = m.slice(at + 1, at + 4);
498
+ const open = brackets.findIndex((b) => b !== void 0);
499
+ const shape = SHAPES[Math.max(open, 0)];
500
+ const text2 = (open < 0 ? "" : brackets[open].trim()) || id2;
501
+ const known = byId.get(id2);
502
+ if (!known) {
503
+ const created = { id: id2, text: text2, shape, explicit: open >= 0 };
504
+ byId.set(id2, created);
505
+ graph.nodes.push(created);
506
+ } else if (open >= 0) {
507
+ if (known.explicit && (known.text !== text2 || known.shape !== shape)) fail(line, `"${id2}" is defined twice with different text`);
508
+ Object.assign(known, { text: text2, shape, explicit: true });
509
+ }
510
+ return id2;
511
+ };
512
+ source.replace(/^/, "").split(/\r?\n/).forEach((raw, index) => {
513
+ const line = index + 1;
514
+ const text2 = raw.trim();
515
+ if (text2 === "" || text2.startsWith("#") || text2.startsWith("//")) return;
516
+ if (/[\u0000-\u0008\u000a-\u001f\u007f]/.test(text2)) fail(line, "control characters are not allowed");
517
+ const direction = /^direction\s+(\S+)$/.exec(text2);
518
+ if (direction) {
519
+ if (direction[1] !== "TB" && direction[1] !== "LR") fail(line, `direction must be TB or LR, got "${direction[1]}"`);
520
+ graph.direction = direction[1];
521
+ return;
522
+ }
523
+ const edge = EDGE.exec(text2);
524
+ if (edge) {
525
+ const from = node(line, edge, 1);
526
+ const to = node(line, edge, 5);
527
+ graph.edges.push({ from, to, text: (edge[9] ?? "").trim() });
528
+ } else {
529
+ const alone = ALONE.exec(text2);
530
+ if (!alone) fail(line, `cannot read "${text2.slice(0, 40)}". Use "A -> B: label".`);
531
+ else node(line, alone, 1);
532
+ }
533
+ if (graph.nodes.length > MAX_FLOW_NODES) throw new CanvasError("LIMIT", `a flow holds at most ${MAX_FLOW_NODES} nodes`);
534
+ if (graph.edges.length > MAX_FLOW_EDGES) throw new CanvasError("LIMIT", `a flow holds at most ${MAX_FLOW_EDGES} edges`);
535
+ });
536
+ if (graph.nodes.length === 0) throw new CanvasError("INVALID", "the flow has no nodes");
537
+ return { ...graph, nodes: graph.nodes.map(({ id: id2, text: text2, shape }) => ({ id: id2, text: text2, shape })) };
538
+ }
539
+ function sizeOf(n) {
540
+ const w = Math.min(360, Math.max(120, 24 + 8 * n.text.length));
541
+ return n.shape === "diamond" ? { w: Math.min(360, Math.round(w * 1.5)), h: 90 } : { w, h: 60 };
542
+ }
543
+ function layoutFlow(graph) {
544
+ const g = new dagre.graphlib.Graph({ multigraph: true });
545
+ g.setGraph({ rankdir: graph.direction, nodesep: 40, ranksep: 70, marginx: 0, marginy: 0 });
546
+ g.setDefaultEdgeLabel(() => ({}));
547
+ const index = new Map(graph.nodes.map((n, i) => [n.id, `n${i}`]));
548
+ const sizes = graph.nodes.map(sizeOf);
549
+ sizes.forEach(({ w, h }, i) => g.setNode(`n${i}`, { width: w, height: h }));
550
+ graph.edges.forEach((e, i) => g.setEdge(index.get(e.from), index.get(e.to), {}, String(i)));
551
+ dagre.layout(g);
552
+ const boxes = graph.nodes.map((n, i) => {
553
+ const { w, h } = sizes[i];
554
+ const c = g.node(`n${i}`);
555
+ return { id: n.id, x: Math.round(c.x - w / 2), y: Math.round(c.y - h / 2), w, h };
556
+ });
557
+ const minX = Math.min(...boxes.map((b) => b.x));
558
+ const minY = Math.min(...boxes.map((b) => b.y));
559
+ return boxes.map((b) => ({ ...b, x: b.x - minX, y: b.y - minY }));
560
+ }
561
+ function drawFlow(board2, source, actor, options = {}) {
562
+ const prefix = options.idPrefix ?? `${randomUUID2().slice(0, 8)}-`;
563
+ if (!/^[A-Za-z0-9_-]{0,16}$/.test(prefix)) throw new CanvasError("INVALID", "idPrefix must be up to 16 of A-Z, a-z, 0-9, _ and -");
564
+ const graph = parseFlow(source);
565
+ const placed = layoutFlow(graph);
566
+ const shapes = board2.list().flatMap((e) => e.type === "connector" || e.type === "group" ? [] : [e]);
567
+ const origin = options.origin ?? {
568
+ x: shapes.length ? Math.min(...shapes.map((s) => s.x)) : 0,
569
+ y: shapes.length ? Math.max(...shapes.map((s) => s.y + s.h)) + 80 : 0
570
+ };
571
+ const work = board2.clone();
572
+ const text2 = new Map(graph.nodes.map((n) => [n.id, n]));
573
+ for (const p of placed) {
574
+ const n = text2.get(p.id);
575
+ work.addShape({ id: prefix + p.id, type: n.shape, text: n.text, x: p.x + origin.x, y: p.y + origin.y, w: p.w, h: p.h }, actor);
576
+ }
577
+ const edges = graph.edges.map((e, i) => {
578
+ const id2 = `${prefix}edge_${i + 1}`;
579
+ work.addConnector({ id: id2, from: { id: prefix + e.from }, to: { id: prefix + e.to }, text: e.text }, actor);
580
+ return id2;
581
+ });
582
+ Y2.applyUpdate(board2.doc, Y2.encodeStateAsUpdate(work.doc, Y2.encodeStateVector(board2.doc)));
583
+ return { nodes: placed.map((p) => prefix + p.id), edges };
584
+ }
585
+
586
+ // src/core/read.ts
587
+ function readBoard(board2) {
588
+ const data = { title: board2.title, nodes: [], edges: [], groups: [], texts: [] };
589
+ const all = board2.list();
590
+ for (const el of all) {
591
+ if (el.type === "group") {
592
+ data.groups.push({ id: el.id, members: all.filter((m) => m.groupId === el.id).map((m) => m.id) });
593
+ continue;
594
+ }
595
+ const labels = { machineMade: isMachineMade(el.author.kind), lastEditMachineMade: isMachineMade(el.editedBy.kind) };
596
+ if (el.text !== "") data.texts.push(el.text);
597
+ if (el.type === "connector") data.edges.push({ ...el, ...labels });
598
+ else {
599
+ const src = el.src?.startsWith("data:") ? `${el.src.slice(0, el.src.indexOf(","))},(${el.src.length} characters left out)` : el.src;
600
+ data.nodes.push({ ...el, ...src === void 0 ? {} : { src }, ...labels });
601
+ }
602
+ }
603
+ return data;
604
+ }
605
+
606
+ // src/core/svg.ts
607
+ var LINE = 17;
608
+ var num2 = (n) => String(Math.round(n * 100) / 100);
609
+ var esc = (value) => (typeof value === "string" ? value : "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f￾￿]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
610
+ var paint = (value, fallback) => typeof value === "string" && COLOR_PATTERN.test(value) ? value : fallback;
611
+ var str = (value) => typeof value === "string" ? value : "";
612
+ var within = (n, low, high) => typeof n === "number" && Number.isFinite(n) && n >= low && n <= high;
613
+ var LIMIT_COORD = 1e7;
614
+ var TYPES = /* @__PURE__ */ new Set([...SHAPE_TYPES, "connector", "group"]);
615
+ var sane = (s) => within(s.x, -LIMIT_COORD, LIMIT_COORD) && within(s.y, -LIMIT_COORD, LIMIT_COORD) && within(s.w, 1e-6, LIMIT_COORD) && within(s.h, 1e-6, LIMIT_COORD);
616
+ var isShape = (v) => "w" in v;
617
+ var center = (s) => ({ x: s.x + s.w / 2, y: s.y + s.h / 2 });
618
+ function edgePoint(s, toward) {
619
+ const c = center(s);
620
+ const dx = toward.x - c.x;
621
+ const dy = toward.y - c.y;
622
+ if (dx === 0 && dy === 0) return c;
623
+ const a = s.w / 2;
624
+ const b = s.h / 2;
625
+ const k = s.type === "ellipse" ? 1 / Math.hypot(dx / a, dy / b) : s.type === "diamond" ? 1 / (Math.abs(dx) / a + Math.abs(dy) / b) : Math.min(a / Math.abs(dx), b / Math.abs(dy));
626
+ return { x: c.x + dx * k, y: c.y + dy * k };
627
+ }
628
+ function wrap(text2, width) {
629
+ const max = Math.max(4, Math.floor((width - 16) / 7.5));
630
+ const out = [];
631
+ for (const paragraph of text2.split(/\r?\n/)) {
632
+ let line = "";
633
+ for (const word of paragraph.split(/\s+/).filter(Boolean)) {
634
+ const chars = [...word];
635
+ while (chars.length > max) {
636
+ if (line) out.push(line);
637
+ line = "";
638
+ out.push(chars.splice(0, max).join(""));
639
+ }
640
+ const rest = chars.join("");
641
+ if (line && [...line].length + 1 + chars.length > max) {
642
+ out.push(line);
643
+ line = rest;
644
+ } else line = line ? `${line} ${rest}` : rest;
645
+ }
646
+ out.push(line);
647
+ }
648
+ return out.slice(0, 40);
649
+ }
650
+ function label(text2, cx, cy, width, extra = "") {
651
+ if (text2 === "") return "";
652
+ const lines = wrap(text2, width);
653
+ const top = cy - (lines.length - 1) * LINE / 2;
654
+ const spans = lines.map((l, i) => `<tspan x="${num2(cx)}" y="${num2(top + i * LINE)}">${esc(l)}</tspan>`);
655
+ return `<text text-anchor="middle" dominant-baseline="central"${extra}>${spans.join("")}</text>`;
656
+ }
657
+ function shapeBody(s) {
658
+ const { x, y, w, h } = s;
659
+ const stroke = paint(s.stroke, "#1f2937");
660
+ const look = (fill, more = "") => `fill="${paint(s.fill, fill)}" stroke="${stroke}" stroke-width="2"${more}`;
661
+ const text2 = label(str(s.text), x + w / 2, y + h / 2, w);
662
+ switch (s.type) {
663
+ case "ellipse":
664
+ return `<ellipse cx="${num2(x + w / 2)}" cy="${num2(y + h / 2)}" rx="${num2(w / 2)}" ry="${num2(h / 2)}" ${look("#ffffff")}/>${text2}`;
665
+ case "diamond": {
666
+ const pts = [[x + w / 2, y], [x + w, y + h / 2], [x + w / 2, y + h], [x, y + h / 2]];
667
+ return `<polygon points="${pts.map(([px, py]) => `${num2(px)},${num2(py)}`).join(" ")}" ${look("#ffffff")}/>${text2}`;
668
+ }
669
+ case "text":
670
+ return text2;
671
+ case "frame":
672
+ return `<rect x="${num2(x)}" y="${num2(y)}" width="${num2(w)}" height="${num2(h)}" ${look("none", ' stroke-dasharray="8 6"')}/>${label(str(s.text), x + w / 2, y - 12, w)}`;
673
+ case "image":
674
+ if (typeof s.src === "string" && s.src.startsWith("data:") && IMAGE_PATTERN.test(s.src)) {
675
+ return `<image href="${s.src}" x="${num2(x)}" y="${num2(y)}" width="${num2(w)}" height="${num2(h)}" preserveAspectRatio="xMidYMid meet"/>`;
676
+ }
677
+ return `<rect x="${num2(x)}" y="${num2(y)}" width="${num2(w)}" height="${num2(h)}" ${look("#f3f4f6", ' stroke-dasharray="4 4"')}/>${label("image", x + w / 2, y + h / 2, w)}`;
678
+ default:
679
+ return `<rect x="${num2(x)}" y="${num2(y)}" width="${num2(w)}" height="${num2(h)}" rx="${s.type === "sticky" ? 2 : 6}" ${look(s.type === "sticky" ? "#fde68a" : "#ffffff")}/>${text2}`;
680
+ }
681
+ }
682
+ var endKey = (e) => "id" in e ? e.id : `${e.x},${e.y}`;
683
+ var wellFormed = (e) => typeof e === "object" && e !== null && (typeof e.id === "string" || within(e.x, -LIMIT_COORD, LIMIT_COORD) && within(e.y, -LIMIT_COORD, LIMIT_COORD));
684
+ function connectorBody(c, shapes, lane) {
685
+ const resolve = (e) => "id" in e ? shapes.get(e.id) : e;
686
+ const a = resolve(c.from);
687
+ const b = resolve(c.to);
688
+ if (!a || !b) return "";
689
+ let d;
690
+ let at;
691
+ if (isShape(a) && a === b) {
692
+ const x0 = a.x + a.w * 0.35;
693
+ const x1 = a.x + a.w * 0.65;
694
+ const lift = 45 + 32 * lane.index;
695
+ d = `M${num2(x0)} ${num2(a.y)}C${num2(x0)} ${num2(a.y - lift)} ${num2(x1)} ${num2(a.y - lift)} ${num2(x1)} ${num2(a.y)}`;
696
+ at = { x: a.x + a.w / 2, y: a.y - lift * 0.75 - 6 };
697
+ } else {
698
+ const ca = isShape(a) ? center(a) : a;
699
+ const cb = isShape(b) ? center(b) : b;
700
+ const flip = endKey(c.from) > endKey(c.to);
701
+ const dx = (flip ? ca.x - cb.x : cb.x - ca.x) || 0;
702
+ const dy = (flip ? ca.y - cb.y : cb.y - ca.y) || 0;
703
+ const len = Math.hypot(dx, dy) || 1;
704
+ const offset = (lane.index - (lane.count - 1) / 2) * 44;
705
+ const mid = { x: (ca.x + cb.x) / 2, y: (ca.y + cb.y) / 2 };
706
+ const ctrl = { x: mid.x - dy / len * 2 * offset, y: mid.y + dx / len * 2 * offset };
707
+ const p = isShape(a) ? edgePoint(a, ctrl) : a;
708
+ const q = isShape(b) ? edgePoint(b, ctrl) : b;
709
+ d = offset === 0 ? `M${num2(p.x)} ${num2(p.y)}L${num2(q.x)} ${num2(q.y)}` : `M${num2(p.x)} ${num2(p.y)}Q${num2(ctrl.x)} ${num2(ctrl.y)} ${num2(q.x)} ${num2(q.y)}`;
710
+ at = offset === 0 ? { x: (p.x + q.x) / 2, y: (p.y + q.y) / 2 } : { x: (p.x + q.x) / 4 + ctrl.x / 2, y: (p.y + q.y) / 4 + ctrl.y / 2 };
711
+ }
712
+ const marks = `${c.startArrow ? ' marker-start="url(#arrow)"' : ""}${c.endArrow ? ' marker-end="url(#arrow)"' : ""}`;
713
+ const halo = ' stroke="#ffffff" stroke-width="4" paint-order="stroke"';
714
+ return `<path d="${d}" fill="none" stroke="${paint(c.stroke, "#1f2937")}" stroke-width="2"${marks}/>${label(str(c.text), at.x, at.y, 160, halo)}`;
715
+ }
716
+ var drawable = (el) => TYPES.has(el.type) && (el.type === "connector" ? wellFormed(el.from) && wellFormed(el.to) : el.type === "group" || sane(el));
717
+ function toSvg(board2) {
718
+ const listed = board2.list();
719
+ if (listed.length > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
720
+ const all = listed.filter((el) => drawable(el));
721
+ const shapes = /* @__PURE__ */ new Map();
722
+ const box = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
723
+ const grow = (x, y) => {
724
+ box.minX = Math.min(box.minX, x);
725
+ box.minY = Math.min(box.minY, y);
726
+ box.maxX = Math.max(box.maxX, x);
727
+ box.maxY = Math.max(box.maxY, y);
728
+ };
729
+ for (const el of all) {
730
+ if (el.type === "connector") {
731
+ for (const e of [el.from, el.to]) if (!("id" in e)) grow(e.x, e.y);
732
+ } else if (el.type !== "group") {
733
+ shapes.set(el.id, el);
734
+ grow(el.x, el.y);
735
+ grow(el.x + el.w, el.y + el.h);
736
+ }
737
+ }
738
+ const pad = 80;
739
+ const any = box.minX !== Infinity;
740
+ const minX = any ? box.minX - pad : 0;
741
+ const minY = any ? box.minY - pad : 0;
742
+ const width = any ? Math.ceil(box.maxX + pad - minX) : 240;
743
+ const height = any ? Math.ceil(box.maxY + pad - minY) : 120;
744
+ const lanes = /* @__PURE__ */ new Map();
745
+ const groups = /* @__PURE__ */ new Map();
746
+ for (const el of all) {
747
+ if (el.type !== "connector") continue;
748
+ const key = [endKey(el.from), endKey(el.to)].sort().join("|");
749
+ groups.set(key, [...groups.get(key) ?? [], el.id]);
750
+ }
751
+ for (const ids of groups.values()) ids.forEach((id2, index) => lanes.set(id2, { index, count: ids.length }));
752
+ const body = all.map((el) => {
753
+ if (el.type === "group") return "";
754
+ const inner = el.type === "connector" ? connectorBody(el, shapes, lanes.get(el.id)) : shapeBody(el);
755
+ if (inner === "") return "";
756
+ const kind = el.author.kind;
757
+ const mark = `data-id="${esc(el.id)}" data-type="${el.type}" data-author-kind="${kind}" data-machine-made="${kind !== "human"}"`;
758
+ return `<g ${mark}>${inner}</g>`;
759
+ });
760
+ const title = esc(board2.title) || "Whiteboard";
761
+ return [
762
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${num2(minX)} ${num2(minY)} ${width} ${height}" font-family="sans-serif" font-size="14" fill="#111827" role="img" aria-label="${title}">`,
763
+ `<title>${title}</title>`,
764
+ '<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#1f2937"/></marker></defs>',
765
+ `<rect x="${num2(minX)}" y="${num2(minY)}" width="${width}" height="${height}" fill="#ffffff"/>`,
766
+ ...body.filter(Boolean),
767
+ "</svg>"
768
+ ].join("\n");
769
+ }
770
+
771
+ // src/png.ts
772
+ var MAX_SIDE = 16384;
773
+ var MAX_PIXELS = 16e6;
774
+ var TIMEOUT_MS = 2e4;
775
+ async function svgToPng(svg) {
776
+ const size2 = /^<svg [^>]*?width="(\d+)" height="(\d+)"/.exec(svg);
777
+ const width = Number(size2?.[1]);
778
+ const height = Number(size2?.[2]);
779
+ if (!(width > 0 && height > 0) || width > MAX_SIDE || height > MAX_SIDE || width * height > MAX_PIXELS) {
780
+ throw new CanvasError("LIMIT", `the board is too large for PNG: up to ${MAX_SIDE} px a side and ${MAX_PIXELS / 1e6} million pixels`);
781
+ }
782
+ let chromium;
783
+ try {
784
+ ({ chromium } = await import("playwright-core"));
785
+ } catch {
786
+ throw new CanvasError("UNAVAILABLE", "PNG export needs the playwright-core package. Install it, or export SVG.");
787
+ }
788
+ const browser = await chromium.launch().catch(() => {
789
+ throw new CanvasError("UNAVAILABLE", "PNG export needs Chromium. Run: npx playwright-core install chromium");
790
+ });
791
+ try {
792
+ const context = await browser.newContext({ viewport: { width, height }, javaScriptEnabled: false, serviceWorkers: "block" });
793
+ context.setDefaultTimeout(TIMEOUT_MS);
794
+ await context.route("**/*", (route) => route.abort());
795
+ const page = await context.newPage();
796
+ await page.setContent(`<!doctype html><body style="margin:0">${svg}</body>`);
797
+ return await page.screenshot({ clip: { x: 0, y: 0, width, height } });
798
+ } finally {
799
+ await browser.close();
800
+ }
801
+ }
802
+
803
+ // src/service.ts
804
+ import { assertId, StoreConflictError } from "agentic-identity";
805
+ var COLLECTION = "boards";
806
+ var BOARD_ID = /^[A-Za-z0-9_-]{1,64}$/;
807
+ function toBase64(bytes) {
808
+ let text2 = "";
809
+ for (let i = 0; i < bytes.length; i += 8192) text2 += String.fromCharCode(...bytes.subarray(i, i + 8192));
810
+ return btoa(text2);
811
+ }
812
+ var fromBase64 = (text2) => Uint8Array.from(atob(text2), (c) => c.charCodeAt(0));
813
+ function boardKey(id2) {
814
+ try {
815
+ if (!BOARD_ID.test(id2)) throw new Error();
816
+ assertId(id2);
817
+ } catch {
818
+ throw new CanvasError("INVALID", "board id must be 1 to 64 of A-Z, a-z, 0-9, _ and -, and not like __x__");
819
+ }
820
+ return id2;
821
+ }
822
+ function storeFailure(error) {
823
+ if (error instanceof StoreConflictError) throw new CanvasError("UNAVAILABLE", "the board changed at the same time. Try again");
824
+ if (error instanceof FileStoreError) throw new CanvasError(error.code === "LOCKED" ? "UNAVAILABLE" : "INVALID", error.message);
825
+ throw error;
826
+ }
827
+ var CanvasService = class {
828
+ store;
829
+ constructor(store) {
830
+ this.store = { ...store, transaction: (fn) => store.transaction(fn).catch(storeFailure) };
831
+ }
832
+ async create(actor, options = {}) {
833
+ const id2 = boardKey(options.id ?? crypto.randomUUID());
834
+ const board2 = new Board();
835
+ if (options.title !== void 0) board2.setTitle(options.title, actor);
836
+ return this.store.transaction(async (tx) => {
837
+ if (await tx.get(COLLECTION, id2)) throw new CanvasError("DUPLICATE_ID", `board "${id2}" already exists`);
838
+ await tx.put(COLLECTION, id2, { update: toBase64(board2.encode()) });
839
+ return { id: id2, title: board2.title };
840
+ });
841
+ }
842
+ async read(id2) {
843
+ boardKey(id2);
844
+ return this.store.transaction(async (tx) => ({ id: id2, ...readBoard(await this.load(tx.get(COLLECTION, id2), id2)) }));
845
+ }
846
+ /** Run `change` on the board and save it, all inside one transaction. */
847
+ async change(id2, change) {
848
+ boardKey(id2);
849
+ return this.store.transaction(async (tx) => {
850
+ const board2 = await this.load(tx.get(COLLECTION, id2), id2);
851
+ const result = change(board2);
852
+ await tx.put(COLLECTION, id2, { update: toBase64(board2.encode()) });
853
+ return result;
854
+ });
855
+ }
856
+ add(id2, shape, actor) {
857
+ return this.change(id2, (b) => b.addShape(shape, actor));
858
+ }
859
+ connect(id2, connector, actor) {
860
+ return this.change(id2, (b) => b.addConnector(connector, actor));
861
+ }
862
+ move(id2, elementId, dx, dy, actor) {
863
+ return this.change(id2, (b) => ({ moved: b.move(elementId, dx, dy, actor) }));
864
+ }
865
+ remove(id2, elementId) {
866
+ return this.change(id2, (b) => ({ removed: b.remove(elementId) }));
867
+ }
868
+ flow(id2, text2, actor, options) {
869
+ return this.change(id2, (b) => drawFlow(b, text2, actor, options));
870
+ }
871
+ async exportSvg(id2) {
872
+ boardKey(id2);
873
+ return this.store.transaction(async (tx) => toSvg(await this.load(tx.get(COLLECTION, id2), id2)));
874
+ }
875
+ async exportPng(id2) {
876
+ return svgToPng(await this.exportSvg(id2));
877
+ }
878
+ /** Owner only. Repair a board and delete the named elements. Only a human actor may call it. */
879
+ async repair(id2, actor, drop = []) {
880
+ const by = principalSchema.safeParse(actor);
881
+ if (!by.success) throw new CanvasError("INVALID", "the actor must be a valid principal");
882
+ if (by.data.kind !== "human") throw new CanvasError("FORBIDDEN", "only a human owner can repair a board");
883
+ boardKey(id2);
884
+ return this.store.transaction(async (tx) => {
885
+ const doc = await tx.get(COLLECTION, id2);
886
+ if (!doc) throw new CanvasError("NOT_FOUND", `no board "${id2}"`);
887
+ const board2 = decodeStored(doc, id2, Board.open);
888
+ const fixed = board2.repair(drop);
889
+ await tx.put(COLLECTION, id2, { update: toBase64(board2.encode()) });
890
+ return { id: id2, fixed, elements: board2.list().length };
891
+ });
892
+ }
893
+ async load(read, id2) {
894
+ const doc = await read;
895
+ if (!doc) throw new CanvasError("NOT_FOUND", `no board "${id2}"`);
896
+ return decodeStored(doc, id2, Board.decode);
897
+ }
898
+ };
899
+ function decodeStored(doc, id2, decode) {
900
+ try {
901
+ return decode(fromBase64(String(doc.update)));
902
+ } catch (error) {
903
+ if (error instanceof CanvasError) throw error;
904
+ throw new CanvasError("INVALID", `stored board "${id2}" is damaged`);
905
+ }
906
+ }
907
+
908
+ export {
909
+ VERSION,
910
+ BRAND,
911
+ PRINCIPAL_KINDS,
912
+ isMachineMade,
913
+ principalSchema,
914
+ CanvasError,
915
+ SHAPE_TYPES,
916
+ MAX_ELEMENTS,
917
+ MAX_TEXT,
918
+ Board,
919
+ ActorConfigError,
920
+ operatorActor,
921
+ createMcpServer,
922
+ runMcp,
923
+ MAX_FLOW_NODES,
924
+ MAX_FLOW_EDGES,
925
+ parseFlow,
926
+ layoutFlow,
927
+ drawFlow,
928
+ readBoard,
929
+ toSvg,
930
+ svgToPng,
931
+ CanvasService
932
+ };