agentic-canvas 0.1.0 → 0.2.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/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
 
8
8
  A shared whiteboard for diagrams, drawn by people and agents.
9
9
 
10
- > Status: 0.1.0, the first release. The board model, the agent operations, the
11
- > exports, the CLI, and the MCP server work. The HTTP API and the web UI are not
12
- > built yet.
10
+ > Status: 0.2.0. The board model, the agent operations, the exports, the CLI, the
11
+ > MCP server, a small Worker API, and live co-editing work. The CLI, the MCP server,
12
+ > and the SDK can join a live room. The web UI is not built yet.
13
13
 
14
14
  ## Install
15
15
 
@@ -101,11 +101,30 @@ the direction. Lines that start with `#` or `//` are comments.
101
101
  and every network request blocked. A board over 16 million pixels exports as SVG only.
102
102
  - A CLI over a local file store. Several processes can write at once.
103
103
  - An MCP server (stdio) over the same service as the CLI.
104
+ - A Worker (`agentic-canvas/worker`) under `/api/v1/canvas`. Boards live in D1 and images in R2
105
+ (EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `CANVAS_API_TOKENS`
106
+ (a JSON map of token to `{ actor, kind }`, tokens of 32 characters or more) and `CANVAS_REGION`.
107
+ Routes: `POST /boards`, `GET /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
108
+ `/connectors`, `/flow`, `/move`, `DELETE /boards/:id/elements/:element`, `POST /images`,
109
+ `GET /images/:id`. An image is stored under the SHA-256 of its bytes, and the reply gives
110
+ the `blob:` ref for a shape. A busy database or bucket answers 503: try again.
111
+ - Live rooms (`agentic-canvas/live-worker`, on `agentic-realtime`). One Durable Object holds one
112
+ board. A token may carry `"role": "viewer"`: a viewer reads and cannot write. `GET /boards/:id/ws`
113
+ opens a room, `GET /me` says who a token is, `GET /boards/:id/state` and
114
+ `POST /boards/:id/checkpoint` seed a room from the stored board and save it back, as a merge. A save
115
+ checks author stamps: a client cannot put another author on its work.
116
+ - The SDK joins a room: `import { LiveBoard, LiveService } from "agentic-canvas/live"`. The CLI and the MCP
117
+ server join when `CANVAS_SERVER` and `CANVAS_TOKEN` are set. The token sets the actor, so
118
+ `CANVAS_ACTOR` is not needed. `--dir`, `CANVAS_DIR`, and `repair` stay local.
104
119
  - Yjs underneath: two copies of a board merge without conflicts.
105
120
  - No UI code. The model is plain TypeScript on Yjs, with no outbound calls.
106
121
 
107
122
  ## Limits
108
123
 
124
+ - Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.4.
125
+ It does not use the sharing API yet. Its editor and viewer tokens are a separate table in
126
+ `CANVAS_API_TOKENS`. It has no board delete or erase. When it adopts sharing, grants are keyed
127
+ on kind and id, and a delete calls `deleteResource`.
109
128
  - A board holds at most 10,000 elements. A flow holds at most 500 nodes and 2,000
110
129
  edges.
111
130
  - Flow text has no chains (`A -> B -> C`). Write one edge per line.
@@ -133,7 +152,14 @@ the direction. Lines that start with `#` or `//` are comments.
133
152
  - Node 22 or newer is needed. `agentic-canvas/file-store` exports `createFileStore(path)` and
134
153
  `openLocalStore(dir)`. A writer that is paused for more than 30 seconds can lose its lock
135
154
  and then still write.
136
- - Delete leaves no audit record yet. The caller logs it.
155
+ - A room remembers its authors for good, so a client can stamp an element with the name of anyone who
156
+ once sent an update in that room. A save corrects the stored board and not the room: after a save that
157
+ reports `fixed`, join again. A client that adds a root Yjs type of its own blocks later saves of that room.
158
+ - Every Worker token reaches every board. A token has an editor or viewer role, and nothing finer yet. Local
159
+ `wrangler dev` cannot confirm a region, so run it with `CANVAS_REGION=unknown`. The region claim covers D1 only: R2 reports no region, so the bucket is in
160
+ the EU jurisdiction only if you create it so. A real D1
161
+ confirms `eu` or `us` on each query. The Worker has no PNG export.
162
+ - Delete leaves no audit record yet. The Worker logs the actor kind of each delete, and no ids.
137
163
  - PNG needs the optional `playwright-core` and a Chromium. Without them it fails with
138
164
  `UNAVAILABLE`. SVG always works.
139
165
 
package/SKILL.md CHANGED
@@ -36,6 +36,9 @@ agentic-canvas read demo
36
36
  agentic-canvas export demo --out demo.svg
37
37
  ```
38
38
 
39
+ To edit a board that a person has open, use live mode. Set `CANVAS_SERVER` and `CANVAS_TOKEN`
40
+ and run the same commands. The token names the actor, so leave `CANVAS_ACTOR` unset.
41
+
39
42
  ## Rules
40
43
 
41
44
  - Name the actor on every write.
@@ -1,18 +1,3 @@
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
1
  // src/core/principal.ts
17
2
  import { PRINCIPAL_KINDS, isMachineMade } from "agentic-identity";
18
3
  import { z } from "zod";
@@ -101,6 +86,7 @@ function parse(schema, value) {
101
86
  var isBindable = (type) => typeof type === "string" && type !== "connector" && type !== "group";
102
87
  var isValidZ = (value) => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAX_Z;
103
88
  var clampZ = (value) => Math.min(MAX_Z, Math.max(-MAX_Z, value));
89
+ var samePrincipal = (a, b) => a.kind === b.kind && a.id === b.id;
104
90
  var hasId = (e) => typeof e === "object" && e !== null && typeof e.id === "string";
105
91
  var Board = class _Board {
106
92
  doc;
@@ -124,10 +110,10 @@ var Board = class _Board {
124
110
  * the element checks, and repairs what concurrent writers left broken.
125
111
  */
126
112
  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;
113
+ const board = _Board.open(update);
114
+ if (board.elements.size > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
115
+ board.repair();
116
+ return board;
131
117
  }
132
118
  encode() {
133
119
  return Y.encodeStateAsUpdate(this.doc);
@@ -192,6 +178,63 @@ var Board = class _Board {
192
178
  });
193
179
  return [.../* @__PURE__ */ new Set([...doomed, ...detached, ...loose])].sort();
194
180
  }
181
+ /**
182
+ * Make the author stamps of a merged board honest. `prior` is the board before the merge, and `trusted` says whether
183
+ * a principal really wrote in this session. A new element stamped by anyone else is deleted. An old element keeps its
184
+ * author, and a changed one needs a trusted editor or it goes back to its prior state. An editor stamp changed alone
185
+ * to an untrusted principal is put back. Unknown keys, nested Yjs types, and bad or foreign title values are deleted
186
+ * or undone. Returns the ids it deleted or restored.
187
+ */
188
+ restamp(prior, trusted) {
189
+ const before = new Map(prior.allStored().map((e) => [e.id, e]));
190
+ const body = ({ author: _a, editedBy: _e, ...rest }) => JSON.stringify(rest);
191
+ const fixed = [];
192
+ this.doc.transact(() => {
193
+ for (const el of this.allStored()) {
194
+ const old = before.get(el.id);
195
+ if (!old) {
196
+ if (trusted(el.author) && trusted(el.editedBy)) continue;
197
+ this.elements.delete(el.id);
198
+ } else {
199
+ const entry = this.elements.get(el.id);
200
+ const authorKept = samePrincipal(el.author, old.author);
201
+ const editorOk = trusted(el.editedBy) || samePrincipal(el.editedBy, old.editedBy);
202
+ const changed = body(el) !== body(old);
203
+ if (authorKept && editorOk && !(changed && !trusted(el.editedBy))) continue;
204
+ if (changed && !trusted(el.editedBy)) this.elements.set(el.id, new Y.Map(Object.entries(old).filter(([, v]) => v !== void 0)));
205
+ else {
206
+ if (!authorKept) entry.set("author", old.author);
207
+ if (!editorOk) entry.set("editedBy", old.editedBy);
208
+ }
209
+ }
210
+ fixed.push(el.id);
211
+ }
212
+ for (const el of this.allStored()) {
213
+ const entry = this.elements.get(el.id);
214
+ const extra = [...entry.keys()].filter((key) => !(key in el) || entry.get(key) instanceof Y.AbstractType);
215
+ extra.forEach((key) => entry.delete(key));
216
+ if (extra.length > 0 && !fixed.includes(el.id)) fixed.push(el.id);
217
+ }
218
+ const same = (key) => !(this.meta.get(key) instanceof Y.AbstractType) && JSON.stringify(this.meta.get(key)) === JSON.stringify(prior.meta.get(key));
219
+ const by = this.titleBy;
220
+ const title = this.meta.get("title");
221
+ const honest = by !== void 0 && trusted(by) && (title === void 0 || titleSchema.safeParse(title).success);
222
+ if (!honest && !(same("title") && same("titleBy"))) {
223
+ for (const key of ["title", "titleBy"]) {
224
+ const old = prior.meta.get(key);
225
+ if (old === void 0) this.meta.delete(key);
226
+ else this.meta.set(key, old);
227
+ }
228
+ fixed.push("title");
229
+ }
230
+ for (const key of [...this.meta.keys()]) {
231
+ if (key === "title" || key === "titleBy") continue;
232
+ this.meta.delete(key);
233
+ fixed.push(`meta:${key}`);
234
+ }
235
+ });
236
+ return fixed;
237
+ }
195
238
  addShape(input, actor) {
196
239
  const { id: given, ...fields } = parse(shapeInput, input);
197
240
  return this.create({ ...fields, id: given }, actor);
@@ -321,12 +364,12 @@ var Board = class _Board {
321
364
  }
322
365
  /** The z value just past the top or the bottom. Not clamped: the caller checks the range. */
323
366
  edgeZ(direction) {
324
- let z4 = 0;
367
+ let z3 = 0;
325
368
  for (const m of this.elements.values()) {
326
369
  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);
370
+ if (isValidZ(value)) z3 = direction === 1 ? Math.max(z3, value) : Math.min(z3, value);
328
371
  }
329
- return z4 + direction;
372
+ return z3 + direction;
330
373
  }
331
374
  /** The element and, for a group, everything below it. Cycle-safe. */
332
375
  subtree(elementId) {
@@ -366,113 +409,6 @@ var Board = class _Board {
366
409
  }
367
410
  };
368
411
 
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
412
  // src/core/flow.ts
477
413
  import { randomUUID as randomUUID2 } from "crypto";
478
414
  import dagre from "@dagrejs/dagre";
@@ -558,17 +494,17 @@ function layoutFlow(graph) {
558
494
  const minY = Math.min(...boxes.map((b) => b.y));
559
495
  return boxes.map((b) => ({ ...b, x: b.x - minX, y: b.y - minY }));
560
496
  }
561
- function drawFlow(board2, source, actor, options = {}) {
497
+ function drawFlow(board, source, actor, options = {}) {
562
498
  const prefix = options.idPrefix ?? `${randomUUID2().slice(0, 8)}-`;
563
499
  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
500
  const graph = parseFlow(source);
565
501
  const placed = layoutFlow(graph);
566
- const shapes = board2.list().flatMap((e) => e.type === "connector" || e.type === "group" ? [] : [e]);
502
+ const shapes = board.list().flatMap((e) => e.type === "connector" || e.type === "group" ? [] : [e]);
567
503
  const origin = options.origin ?? {
568
504
  x: shapes.length ? Math.min(...shapes.map((s) => s.x)) : 0,
569
505
  y: shapes.length ? Math.max(...shapes.map((s) => s.y + s.h)) + 80 : 0
570
506
  };
571
- const work = board2.clone();
507
+ const work = board.clone();
572
508
  const text2 = new Map(graph.nodes.map((n) => [n.id, n]));
573
509
  for (const p of placed) {
574
510
  const n = text2.get(p.id);
@@ -579,14 +515,14 @@ function drawFlow(board2, source, actor, options = {}) {
579
515
  work.addConnector({ id: id2, from: { id: prefix + e.from }, to: { id: prefix + e.to }, text: e.text }, actor);
580
516
  return id2;
581
517
  });
582
- Y2.applyUpdate(board2.doc, Y2.encodeStateAsUpdate(work.doc, Y2.encodeStateVector(board2.doc)));
518
+ Y2.applyUpdate(board.doc, Y2.encodeStateAsUpdate(work.doc, Y2.encodeStateVector(board.doc)));
583
519
  return { nodes: placed.map((p) => prefix + p.id), edges };
584
520
  }
585
521
 
586
522
  // src/core/read.ts
587
- function readBoard(board2) {
588
- const data = { title: board2.title, nodes: [], edges: [], groups: [], texts: [] };
589
- const all = board2.list();
523
+ function readBoard(board) {
524
+ const data = { title: board.title, nodes: [], edges: [], groups: [], texts: [] };
525
+ const all = board.list();
590
526
  for (const el of all) {
591
527
  if (el.type === "group") {
592
528
  data.groups.push({ id: el.id, members: all.filter((m) => m.groupId === el.id).map((m) => m.id) });
@@ -605,7 +541,7 @@ function readBoard(board2) {
605
541
 
606
542
  // src/core/svg.ts
607
543
  var LINE = 17;
608
- var num2 = (n) => String(Math.round(n * 100) / 100);
544
+ var num = (n) => String(Math.round(n * 100) / 100);
609
545
  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
546
  var paint = (value, fallback) => typeof value === "string" && COLOR_PATTERN.test(value) ? value : fallback;
611
547
  var str = (value) => typeof value === "string" ? value : "";
@@ -651,7 +587,7 @@ function label(text2, cx, cy, width, extra = "") {
651
587
  if (text2 === "") return "";
652
588
  const lines = wrap(text2, width);
653
589
  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>`);
