@particle-academy/agent-integrations 0.2.4

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.
Files changed (49) hide show
  1. package/README.md +131 -0
  2. package/dist/bridges/flow.d.cts +72 -0
  3. package/dist/bridges/flow.d.ts +72 -0
  4. package/dist/bridges/whiteboard.d.cts +40 -0
  5. package/dist/bridges/whiteboard.d.ts +40 -0
  6. package/dist/bridges-flow.cjs +330 -0
  7. package/dist/bridges-flow.cjs.map +1 -0
  8. package/dist/bridges-flow.js +4 -0
  9. package/dist/bridges-flow.js.map +1 -0
  10. package/dist/bridges-whiteboard.cjs +409 -0
  11. package/dist/bridges-whiteboard.cjs.map +1 -0
  12. package/dist/bridges-whiteboard.js +4 -0
  13. package/dist/bridges-whiteboard.js.map +1 -0
  14. package/dist/chunk-2VOQJKSU.js +320 -0
  15. package/dist/chunk-2VOQJKSU.js.map +1 -0
  16. package/dist/chunk-5ZUHNNLR.js +398 -0
  17. package/dist/chunk-5ZUHNNLR.js.map +1 -0
  18. package/dist/chunk-6LTKCNLF.js +68 -0
  19. package/dist/chunk-6LTKCNLF.js.map +1 -0
  20. package/dist/chunk-FLEOQUKF.js +157 -0
  21. package/dist/chunk-FLEOQUKF.js.map +1 -0
  22. package/dist/chunk-QGCF7YKW.js +130 -0
  23. package/dist/chunk-QGCF7YKW.js.map +1 -0
  24. package/dist/index.cjs +1632 -0
  25. package/dist/index.cjs.map +1 -0
  26. package/dist/index.d.cts +155 -0
  27. package/dist/index.d.ts +155 -0
  28. package/dist/index.js +567 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/mcp/index.d.cts +73 -0
  31. package/dist/mcp/index.d.ts +73 -0
  32. package/dist/mcp.cjs +210 -0
  33. package/dist/mcp.cjs.map +1 -0
  34. package/dist/mcp.js +4 -0
  35. package/dist/mcp.js.map +1 -0
  36. package/dist/server-Bv985us3.d.cts +173 -0
  37. package/dist/server-Bv985us3.d.ts +173 -0
  38. package/dist/sharing/index.d.cts +89 -0
  39. package/dist/sharing/index.d.ts +89 -0
  40. package/dist/sharing.cjs +166 -0
  41. package/dist/sharing.cjs.map +1 -0
  42. package/dist/sharing.js +3 -0
  43. package/dist/sharing.js.map +1 -0
  44. package/dist/styles.css +331 -0
  45. package/dist/styles.css.map +1 -0
  46. package/dist/types-CRPA_D0z.d.ts +18 -0
  47. package/dist/types-DR5AS6Rd.d.cts +18 -0
  48. package/docs/relay-protocol.md +57 -0
  49. package/package.json +61 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1632 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var fancyWhiteboard = require('@particle-academy/fancy-whiteboard');
