agentic-canvas 0.3.0 → 0.5.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.
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  CanvasService,
3
3
  MAX_LIVE_BYTES
4
- } from "./chunk-W7NRP3EX.js";
4
+ } from "./chunk-G4YTESPS.js";
5
+ import {
6
+ CanvasError
7
+ } from "./chunk-JCYNPYYJ.js";
5
8
  import {
6
- CanvasError,
7
9
  principalSchema
8
- } from "./chunk-GGQPE65T.js";
10
+ } from "./chunk-SPP6PINW.js";
9
11
 
10
12
  // src/worker.ts
11
13
  import { parseRegion } from "agentic-identity";
@@ -1788,6 +1790,145 @@ var Hono3 = class extends Hono {
1788
1790
  }
1789
1791
  };
1790
1792
 
1793
+ // src/room-sync.ts
1794
+ import * as Y from "yjs";
1795
+ var SEEDER = { kind: "agent", id: "canvas-room" };
1796
+ var MAX_SEED_BYTES = 1e6;
1797
+ var SYNC = 0;
1798
+ var AUTH = 2;
1799
+ var SYNC_STEP2 = 1;
1800
+ var SYNC_UPDATE = 2;
1801
+ var same = (a, b) => a.kind === b.kind && a.id === b.id;
1802
+ function varUint(value) {
1803
+ const out = [];
1804
+ for (; value > 127; value = Math.floor(value / 128)) out.push(128 | value % 128);
1805
+ return [...out, value];
1806
+ }
1807
+ async function roomState(live, id) {
1808
+ const meta = await live.meta(id);
1809
+ if (meta.updates === 0 && meta.compactions === 0) return null;
1810
+ const reader = (await live.exportHistory(id)).getReader();
1811
+ const decoder = new TextDecoder();
1812
+ let text2 = "";
1813
+ try {
1814
+ for (; ; ) {
1815
+ const { done, value } = await reader.read();
1816
+ if (value) text2 += decoder.decode(value, { stream: true });
1817
+ for (let end = text2.indexOf("\n"); end >= 0; end = text2.indexOf("\n")) {
1818
+ let record;
1819
+ try {
1820
+ record = JSON.parse(text2.slice(0, end));
1821
+ } catch {
1822
+ throw new CanvasError("UNAVAILABLE", "the room sent an export this server cannot read");
1823
+ }
1824
+ text2 = text2.slice(end + 1);
1825
+ if (record.type === "state" && typeof record.base64 === "string") return Uint8Array.from(atob(record.base64), (c) => c.charCodeAt(0));
1826
+ }
1827
+ if (done) throw new CanvasError("UNAVAILABLE", "the room sent no state");
1828
+ }
1829
+ } finally {
1830
+ await reader.cancel().catch(() => void 0);
1831
+ }
1832
+ }
1833
+ async function send(live, id, update, roomVector, who) {
1834
+ if (update.length > MAX_SEED_BYTES) throw new CanvasError("LIMIT", "the update is too big to put in a room");
1835
+ const upgrade = new Request("https://room.internal/ws", { headers: { upgrade: "websocket" } });
1836
+ const response = await live.connect(id, upgrade, { principal: who.principal, role: "editor", name: who.principal.id });
1837
+ const socket = response.webSocket;
1838
+ if (!socket) throw new CanvasError("UNAVAILABLE", "the room did not open");
1839
+ socket.accept();
1840
+ await new Promise((resolve, reject) => {
1841
+ const timer = setTimeout(() => fail2(new CanvasError("UNAVAILABLE", "the room did not answer")), 1e4);
1842
+ const fail2 = (error) => (clearTimeout(timer), reject(error));
1843
+ socket.addEventListener("message", ({ data }) => {
1844
+ const bytes = new Uint8Array(data);
1845
+ if (bytes[0] === AUTH && bytes[1] === 0) fail2(new CanvasError(who.refusal, "the room refused the update"));
1846
+ else if (bytes[0] === SYNC && bytes[1] === SYNC_STEP2) clearTimeout(timer), resolve();
1847
+ });
1848
+ socket.addEventListener("close", () => fail2(new CanvasError("UNAVAILABLE", "the room closed before it took the update")));
1849
+ socket.send(new Uint8Array([SYNC, SYNC_UPDATE, ...varUint(update.length), ...update]));
1850
+ socket.send(new Uint8Array([SYNC, 0, ...varUint(roomVector.length), ...roomVector]));
1851
+ }).finally(() => socket.close(1e3, "done"));
1852
+ }
1853
+ async function writers(live, id) {
1854
+ const log = await live.attribution(id);
1855
+ if (!log) throw new CanvasError("UNAVAILABLE", "the room has no log of who sent its updates");
1856
+ const ranges = /* @__PURE__ */ new Map();
1857
+ for (const range of log.ranges) ranges.set(range.client, [...ranges.get(range.client) ?? [], range]);
1858
+ const reach = /* @__PURE__ */ new Map();
1859
+ for (const [client, list] of ranges) {
1860
+ list.sort((a, b) => a.clock - b.clock);
1861
+ reach.set(client, list.map((r) => r.clock + r.length));
1862
+ const ends = reach.get(client);
1863
+ for (let i = 1; i < ends.length; i++) ends[i] = Math.max(ends[i], ends[i - 1]);
1864
+ }
1865
+ return ({ client, clock }) => {
1866
+ const list = ranges.get(client) ?? [];
1867
+ let low = 0;
1868
+ let high = list.length;
1869
+ while (low < high) {
1870
+ const mid = low + high >> 1;
1871
+ if (list[mid].clock <= clock) low = mid + 1;
1872
+ else high = mid;
1873
+ }
1874
+ let first;
1875
+ for (let i = low - 1; i >= 0 && reach.get(client)[i] > clock && !first; i--) {
1876
+ const r = list[i];
1877
+ if (clock < r.clock + r.length) first = r;
1878
+ }
1879
+ const who = first ? log.authors[first.author] : void 0;
1880
+ return who && !same(who, SEEDER) ? { id: who.id, kind: who.kind } : void 0;
1881
+ };
1882
+ }
1883
+ async function handSave(live, service, id, update, poster) {
1884
+ const state = await roomState(live, id);
1885
+ const vector = Y.encodeStateVectorFromUpdate(state ?? Y.encodeStateAsUpdate(new Y.Doc()));
1886
+ let missing;
1887
+ try {
1888
+ missing = Y.diffUpdate(update, vector);
1889
+ } catch {
1890
+ throw new CanvasError("INVALID", "the update is not a Yjs update");
1891
+ }
1892
+ const { structs, ds } = Y.decodeUpdate(missing);
1893
+ if (structs.length > 0 || ds.clients.size > 0) await send(live, id, missing, vector, { principal: poster, refusal: "FORBIDDEN" });
1894
+ const saved2 = await pull(live, service, id, true);
1895
+ if (saved2) return saved2;
1896
+ const board = await service.read(id);
1897
+ return { elements: board.nodes.length + board.edges.length + board.groups.length, fixed: [] };
1898
+ }
1899
+ async function roomAuthors(live, id) {
1900
+ return (await live.meta(id)).contributors.filter((c) => !same(c, SEEDER)).map(({ id: who, kind }) => ({ id: who, kind }));
1901
+ }
1902
+ var saved = /* @__PURE__ */ new Map();
1903
+ function remember(id, mark) {
1904
+ if (saved.size > 500) saved.clear();
1905
+ saved.set(id, mark);
1906
+ }
1907
+ async function pull(live, service, id, force = false) {
1908
+ const { updates, compactions } = await live.meta(id);
1909
+ const mark = `${updates}:${compactions}`;
1910
+ if (!force && saved.get(id) === mark) return null;
1911
+ const state = await roomState(live, id);
1912
+ if (!state) return null;
1913
+ let result;
1914
+ try {
1915
+ result = await service.checkpoint(id, state, null, await roomAuthors(live, id), await writers(live, id));
1916
+ } catch (error) {
1917
+ if (error instanceof CanvasError && error.code !== "UNAVAILABLE") remember(id, mark);
1918
+ throw error;
1919
+ }
1920
+ remember(id, mark);
1921
+ return result;
1922
+ }
1923
+ async function seed(live, service, id) {
1924
+ const state = await roomState(live, id);
1925
+ const missing = await service.missingFrom(id, state ?? void 0);
1926
+ if (!missing) return;
1927
+ const known = new Y.Doc();
1928
+ if (state) Y.applyUpdate(known, state);
1929
+ await send(live, id, missing, Y.encodeStateVector(known), { principal: SEEDER, refusal: "UNAVAILABLE" });
1930
+ }
1931
+
1791
1932
  // src/worker.ts