590
+ const spans = lines.map((l, i) => `<tspan x="${num(cx)}" y="${num(top + i * LINE)}">${esc(l)}</tspan>`);
655
591
  return `<text text-anchor="middle" dominant-baseline="central"${extra}>${spans.join("")}</text>`;
656
592
  }
657
593
  function shapeBody(s) {
@@ -661,22 +597,22 @@ function shapeBody(s) {
661
597
  const text2 = label(str(s.text), x + w / 2, y + h / 2, w);
662
598
  switch (s.type) {
663
599
  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}`;
600
+ return `<ellipse cx="${num(x + w / 2)}" cy="${num(y + h / 2)}" rx="${num(w / 2)}" ry="${num(h / 2)}" ${look("#ffffff")}/>${text2}`;
665
601
  case "diamond": {
666
602
  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}`;
603
+ return `<polygon points="${pts.map(([px, py]) => `${num(px)},${num(py)}`).join(" ")}" ${look("#ffffff")}/>${text2}`;
668
604
  }
669
605
  case "text":
670
606
  return text2;
671
607
  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)}`;
608
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" ${look("none", ' stroke-dasharray="8 6"')}/>${label(str(s.text), x + w / 2, y - 12, w)}`;
673
609
  case "image":
674
610
  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"/>`;
