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
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.
|
|
11
|
-
>
|
|
12
|
-
> built yet.
|
|
10
|
+
> Status: 0.3.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. A remote client calls the Worker API. The web UI is not built yet.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -84,6 +84,33 @@ The flow text takes one statement per line: `A -> B: label`. Use `[text]` for a
|
|
|
84
84
|
box, `(text)` for an ellipse, and `{text}` for a diamond. `direction LR` sets
|
|
85
85
|
the direction. Lines that start with `#` or `//` are comments.
|
|
86
86
|
|
|
87
|
+
### Remote client
|
|
88
|
+
|
|
89
|
+
Call a running Worker from a browser, a Worker, or Node 22. The client uses only `fetch`.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { createClient, CanvasApiError } from "agentic-canvas/client";
|
|
93
|
+
|
|
94
|
+
const canvas = createClient({ baseUrl: "https://canvas.example.com/api/v1/canvas", token });
|
|
95
|
+
await canvas.createBoard({ id: "plan", title: "Plan" });
|
|
96
|
+
await canvas.addShape("plan", { type: "rect", x: 0, y: 0, text: "Start" });
|
|
97
|
+
await canvas.drawFlow("plan", "A -> B: go");
|
|
98
|
+
const board = await canvas.readBoard("plan");
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await canvas.readBoard("missing");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof CanvasApiError) console.log(error.code, error.status); // NOT_FOUND 404
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
There is one method per route: `me`, `createBoard`, `readBoard`, `exportSvg`, `addShape`, `addConnector`,
|
|
108
|
+
`drawFlow`, `move`, `remove`, `uploadImage`, `getImage`, `state`, and `checkpoint`. Each takes
|
|
109
|
+
`{ signal }` last, so an `AbortSignal` can stop a call. The token sets the actor. A failed call throws
|
|
110
|
+
`CanvasApiError` with the API `code` and the HTTP `status`. A call that gets no reply has code `NETWORK`
|
|
111
|
+
and status 0. The client never retries. A 503 means "try again", and the caller decides, because a
|
|
112
|
+
repeated write can change a board twice. Pass `fetch` to use your own.
|
|
113
|
+
|
|
87
114
|
## Features
|
|
88
115
|
|
|
89
116
|
- Shapes: rectangle, ellipse, diamond, text, sticky note, frame, image.
|
|
@@ -101,11 +128,30 @@ the direction. Lines that start with `#` or `//` are comments.
|
|
|
101
128
|
and every network request blocked. A board over 16 million pixels exports as SVG only.
|
|
102
129
|
- A CLI over a local file store. Several processes can write at once.
|
|
103
130
|
- An MCP server (stdio) over the same service as the CLI.
|
|
131
|
+
- A Worker (`agentic-canvas/worker`) under `/api/v1/canvas`. Boards live in D1 and images in R2
|
|
132
|
+
(EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `CANVAS_API_TOKENS`
|
|
133
|
+
(a JSON map of token to `{ actor, kind }`, tokens of 32 characters or more) and `CANVAS_REGION`.
|
|
134
|
+
Routes: `POST /boards`, `GET /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
|
|
135
|
+
`/connectors`, `/flow`, `/move`, `DELETE /boards/:id/elements/:element`, `POST /images`,
|
|
136
|
+
`GET /images/:id`. An image is stored under the SHA-256 of its bytes, and the reply gives
|
|
137
|
+
the `blob:` ref for a shape. A busy database or bucket answers 503: try again.
|
|
138
|
+
- Live rooms (`agentic-canvas/live-worker`, on `agentic-realtime`). One Durable Object holds one
|
|
139
|
+
board. A token may carry `"role": "viewer"`: a viewer reads and cannot write. `GET /boards/:id/ws`
|
|
140
|
+
opens a room, `GET /me` says who a token is, `GET /boards/:id/state` and
|
|
141
|
+
`POST /boards/:id/checkpoint` seed a room from the stored board and save it back, as a merge. A save
|
|
142
|
+
checks author stamps: a client cannot put another author on its work.
|
|
143
|
+
- The SDK joins a room: `import { LiveBoard, LiveService } from "agentic-canvas/live"`. The CLI and the MCP
|
|
144
|
+
server join when `CANVAS_SERVER` and `CANVAS_TOKEN` are set. The token sets the actor, so
|
|
145
|
+
`CANVAS_ACTOR` is not needed. `--dir`, `CANVAS_DIR`, and `repair` stay local.
|
|
104
146
|
- Yjs underneath: two copies of a board merge without conflicts.
|
|
105
147
|
- No UI code. The model is plain TypeScript on Yjs, with no outbound calls.
|
|
106
148
|
|
|
107
149
|
## Limits
|
|
108
150
|
|
|
151
|
+
- Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.4.
|
|
152
|
+
It does not use the sharing API yet. Its editor and viewer tokens are a separate table in
|
|
153
|
+
`CANVAS_API_TOKENS`. It has no board delete or erase. When it adopts sharing, grants are keyed
|
|
154
|
+
on kind and id, and a delete calls `deleteResource`.
|
|
109
155
|
- A board holds at most 10,000 elements. A flow holds at most 500 nodes and 2,000
|
|
110
156
|
edges.
|
|
111
157
|
- Flow text has no chains (`A -> B -> C`). Write one edge per line.
|
|
@@ -133,7 +179,14 @@ the direction. Lines that start with `#` or `//` are comments.
|
|
|
133
179
|
- Node 22 or newer is needed. `agentic-canvas/file-store` exports `createFileStore(path)` and
|
|
134
180
|
`openLocalStore(dir)`. A writer that is paused for more than 30 seconds can lose its lock
|
|
135
181
|
and then still write.
|
|
136
|
-
-
|
|
182
|
+
- A room remembers its authors for good, so a client can stamp an element with the name of anyone who
|
|
183
|
+
once sent an update in that room. A save corrects the stored board and not the room: after a save that
|
|
184
|
+
reports `fixed`, join again. A client that adds a root Yjs type of its own blocks later saves of that room.
|
|
185
|
+
- Every Worker token reaches every board. A token has an editor or viewer role, and nothing finer yet. Local
|
|
186
|
+
`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
|
|
187
|
+
the EU jurisdiction only if you create it so. A real D1
|
|
188
|
+
confirms `eu` or `us` on each query. The Worker has no PNG export.
|
|
189
|
+
- Delete leaves no audit record yet. The Worker logs the actor kind of each delete, and no ids.
|
|
137
190
|
- PNG needs the optional `playwright-core` and a Chromium. Without them it fails with
|
|
138
191
|
`UNAVAILABLE`. SVG always works.
|
|
139
192
|
|
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.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanvasError,
|
|
3
|
+
PRINCIPAL_KINDS,
|
|
4
|
+
SHAPE_TYPES,
|
|
5
|
+
principalSchema
|
|
6
|
+
} from "./chunk-GGQPE65T.js";
|
|
7
|
+
|
|
8
|
+
// src/version.ts
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
var VERSION = true ? "0.3.0" : readPackageVersion();
|
|
11
|
+
|
|
12
|
+
// src/brand.ts
|
|
13
|
+
var BRAND = {
|
|
14
|
+
command: "agentic-canvas",
|
|
15
|
+
description: "A shared whiteboard for diagrams, drawn by people and agents.",
|
|
16
|
+
version: VERSION
|
|
17
|
+
};
|
|
18
|
+
|
|
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
|
+
// src/png.ts
|
|
34
|
+
var MAX_SIDE = 16384;
|
|
35
|
+
var MAX_PIXELS = 16e6;
|
|
36
|
+
var TIMEOUT_MS = 2e4;
|
|
37
|
+
async function svgToPng(svg) {
|
|
38
|
+
const size = /^<svg [^>]*?width="(\d+)" height="(\d+)"/.exec(svg);
|
|
39
|
+
const width = Number(size?.[1]);
|
|
40
|
+
const height = Number(size?.[2]);
|
|
41
|
+
if (!(width > 0 && height > 0) || width > MAX_SIDE || height > MAX_SIDE || width * height > MAX_PIXELS) {
|
|
42
|
+
throw new CanvasError("LIMIT", `the board is too large for PNG: up to ${MAX_SIDE} px a side and ${MAX_PIXELS / 1e6} million pixels`);
|
|
43
|
+
}
|
|
44
|
+
let chromium;
|
|
45
|
+
try {
|
|
46
|
+
({ chromium } = await import("playwright-core"));
|
|
47
|
+
} catch {
|
|
48
|
+
throw new CanvasError("UNAVAILABLE", "PNG export needs the playwright-core package. Install it, or export SVG.");
|
|
49
|
+
}
|
|
50
|
+
const browser = await chromium.launch().catch(() => {
|
|
51
|
+
throw new CanvasError("UNAVAILABLE", "PNG export needs Chromium. Run: npx playwright-core install chromium");
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const context = await browser.newContext({ viewport: { width, height }, javaScriptEnabled: false, serviceWorkers: "block" });
|
|
55
|
+
context.setDefaultTimeout(TIMEOUT_MS);
|
|
56
|
+
await context.route("**/*", (route) => route.abort());
|
|
57
|
+
const page = await context.newPage();
|
|
58
|
+
await page.setContent(`<!doctype html><body style="margin:0">${svg}</body>`);
|
|
59
|
+
return await page.screenshot({ clip: { x: 0, y: 0, width, height } });
|
|
60
|
+
} finally {
|
|
61
|
+
await browser.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/mcp.ts
|
|
66
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
67
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
68
|
+
import { z } from "zod";
|
|
69
|
+
var json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
70
|
+
function failure(error) {
|
|
71
|
+
if (!(error instanceof CanvasError)) console.error(error instanceof Error ? error.message : String(error));
|
|
72
|
+
const body = error instanceof CanvasError ? { code: error.code, message: error.message } : { code: "INTERNAL", message: "the board operation failed" };
|
|
73
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
74
|
+
}
|
|
75
|
+
function tool(run) {
|
|
76
|
+
return async (args) => {
|
|
77
|
+
try {
|
|
78
|
+
return await run(args);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return failure(error);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
var board = z.string().describe("Board id.");
|
|
85
|
+
var num = z.number();
|
|
86
|
+
function createMcpServer(service, actor) {
|
|
87
|
+
const server = new McpServer({ name: BRAND.command, version: BRAND.version });
|
|
88
|
+
const read = { readOnlyHint: true };
|
|
89
|
+
server.registerTool(
|
|
90
|
+
"create_board",
|
|
91
|
+
{ description: "Make a board.", inputSchema: { id: z.string().optional(), title: z.string().optional() } },
|
|
92
|
+
tool(async (a) => json(await service.create(actor, a)))
|
|
93
|
+
);
|
|
94
|
+
server.registerTool(
|
|
95
|
+
"read_board",
|
|
96
|
+
{ description: "Read a board as data: nodes, edges, groups, text, and who made what.", inputSchema: { board }, annotations: read },
|
|
97
|
+
tool(async (a) => json(await service.read(a.board)))
|
|
98
|
+
);
|
|
99
|
+
server.registerTool(
|
|
100
|
+
"add_shape",
|
|
101
|
+
{
|
|
102
|
+
description: "Add a shape. Image shapes need src.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
board,
|
|
105
|
+
id: z.string().optional(),
|
|
106
|
+
type: z.enum(SHAPE_TYPES),
|
|
107
|
+
x: num,
|
|
108
|
+
y: num,
|
|
109
|
+
w: num.optional(),
|
|
110
|
+
h: num.optional(),
|
|
111
|
+
text: z.string().optional(),
|
|
112
|
+
fill: z.string().optional(),
|
|
113
|
+
stroke: z.string().optional(),
|
|
114
|
+
src: z.string().optional()
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
tool(async ({ board: b, ...shape }) => json(await service.add(b, shape, actor)))
|
|
118
|
+
);
|
|
119
|
+
server.registerTool(
|
|
120
|
+
"connect",
|
|
121
|
+
{ description: "Join two shapes with an arrow.", inputSchema: { board, from: z.string(), to: z.string(), id: z.string().optional(), text: z.string().optional() } },
|
|
122
|
+
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
|
+
);
|
|
124
|
+
server.registerTool(
|
|
125
|
+
"move",
|
|
126
|
+
{ description: "Move an element, or a whole group.", inputSchema: { board, element: z.string(), dx: num, dy: num } },
|
|
127
|
+
tool(async (a) => json(await service.move(a.board, a.element, a.dx, a.dy, actor)))
|
|
128
|
+
);
|
|
129
|
+
server.registerTool(
|
|
130
|
+
"remove",
|
|
131
|
+
{ description: "Delete an element. A shape takes its connectors, a group its members.", inputSchema: { board, element: z.string() }, annotations: { destructiveHint: true } },
|
|
132
|
+
tool(async (a) => json(await service.remove(a.board, a.element)))
|
|
133
|
+
);
|
|
134
|
+
server.registerTool(
|
|
135
|
+
"draw_flow",
|
|
136
|
+
{
|
|
137
|
+
description: 'Draw a flowchart from text, one statement per line: "A[Box] -> B{Choice}: label". [] box, () ellipse, {} diamond.',
|
|
138
|
+
inputSchema: { board, text: z.string(), idPrefix: z.string().optional() }
|
|
139
|
+
},
|
|
140
|
+
tool(async (a) => json(await service.flow(a.board, a.text, actor, { idPrefix: a.idPrefix })))
|
|
141
|
+
);
|
|
142
|
+
server.registerTool(
|
|
143
|
+
"export_svg",
|
|
144
|
+
{ description: "Export the board as SVG text.", inputSchema: { board }, annotations: read },
|
|
145
|
+
tool(async (a) => ({ content: [{ type: "text", text: await service.exportSvg(a.board) }] }))
|
|
146
|
+
);
|
|
147
|
+
server.registerTool(
|
|
148
|
+
"export_png",
|
|
149
|
+
{ description: "Export the board as a PNG image. Needs the optional playwright-core and a Chromium.", inputSchema: { board }, annotations: read },
|
|
150
|
+
tool(async (a) => ({ content: [{ type: "image", data: Buffer.from(await svgToPng(await service.exportSvg(a.board))).toString("base64"), mimeType: "image/png" }] }))
|
|
151
|
+
);
|
|
152
|
+
return server;
|
|
153
|
+
}
|
|
154
|
+
async function runMcp(service, actor) {
|
|
155
|
+
await createMcpServer(service, actor).connect(new StdioServerTransport());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export {
|
|
159
|
+
VERSION,
|
|
160
|
+
BRAND,
|
|
161
|
+
ActorConfigError,
|
|
162
|
+
operatorActor,
|
|
163
|
+
svgToPng,
|
|
164
|
+
createMcpServer,
|
|
165
|
+
runMcp
|
|
166
|
+
};
|