6
+
7
+ // src/mcp/types.ts
8
+ var JSONRPC_METHOD_NOT_FOUND = -32601;
9
+ var JSONRPC_INVALID_PARAMS = -32602;
10
+ var JSONRPC_INTERNAL_ERROR = -32603;
11
+ var MCP_PROTOCOL_VERSION = "2025-06-18";
12
+
13
+ // src/mcp/server.ts
14
+ var MicroMcpServer = class {
15
+ constructor(options) {
16
+ this.tools = /* @__PURE__ */ new Map();
17
+ this.transports = /* @__PURE__ */ new Set();
18
+ this.notifyListChangedScheduled = false;
19
+ this.info = options.info;
20
+ this.capabilities = options.capabilities ?? { tools: { listChanged: true } };
21
+ this.instructions = options.instructions;
22
+ }
23
+ attach(transport) {
24
+ this.transports.add(transport);
25
+ return () => this.detach(transport);
26
+ }
27
+ detach(transport) {
28
+ if (this.transports.delete(transport)) {
29
+ transport.close?.();
30
+ }
31
+ }
32
+ registerTool(definition, handler) {
33
+ this.tools.set(definition.name, { definition, handler });
34
+ this.scheduleListChangedNotification();
35
+ return () => this.unregisterTool(definition.name);
36
+ }
37
+ unregisterTool(name) {
38
+ if (this.tools.delete(name)) {
39
+ this.scheduleListChangedNotification();
40
+ }
41
+ }
42
+ listTools() {
43
+ return Array.from(this.tools.values()).map((t) => t.definition);
44
+ }
45
+ /**
46
+ * Receive a JSON-RPC frame from a client (called by the transport).
47
+ * The transport is responsible for sending the response back.
48
+ */
49
+ async receive(transport, message) {
50
+ if (!("method" in message)) return;
51
+ const isNotification = !("id" in message);
52
+ if (isNotification) {
53
+ return;
54
+ }
55
+ const request = message;
56
+ try {
57
+ const result = await this.handle(request);
58
+ transport.send({ jsonrpc: "2.0", id: request.id, result });
59
+ } catch (err) {
60
+ transport.send({
61
+ jsonrpc: "2.0",
62
+ id: request.id,
63
+ error: this.toRpcError(err)
64
+ });
65
+ }
66
+ }
67
+ async handle(request) {
68
+ const { method, params } = request;
69
+ switch (method) {
70
+ case "initialize":
71
+ return {
72
+ protocolVersion: MCP_PROTOCOL_VERSION,
73
+ capabilities: this.capabilities,
74
+ serverInfo: this.info,
75
+ ...this.instructions ? { instructions: this.instructions } : {}
76
+ };
77
+ case "tools/list":
78
+ return { tools: this.listTools() };
79
+ case "tools/call": {
80
+ const name = params?.name;
81
+ const args = params?.arguments ?? {};
82
+ if (typeof name !== "string") {
83
+ throw rpcError(JSONRPC_INVALID_PARAMS, "tools/call requires `name`");
84
+ }
85
+ const tool = this.tools.get(name);
86
+ if (!tool) {
87
+ throw rpcError(JSONRPC_METHOD_NOT_FOUND, `Unknown tool: ${name}`);
88
+ }
89
+ const result = await tool.handler(args);
90
+ return result;
91
+ }
92
+ case "ping":
93
+ return {};
94
+ default:
95
+ throw rpcError(JSONRPC_METHOD_NOT_FOUND, `Unsupported method: ${method}`);
96
+ }
97
+ }
98
+ scheduleListChangedNotification() {
99
+ if (this.notifyListChangedScheduled) return;
100
+ this.notifyListChangedScheduled = true;
101
+ queueMicrotask(() => {
102
+ this.notifyListChangedScheduled = false;
103
+ this.broadcast({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
104
+ });
105
+ }
106
+ broadcast(message) {
107
+ for (const t of this.transports) t.send(message);
108
+ }
109
+ toRpcError(err) {
110
+ if (err && typeof err === "object" && "code" in err && "message" in err) {
111
+ return err;
112
+ }
113
+ return {
114
+ code: JSONRPC_INTERNAL_ERROR,
115
+ message: err instanceof Error ? err.message : String(err)
116
+ };
117
+ }
118
+ };
119
+ function rpcError(code, message, data) {
120
+ return { code, message, ...data !== void 0 ? { data } : {} };
121
+ }
122
+ function textResult(text, structured) {
123
+ return {
124
+ content: [{ type: "text", text }],
125
+ ...structured !== void 0 ? { structuredContent: structured } : {}
126
+ };
127
+ }
128
+ function errorResult(text) {
129
+ return { content: [{ type: "text", text }], isError: true };
130
+ }
131
+
132
+ // src/mcp/transports/in-process.ts
133
+ var InProcessTransport = class {
134
+ constructor() {
135
+ this.listeners = /* @__PURE__ */ new Set();
136
+ }
137
+ /** Bind to a server. Called from the client's setup, not directly. */
138
+ bindServer(server) {
139
+ this.server = server;
140
+ }
141
+ /** Server → client (delivered to subscribed listeners). */
142
+ send(message) {
143
+ for (const l of this.listeners) l(message);
144
+ }
145
+ /** Client → server. Awaitable so callers can flush. */
146
+ async deliver(message) {
147
+ if (!this.server) throw new Error("InProcessTransport has no bound server");
148
+ await this.server.receive(this, message);
149
+ }
150
+ /** Subscribe to messages the server pushes to this client. */
151
+ onServerMessage(listener) {
152
+ this.listeners.add(listener);
153
+ return () => this.listeners.delete(listener);
154
+ }
155
+ close() {
156
+ this.listeners.clear();
157
+ }
158
+ };
159
+ function attachInProcess(server) {
160
+ const transport = new InProcessTransport();
161
+ transport.bindServer(server);
162
+ server.attach(transport);
163
+ return transport;
164
+ }
165
+
166
+ // src/mcp/transports/relay.ts
167
+ var RelayTransport = class {
168
+ constructor(channel) {
169
+ this.channel = channel;
170
+ }
171
+ bindServer(server) {
172
+ this.server = server;
173
+ }
174
+ send(message) {
175
+ this.channel.sendToRemote(message);
176
+ }
177
+ /**
178
+ * Host calls this with each frame received from the remote agent. Accepts
179
+ * either a parsed object or a raw JSON string.
180
+ */
181
+ async deliverFromRemote(payload) {
182
+ if (!this.server) throw new Error("RelayTransport has no bound server");
183
+ const message = typeof payload === "string" ? JSON.parse(payload) : payload;
184
+ await this.server.receive(this, message);
185
+ }
186
+ close() {
187
+ this.channel.onClose?.();
188
+ }
189
+ };
190
+ function attachRelay(server, channel) {
191
+ const transport = new RelayTransport(channel);
192
+ transport.bindServer(server);
193
+ server.attach(transport);
194
+ return transport;
195
+ }
196
+
197
+ // src/bridges/whiteboard.ts
198
+ var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
199
+ var VALID_SHAPES = ["rect", "rounded-rect", "ellipse", "diamond", "triangle", "line", "arrow", "text"];
200
+ var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : fallback ?? 0;
201
+ var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
202
+ var bool = (v, fallback = false) => typeof v === "boolean" ? v : fallback;
203
+ var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
204
+ function registerWhiteboardBridge(server, options) {
205
+ const { adapter } = options;
206
+ const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
207
+ const disposers = [];
208
+ const reg = (name, description, inputProperties, required, handler) => {
209
+ disposers.push(
210
+ server.registerTool(
211
+ {
212
+ name,
213
+ description,
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: inputProperties,
217
+ required,
218
+ additionalProperties: false
219
+ }
220
+ },
221
+ async (args) => {
222
+ try {
223
+ return await handler(args);
224
+ } catch (e) {
225
+ return errorResult(e instanceof Error ? e.message : String(e));
226
+ }
227
+ }
228
+ )
229
+ );
230
+ };
231
+ reg("whiteboard_get_state", "Get the full board state: viewport, all items, strokes.", {}, [], () => {
232
+ const state = {
233
+ viewport: adapter.getViewport(),
234
+ notes: adapter.getNotes(),
235
+ shapes: adapter.getShapes(),
236
+ connectors: adapter.getConnectors(),
237
+ strokes: adapter.getStrokes()
238
+ };
239
+ return textResult(JSON.stringify(state, null, 2), state);
240
+ });
241
+ reg("whiteboard_list_items", "List notes, shapes, and connectors with id, kind, and bounds.", {}, [], () => {
242
+ const items = [];
243
+ for (const n of adapter.getNotes()) {
244
+ items.push({
245
+ id: n.id,
246
+ kind: "sticky",
247
+ summary: `"${(n.text ?? "").slice(0, 40)}" @(${Math.round(n.x)},${Math.round(n.y)}) ${n.width}\xD7${n.height}`
248
+ });
249
+ }
250
+ for (const s of adapter.getShapes()) {
251
+ items.push({
252
+ id: s.id,
253
+ kind: `shape:${s.shape}`,
254
+ summary: `${s.text ? `"${s.text}" ` : ""}@(${Math.round(s.x)},${Math.round(s.y)}) ${s.width}\xD7${s.height}`
255
+ });
256
+ }
257
+ for (const c of adapter.getConnectors()) {
258
+ items.push({ id: c.id, kind: "connector", summary: `from=${JSON.stringify(c.from)} to=${JSON.stringify(c.to)}` });
259
+ }
260
+ return textResult(items.map((i) => `${i.kind} ${i.id}: ${i.summary}`).join("\n") || "(empty board)", items);
261
+ });
262
+ reg(
263
+ "whiteboard_get_item",
264
+ "Get a single item (sticky / shape / connector) by id.",
265
+ { id: { type: "string" } },
266
+ ["id"],
267
+ (args) => {
268
+ const id = str(args.id);
269
+ const all = [...adapter.getNotes(), ...adapter.getShapes(), ...adapter.getConnectors()];
270
+ const found = all.find((x) => x.id === id);
271
+ if (!found) return errorResult(`No item with id ${id}`);
272
+ return textResult(JSON.stringify(found, null, 2), found);
273
+ }
274
+ );
275
+ reg(
276
+ "whiteboard_add_sticky",
277
+ "Add a sticky note. Position is in world coordinates.",
278
+ {
279
+ x: { type: "number" },
280
+ y: { type: "number" },
281
+ text: { type: "string" },
282
+ width: { type: "number" },
283
+ height: { type: "number" },
284
+ color: { type: "string", description: "CSS color, e.g. #fde68a" }
285
+ },
286
+ ["x", "y"],
287
+ async (args) => {
288
+ const x = num(args.x);
289
+ const y = num(args.y);
290
+ const width = num(args.width, 180);
291
+ const height = num(args.height, 140);
292
+ const note = {
293
+ id: newId("n"),
294
+ kind: "sticky",
295
+ x,
296
+ y,
297
+ width,
298
+ height,
299
+ text: str(args.text),
300
+ color: typeof args.color === "string" ? args.color : "#fde68a",
301
+ authorId: agent.id
302
+ };
303
+ adapter.setNotes((all) => [...all, note]);
304
+ return textResult(`Added sticky ${note.id}`, note);
305
+ }
306
+ );
307
+ reg(
308
+ "whiteboard_stream_text",
309
+ "Type text into a sticky note character-by-character so the human can read it forming. The tool returns once streaming finishes.",
310
+ {
311
+ id: { type: "string" },
312
+ text: { type: "string" },
313
+ cps: { type: "number", description: "Characters per second. Default 25." },
314
+ append: { type: "boolean", description: "Append to existing text instead of replacing. Default false." }
315
+ },
316
+ ["id", "text"],
317
+ async (args) => {
318
+ const id = str(args.id);
319
+ const target = str(args.text);
320
+ const cps = Math.max(1, num(args.cps, 25));
321
+ const append = bool(args.append);
322
+ const startNote = adapter.getNotes().find((n) => n.id === id);
323
+ if (!startNote) return errorResult(`No sticky with id ${id}`);
324
+ const base = append ? startNote.text ?? "" : "";
325
+ const interval = Math.max(8, Math.round(1e3 / cps));
326
+ for (let i = 0; i <= target.length; i++) {
327
+ const nextText = base + target.slice(0, i);
328
+ adapter.setNotes((all) => all.map((n) => n.id === id ? { ...n, text: nextText } : n));
329
+ if (i < target.length) await new Promise((r) => setTimeout(r, interval));
330
+ }
331
+ return textResult(`Streamed ${target.length} chars to ${id}`, { id, text: base + target });
332
+ }
333
+ );
334
+ reg(
335
+ "whiteboard_update_sticky",
336
+ "Update fields on a sticky note. Only provided fields are changed.",
337
+ {
338
+ id: { type: "string" },
339
+ x: { type: "number" },
340
+ y: { type: "number" },
341
+ width: { type: "number" },
342
+ height: { type: "number" },
343
+ text: { type: "string" },
344
+ color: { type: "string" }
345
+ },
346
+ ["id"],
347
+ async (args) => {
348
+ const id = str(args.id);
349
+ const existing = adapter.getNotes().find((n) => n.id === id);
350
+ if (!existing) return errorResult(`No sticky with id ${id}`);
351
+ const nextX = args.x !== void 0 ? num(args.x) : existing.x;
352
+ const nextY = args.y !== void 0 ? num(args.y) : existing.y;
353
+ const nextW = args.width !== void 0 ? num(args.width) : existing.width;
354
+ const nextH = args.height !== void 0 ? num(args.height) : existing.height;
355
+ let updated = null;
356
+ adapter.setNotes(
357
+ (all) => all.map((n) => {
358
+ if (n.id !== id) return n;
359
+ updated = {
360
+ ...n,
361
+ x: nextX,
362
+ y: nextY,
363
+ width: nextW,
364
+ height: nextH,
365
+ ...args.text !== void 0 ? { text: str(args.text) } : {},
366
+ ...args.color !== void 0 ? { color: str(args.color) } : {}
367
+ };
368
+ return updated;
369
+ })
370
+ );
371
+ return textResult(`Updated sticky ${id}`, updated);
372
+ }
373
+ );
374
+ reg(
375
+ "whiteboard_add_shape",
376
+ `Add a shape. Kind must be one of: ${VALID_SHAPES.join(", ")}.`,
377
+ {
378
+ shape: { type: "string", enum: VALID_SHAPES },
379
+ x: { type: "number" },
380
+ y: { type: "number" },
381
+ width: { type: "number" },
382
+ height: { type: "number" },
383
+ text: { type: "string" },
384
+ fill: { type: "string" },
385
+ stroke: { type: "string" },
386
+ flipX: { type: "boolean" },
387
+ flipY: { type: "boolean" }
388
+ },
389
+ ["shape", "x", "y", "width", "height"],
390
+ async (args) => {
391
+ const kind = str(args.shape);
392
+ if (!VALID_SHAPES.includes(kind)) return errorResult(`Invalid shape kind: ${kind}`);
393
+ const x = num(args.x);
394
+ const y = num(args.y);
395
+ const width = num(args.width);
396
+ const height = num(args.height);
397
+ const shape = {
398
+ id: newId("s"),
399
+ kind: "shape",
400
+ shape: kind,
401
+ x,
402
+ y,
403
+ width,
404
+ height,
405
+ ...args.text !== void 0 ? { text: str(args.text) } : {},
406
+ ...args.fill !== void 0 ? { fill: str(args.fill) } : {},
407
+ ...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {},
408
+ ...args.flipX !== void 0 ? { flipX: bool(args.flipX) } : {},
409
+ ...args.flipY !== void 0 ? { flipY: bool(args.flipY) } : {}
410
+ };
411
+ adapter.setShapes((all) => [...all, shape]);
412
+ return textResult(`Added ${kind} ${shape.id}`, shape);
413
+ }
414
+ );
415
+ reg(
416
+ "whiteboard_update_shape",
417
+ "Update fields on a shape.",
418
+ {
419
+ id: { type: "string" },
420
+ x: { type: "number" },
421
+ y: { type: "number" },
422
+ width: { type: "number" },
423
+ height: { type: "number" },
424
+ text: { type: "string" },
425
+ fill: { type: "string" },
426
+ stroke: { type: "string" }
427
+ },
428
+ ["id"],
429
+ async (args) => {
430
+ const id = str(args.id);
431
+ const existing = adapter.getShapes().find((s) => s.id === id);
432
+ if (!existing) return errorResult(`No shape with id ${id}`);
433
+ const nextX = args.x !== void 0 ? num(args.x) : existing.x;
434
+ const nextY = args.y !== void 0 ? num(args.y) : existing.y;
435
+ const nextW = args.width !== void 0 ? num(args.width) : existing.width;
436
+ const nextH = args.height !== void 0 ? num(args.height) : existing.height;
437
+ let updated = null;
438
+ adapter.setShapes(
439
+ (all) => all.map((s) => {
440
+ if (s.id !== id) return s;
441
+ updated = {
442
+ ...s,
443
+ x: nextX,
444
+ y: nextY,
445
+ width: nextW,
446
+ height: nextH,
447
+ ...args.text !== void 0 ? { text: str(args.text) } : {},
448
+ ...args.fill !== void 0 ? { fill: str(args.fill) } : {},
449
+ ...args.stroke !== void 0 ? { stroke: str(args.stroke) } : {}
450
+ };
451
+ return updated;
452
+ })
453
+ );
454
+ return textResult(`Updated shape ${id}`, updated);
455
+ }
456
+ );
457
+ reg(
458
+ "whiteboard_add_connector",
459
+ "Connect two items by id, or specify explicit world-space points.",
460
+ {
461
+ from: { description: "Item id (string) or {x,y}" },
462
+ to: { description: "Item id (string) or {x,y}" },
463
+ color: { type: "string" }
464
+ },
465
+ ["from", "to"],
466
+ (args) => {
467
+ const c = {
468
+ id: newId("c"),
469
+ kind: "connector",
470
+ from: args.from,
471
+ to: args.to,
472
+ ...args.color !== void 0 ? { color: str(args.color) } : {}
473
+ };
474
+ adapter.setConnectors((all) => [...all, c]);
475
+ return textResult(`Added connector ${c.id}`, c);
476
+ }
477
+ );
478
+ reg(
479
+ "whiteboard_add_stroke",
480
+ "Add a freeform pen stroke. Points are absolute screen coords (matching the Drawing layer).",
481
+ {
482
+ points: {
483
+ type: "array",
484
+ description: "Array of {x,y} points"
485
+ },
486
+ color: { type: "string" },
487
+ size: { type: "number" }
488
+ },
489
+ ["points"],
490
+ (args) => {
491
+ const points = (Array.isArray(args.points) ? args.points : []).map((p) => ({
492
+ x: num(p?.x),
493
+ y: num(p?.y)
494
+ }));
495
+ if (!points.length) return errorResult("Stroke requires at least one point");
496
+ const stroke = {
497
+ id: newId("st"),
498
+ points,
499
+ color: typeof args.color === "string" ? args.color : "#0f172a",
500
+ size: typeof args.size === "number" ? args.size : 2,
501
+ authorId: agent.id
502
+ };
503
+ adapter.setStrokes((all) => [...all, stroke]);
504
+ return textResult(`Added stroke ${stroke.id} (${points.length} points)`, stroke);
505
+ }
506
+ );
507
+ reg(
508
+ "whiteboard_delete_item",
509
+ "Remove any item by id (sticky / shape / connector / stroke).",
510
+ { id: { type: "string" } },
511
+ ["id"],
512
+ (args) => {
513
+ const id = str(args.id);
514
+ let removed = false;
515
+ adapter.setNotes((all) => {
516
+ const next = all.filter((x) => x.id !== id);
517
+ if (next.length !== all.length) removed = true;
518
+ return next;
519
+ });
520
+ adapter.setShapes((all) => {
521
+ const next = all.filter((x) => x.id !== id);
522
+ if (next.length !== all.length) removed = true;
523
+ return next;
524
+ });
525
+ adapter.setConnectors((all) => {
526
+ const next = all.filter((x) => x.id !== id);
527
+ if (next.length !== all.length) removed = true;
528
+ return next;
529
+ });
530
+ adapter.setStrokes((all) => {
531
+ const next = all.filter((x) => x.id !== id);
532
+ if (next.length !== all.length) removed = true;
533
+ return next;
534
+ });
535
+ return removed ? textResult(`Deleted ${id}`) : errorResult(`No item with id ${id}`);
536
+ }
537
+ );
538
+ reg(
539
+ "whiteboard_set_viewport",
540
+ "Pan / zoom the viewport.",
541
+ { x: { type: "number" }, y: { type: "number" }, zoom: { type: "number" } },
542
+ [],
543
+ (args) => {
544
+ const v = adapter.getViewport();
545
+ const next = {
546
+ x: args.x !== void 0 ? num(args.x) : v.x,
547
+ y: args.y !== void 0 ? num(args.y) : v.y,
548
+ zoom: args.zoom !== void 0 ? num(args.zoom) : v.zoom
549
+ };
550
+ adapter.setViewport(next);
551
+ return textResult(`Viewport \u2192 ${JSON.stringify(next)}`, next);
552
+ }
553
+ );
554
+ reg(
555
+ "whiteboard_set_agent_cursor",
556
+ "Move the agent's presence cursor (or pass null to hide it).",
557
+ {
558
+ x: { type: "number" },
559
+ y: { type: "number" },
560
+ hide: { type: "boolean" }
561
+ },
562
+ [],
563
+ (args) => {
564
+ if (!adapter.setAgentCursor) return errorResult("Host did not provide setAgentCursor");
565
+ if (bool(args.hide)) {
566
+ adapter.setAgentCursor(null);
567
+ return textResult("Agent cursor hidden");
568
+ }
569
+ const cursor = {
570
+ userId: agent.id,
571
+ name: agent.name,
572
+ color: agent.color,
573
+ x: num(args.x),
574
+ y: num(args.y)
575
+ };
576
+ adapter.setAgentCursor(cursor);
577
+ return textResult(`Cursor \u2192 (${cursor.x}, ${cursor.y})`, cursor);
578
+ }
579
+ );
580
+ return {
581
+ id: "whiteboard",
582
+ title: "Whiteboard",
583
+ dispose: () => {
584
+ for (const d of disposers) d();
585
+ adapter.setAgentCursor?.(null);
586
+ }
587
+ };
588
+ }
589
+ var num2 = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : 0;
590
+ var str2 = (v, fallback = "") => typeof v === "string" ? v : fallback;
591
+ var newId2 = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
592
+ function registerFlowBridge(server, options) {
593
+ const { adapter } = options;
594
+ ({ ...options.agent ?? {} });
595
+ const disposers = [];
596
+ const reg = (name, description, properties, required, handler) => {
597
+ disposers.push(
598
+ server.registerTool(
599
+ {
600
+ name,
601
+ description,
602
+ inputSchema: { type: "object", properties, required, additionalProperties: false }
603
+ },
604
+ async (args) => {
605
+ try {
606
+ return await handler(args);
607
+ } catch (e) {
608
+ return errorResult(e instanceof Error ? e.message : String(e));
609
+ }
610
+ }
611
+ )
612
+ );
613
+ };
614
+ reg("flow_get_state", "Get the full graph: nodes + edges.", {}, [], () => {
615
+ const state = { nodes: adapter.getNodes(), edges: adapter.getEdges() };
616
+ return textResult(JSON.stringify(state, null, 2), state);
617
+ });
618
+ reg("flow_list_nodes", "Summarise every node: id, kind, label, position, status.", {}, [], () => {
619
+ const items = adapter.getNodes().map((n) => ({
620
+ id: n.id,
621
+ kind: n.type,
622
+ label: n.data?.label,
623
+ x: Math.round(n.position.x),
624
+ y: Math.round(n.position.y),
625
+ status: n.data?.status ?? "idle"
626
+ }));
627
+ const text = items.map((i) => `${i.kind} ${i.id}: "${i.label}" @(${i.x},${i.y}) [${i.status}]`).join("\n") || "(empty graph)";
628
+ return textResult(text, items);
629
+ });
630
+ reg(
631
+ "flow_get_node",
632
+ "Get a single node's full record by id.",
633
+ { id: { type: "string" } },
634
+ ["id"],
635
+ (args) => {
636
+ const id = str2(args.id);
637
+ const node = adapter.getNodes().find((n) => n.id === id);
638
+ if (!node) return errorResult(`No node with id ${id}`);
639
+ return textResult(JSON.stringify(node, null, 2), node);
640
+ }
641
+ );
642
+ reg(
643
+ "flow_list_node_kinds",
644
+ "List every node kind registered in fancy-flow's registry. Use this to discover what's authorable before adding nodes.",
645
+ { category: { type: "string", description: "Optional category filter: trigger | logic | data | ai | io | human | output | custom." } },
646
+ [],
647
+ async () => {
648
+ try {
649
+ const { listNodeKinds } = await import('@particle-academy/fancy-flow');
650
+ const cat = adapter ? void 0 : void 0;
651
+ const all = (cat ? listNodeKinds(cat) : listNodeKinds()).map((k) => ({
652
+ name: k.name,
653
+ category: k.category,
654
+ label: k.label,
655
+ description: k.description,
656
+ icon: k.icon,
657
+ accent: k.accent,
658
+ inputs: k.inputs ?? [],
659
+ outputs: k.outputs ?? [],
660
+ configFields: (k.configSchema ?? []).map((f) => ({ key: f.key, type: f.type, label: f.label, required: !!f.required }))
661
+ }));
662
+ const text = all.map((k) => `${k.category}/${k.name}: ${k.label}${k.description ? " \u2014 " + k.description : ""}`).join("\n");
663
+ return textResult(text || "(no kinds registered)", all);
664
+ } catch (e) {
665
+ return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
666
+ }
667
+ }
668
+ );
669
+ reg(
670
+ "flow_get_node_schema",
671
+ "Get the full configSchema + ports for a node kind. Use to know exactly what fields a kind accepts before calling flow_add_node.",
672
+ { name: { type: "string" } },
673
+ ["name"],
674
+ async (args) => {
675
+ try {
676
+ const { getNodeKind } = await import('@particle-academy/fancy-flow');
677
+ const k = getNodeKind(str2(args.name));
678
+ if (!k) return errorResult(`No kind registered: ${args.name}`);
679
+ const summary = {
680
+ name: k.name,
681
+ category: k.category,
682
+ label: k.label,
683
+ description: k.description,
684
+ inputs: k.inputs ?? [],
685
+ outputs: k.outputs ?? [],
686
+ configSchema: k.configSchema ?? [],
687
+ defaultConfig: k.defaultConfig ?? null
688
+ };
689
+ return textResult(JSON.stringify(summary, null, 2), summary);
690
+ } catch (e) {
691
+ return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
692
+ }
693
+ }
694
+ );
695
+ reg("flow_list_edges", "Summarise every edge.", {}, [], () => {
696
+ const items = adapter.getEdges().map((e) => ({
697
+ id: e.id,
698
+ from: `${e.source}${e.sourceHandle ? `:${e.sourceHandle}` : ""}`,
699
+ to: `${e.target}${e.targetHandle ? `:${e.targetHandle}` : ""}`
700
+ }));
701
+ return textResult(items.map((i) => `${i.id}: ${i.from} \u2192 ${i.to}`).join("\n") || "(no edges)", items);
702
+ });
703
+ reg(
704
+ "flow_add_node",
705
+ "Add a node of any kind registered in fancy-flow's registry. Call flow_list_node_kinds first to discover what's available.",
706
+ {
707
+ kind: { type: "string", description: "Registry kind name (e.g. memory_store, llm_call, branch)." },
708
+ label: { type: "string" },
709
+ x: { type: "number" },
710
+ y: { type: "number" },
711
+ description: { type: "string" },
712
+ config: { type: "object", description: "Config fields per the kind's configSchema." },
713
+ body: { type: "string", description: "Note kinds only \u2014 body text." }
714
+ },
715
+ ["kind", "label", "x", "y"],
716
+ async (args) => {
717
+ const kindName = str2(args.kind);
718
+ let kindDef = null;
719
+ try {
720
+ const { getNodeKind, defaultConfigFor } = await import('@particle-academy/fancy-flow');
721
+ kindDef = getNodeKind(kindName);
722
+ var defaults = kindDef ? defaultConfigFor(kindDef) : {};
723
+ } catch {
724
+ var defaults = {};
725
+ }
726
+ const isLegacy = ["trigger", "action", "decision", "output", "note", "subgraph"].includes(kindName);
727
+ if (!kindDef && !isLegacy) {
728
+ return errorResult(`Unknown kind: ${kindName} \u2014 call flow_list_node_kinds for the registry.`);
729
+ }
730
+ const id = newId2("n");
731
+ const config = { ...defaults, ...args.config && typeof args.config === "object" ? args.config : {} };
732
+ const node = {
733
+ id,
734
+ type: kindName,
735
+ position: { x: num2(args.x), y: num2(args.y) },
736
+ data: {
737
+ kind: kindName,
738
+ label: str2(args.label),
739
+ ...args.description ? { description: str2(args.description) } : {},
740
+ config,
741
+ ...kindName === "note" && args.body ? { body: str2(args.body) } : {}
742
+ }
743
+ };
744
+ adapter.setNodes((all) => [...all, node]);
745
+ return textResult(`Added ${kindName} ${id} ("${str2(args.label)}")`, node);
746
+ }
747
+ );
748
+ reg(
749
+ "flow_update_node",
750
+ "Update fields on a node. Only provided fields change.",
751
+ {
752
+ id: { type: "string" },
753
+ label: { type: "string" },
754
+ x: { type: "number" },
755
+ y: { type: "number" },
756
+ description: { type: "string" },
757
+ config: { type: "object" }
758
+ },
759
+ ["id"],
760
+ (args) => {
761
+ const id = str2(args.id);
762
+ let updated = null;
763
+ adapter.setNodes(
764
+ (all) => all.map((n) => {
765
+ if (n.id !== id) return n;
766
+ updated = {
767
+ ...n,
768
+ position: {
769
+ x: args.x !== void 0 ? num2(args.x) : n.position.x,
770
+ y: args.y !== void 0 ? num2(args.y) : n.position.y
771
+ },
772
+ data: {
773
+ ...n.data,
774
+ ...args.label !== void 0 ? { label: str2(args.label) } : {},
775
+ ...args.description !== void 0 ? { description: str2(args.description) } : {},
776
+ ...args.config && typeof args.config === "object" ? { config: { ...n.data.config ?? {}, ...args.config } } : {}
777
+ }
778
+ };
779
+ return updated;
780
+ })
781
+ );
782
+ if (!updated) return errorResult(`No node with id ${id}`);
783
+ return textResult(`Updated node ${id}`, updated);
784
+ }
785
+ );
786
+ reg(
787
+ "flow_delete_node",
788
+ "Remove a node by id (also removes any connected edges).",
789
+ { id: { type: "string" } },
790
+ ["id"],
791
+ (args) => {
792
+ const id = str2(args.id);
793
+ if (!adapter.getNodes().some((n) => n.id === id)) {
794
+ return errorResult(`No node with id ${id}`);
795
+ }
796
+ adapter.setNodes((all) => all.filter((n) => n.id !== id));
797
+ adapter.setEdges((all) => all.filter((e) => e.source !== id && e.target !== id));
798
+ return textResult(`Deleted node ${id}`);
799
+ }
800
+ );
801
+ reg(
802
+ "flow_connect",
803
+ "Create an edge between two nodes (optionally specifying handle ids).",
804
+ {
805
+ source: { type: "string" },
806
+ target: { type: "string" },
807
+ sourceHandle: { type: "string" },
808
+ targetHandle: { type: "string" },
809
+ label: { type: "string" }
810
+ },
811
+ ["source", "target"],
812
+ (args) => {
813
+ const source = str2(args.source);
814
+ const target = str2(args.target);
815
+ const all = adapter.getNodes();
816
+ if (!all.find((n) => n.id === source)) return errorResult(`No source node ${source}`);
817
+ if (!all.find((n) => n.id === target)) return errorResult(`No target node ${target}`);
818
+ const edge = {
819
+ id: newId2("e"),
820
+ source,
821
+ target,
822
+ ...args.sourceHandle ? { sourceHandle: str2(args.sourceHandle) } : {},
823
+ ...args.targetHandle ? { targetHandle: str2(args.targetHandle) } : {},
824
+ ...args.label ? { label: str2(args.label) } : {}
825
+ };
826
+ adapter.setEdges((existing) => [...existing, edge]);
827
+ return textResult(`Connected ${source}${edge.sourceHandle ? `:${edge.sourceHandle}` : ""} \u2192 ${target}${edge.targetHandle ? `:${edge.targetHandle}` : ""}`, edge);
828
+ }
829
+ );
830
+ reg(
831
+ "flow_disconnect",
832
+ "Remove an edge by id.",
833
+ { id: { type: "string" } },
834
+ ["id"],
835
+ (args) => {
836
+ const id = str2(args.id);
837
+ if (!adapter.getEdges().some((e) => e.id === id)) {
838
+ return errorResult(`No edge ${id}`);
839
+ }
840
+ adapter.setEdges((all) => all.filter((e) => e.id !== id));
841
+ return textResult(`Disconnected ${id}`);
842
+ }
843
+ );
844
+ reg(
845
+ "flow_set_node_status",
846
+ "Manually set a node's status badge (idle | queued | running | done | error) and optional text. Useful for narration outside a run.",
847
+ {
848
+ id: { type: "string" },
849
+ status: { type: "string", enum: ["idle", "queued", "running", "done", "error"] },
850
+ text: { type: "string" }
851
+ },
852
+ ["id", "status"],
853
+ (args) => {
854
+ const id = str2(args.id);
855
+ const status = str2(args.status);
856
+ const text = args.text !== void 0 ? str2(args.text) : void 0;
857
+ if (adapter.setNodeStatus) {
858
+ adapter.setNodeStatus(id, status, text);
859
+ } else {
860
+ let found = false;
861
+ adapter.setNodes(
862
+ (all) => all.map((n) => {
863
+ if (n.id !== id) return n;
864
+ found = true;
865
+ return { ...n, data: { ...n.data, status, statusText: text } };
866
+ })
867
+ );
868
+ if (!found) return errorResult(`No node with id ${id}`);
869
+ }
870
+ return textResult(`${id} \u2192 ${status}${text ? ` (${text})` : ""}`);
871
+ }
872
+ );
873
+ reg(
874
+ "flow_run",
875
+ "Trigger a run of the current graph. Returns the topological result. Requires the host to have wired `run` into the adapter.",
876
+ {},
877
+ [],
878
+ async () => {
879
+ if (!adapter.run) return errorResult("Host did not provide a run handler.");
880
+ const result = await adapter.run();
881
+ return textResult(result.ok ? "Run complete" : `Run failed: ${result.error ?? "unknown"}`, result);
882
+ }
883
+ );
884
+ reg(
885
+ "flow_cancel",
886
+ "Cancel an in-flight run.",
887
+ {},
888
+ [],
889
+ () => {
890
+ if (!adapter.cancel) return errorResult("Host did not provide a cancel handler.");
891
+ adapter.cancel();
892
+ return textResult("Run cancelled");
893
+ }
894
+ );
895
+ return {
896
+ id: "flow",
897
+ title: "Flow",
898
+ dispose: () => {
899
+ for (const d of disposers) d();
900
+ }
901
+ };
902
+ }
903
+ function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }) {
904
+ const scrollRef = react.useRef(null);
905
+ const inputRef = react.useRef(null);
906
+ react.useEffect(() => {
907
+ const el = scrollRef.current;
908
+ if (!el) return;
909
+ el.scrollTop = el.scrollHeight;
910
+ }, [activity.length]);
911
+ const handleSubmit = (e) => {
912
+ e.preventDefault();
913
+ const value = inputRef.current?.value.trim();
914
+ if (!value || !onSubmit) return;
915
+ onSubmit(value);
916
+ if (inputRef.current) inputRef.current.value = "";
917
+ };
918
+ const color = agent?.color ?? "#a855f7";
919
+ const name = agent?.name ?? "Agent";
920
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-panel", className ?? ""].filter(Boolean).join(" "), style, children: [
921
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "fai-panel__header", children: [
922
+ /* @__PURE__ */ jsxRuntime.jsx(
923
+ "div",
924
+ {
925
+ className: "fai-panel__avatar",
926
+ style: { background: color },
927
+ "aria-hidden": true,
928
+ children: name.slice(0, 1)
929
+ }
930
+ ),
931
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-panel__title", children: [
932
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: name }),
933
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-panel__subtitle", children: busy ? "Working\u2026" : `${activity.length} event${activity.length === 1 ? "" : "s"}` })
934
+ ] }),
935
+ actions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-panel__actions", children: actions })
936
+ ] }),
937
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: scrollRef, className: "fai-panel__stream", children: activity.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "fai-panel__empty", children: "No activity yet." }) : activity.map((a) => /* @__PURE__ */ jsxRuntime.jsx(ActivityRow, { item: a }, a.id)) }),
938
+ onSubmit && /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "fai-panel__composer", onSubmit: handleSubmit, children: [
939
+ /* @__PURE__ */ jsxRuntime.jsx(
940
+ "textarea",
941
+ {
942
+ ref: inputRef,
943
+ className: "fai-panel__input",
944
+ placeholder: busy ? "Working\u2026" : "Ask the agent\u2026",
945
+ disabled: busy,
946
+ rows: 2,
947
+ onKeyDown: (e) => {
948
+ if (e.key === "Enter" && !e.shiftKey) {
949
+ e.preventDefault();
950
+ handleSubmit(e);
951
+ }
952
+ }
953
+ }
954
+ ),
955
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "fai-panel__send", disabled: busy, children: "Send" })
956
+ ] })
957
+ ] });
958
+ }
959
+ function ActivityRow({ item }) {
960
+ const time = formatTime(item.at);
961
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `fai-row fai-row--${item.kind}`, children: [
962
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-row__meta", children: [
963
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-row__source", children: item.source }),
964
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-row__time", children: time })
965
+ ] }),
966
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-row__text", children: item.text }),
967
+ item.detail !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "fai-row__detail", children: [
968
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { children: "details" }),
969
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { children: safeJson(item.detail) })
970
+ ] })
971
+ ] });
972
+ }
973
+ function formatTime(at) {
974
+ const d = new Date(at);
975
+ const hh = d.getHours().toString().padStart(2, "0");
976
+ const mm = d.getMinutes().toString().padStart(2, "0");
977
+ const ss = d.getSeconds().toString().padStart(2, "0");
978
+ return `${hh}:${mm}:${ss}`;
979
+ }
980
+ function safeJson(v) {
981
+ try {
982
+ return JSON.stringify(v, null, 2);
983
+ } catch {
984
+ return String(v);
985
+ }
986
+ }
987
+ function AgentCursor({ x, y, name, color = "#a855f7", status, className, style }) {
988
+ return /* @__PURE__ */ jsxRuntime.jsxs(
989
+ "div",
990
+ {
991
+ className: ["fai-cursor", className ?? ""].filter(Boolean).join(" "),
992
+ style: {
993
+ position: "absolute",
994
+ left: x,
995
+ top: y,
996
+ pointerEvents: "none",
997
+ transform: "translate(-2px, -2px)",
998
+ ...style
999
+ },
1000
+ children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "22", height: "22", viewBox: "0 0 22 22", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(
1002
+ "path",
1003
+ {
1004
+ d: "M2 2 L2 17 L7 13 L10 19 L12 18 L9 12 L15 12 Z",
1005
+ fill: color,
1006
+ stroke: "white",
1007
+ strokeWidth: "1.2"
1008
+ }
1009
+ ) }),
1010
+ name && /* @__PURE__ */ jsxRuntime.jsxs(
1011
+ "span",
1012
+ {
1013
+ className: "fai-cursor__tag",
1014
+ style: { background: color },
1015
+ children: [
1016
+ name,
1017
+ status ? /* @__PURE__ */ jsxRuntime.jsxs("em", { className: "fai-cursor__status", children: [
1018
+ " \xB7 ",
1019
+ status
1020
+ ] }) : null
1021
+ ]
1022
+ }
1023
+ )
1024
+ ]
1025
+ }
1026
+ );
1027
+ }
1028
+ function AgentActivityHighlight({
1029
+ x,
1030
+ y,
1031
+ width,
1032
+ height,
1033
+ pulseKey,
1034
+ color = "#a855f7",
1035
+ duration = 1200,
1036
+ className,
1037
+ style
1038
+ }) {
1039
+ const [visible, setVisible] = react.useState(false);
1040
+ react.useEffect(() => {
1041
+ if (pulseKey === void 0) return;
1042
+ setVisible(true);
1043
+ const t = setTimeout(() => setVisible(false), duration);
1044
+ return () => clearTimeout(t);
1045
+ }, [pulseKey, duration]);
1046
+ if (!visible) return null;
1047
+ return /* @__PURE__ */ jsxRuntime.jsx(
1048
+ "div",
1049
+ {
1050
+ className: ["fai-highlight", className ?? ""].filter(Boolean).join(" "),
1051
+ style: {
1052
+ position: "absolute",
1053
+ left: x - 4,
1054
+ top: y - 4,
1055
+ width: width + 8,
1056
+ height: height + 8,
1057
+ borderRadius: 8,
1058
+ boxShadow: `0 0 0 2px ${color}, 0 0 16px ${color}66`,
1059
+ pointerEvents: "none",
1060
+ animation: `fai-pulse ${duration}ms ease-out forwards`,
1061
+ ...style
1062
+ }
1063
+ }
1064
+ );
1065
+ }
1066
+
1067
+ // src/sharing/token.ts
1068
+ var TOKEN_BYTES = 24;
1069
+ function createSessionDescriptor() {
1070
+ const id = randomId(8);
1071
+ const token = randomToken();
1072
+ return { id, token, display: token.slice(0, 8) };
1073
+ }
1074
+ function describeSession(id, token) {
1075
+ return { id, token, display: token.slice(0, 8) };
1076
+ }
1077
+ function buildShareUrl(descriptor, baseUrl = typeof window !== "undefined" ? window.location.href.split("?")[0] : "") {
1078
+ const u = new URL(baseUrl);
1079
+ u.searchParams.set("session", descriptor.id);
1080
+ u.searchParams.set("token", descriptor.token);
1081
+ return u.toString();
1082
+ }
1083
+ function buildShareConfig(descriptor, transport = "broadcast-channel") {
1084
+ return {
1085
+ name: `whiteboard-${descriptor.id}`,
1086
+ transport,
1087
+ session: descriptor.id,
1088
+ token: descriptor.token,
1089
+ channel: `fai:share:${descriptor.id}`,
1090
+ protocol_version: "2025-06-18"
1091
+ };
1092
+ }
1093
+ function readSessionFromUrl() {
1094
+ if (typeof window === "undefined") return null;
1095
+ const params = new URL(window.location.href).searchParams;
1096
+ const id = params.get("session");
1097
+ const token = params.get("token");
1098
+ if (!id || !token) return null;
1099
+ return describeSession(id, token);
1100
+ }
1101
+ function randomToken() {
1102
+ const bytes = new Uint8Array(TOKEN_BYTES);
1103
+ crypto.getRandomValues(bytes);
1104
+ return base64Url(bytes);
1105
+ }
1106
+ function randomId(len) {
1107
+ const bytes = new Uint8Array(Math.ceil(len * 3 / 4));
1108
+ crypto.getRandomValues(bytes);
1109
+ return base64Url(bytes).slice(0, len);
1110
+ }
1111
+ function base64Url(bytes) {
1112
+ let s = "";
1113
+ for (const b of bytes) s += String.fromCharCode(b);
1114
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1115
+ }
1116
+ function constantTimeEqual(a, b) {
1117
+ if (a.length !== b.length) return false;
1118
+ let diff = 0;
1119
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1120
+ return diff === 0;
1121
+ }
1122
+ function ShareControls({
1123
+ session,
1124
+ onStart,
1125
+ onStop,
1126
+ status,
1127
+ shareBaseUrl,
1128
+ className,
1129
+ style
1130
+ }) {
1131
+ const [tab, setTab] = react.useState("url");
1132
+ if (!session) {
1133
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-share fai-share--idle", className ?? ""].filter(Boolean).join(" "), style, children: [
1134
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__start", onClick: onStart, children: "Start shared session" }),
1135
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "fai-share__hint", children: "Generates a session id + secret token. Share the URL with humans, or hand the JSON config to an MCP-capable agent." })
1136
+ ] });
1137
+ }
1138
+ const url = buildShareUrl(session, shareBaseUrl);
1139
+ const config = buildShareConfig(session);
1140
+ const curl = buildCurlRecipe(session);
1141
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-share fai-share--active", className ?? ""].filter(Boolean).join(" "), style, children: [
1142
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__header", children: [
1143
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1144
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Sharing" }),
1145
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fai-share__id", children: [
1146
+ "session ",
1147
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: session.id }),
1148
+ " \xB7 token ",
1149
+ /* @__PURE__ */ jsxRuntime.jsxs("code", { children: [
1150
+ session.display,
1151
+ "\u2026"
1152
+ ] })
1153
+ ] })
1154
+ ] }),
1155
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__header-actions", children: [
1156
+ status && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fai-share__status", children: status }),
1157
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__stop", onClick: onStop, children: "Stop" })
1158
+ ] })
1159
+ ] }),
1160
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__tabs", role: "tablist", children: [
1161
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "url", active: tab, setTab, children: "URL" }),
1162
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "json", active: tab, setTab, children: "JSON" }),
1163
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { tab: "curl", active: tab, setTab, children: "cURL recipe" })
1164
+ ] }),
1165
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__panel", children: [
1166
+ tab === "url" && /* @__PURE__ */ jsxRuntime.jsx(CopyBox, { label: "Open this URL in another tab to join the session", value: url }),
1167
+ tab === "json" && /* @__PURE__ */ jsxRuntime.jsx(
1168
+ CopyBox,
1169
+ {
1170
+ label: "Paste into Claude Desktop / Cline MCP server config",
1171
+ value: JSON.stringify(config, null, 2)
1172
+ }
1173
+ ),
1174
+ tab === "curl" && /* @__PURE__ */ jsxRuntime.jsx(
1175
+ CopyBox,
1176
+ {
1177
+ label: "Connect from a terminal (verifies the relay is reachable)",
1178
+ value: curl,
1179
+ multiline: true
1180
+ }
1181
+ )
1182
+ ] })
1183
+ ] });
1184
+ }
1185
+ function TabButton({ tab, active, setTab, children }) {
1186
+ return /* @__PURE__ */ jsxRuntime.jsx(
1187
+ "button",
1188
+ {
1189
+ type: "button",
1190
+ role: "tab",
1191
+ "aria-selected": tab === active,
1192
+ className: `fai-share__tab${tab === active ? " is-active" : ""}`,
1193
+ onClick: () => setTab(tab),
1194
+ children
1195
+ }
1196
+ );
1197
+ }
1198
+ function CopyBox({ label, value, multiline }) {
1199
+ const [copied, setCopied] = react.useState(false);
1200
+ const copy = async () => {
1201
+ try {
1202
+ await navigator.clipboard.writeText(value);
1203
+ setCopied(true);
1204
+ setTimeout(() => setCopied(false), 1200);
1205
+ } catch {
1206
+ }
1207
+ };
1208
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-share__panel-label", children: label }),
1210
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fai-share__copy", children: [
1211
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { className: `fai-share__pre${multiline ? " is-multi" : ""}`, children: value }),
1212
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "fai-share__copy-btn", onClick: copy, children: copied ? "Copied" : "Copy" })
1213
+ ] })
1214
+ ] });
1215
+ }
1216
+ function buildCurlRecipe(session) {
1217
+ const base = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost";
1218
+ const inbox = `${base}/whiteboard-share/${session.id}/inbox?token=${session.token}`;
1219
+ const events = `${base}/whiteboard-share/${session.id}/events?token=${session.token}`;
1220
+ return [
1221
+ `# 1) In one terminal, subscribe to server-pushed frames (SSE)`,
1222
+ `curl -N "${events}"`,
1223
+ ``,
1224
+ `# 2) In another terminal, send an initialize handshake`,
1225
+ `curl -X POST "${inbox}" \\`,
1226
+ ` -H 'content-type: application/json' \\`,
1227
+ ` -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'`,
1228
+ ``,
1229
+ `# 3) List the tools the bridge exposes`,
1230
+ `curl -X POST "${inbox}" \\`,
1231
+ ` -H 'content-type: application/json' \\`,
1232
+ ` -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'`,
1233
+ ``,
1234
+ `# 4) Add a sticky note`,
1235
+ `curl -X POST "${inbox}" \\`,
1236
+ ` -H 'content-type: application/json' \\`,
1237
+ ` -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whiteboard_add_sticky","arguments":{"x":300,"y":300,"text":"hello from curl"}}}'`
1238
+ ].join("\n");
1239
+ }
1240
+
1241
+ // src/sharing/sse-relay.ts
1242
+ var SseRelayTransport = class {
1243
+ constructor(options) {
1244
+ this.sendQueue = [];
1245
+ this.connected = false;
1246
+ this.listeners = /* @__PURE__ */ new Set();
1247
+ this.state = "idle";
1248
+ this.opts = options;
1249
+ this.expectedToken = options.token;
1250
+ }
1251
+ bindServer(server) {
1252
+ this.server = server;
1253
+ }
1254
+ /** Open the SSE channel. Idempotent. */
1255
+ start() {
1256
+ if (this.connected || typeof window === "undefined") return;
1257
+ const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/events?token=${encodeURIComponent(this.opts.token)}`;
1258
+ this.setState("connecting");
1259
+ const es = new EventSource(url, { withCredentials: false });
1260
+ this.es = es;
1261
+ es.addEventListener("open", () => {
1262
+ this.connected = true;
1263
+ this.setState("open");
1264
+ const queued = this.sendQueue.splice(0);
1265
+ for (const msg of queued) this.postOut(msg);
1266
+ });
1267
+ es.addEventListener("mcp", (ev) => {
1268
+ const raw = ev.data;
1269
+ this.handleInbound(raw);
1270
+ });
1271
+ es.addEventListener("error", () => {
1272
+ this.setState("error");
1273
+ });
1274
+ }
1275
+ send(message) {
1276
+ if (!this.connected) {
1277
+ this.sendQueue.push(message);
1278
+ return;
1279
+ }
1280
+ this.postOut(message);
1281
+ }
1282
+ close() {
1283
+ this.es?.close();
1284
+ this.es = void 0;
1285
+ this.connected = false;
1286
+ this.setState("closed");
1287
+ }
1288
+ onStateChange(listener) {
1289
+ this.listeners.add(listener);
1290
+ listener(this.state);
1291
+ return () => this.listeners.delete(listener);
1292
+ }
1293
+ /**
1294
+ * For relays that wrap each frame with auth metadata: hosts can call this
1295
+ * directly when a frame arrives via a non-SSE path. The transport will
1296
+ * dispatch it to the bound server.
1297
+ */
1298
+ async deliverFromRemote(payload, token) {
1299
+ if (token !== void 0 && !constantTimeEqual(token, this.expectedToken)) return;
1300
+ if (!this.server) throw new Error("SseRelayTransport has no bound server");
1301
+ const message = typeof payload === "string" ? JSON.parse(payload) : payload;
1302
+ await this.server.receive(this, message);
1303
+ }
1304
+ async postOut(message) {
1305
+ const url = `${this.opts.baseUrl}/${encodeURIComponent(this.opts.sessionId)}/outbox?token=${encodeURIComponent(this.opts.token)}`;
1306
+ const f = this.opts.fetch ?? fetch;
1307
+ try {
1308
+ await f(url, {
1309
+ method: "POST",
1310
+ headers: { "content-type": "application/json", "accept": "application/json" },
1311
+ body: JSON.stringify(message)
1312
+ });
1313
+ } catch {
1314
+ }
1315
+ }
1316
+ async handleInbound(raw) {
1317
+ if (!this.server) return;
1318
+ let message;
1319
+ try {
1320
+ message = JSON.parse(raw);
1321
+ } catch {
1322
+ return;
1323
+ }
1324
+ await this.server.receive(this, message);
1325
+ }
1326
+ setState(state) {
1327
+ this.state = state;
1328
+ for (const l of this.listeners) l(state);
1329
+ }
1330
+ };
1331
+ function attachSseRelay(server, options) {
1332
+ const transport = new SseRelayTransport(options);
1333
+ transport.bindServer(server);
1334
+ server.attach(transport);
1335
+ transport.start();
1336
+ return transport;
1337
+ }
1338
+ var DEFAULT_AGENT3 = { id: "agent", name: "Agent", color: "#a855f7" };
1339
+ function SharedWhiteboard({
1340
+ initialNotes = [],
1341
+ initialShapes = [],
1342
+ initialConnectors = [],
1343
+ initialStrokes = [],
1344
+ initialViewport = { x: 0, y: 0, zoom: 1 },
1345
+ agent = DEFAULT_AGENT3,
1346
+ shareBaseUrl = "/whiteboard-share",
1347
+ onRegisterSession,
1348
+ showAgentPanel = true,
1349
+ showShareControls = true,
1350
+ broadcastEdits = true,
1351
+ height = 640,
1352
+ header,
1353
+ className,
1354
+ style
1355
+ }) {
1356
+ const [notes, setNotes] = react.useState(initialNotes);
1357
+ const [shapes, setShapes] = react.useState(initialShapes);
1358
+ const [connectors, setConnectors] = react.useState(initialConnectors);
1359
+ const [strokes, setStrokes] = react.useState(initialStrokes);
1360
+ const [viewport, setViewport] = react.useState(initialViewport);
1361
+ const [agentCursor, setAgentCursor] = react.useState(null);
1362
+ const [activity, setActivity] = react.useState([]);
1363
+ const [highlight, setHighlight] = react.useState(null);
1364
+ const stateRefs = react.useRef({ notes, shapes, connectors, strokes, viewport });
1365
+ react.useEffect(() => {
1366
+ stateRefs.current = { notes, shapes, connectors, strokes, viewport };
1367
+ }, [notes, shapes, connectors, strokes, viewport]);
1368
+ const serverRef = react.useRef(null);
1369
+ const inProcRef = react.useRef(null);
1370
+ const bridgeRef = react.useRef(null);
1371
+ react.useEffect(() => {
1372
+ const server = new MicroMcpServer({
1373
+ info: { name: "shared-whiteboard", version: "0.2.0" },
1374
+ instructions: "Collaborative whiteboard. Use whiteboard_* tools to read or modify the board."
1375
+ });
1376
+ bridgeRef.current = registerWhiteboardBridge(server, {
1377
+ adapter: {
1378
+ getNotes: () => stateRefs.current.notes,
1379
+ setNotes: (next) => setNotes(typeof next === "function" ? next : () => next),
1380
+ getShapes: () => stateRefs.current.shapes,
1381
+ setShapes: (next) => setShapes(typeof next === "function" ? next : () => next),
1382
+ getConnectors: () => stateRefs.current.connectors,
1383
+ setConnectors: (next) => setConnectors(typeof next === "function" ? next : () => next),
1384
+ getStrokes: () => stateRefs.current.strokes,
1385
+ setStrokes: (next) => setStrokes(typeof next === "function" ? next : () => next),
1386
+ getViewport: () => stateRefs.current.viewport,
1387
+ setViewport,
1388
+ setAgentCursor
1389
+ },
1390
+ agent
1391
+ });
1392
+ inProcRef.current = attachInProcess(server);
1393
+ serverRef.current = server;
1394
+ const off = inProcRef.current.onServerMessage((msg) => {
1395
+ if (msg?.id !== void 0 && "result" in msg && msg.result?.structuredContent?.id) {
1396
+ const id = msg.result.structuredContent.id;
1397
+ requestAnimationFrame(() => pulseFor(id));
1398
+ }
1399
+ });
1400
+ return () => {
1401
+ off();
1402
+ bridgeRef.current?.dispose();
1403
+ bridgeRef.current = null;
1404
+ if (inProcRef.current) server.detach(inProcRef.current);
1405
+ };
1406
+ }, []);
1407
+ const pulseFor = (id) => {
1408
+ const n = stateRefs.current.notes.find((x) => x.id === id);
1409
+ if (n) return setHighlight({ pulseKey: Date.now(), bounds: { x: n.x, y: n.y, width: n.width, height: n.height } });
1410
+ const s = stateRefs.current.shapes.find((x) => x.id === id);
1411
+ if (s) return setHighlight({ pulseKey: Date.now(), bounds: { x: s.x, y: s.y, width: s.width, height: s.height } });
1412
+ };
1413
+ const log = react.useCallback((entry) => {
1414
+ setActivity((all) => [...all.slice(-200), { id: `a_${Date.now()}_${all.length}`, at: Date.now(), ...entry }]);
1415
+ }, []);
1416
+ const [session, setSession] = react.useState(null);
1417
+ const [relayState, setRelayState] = react.useState("idle");
1418
+ const sseRef = react.useRef(null);
1419
+ const logEsRef = react.useRef(null);
1420
+ const startShare = async () => {
1421
+ if (session || !serverRef.current || !shareBaseUrl) return;
1422
+ const desc = createSessionDescriptor();
1423
+ try {
1424
+ if (onRegisterSession) {
1425
+ await onRegisterSession(desc);
1426
+ } else {
1427
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
1428
+ const reg = await fetch(`${shareBaseUrl}/register`, {
1429
+ method: "POST",
1430
+ headers: { "content-type": "application/json", "x-csrf-token": csrf, accept: "application/json" },
1431
+ body: JSON.stringify({ session: desc.id, token: desc.token })
1432
+ });
1433
+ if (!reg.ok) throw new Error(`registration failed (HTTP ${reg.status})`);
1434
+ }
1435
+ } catch (e) {
1436
+ log({ kind: "error", source: "share", text: e instanceof Error ? e.message : String(e) });
1437
+ return;
1438
+ }
1439
+ const relay = attachSseRelay(serverRef.current, {
1440
+ baseUrl: shareBaseUrl,
1441
+ sessionId: desc.id,
1442
+ token: desc.token
1443
+ });
1444
+ sseRef.current = relay;
1445
+ relay.onStateChange(setRelayState);
1446
+ const es = new EventSource(`${shareBaseUrl}/${desc.id}/events?token=${desc.token}&direction=inbound`);
1447
+ es.addEventListener("mcp", (ev) => {
1448
+ try {
1449
+ const frame = JSON.parse(ev.data);
1450
+ if (frame.method === "notifications/peer_joined") {
1451
+ setAgentCursor((c) => c ?? { userId: agent.id, name: agent.name, color: agent.color, x: 60, y: 60 });
1452
+ log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} connected` });
1453
+ return;
1454
+ }
1455
+ if (frame.method === "notifications/peer_left") {
1456
+ setAgentCursor(null);
1457
+ log({ kind: "info", source: "presence", text: `${agent.name ?? "Agent"} disconnected` });
1458
+ return;
1459
+ }
1460
+ if (frame.method === "notifications/agent_message") {
1461
+ log({ kind: "message", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
1462
+ } else if (frame.method === "notifications/agent_status") {
1463
+ log({ kind: "info", source: agent.name ?? "Agent", text: String(frame.params?.text ?? "") });
1464
+ } else if (frame.method?.startsWith("notifications/")) {
1465
+ } else {
1466
+ log({ kind: "tool", source: "remote", text: `\u2190 ${frame.method ?? `id:${frame.id}`}`, detail: frame });
1467
+ }
1468
+ } catch {
1469
+ }
1470
+ });
1471
+ logEsRef.current = es;
1472
+ setSession(desc);
1473
+ log({ kind: "info", source: "share", text: `Sharing started \xB7 session ${desc.id}` });
1474
+ };
1475
+ const stopShare = async () => {
1476
+ if (!session) return;
1477
+ const desc = session;
1478
+ setSession(null);
1479
+ logEsRef.current?.close();
1480
+ logEsRef.current = null;
1481
+ if (sseRef.current && serverRef.current) serverRef.current.detach(sseRef.current);
1482
+ sseRef.current = null;
1483
+ setRelayState("closed");
1484
+ if (shareBaseUrl) {
1485
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content ?? "";
1486
+ await fetch(`${shareBaseUrl}/${desc.id}/unregister?token=${encodeURIComponent(desc.token)}`, {
1487
+ method: "POST",
1488
+ headers: { "x-csrf-token": csrf, accept: "application/json" }
1489
+ }).catch(() => {
1490
+ });
1491
+ }
1492
+ log({ kind: "info", source: "share", text: "Sharing stopped." });
1493
+ };
1494
+ const lastBroadcastRef = react.useRef(0);
1495
+ react.useEffect(() => {
1496
+ if (!broadcastEdits || !sseRef.current || !session) return;
1497
+ const now = Date.now();
1498
+ if (now - lastBroadcastRef.current < 80) return;
1499
+ lastBroadcastRef.current = now;
1500
+ sseRef.current.send({
1501
+ jsonrpc: "2.0",
1502
+ method: "notifications/state_update",
1503
+ params: { notes, shapes, connectors, viewport, ts: now }
1504
+ });
1505
+ }, [notes, shapes, connectors, viewport, session, broadcastEdits]);
1506
+ const handleSubmit = (text) => {
1507
+ if (!sseRef.current) {
1508
+ log({ kind: "error", source: "you", text: "Start a shared session first." });
1509
+ return;
1510
+ }
1511
+ sseRef.current.send({
1512
+ jsonrpc: "2.0",
1513
+ method: "notifications/user_message",
1514
+ params: { text, ts: Date.now() }
1515
+ });
1516
+ log({ kind: "message", source: "You", text });
1517
+ };
1518
+ const cursors = react.useMemo(() => [], []);
1519
+ const statusText = (() => {
1520
+ switch (relayState) {
1521
+ case "open":
1522
+ return "live";
1523
+ case "connecting":
1524
+ return "connecting\u2026";
1525
+ case "error":
1526
+ return "error";
1527
+ case "closed":
1528
+ return "closed";
1529
+ default:
1530
+ return void 0;
1531
+ }
1532
+ })();
1533
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["fai-shared-whiteboard", className ?? ""].filter(Boolean).join(" "), style, children: [
1534
+ header,
1535
+ showShareControls && shareBaseUrl !== null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fai-shared-whiteboard__controls", children: /* @__PURE__ */ jsxRuntime.jsx(ShareControls, { session, onStart: startShare, onStop: stopShare, status: statusText }) }),
1536
+ /* @__PURE__ */ jsxRuntime.jsxs(
1537
+ "div",
1538
+ {
1539
+ className: "fai-shared-whiteboard__layout",
1540
+ style: {
1541
+ display: "grid",
1542
+ gap: 16,
1543
+ gridTemplateColumns: showAgentPanel ? "1fr 360px" : "1fr"
1544
+ },
1545
+ children: [
1546
+ /* @__PURE__ */ jsxRuntime.jsx(
1547
+ "div",
1548
+ {
1549
+ className: "fai-shared-whiteboard__board",
1550
+ style: {
1551
+ position: "relative",
1552
+ overflow: "hidden",
1553
+ borderRadius: 12,
1554
+ border: "1px solid #e4e4e7",
1555
+ background: "radial-gradient(circle at 1px 1px, rgba(0,0,0,0.07) 1px, transparent 0)",
1556
+ backgroundSize: "20px 20px",
1557
+ height
1558
+ },
1559
+ children: /* @__PURE__ */ jsxRuntime.jsxs(fancyWhiteboard.Board, { viewport, onViewportChange: setViewport, style: { width: "100%", height: "100%" }, children: [
1560
+ connectors.map((c) => {
1561
+ const a = resolveCenter(c.from, notes, shapes);
1562
+ const b = resolveCenter(c.to, notes, shapes);
1563
+ if (!a || !b) return null;
1564
+ return /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.Connector, { from: a, to: b, color: c.color ?? "#64748b" }, c.id);
1565
+ }),
1566
+ shapes.map((s) => /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.Shape, { item: s, onChange: (next) => setShapes((all) => all.map((x) => x.id === next.id ? next : x)) }, s.id)),
1567
+ notes.map((n) => /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.StickyNote, { item: n, onChange: (next) => setNotes((all) => all.map((x) => x.id === next.id ? next : x)) }, n.id)),
1568
+ /* @__PURE__ */ jsxRuntime.jsx(fancyWhiteboard.CursorLayer, { cursors }),
1569
+ agentCursor && /* @__PURE__ */ jsxRuntime.jsx(AgentCursor, { x: agentCursor.x, y: agentCursor.y, name: agentCursor.name, color: agentCursor.color }),
1570
+ highlight && /* @__PURE__ */ jsxRuntime.jsx(
1571
+ AgentActivityHighlight,
1572
+ {
1573
+ x: highlight.bounds.x,
1574
+ y: highlight.bounds.y,
1575
+ width: highlight.bounds.width,
1576
+ height: highlight.bounds.height,
1577
+ color: agent.color ?? "#a855f7",
1578
+ pulseKey: highlight.pulseKey
1579
+ }
1580
+ )
1581
+ ] })
1582
+ }
1583
+ ),
1584
+ showAgentPanel && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { height }, children: /* @__PURE__ */ jsxRuntime.jsx(
1585
+ AgentPanel,
1586
+ {
1587
+ agent,
1588
+ activity,
1589
+ onSubmit: handleSubmit
1590
+ }
1591
+ ) })
1592
+ ]
1593
+ }
1594
+ )
1595
+ ] });
1596
+ }
1597
+ function resolveCenter(ref, notes, shapes) {
1598
+ if (typeof ref === "string") {
1599
+ const n = notes.find((x) => x.id === ref);
1600
+ if (n) return { x: n.x + n.width / 2, y: n.y + n.height / 2 };
1601
+ const s = shapes.find((x) => x.id === ref);
1602
+ if (s) return { x: s.x + s.width / 2, y: s.y + s.height / 2 };
1603
+ return null;
1604
+ }
1605
+ return ref;
1606
+ }
1607
+
1608
+ exports.AgentActivityHighlight = AgentActivityHighlight;
1609
+ exports.AgentCursor = AgentCursor;
1610
+ exports.AgentPanel = AgentPanel;
1611
+ exports.InProcessTransport = InProcessTransport;
1612
+ exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
1613
+ exports.MicroMcpServer = MicroMcpServer;
1614
+ exports.RelayTransport = RelayTransport;
1615
+ exports.ShareControls = ShareControls;
1616
+ exports.SharedWhiteboard = SharedWhiteboard;
1617
+ exports.SseRelayTransport = SseRelayTransport;
1618
+ exports.attachInProcess = attachInProcess;
1619
+ exports.attachRelay = attachRelay;
1620
+ exports.attachSseRelay = attachSseRelay;
1621
+ exports.buildShareConfig = buildShareConfig;
1622
+ exports.buildShareUrl = buildShareUrl;
1623
+ exports.createSessionDescriptor = createSessionDescriptor;
1624
+ exports.describeSession = describeSession;
1625
+ exports.errorResult = errorResult;
1626
+ exports.readSessionFromUrl = readSessionFromUrl;
1627
+ exports.registerFlowBridge = registerFlowBridge;
1628
+ exports.registerWhiteboardBridge = registerWhiteboardBridge;
1629
+ exports.rpcError = rpcError;
1630
+ exports.textResult = textResult;
1631
+ //# sourceMappingURL=index.cjs.map
1632
+ //# sourceMappingURL=index.cjs.map