611
+ return `<image href="${s.src}" x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" preserveAspectRatio="xMidYMid meet"/>`;
676
612
  }
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)}`;
613
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" ${look("#f3f4f6", ' stroke-dasharray="4 4"')}/>${label("image", x + w / 2, y + h / 2, w)}`;
678
614
  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}`;
615
+ return `<rect x="${num(x)}" y="${num(y)}" width="${num(w)}" height="${num(h)}" rx="${s.type === "sticky" ? 2 : 6}" ${look(s.type === "sticky" ? "#fde68a" : "#ffffff")}/>${text2}`;
680
616
  }
681
617
  }
682
618
  var endKey = (e) => "id" in e ? e.id : `${e.x},${e.y}`;
@@ -692,7 +628,7 @@ function connectorBody(c, shapes, lane) {
692
628
  const x0 = a.x + a.w * 0.35;
693
629
  const x1 = a.x + a.w * 0.65;
694
630
  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)}`;
631
+ d = `M${num(x0)} ${num(a.y)}C${num(x0)} ${num(a.y - lift)} ${num(x1)} ${num(a.y - lift)} ${num(x1)} ${num(a.y)}`;
696
632
  at = { x: a.x + a.w / 2, y: a.y - lift * 0.75 - 6 };
697
633
  } else {
698
634
  const ca = isShape(a) ? center(a) : a;
@@ -706,7 +642,7 @@ function connectorBody(c, shapes, lane) {
706
642
  const ctrl = { x: mid.x - dy / len * 2 * offset, y: mid.y + dx / len * 2 * offset };
707
643
  const p = isShape(a) ? edgePoint(a, ctrl) : a;
708
644
  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)}`;
645
+ d = offset === 0 ? `M${num(p.x)} ${num(p.y)}L${num(q.x)} ${num(q.y)}` : `M${num(p.x)} ${num(p.y)}Q${num(ctrl.x)} ${num(ctrl.y)} ${num(q.x)} ${num(q.y)}`;
710
646
  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
647
  }
712
648
  const marks = `${c.startArrow ? ' marker-start="url(#arrow)"' : ""}${c.endArrow ? ' marker-end="url(#arrow)"' : ""}`;
@@ -714,8 +650,8 @@ function connectorBody(c, shapes, lane) {
714
650
  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
651
  }
716
652
  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();
653
+ function toSvg(board) {
654
+ const listed = board.list();
719
655
  if (listed.length > MAX_ELEMENTS) throw new CanvasError("LIMIT", `a board holds at most ${MAX_ELEMENTS} elements`);
720
656
  const all = listed.filter((el) => drawable(el));
721
657
  const shapes = /* @__PURE__ */ new Map();
@@ -757,157 +693,18 @@ function toSvg(board2) {
757
693
  const mark = `data-id="${esc(el.id)}" data-type="${el.type}" data-author-kind="${kind}" data-machine-made="${kind !== "human"}"`;
758
694
  return `<g ${mark}>${inner}</g>`;
759
695
  });
760
- const title = esc(board2.title) || "Whiteboard";
696
+ const title = esc(board.title) || "Whiteboard";
761
697
  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}">`,