1792
1933
  var MIN_TOKEN_LENGTH = 32;
1793
1934
  var DEFAULT_MAX_IMAGE_BYTES = 1e6;
@@ -1809,8 +1950,19 @@ function sameText(a, b) {
1809
1950
  for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
1810
1951
  return diff === 0;
1811
1952
  }
1953
+ var WS_PROTOCOL = "agentic-canvas.bearer";
1954
+ function socketToken(request) {
1955
+ if (request.headers.get("upgrade") !== "websocket") return void 0;
1956
+ const [name, encoded, ...more] = (request.headers.get("sec-websocket-protocol") ?? "").split(",").map((p) => p.trim());
1957
+ if (name !== WS_PROTOCOL || !encoded || more.length > 0 || !/^[A-Za-z0-9_-]+$/.test(encoded)) return void 0;
1958
+ try {
1959
+ return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
1960
+ } catch {
1961
+ return void 0;
1962
+ }
1963
+ }
1812
1964
  function authenticate(request, env) {
1813
- const token = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
1965
+ const token = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1] ?? socketToken(request);
1814
1966
  if (!token || !env.CANVAS_API_TOKENS) return null;
1815
1967
  let grants;
1816
1968
  try {
@@ -1826,7 +1978,8 @@ function authenticate(request, env) {
1826
1978
  if (typeof found !== "object" || found === null) return null;
1827
1979
  const { actor, kind, role = "editor" } = found;
1828
1980
  const parsed = principalSchema.safeParse({ kind, id: actor });
1829
- return parsed.success && (role === "editor" || role === "viewer") ? { actor: parsed.data, role } : null;
1981
+ const taken = parsed.success && parsed.data.kind === SEEDER.kind && parsed.data.id === SEEDER.id;
1982
+ return parsed.success && !taken && (role === "editor" || role === "viewer") ? { actor: parsed.data, role } : null;
1830
1983
  }
1831
1984
  function sniffImage(bytes) {
1832
1985
  const at = (offset, ...sig) => sig.every((b, i) => bytes[offset + i] === b);
@@ -1916,13 +2069,35 @@ function createApp(options = {}) {
1916
2069
  await next();
1917
2070
  });
1918
2071
  api.get("/me", (c) => c.json({ actor: c.var.actor.id, kind: c.var.actor.kind, role: c.var.role }));
2072
+ const saveRoom = async (c, seeding) => {
2073
+ if (!options.live) return;
2074
+ const id = c.req.param("id");
2075
+ await c.var.service.assertExists(id);
2076
+ const live = options.live(c.env);
2077
+ await pull(live, c.var.service, id).catch((error) => console.error(JSON.stringify({ level: "error", op: "save-room", message: loggable(error) })));
2078
+ if (seeding) {
2079
+ await seed(live, c.var.service, id).catch((error) => {
2080
+ console.error(JSON.stringify({ level: "error", op: "seed-room", message: loggable(error) }));
2081
+ throw error instanceof CanvasError ? error : new CanvasError("UNAVAILABLE", "the room could not take the stored board. Try again");
2082
+ });
2083
+ }
2084
+ };
2085
+ const sync = async (c, next) => {
2086
+ if (!/\/(ws|checkpoint)$/.test(new URL(c.req.url).pathname)) await saveRoom(c, false);
2087
+ await next();
2088
+ };
2089
+ api.use("/boards/:id/*", sync);
1919
2090
  api.get("/boards/:id/ws", async (c) => {
1920
2091
  if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
1921
2092
  if (c.req.header("upgrade") !== "websocket") return fail("UPGRADE_REQUIRED", "open this route as a WebSocket", 426);
1922
2093
  const id = c.req.param("id");
1923
- await c.var.service.assertExists(id);
2094
+ await saveRoom(c, true);
1924
2095
  const { actor, role } = c.var;
1925
- return options.live(c.env).connect(id, c.req.raw, { principal: { id: actor.id, kind: actor.kind }, role, name: actor.id });
2096
+ const opened = await options.live(c.env).connect(id, c.req.raw, { principal: { id: actor.id, kind: actor.kind }, role, name: actor.id });
2097
+ if (opened.status !== 101 || c.req.header("authorization") || !socketToken(c.req.raw)) return opened;
2098
+ const headers = new Headers(opened.headers);
2099
+ headers.set("sec-websocket-protocol", WS_PROTOCOL);
2100
+ return new Response(null, { status: 101, headers, webSocket: opened.webSocket });
1926
2101
  });
1927
2102
  api.get("/boards/:id/state", async (c) => !options.live ? fail("NOT_FOUND", "this server hosts no live rooms", 404) : c.body(new Uint8Array(await c.var.service.state(c.req.param("id"))), 200, { "content-type": "application/octet-stream" }));
1928
2103
  api.post("/boards/:id/checkpoint", async (c) => {
@@ -1931,9 +2106,9 @@ function createApp(options = {}) {
1931
2106
  await c.var.service.assertExists(id);
1932
2107
  const update = await readLimited(c.req.raw, MAX_LIVE_BYTES);
1933
2108
  if (!update) throw new CanvasError("INVALID", `the update must be at most ${MAX_LIVE_BYTES} bytes`);
1934
- const authors = (await options.live(c.env).meta(id)).contributors.map(({ id: who, kind }) => ({ id: who, kind }));
1935
- return c.json(await c.var.service.checkpoint(id, update, c.var.actor, authors));
2109
+ return c.json(await handSave(options.live(c.env), c.var.service, id, update, c.var.actor));
1936
2110
  });
2111
+ api.get("/boards", async (c) => c.json({ boards: await c.var.service.list() }));
1937
2112
  api.post("/boards", async (c) => {
1938
2113
  const b = await jsonBody(c);
1939
2114
  const id = b.id === void 0 ? void 0 : text(b.id, "id");
@@ -1949,6 +2124,7 @@ function createApp(options = {}) {
1949
2124
  const b = await jsonBody(c);
1950
2125
  return c.json(await c.var.service.move(c.req.param("id"), text(b.element, "element"), num(b.dx, "dx"), num(b.dy, "dy"), c.var.actor));
1951
2126
  });
2127
+ api.patch("/boards/:id/elements/:element", async (c) => c.json(await c.var.service.update(c.req.param("id"), c.req.param("element"), await jsonBody(c), c.var.actor)));
1952
2128
  api.delete("/boards/:id/elements/:element", async (c) => {
1953
2129
  const removed = await c.var.service.remove(c.req.param("id"), c.req.param("element"));
1954
2130
  console.log(JSON.stringify({ level: "info", op: "remove", actorKind: c.var.actor.kind }));
@@ -1998,6 +2174,7 @@ var app = createApp();
1998
2174
  var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
1999
2175
 
2000
2176
  export {
2177
+ WS_PROTOCOL,
2001
2178
  authenticate,
2002
2179
  sniffImage,
2003
2180
  createApp,
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  CanvasError,
3
+ SHAPE_TYPES
4
+ } from "./chunk-JCYNPYYJ.js";
5
+ import {
3
6
  PRINCIPAL_KINDS,
4
- SHAPE_TYPES,
5
7
  principalSchema
6
- } from "./chunk-GGQPE65T.js";
8
+ } from "./chunk-SPP6PINW.js";
7
9
 
8
10
  // src/version.ts
9
11
  import { createRequire } from "module";
10
- var VERSION = true ? "0.3.0" : readPackageVersion();
12
+ var VERSION = true ? "0.5.0" : readPackageVersion();
11
13
 
12
14
  // src/brand.ts
13
15
  var BRAND = {
@@ -16,20 +18,6 @@ var BRAND = {
16
18
  version: VERSION
17
19
  };
18
20
 
19
- // src/actor.ts
20
- var ActorConfigError = class extends Error {
21
- };
22
- function operatorActor(env = process.env) {
23
- const raw = env.CANVAS_ACTOR;
24
- if (!raw) throw new ActorConfigError("the operator must set CANVAS_ACTOR=<kind>:<id>");
25
- const at = raw.indexOf(":");
26
- const parsed = principalSchema.safeParse({ kind: raw.slice(0, at), id: raw.slice(at + 1) });
27
- if (at < 0 || !parsed.success) {
28
- throw new ActorConfigError(`bad CANVAS_ACTOR "${raw}": use <kind>:<id>, kind is one of ${PRINCIPAL_KINDS.join(", ")}`);
29
- }
30
- return parsed.data;
31
- }
32
-
33
21
  // src/png.ts
34
22
  var MAX_SIDE = 16384;
35
23
  var MAX_PIXELS = 16e6;
@@ -62,6 +50,20 @@ async function svgToPng(svg) {
62
50
  }
63
51
  }
64
52
 
53
+ // src/actor.ts
54
+ var ActorConfigError = class extends Error {
55
+ };
56
+ function operatorActor(env = process.env) {
57
+ const raw = env.CANVAS_ACTOR;
58
+ if (!raw) throw new ActorConfigError("the operator must set CANVAS_ACTOR=<kind>:<id>");
59
+ const at = raw.indexOf(":");
60
+ const parsed = principalSchema.safeParse({ kind: raw.slice(0, at), id: raw.slice(at + 1) });
61
+ if (at < 0 || !parsed.success) {
62
+ throw new ActorConfigError(`bad CANVAS_ACTOR "${raw}": use <kind>:<id>, kind is one of ${PRINCIPAL_KINDS.join(", ")}`);
63
+ }
64
+ return parsed.data;
65
+ }
66
+
65
67
  // src/mcp.ts
66
68
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
67
69
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -91,6 +93,11 @@ function createMcpServer(service, actor) {
91
93
  { description: "Make a board.", inputSchema: { id: z.string().optional(), title: z.string().optional() } },
92
94
  tool(async (a) => json(await service.create(actor, a)))
93
95
  );
96
+ server.registerTool(
97
+ "list_boards",
98
+ { description: "List the first 100 boards with their titles.", inputSchema: {}, annotations: read },
99
+ tool(async () => json({ boards: await service.list() }))
100
+ );
94
101
  server.registerTool(
95
102
  "read_board",
96
103
  { description: "Read a board as data: nodes, edges, groups, text, and who made what.", inputSchema: { board }, annotations: read },
@@ -121,6 +128,14 @@ function createMcpServer(service, actor) {
121
128
  { description: "Join two shapes with an arrow.", inputSchema: { board, from: z.string(), to: z.string(), id: z.string().optional(), text: z.string().optional() } },
122
129
  tool(async (a) => json(await service.connect(a.board, { id: a.id, from: { id: a.from }, to: { id: a.to }, text: a.text }, actor)))
123
130
  );
131
+ server.registerTool(
132
+ "update_element",
133
+ {
134
+ description: "Change fields of a shape (x, y, w, h, text, fill, stroke) or a connector. Resize with w and h.",
135
+ inputSchema: { board, element: z.string(), x: num.optional(), y: num.optional(), w: num.optional(), h: num.optional(), text: z.string().optional(), fill: z.string().optional(), stroke: z.string().optional() }
136
+ },
137
+ tool(async ({ board: b, element, ...patch }) => json(await service.update(b, element, patch, actor)))
138
+ );
124
139
  server.registerTool(
125
140
  "move",
126
141
  { description: "Move an element, or a whole group.", inputSchema: { board, element: z.string(), dx: num, dy: num } },
@@ -158,9 +173,9 @@ async function runMcp(service, actor) {
158
173
  export {
159
174
  VERSION,
160
175
  BRAND,
176
+ svgToPng,
161
177
  ActorConfigError,
162
178
  operatorActor,
163
- svgToPng,
164
179
  createMcpServer,
165
180
  runMcp
166
181
  };
package/dist/cli.js CHANGED
@@ -5,17 +5,18 @@ import {
5
5
  operatorActor,
6
6
  runMcp,
7
7
  svgToPng
8
- } from "./chunk-AGODNBN7.js";
8
+ } from "./chunk-VHHJIBAB.js";
9
9
  import {
10
10
  openLocalStore
11
11
  } from "./chunk-UKST5A57.js";
12
12
  import {
13
13
  CanvasService
14
- } from "./chunk-W7NRP3EX.js";
14
+ } from "./chunk-G4YTESPS.js";
15
+ import "./chunk-NWQED42L.js";
15
16
  import {
16
17
  CanvasError
17
- } from "./chunk-GGQPE65T.js";
18
- import "./chunk-NWQED42L.js";
18
+ } from "./chunk-JCYNPYYJ.js";
19
+ import "./chunk-SPP6PINW.js";
19
20
 
20
21
  // src/cli.ts
21
22
  import { readFileSync, writeFileSync } from "fs";
@@ -29,9 +30,11 @@ Usage: ${BRAND.command} [--dir <path>] <command> [flags]
29
30
 
30
31
  Commands:
31
32
  new [--id <id>] [--title <t>] Make a board.
33
+ list List the boards with their titles.
32
34
  read <board> Print the board as JSON.
33
35
  add <board> --type <t> --x <n> --y <n> Add a shape. Also: --w --h --text --fill --stroke --src --id.
34
36
  connect <board> <from> <to> [--text <t>] Join two shapes with an arrow.
37
+ update <board> <element> [--x --y --w --h --text --fill --stroke] Change fields of an element. Resize with --w and --h.
35
38
  move <board> <element> --dx <n> --dy <n> Move an element or a group.
36
39
  rm <board> <element> Delete an element.
37
40
  flow <board> --text <t> | --file <p> Draw a flowchart from text ("A -> B: label"). Use --file - for stdin.
@@ -118,6 +121,9 @@ async function run(argv) {
118
121
  case "new":
119
122
  need(0, "[--id <id>] [--title <t>]");
120
123
  return print(await service.create(actor(), { id: v.id, title: v.title }));
124
+ case "list":
125
+ need(0, "");
126
+ return print({ boards: await service.list() });
121
127
  case "read":
122
128
  need(1, "<board>");
123
129
  return print(await service.read(board));
@@ -127,6 +133,11 @@ async function run(argv) {
127
133
  case "connect":
128
134
  need(3, "<board> <from> <to> [--text <t>]");
129
135
  return print(await service.connect(board, { id: v.id, from: { id: rest[0] }, to: { id: rest[1] }, text: v.text }, actor()));
136
+ case "update": {
137
+ need(2, "<board> <element> [--x <n>] [--y <n>] [--w <n>] [--h <n>] [--text <t>] [--fill <c>] [--stroke <c>]");
138
+ const patch = Object.fromEntries(Object.entries({ x: num(v.x), y: num(v.y), w: num(v.w), h: num(v.h), text: v.text, fill: v.fill, stroke: v.stroke }).filter(([, value]) => value !== void 0));
139
+ return print(await service.update(board, rest[0], patch, actor()));
140
+ }
130
141
  case "move":
131
142
  need(2, "<board> <element> --dx <n> --dy <n>");
132
143
  return print(await service.move(board, rest[0], num(v.dx) ?? Number.NaN, num(v.dy) ?? Number.NaN, actor()));
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as ErrorCode, B as BoardData, S as ShapeType, a as Shape, b as Endpoint, C as Connector } from './read-BY6f-mmy.js';
1
+ import { E as ErrorCode, a as BoardData, c as ShapeType, S as Shape, d as Endpoint, C as Connector } from './read-BupIJSPJ.js';
2
2
  import { PrincipalKind } from 'agentic-identity';
3
3
  import 'yjs';
4
4
  import 'zod';
@@ -37,6 +37,8 @@ interface ConnectorInput {
37
37
  endArrow?: boolean;
38
38
  stroke?: string;
39
39
  }
40
+ type ShapePatch = Partial<Pick<ShapeInput, "x" | "y" | "w" | "h" | "text" | "fill" | "stroke" | "src">>;
41
+ type ConnectorPatch = Partial<Pick<ConnectorInput, "from" | "to" | "text" | "startArrow" | "endArrow" | "stroke">>;
40
42
  interface ImageInfo {
41
43
  id: string;
42
44
  ref: string;
@@ -62,6 +64,13 @@ interface CanvasClient {
62
64
  kind: PrincipalKind;
63
65
  role: "editor" | "viewer";
64
66
  }>;
67
+ /** The first 100 boards, with titles. */
68
+ listBoards(options?: CallOptions): Promise<{
69
+ boards: {
70
+ id: string;
71
+ title: string;
72
+ }[];
73
+ }>;
65
74
  createBoard(input?: {
66
75
  id?: string;
67
76
  title?: string;
@@ -81,6 +90,8 @@ interface CanvasClient {
81
90
  nodes: string[];
82
91
  edges: string[];
83
92
  }>;
93
+ /** Change fields of one shape or connector, such as its size or text. The patch holds at least one field. */
94
+ update(id: string, element: string, patch: ShapePatch | ConnectorPatch, options?: CallOptions): Promise<Shape | Connector>;
84
95
  /** Move an element. Returns the ids that moved: a shape takes its connectors with it. */
85
96
  move(id: string, element: string, dx: number, dy: number, options?: CallOptions): Promise<{
86
97
  moved: string[];
@@ -105,4 +116,4 @@ interface CanvasClient {
105
116
  }
106
117
  declare function createClient(options: ClientOptions): CanvasClient;
107
118
 
108
- export { type ApiErrorCode, type CallOptions, CanvasApiError, type CanvasClient, type ClientOptions, type ConnectorInput, type ImageInfo, type ShapeInput, createClient };
119
+ export { type ApiErrorCode, type CallOptions, CanvasApiError, type CanvasClient, type ClientOptions, type ConnectorInput, type ConnectorPatch, type ImageInfo, type ShapeInput, type ShapePatch, createClient };
package/dist/client.js CHANGED
@@ -55,12 +55,14 @@ function createClient(options) {
55
55
  const bytes = async (response) => new Uint8Array(await response.arrayBuffer());
56
56
  return {
57
57
  me: (o) => json("GET", "/me", void 0, o),
58
+ listBoards: (o) => json("GET", "/boards", void 0, o),
58
59
  createBoard: (input = {}, o) => json("POST", "/boards", input, o),
59
60
  readBoard: (id, o) => json("GET", `/boards/${seg(id)}`, void 0, o),
60
61
  exportSvg: async (id, o) => (await send("GET", `/boards/${seg(id)}/svg`, {}, o)).text(),
61
62
  addShape: (id, shape, o) => json("POST", `/boards/${seg(id)}/shapes`, shape, o),
62
63
  addConnector: (id, connector, o) => json("POST", `/boards/${seg(id)}/connectors`, connector, o),
63
64
  drawFlow: (id, text, o) => json("POST", `/boards/${seg(id)}/flow`, { text }, o),
65
+ update: (id, element, patch, o) => json("PATCH", `/boards/${seg(id)}/elements/${seg(element)}`, patch, o),
64
66
  move: (id, element, dx, dy, o) => json("POST", `/boards/${seg(id)}/move`, { element, dx, dy }, o),
65
67
  remove: (id, element, o) => json("DELETE", `/boards/${seg(id)}/elements/${seg(element)}`, void 0, o),
66
68
  uploadImage: async (data, o) => parse(await send("POST", "/images", { body: data, contentType: "application/octet-stream" }, o), o),
@@ -0,0 +1,46 @@
1
+ import * as react from 'react';
2
+ import { CanvasClient } from './client.js';
3
+ import { PrincipalKind } from 'agentic-identity';
4
+ import './read-BupIJSPJ.js';
5
+ import 'yjs';
6
+ import 'zod';
7
+
8
+ interface RoomOptions {
9
+ /** The API root, for example `https://canvas.example.com/api/v1/canvas`. */
10
+ baseUrl: string;
11
+ /** The bearer token for the room. */
12
+ token: string;
13
+ }
14
+
15
+ /** Who uses the board. The server still checks every write against its own token. */
16
+ interface BoardPrincipal {
17
+ kind: PrincipalKind;
18
+ name: string;
19
+ /** Only `editor` can change the board. Any other value, or no value, opens it read only. */
20
+ role?: "editor" | "viewer";
21
+ }
22
+ interface BoardEditorOptions {
23
+ /** The board id. */
24
+ boardId: string;
25
+ /** Loads and changes the board. A client from `createClient` in `agentic-canvas/client` works. */
26
+ client: CanvasClient;
27
+ principal: BoardPrincipal;
28
+ /** Open the board read only, even for an editor. */
29
+ readOnly?: boolean;
30
+ /** The live room. With one, the board shows who else is here and reads again when they save. */
31
+ room?: RoomOptions;
32
+ /** A link for a missing board to go back to. */
33
+ backHref?: string;
34
+ }
35
+ /** The board for one id, with its toolbar, presence, and side panel. It needs the providers of `agentic-design`. A bad principal shows an alert. */
36
+ declare function BoardEditor(options: BoardEditorOptions): react.JSX.Element;
37
+ interface MountedBoard {
38
+ /** Show another board, principal, or room in the same place. A bad principal throws and keeps the board as it is. */
39
+ update(options: BoardEditorOptions): void;
40
+ /** Remove the board, leave the live room, and stop its timers. */
41
+ unmount(): void;
42
+ }
43
+ /** Render the board into an element, for an app that does not use React itself. */
44
+ declare function mount(element: HTMLElement, options: BoardEditorOptions): MountedBoard;
45
+
46
+ export { BoardEditor, type BoardEditorOptions, type BoardPrincipal, CanvasClient, type MountedBoard, type RoomOptions, mount };