@browxai/plugin-excalidraw 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kalebtec
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @browxai/plugin-excalidraw
2
+
3
+ First-party browxai canvas-app adapter for Excalidraw. Exposes five
4
+ small, useful tools (`excalidraw.get_scene_state`,
5
+ `excalidraw.get_viewport`, `excalidraw.add_element`,
6
+ `excalidraw.delete_element`, `excalidraw.set_scroll`) over the
7
+ `window.excalidrawAPI` global that the host page sets when embedding the
8
+ Excalidraw React component. Each tool is a thin wrapper around an
9
+ `eval_js` round-trip: the plugin builds the appropriate
10
+ `excalidrawAPI.*` expression, dispatches through `eval_js`, and parses
11
+ the value back. When `window.excalidrawAPI` is undefined (Excalidraw not
12
+ mounted, or the host page didn't forward the ref), every tool returns
13
+ the structured `code:"excalidraw-not-loaded"` envelope.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ $ browxai plugin install @browxai/plugin-excalidraw
19
+ ```
20
+
21
+ The host must have the `eval` and `canvas` capabilities enabled — the
22
+ plugin declares both at the manifest level. Restart the browxai server
23
+ after install (plugin lifecycle is resolved-once-at-server-start).
24
+
25
+ The tools surface as `excalidraw.get_scene_state` (etc.) on MCP
26
+ `tools/list`, and on the SDK as
27
+ `client.plugins.excalidraw.get_scene_state(...)`.
28
+
29
+ ## Targeted Excalidraw API surface
30
+
31
+ This plugin pokes the Excalidraw 0.17+ ref API as of 2026-06:
32
+ `excalidrawAPI.getSceneElements()`, `excalidrawAPI.getAppState()`,
33
+ `excalidrawAPI.updateScene({elements, appState})`. These are the
34
+ top-level stable methods of the imperative API. Host pages that embed
35
+ the Excalidraw component must forward the `excalidrawAPI` ref to
36
+ `window.excalidrawAPI` for this plugin to find it; the public
37
+ excalidraw.com deployment does this by default.
38
+
39
+ ## Full reference
40
+
41
+ The per-tool reference for this adapter — every op with args, return
42
+ shape, and error codes, plus a usage walkthrough — lives at
43
+ <https://browxai.com/plugins/first-party/>.
@@ -0,0 +1,38 @@
1
+ interface ToolResponse {
2
+ readonly content: ReadonlyArray<{
3
+ type: "text";
4
+ text: string;
5
+ } | {
6
+ type: "image";
7
+ data: string;
8
+ mimeType: string;
9
+ }>;
10
+ }
11
+ interface PluginApi {
12
+ readonly namespace: string;
13
+ readonly declaredCapabilities: ReadonlyArray<string>;
14
+ registerTool(name: string, def: {
15
+ description: string;
16
+ inputSchema?: Record<string, any> | undefined;
17
+ }, handler: (args: unknown) => Promise<ToolResponse>): void;
18
+ callTool(name: string, args?: Record<string, unknown>): Promise<ToolResponse>;
19
+ log: {
20
+ info(msg: string, meta?: Record<string, unknown>): void;
21
+ warn(msg: string, meta?: Record<string, unknown>): void;
22
+ error(msg: string, meta?: Record<string, unknown>): void;
23
+ };
24
+ }
25
+ export declare const handlers: {
26
+ /** `excalidraw.get_scene_state()` → `{ok, elements:[...], appState:{...}}`. */
27
+ get_scene_state(api: PluginApi, _args: unknown): Promise<ToolResponse>;
28
+ /** `excalidraw.get_viewport()` → `{ok, scrollX, scrollY, zoom}`. */
29
+ get_viewport(api: PluginApi, _args: unknown): Promise<ToolResponse>;
30
+ /** `excalidraw.add_element({type, x, y, width, height, ...})` — append via updateScene. */
31
+ add_element(api: PluginApi, args: unknown): Promise<ToolResponse>;
32
+ /** `excalidraw.delete_element({elementId})` — updateScene without that element. */
33
+ delete_element(api: PluginApi, args: unknown): Promise<ToolResponse>;
34
+ /** `excalidraw.set_scroll({scrollX, scrollY})` — updateScene with appState. */
35
+ set_scroll(api: PluginApi, args: unknown): Promise<ToolResponse>;
36
+ };
37
+ export declare function register(api: PluginApi): void;
38
+ export default register;
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ // @browxai/plugin-excalidraw — Excalidraw canvas-app adapter.
2
+ //
3
+ // Surfaces five small, useful tools over the `window.excalidrawAPI`
4
+ // global that the host page sets when embedding the Excalidraw
5
+ // component (the Excalidraw React component takes an `excalidrawAPI`
6
+ // ref callback; community deployments typically forward it to
7
+ // `window.excalidrawAPI`). Each tool routes through `eval_js`.
8
+ //
9
+ // Targeted API surface (Excalidraw 0.17+, current as of 2026-06):
10
+ // - excalidrawAPI.getSceneElements() → Element[]
11
+ // - excalidrawAPI.getAppState() → { viewBackgroundColor, viewModeEnabled, zoom, scrollX, scrollY, ... }
12
+ // - excalidrawAPI.updateScene({elements?, appState?})
13
+ //
14
+ // When `window.excalidrawAPI` is undefined (Excalidraw not mounted, or
15
+ // the host page didn't expose the ref), every tool returns the
16
+ // structured `code:"excalidraw-not-loaded"` envelope.
17
+ const json = (obj) => ({
18
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
19
+ });
20
+ const NOT_LOADED = {
21
+ ok: false,
22
+ error: "Excalidraw not loaded — open the app first OR the surface is not exposed on this version of the app",
23
+ code: "excalidraw-not-loaded",
24
+ };
25
+ const badArg = (which) => ({
26
+ ok: false,
27
+ error: `bad-arg: missing or invalid \`${which}\``,
28
+ code: "bad-arg",
29
+ });
30
+ function parseEvalEnvelope(res) {
31
+ const first = res.content[0];
32
+ if (!first || first.type !== "text") {
33
+ return { ok: false, error: "eval_js returned no text content" };
34
+ }
35
+ try {
36
+ return JSON.parse(first.text);
37
+ }
38
+ catch (e) {
39
+ return { ok: false, error: `eval_js envelope parse failure: ${e.message}` };
40
+ }
41
+ }
42
+ async function runEval(api, expr) {
43
+ const res = await api.callTool("eval_js", { expr });
44
+ const env = parseEvalEnvelope(res);
45
+ if (!env.ok)
46
+ return { ok: false, error: env.error ?? "eval_js failed" };
47
+ return { ok: true, value: env.value };
48
+ }
49
+ async function excalidrawLoaded(api) {
50
+ const r = await runEval(api, `(typeof window !== "undefined" && typeof window.excalidrawAPI !== "undefined" && window.excalidrawAPI !== null)`);
51
+ return r.ok && r.value === true;
52
+ }
53
+ export const handlers = {
54
+ /** `excalidraw.get_scene_state()` → `{ok, elements:[...], appState:{...}}`. */
55
+ async get_scene_state(api, _args) {
56
+ if (!(await excalidrawLoaded(api)))
57
+ return json(NOT_LOADED);
58
+ const expr = `(() => {
59
+ const a = window.excalidrawAPI;
60
+ const els = (a.getSceneElements ? a.getSceneElements() : []) || [];
61
+ const st = (a.getAppState ? a.getAppState() : {}) || {};
62
+ return {
63
+ elements: els.map(e => ({
64
+ id: e.id,
65
+ type: e.type,
66
+ x: e.x,
67
+ y: e.y,
68
+ width: e.width,
69
+ height: e.height,
70
+ })),
71
+ appState: {
72
+ viewBackgroundColor: st.viewBackgroundColor,
73
+ viewModeEnabled: !!st.viewModeEnabled,
74
+ zoom: st.zoom,
75
+ },
76
+ };
77
+ })()`;
78
+ const r = await runEval(api, expr);
79
+ if (!r.ok)
80
+ return json({ ok: false, error: r.error, code: "eval-failed" });
81
+ return json({ ok: true, ...r.value });
82
+ },
83
+ /** `excalidraw.get_viewport()` → `{ok, scrollX, scrollY, zoom}`. */
84
+ async get_viewport(api, _args) {
85
+ if (!(await excalidrawLoaded(api)))
86
+ return json(NOT_LOADED);
87
+ const expr = `(() => {
88
+ const a = window.excalidrawAPI;
89
+ const st = (a.getAppState ? a.getAppState() : {}) || {};
90
+ const zoomVal = st.zoom && typeof st.zoom === "object" ? st.zoom.value : st.zoom;
91
+ return { scrollX: st.scrollX || 0, scrollY: st.scrollY || 0, zoom: zoomVal || 1 };
92
+ })()`;
93
+ const r = await runEval(api, expr);
94
+ if (!r.ok)
95
+ return json({ ok: false, error: r.error, code: "eval-failed" });
96
+ return json({ ok: true, ...r.value });
97
+ },
98
+ /** `excalidraw.add_element({type, x, y, width, height, ...})` — append via updateScene. */
99
+ async add_element(api, args) {
100
+ const a = (args ?? {});
101
+ if (typeof a.type !== "string" || a.type.length === 0)
102
+ return json(badArg("type"));
103
+ if (typeof a.x !== "number")
104
+ return json(badArg("x"));
105
+ if (typeof a.y !== "number")
106
+ return json(badArg("y"));
107
+ if (typeof a.width !== "number")
108
+ return json(badArg("width"));
109
+ if (typeof a.height !== "number")
110
+ return json(badArg("height"));
111
+ if (!(await excalidrawLoaded(api)))
112
+ return json(NOT_LOADED);
113
+ const elementJson = JSON.stringify(a);
114
+ const expr = `(() => {
115
+ const apiRef = window.excalidrawAPI;
116
+ const before = (apiRef.getSceneElements ? apiRef.getSceneElements() : []) || [];
117
+ const seed = ${elementJson};
118
+ // Excalidraw needs every element to carry a stable id; mint one
119
+ // if the caller didn't supply it. The crypto.randomUUID() path
120
+ // matches what Excalidraw itself does on internal creation.
121
+ const id = seed.id || (typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : "el-" + Date.now() + "-" + Math.random().toString(36).slice(2));
122
+ const newEl = Object.assign({}, seed, { id });
123
+ apiRef.updateScene({ elements: before.concat([newEl]) });
124
+ return { elementId: id };
125
+ })()`;
126
+ const r = await runEval(api, expr);
127
+ if (!r.ok)
128
+ return json({ ok: false, error: r.error, code: "eval-failed" });
129
+ const v = r.value;
130
+ return json({ ok: true, elementId: v.elementId });
131
+ },
132
+ /** `excalidraw.delete_element({elementId})` — updateScene without that element. */
133
+ async delete_element(api, args) {
134
+ const a = (args ?? {});
135
+ if (typeof a.elementId !== "string" || a.elementId.length === 0)
136
+ return json(badArg("elementId"));
137
+ if (!(await excalidrawLoaded(api)))
138
+ return json(NOT_LOADED);
139
+ const id = JSON.stringify(a.elementId);
140
+ const expr = `(() => {
141
+ const apiRef = window.excalidrawAPI;
142
+ const before = (apiRef.getSceneElements ? apiRef.getSceneElements() : []) || [];
143
+ const after = before.filter(e => e.id !== ${id});
144
+ apiRef.updateScene({ elements: after });
145
+ return { removed: before.length - after.length };
146
+ })()`;
147
+ const r = await runEval(api, expr);
148
+ if (!r.ok)
149
+ return json({ ok: false, error: r.error, code: "eval-failed" });
150
+ const v = r.value;
151
+ if (v.removed === 0)
152
+ return json({
153
+ ok: false,
154
+ error: `element not found: ${a.elementId}`,
155
+ code: "element-not-found",
156
+ });
157
+ return json({ ok: true, elementId: a.elementId });
158
+ },
159
+ /** `excalidraw.set_scroll({scrollX, scrollY})` — updateScene with appState. */
160
+ async set_scroll(api, args) {
161
+ const a = (args ?? {});
162
+ if (typeof a.scrollX !== "number")
163
+ return json(badArg("scrollX"));
164
+ if (typeof a.scrollY !== "number")
165
+ return json(badArg("scrollY"));
166
+ if (!(await excalidrawLoaded(api)))
167
+ return json(NOT_LOADED);
168
+ const expr = `(() => {
169
+ const apiRef = window.excalidrawAPI;
170
+ const cur = (apiRef.getAppState ? apiRef.getAppState() : {}) || {};
171
+ apiRef.updateScene({ appState: Object.assign({}, cur, { scrollX: ${a.scrollX}, scrollY: ${a.scrollY} }) });
172
+ return { scrollX: ${a.scrollX}, scrollY: ${a.scrollY} };
173
+ })()`;
174
+ const r = await runEval(api, expr);
175
+ if (!r.ok)
176
+ return json({ ok: false, error: r.error, code: "eval-failed" });
177
+ return json({ ok: true, scrollX: a.scrollX, scrollY: a.scrollY });
178
+ },
179
+ };
180
+ export function register(api) {
181
+ api.log.info("excalidraw plugin: registering tools", { namespace: api.namespace });
182
+ api.registerTool(`${api.namespace}.get_scene_state`, {
183
+ description: "Read `excalidrawAPI.getSceneElements()` + `excalidrawAPI.getAppState()` — returns `{ok, elements:[{id,type,x,y,width,height}], appState:{viewBackgroundColor, viewModeEnabled, zoom}}`. App-not-loaded surfaces `code:'excalidraw-not-loaded'`.",
184
+ inputSchema: {},
185
+ }, (args) => handlers.get_scene_state(api, args));
186
+ api.registerTool(`${api.namespace}.get_viewport`, {
187
+ description: "Derive viewport from `appState.scrollX/scrollY/zoom` — returns `{ok, scrollX, scrollY, zoom}`. App-not-loaded surfaces `code:'excalidraw-not-loaded'`.",
188
+ inputSchema: {},
189
+ }, (args) => handlers.get_viewport(api, args));
190
+ api.registerTool(`${api.namespace}.add_element`, {
191
+ description: "Append a scene element via `excalidrawAPI.updateScene({elements: [...existing, new]})`. Required: `type, x, y, width, height`; any extra fields are passed through to Excalidraw. Returns `{ok, elementId}` — id minted via `crypto.randomUUID()` if not supplied. App-not-loaded / bad-arg surface structured errors.",
192
+ inputSchema: {},
193
+ }, (args) => handlers.add_element(api, args));
194
+ api.registerTool(`${api.namespace}.delete_element`, {
195
+ description: "Remove the element with `elementId` from the scene via `excalidrawAPI.updateScene`. Returns `{ok, elementId}` on success, `{code:'element-not-found'}` if the id isn't present.",
196
+ inputSchema: {},
197
+ }, (args) => handlers.delete_element(api, args));
198
+ api.registerTool(`${api.namespace}.set_scroll`, {
199
+ description: "Set `appState.scrollX/scrollY` via `excalidrawAPI.updateScene`. Returns `{ok, scrollX, scrollY}`.",
200
+ inputSchema: {},
201
+ }, (args) => handlers.set_scroll(api, args));
202
+ }
203
+ export default register;
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@browxai/plugin-excalidraw",
3
+ "version": "0.1.0",
4
+ "description": "Excalidraw canvas-app adapter plugin for browxai — surfaces scene/viewport/add/delete/scroll over the `window.excalidrawAPI` page-side global.",
5
+ "author": "Kalebtec",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "keywords": [
9
+ "browxai",
10
+ "browxai-plugin",
11
+ "excalidraw",
12
+ "canvas",
13
+ "mcp",
14
+ "browser-automation",
15
+ "ai-agent"
16
+ ],
17
+ "homepage": "https://browxai.com/plugins/first-party/",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kalebteccom/browxai.git",
21
+ "directory": "packages/plugins/excalidraw"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/kalebteccom/browxai/issues"
25
+ },
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "files": [
29
+ "dist",
30
+ "schema.d.ts",
31
+ "LICENSE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "provenance": true
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "vitest run"
41
+ },
42
+ "browxai": {
43
+ "apiVersion": "1.0.0",
44
+ "browxaiVersion": "^0.7.0",
45
+ "namespace": "excalidraw",
46
+ "register": "dist/index.js",
47
+ "capabilities": [
48
+ "eval",
49
+ "canvas"
50
+ ],
51
+ "trust": "kalebtec",
52
+ "dependsOn": []
53
+ },
54
+ "engines": {
55
+ "node": ">=20"
56
+ },
57
+ "devDependencies": {
58
+ "typescript": "^5.5.0",
59
+ "vitest": "^2.0.0"
60
+ }
61
+ }
package/schema.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ // Typed SDK overlay for `@browxai/plugin-excalidraw` consumers.
2
+
3
+ interface ExcalidrawBrowxaiResult {
4
+ readonly content: ReadonlyArray<
5
+ { type: "text"; text: string } | { type: "image"; data: string; mimeType: string }
6
+ >;
7
+ readonly data?: Record<string, unknown>;
8
+ }
9
+
10
+ export interface ExcalidrawElement {
11
+ readonly id: string;
12
+ readonly type: string;
13
+ readonly x: number;
14
+ readonly y: number;
15
+ readonly width: number;
16
+ readonly height: number;
17
+ }
18
+
19
+ export interface ExcalidrawAppState {
20
+ readonly viewBackgroundColor: string;
21
+ readonly viewModeEnabled: boolean;
22
+ readonly zoom: { value: number };
23
+ }
24
+
25
+ export interface ExcalidrawPluginSchema {
26
+ readonly excalidraw: {
27
+ /** Read scene elements + app state — `{ok, elements:[...], appState:{...}}`. */
28
+ get_scene_state(args?: Record<string, never>): Promise<ExcalidrawBrowxaiResult>;
29
+ /** Derive viewport from `appState.scrollX/Y/zoom` — `{ok, scrollX, scrollY, zoom}`. */
30
+ get_viewport(args?: Record<string, never>): Promise<ExcalidrawBrowxaiResult>;
31
+ /** Append an element via `excalidrawAPI.updateScene({elements:[...existing, new]})`. */
32
+ add_element(args: {
33
+ type: string;
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ [key: string]: unknown;
39
+ }): Promise<ExcalidrawBrowxaiResult>;
40
+ /** Remove an element by id via `excalidrawAPI.updateScene`. */
41
+ delete_element(args: { elementId: string }): Promise<ExcalidrawBrowxaiResult>;
42
+ /** Set `appState.scrollX/Y` via `excalidrawAPI.updateScene`. */
43
+ set_scroll(args: { scrollX: number; scrollY: number }): Promise<ExcalidrawBrowxaiResult>;
44
+ };
45
+ }