agentic-canvas 0.1.0 → 0.3.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 +57 -4
- package/SKILL.md +3 -0
- package/dist/chunk-AGODNBN7.js +166 -0
- package/dist/{chunk-NSZ2AC5O.js → chunk-GGQPE65T.js} +88 -297
- package/dist/chunk-NWQED42L.js +13 -0
- package/dist/{chunk-I3EJ5JB7.js → chunk-UKST5A57.js} +4 -13
- package/dist/chunk-V477KSZQ.js +2005 -0
- package/dist/chunk-W7NRP3EX.js +164 -0
- package/dist/cli.js +33 -7
- package/dist/client.d.ts +108 -0
- package/dist/client.js +78 -0
- package/dist/index.d.ts +10 -247
- package/dist/index.js +16 -11
- package/dist/live-worker.d.ts +22 -0
- package/dist/live-worker.js +25 -0
- package/dist/live.d.ts +102 -0
- package/dist/live.js +192 -0
- package/dist/read-BY6f-mmy.d.ts +167 -0
- package/dist/service-Cpl6wKeY.d.ts +109 -0
- package/dist/store-file.js +4 -2
- package/dist/worker.d.ts +55 -0
- package/dist/worker.js +15 -0
- package/llms.txt +3 -0
- package/package.json +23 -3
|
@@ -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
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
return
|
|
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
|
|
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))
|
|
370
|
+
if (isValidZ(value)) z3 = direction === 1 ? Math.max(z3, value) : Math.min(z3, value);
|
|
328
371
|
}
|
|
329
|
-
return
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
588
|
-
const data = { title:
|
|
589
|
-
const all =
|
|
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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="${
|
|
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="${
|
|
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]) => `${
|
|
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="${
|
|
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="${
|
|
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="${
|
|
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="${
|
|
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${
|
|
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${
|
|
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(
|
|
718
|
-
const listed =
|
|
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(
|
|
696
|
+
const title = esc(board.title) || "Whiteboard";
|
|
761
697
|
return [
|
|
762
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${
|
|
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="${
|
|
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
|
};
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileStoreError
|
|
3
|
+
} from "./chunk-NWQED42L.js";
|
|
4
|
+
|
|
1
5
|
// src/store-file.ts
|
|
2
6
|
import { randomUUID } from "crypto";
|
|
3
7
|
import { mkdirSync } from "fs";
|
|
4
8
|
import { link, open, readFile, rename, unlink } from "fs/promises";
|
|
5
9
|
import { join } from "path";
|
|
6
|
-
|
|
7
|
-
// src/store-errors.ts
|
|
8
|
-
var FileStoreError = class extends Error {
|
|
9
|
-
constructor(code, message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.code = code;
|
|
12
|
-
this.name = "FileStoreError";
|
|
13
|
-
}
|
|
14
|
-
code;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// src/store-file.ts
|
|
18
10
|
import { assertCollection, assertId, createMemoryStore, parseRegion, StoreConflictError } from "agentic-identity";
|
|
19
11
|
var MAX_TRANSACTION_RUNS = 50;
|
|
20
12
|
var clone = (value) => JSON.parse(JSON.stringify(value));
|
|
@@ -230,7 +222,6 @@ function openLocalStore(dir) {
|
|
|
230
222
|
var STORE_FILE = "canvas.json";
|
|
231
223
|
|
|
232
224
|
export {
|
|
233
|
-
FileStoreError,
|
|
234
225
|
createFileStore,
|
|
235
226
|
openLocalStore,
|
|
236
227
|
STORE_FILE
|