698
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${num(minX)} ${num(minY)} ${width} ${height}" font-family="sans-serif" font-size="14" fill="#111827" role="img" aria-label="${title}">`,
763
699
  `<title>${title}</title>`,
764
700
  '<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"/>`,
701
+ `<rect x="${num(minX)}" y="${num(minY)}" width="${width}" height="${height}" fill="#ffffff"/>`,
766
702
  ...body.filter(Boolean),
767
703
  "</svg>"
768
704
  ].join("\n");
769
705
  }
770
706
 
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
707
  export {
909
- VERSION,
910
- BRAND,
911
708
  PRINCIPAL_KINDS,
912
709
  isMachineMade,
913
710
  principalSchema,
@@ -916,17 +713,11 @@ export {
916
713
  MAX_ELEMENTS,
917
714
  MAX_TEXT,
918
715
  Board,
919
- ActorConfigError,
920
- operatorActor,
921
- createMcpServer,
922
- runMcp,
923
716
  MAX_FLOW_NODES,
924
717
  MAX_FLOW_EDGES,
925
718
  parseFlow,
926
719
  layoutFlow,
927
720
  drawFlow,
928
721
  readBoard,
929
- toSvg,
930
- svgToPng,
931
- CanvasService
722
+ toSvg
932
723
  };