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.
- package/README.md +206 -12
- package/dist/{chunk-W7NRP3EX.js → chunk-G4YTESPS.js} +44 -11
- package/dist/{chunk-GGQPE65T.js → chunk-JCYNPYYJ.js} +96 -40
- package/dist/chunk-SPP6PINW.js +10 -0
- package/dist/{chunk-V477KSZQ.js → chunk-V74PWJKY.js} +186 -9
- package/dist/{chunk-AGODNBN7.js → chunk-VHHJIBAB.js} +33 -18
- package/dist/cli.js +15 -4
- package/dist/client.d.ts +13 -2
- package/dist/client.js +2 -0
- package/dist/embed.d.ts +46 -0
- package/dist/embed.js +535 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +9 -7
- package/dist/live-worker.d.ts +2 -2
- package/dist/live-worker.js +5 -4
- package/dist/live.d.ts +7 -2
- package/dist/live.js +15 -12
- package/dist/{read-BY6f-mmy.d.ts → read-BupIJSPJ.d.ts} +13 -4
- package/dist/{service-Cpl6wKeY.d.ts → service-CR4-r1yx.d.ts} +24 -5
- package/dist/ui.css +38 -0
- package/dist/worker.d.ts +5 -3
- package/dist/worker.js +6 -3
- package/llms.txt +5 -2
- package/package.json +34 -8
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasService,
|
|
3
3
|
MAX_LIVE_BYTES
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
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
|
|
2094
|
+
await saveRoom(c, true);
|
|
1924
2095
|
const { actor, role } = c.var;
|
|
1925
|
-
|
|
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
|
-
|
|
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-
|
|
8
|
+
} from "./chunk-SPP6PINW.js";
|
|
7
9
|
|
|
8
10
|
// src/version.ts
|
|
9
11
|
import { createRequire } from "module";
|
|
10
|
-
var VERSION = true ? "0.
|
|
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-
|
|
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-
|
|
14
|
+
} from "./chunk-G4YTESPS.js";
|
|
15
|
+
import "./chunk-NWQED42L.js";
|
|
15
16
|
import {
|
|
16
17
|
CanvasError
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
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,
|
|
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),
|
package/dist/embed.d.ts
ADDED
|
@@ -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 };
|