@particle-academy/agent-integrations 0.21.1 → 0.22.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.
@@ -0,0 +1,62 @@
1
+ import { T as ToolHost } from '../tool-host-BQuUygLF.cjs';
2
+ import { B as Bridge } from '../types-CCSBGW9T.cjs';
3
+ import '../types-aOQLTW0E.cjs';
4
+
5
+ type LatLng = {
6
+ lat: number;
7
+ lng: number;
8
+ };
9
+ type MapView = {
10
+ center: LatLng;
11
+ zoom: number;
12
+ bearing?: number;
13
+ pitch?: number;
14
+ };
15
+ type MapMarker = {
16
+ id: string;
17
+ position: LatLng;
18
+ label?: string;
19
+ color?: string;
20
+ icon?: string;
21
+ draggable?: boolean;
22
+ data?: Record<string, unknown>;
23
+ };
24
+
25
+ /**
26
+ * Adapter the host provides — getters/setters over the SAME controlled state it
27
+ * passes to fancy-map's `<Map>` (view, markers, selection), plus optional
28
+ * `fitBounds` (from the map's onReady handle) and `setFollow` (drives the map's
29
+ * `follow` prop so an agent can make the camera track a moving marker).
30
+ */
31
+ type MapBridgeAdapter = {
32
+ getView: () => MapView;
33
+ setView: (next: MapView) => void;
34
+ getMarkers: () => MapMarker[];
35
+ setMarkers: (next: MapMarker[] | ((prev: MapMarker[]) => MapMarker[])) => void;
36
+ getSelected: () => string | null;
37
+ setSelected: (id: string | null) => void;
38
+ /** Fit the camera to points (wire to the `MapHandle.fitBounds` from `<Map onReady>`). */
39
+ fitBounds?: (points: LatLng[], padding?: number) => void;
40
+ /** Make the camera follow a marker as it moves (drives the map's `follow` prop). */
41
+ setFollow?: (id: string | null) => void;
42
+ };
43
+ type MapBridgeOptions = {
44
+ adapter: MapBridgeAdapter;
45
+ /** Identity tagged onto agent-authored markers. */
46
+ agent?: {
47
+ id: string;
48
+ name?: string;
49
+ color?: string;
50
+ };
51
+ };
52
+ /**
53
+ * registerMapBridge — an MCP tool set over a fancy-map `<Map>`'s controlled
54
+ * state, so a human and an agent cohabit one map. The agent moves the camera,
55
+ * drops/updates/removes markers, selects, fits bounds, and follows a live
56
+ * track; every mutation broadcasts AgentActivity (for presence) and pushes an
57
+ * undo entry (so the human can step the agent back). Mirrors the whiteboard /
58
+ * flow bridges in shape.
59
+ */
60
+ declare function registerMapBridge(host: ToolHost, options: MapBridgeOptions): Bridge;
61
+
62
+ export { type MapBridgeAdapter, type MapBridgeOptions, registerMapBridge };
@@ -0,0 +1,62 @@
1
+ import { T as ToolHost } from '../tool-host-C8JMMGYq.js';
2
+ import { B as Bridge } from '../types-DIVNcIQO.js';
3
+ import '../types-aOQLTW0E.js';
4
+
5
+ type LatLng = {
6
+ lat: number;
7
+ lng: number;
8
+ };
9
+ type MapView = {
10
+ center: LatLng;
11
+ zoom: number;
12
+ bearing?: number;
13
+ pitch?: number;
14
+ };
15
+ type MapMarker = {
16
+ id: string;
17
+ position: LatLng;
18
+ label?: string;
19
+ color?: string;
20
+ icon?: string;
21
+ draggable?: boolean;
22
+ data?: Record<string, unknown>;
23
+ };
24
+
25
+ /**
26
+ * Adapter the host provides — getters/setters over the SAME controlled state it
27
+ * passes to fancy-map's `<Map>` (view, markers, selection), plus optional
28
+ * `fitBounds` (from the map's onReady handle) and `setFollow` (drives the map's
29
+ * `follow` prop so an agent can make the camera track a moving marker).
30
+ */
31
+ type MapBridgeAdapter = {
32
+ getView: () => MapView;
33
+ setView: (next: MapView) => void;
34
+ getMarkers: () => MapMarker[];
35
+ setMarkers: (next: MapMarker[] | ((prev: MapMarker[]) => MapMarker[])) => void;
36
+ getSelected: () => string | null;
37
+ setSelected: (id: string | null) => void;
38
+ /** Fit the camera to points (wire to the `MapHandle.fitBounds` from `<Map onReady>`). */
39
+ fitBounds?: (points: LatLng[], padding?: number) => void;
40
+ /** Make the camera follow a marker as it moves (drives the map's `follow` prop). */
41
+ setFollow?: (id: string | null) => void;
42
+ };
43
+ type MapBridgeOptions = {
44
+ adapter: MapBridgeAdapter;
45
+ /** Identity tagged onto agent-authored markers. */
46
+ agent?: {
47
+ id: string;
48
+ name?: string;
49
+ color?: string;
50
+ };
51
+ };
52
+ /**
53
+ * registerMapBridge — an MCP tool set over a fancy-map `<Map>`'s controlled
54
+ * state, so a human and an agent cohabit one map. The agent moves the camera,
55
+ * drops/updates/removes markers, selects, fits bounds, and follows a live
56
+ * track; every mutation broadcasts AgentActivity (for presence) and pushes an
57
+ * undo entry (so the human can step the agent back). Mirrors the whiteboard /
58
+ * flow bridges in shape.
59
+ */
60
+ declare function registerMapBridge(host: ToolHost, options: MapBridgeOptions): Bridge;
61
+
62
+ export { type MapBridgeAdapter, type MapBridgeOptions, registerMapBridge };
@@ -0,0 +1,454 @@
1
+ 'use strict';
2
+
3
+ var fancyAutoCommon = require('@particle-academy/fancy-auto-common');
4
+
5
+ // src/mcp/server.ts
6
+ function textResult(text, structured) {
7
+ return {
8
+ content: [{ type: "text", text }],
9
+ ...structured !== void 0 ? { structuredContent: structured } : {}
10
+ };
11
+ }
12
+ function errorResult(text) {
13
+ return { content: [{ type: "text", text }], isError: true };
14
+ }
15
+
16
+ // src/presence/wrap-tool-with-activity.ts
17
+ function wrapToolWithActivity(handler, options) {
18
+ return async (args) => {
19
+ const result = await handler(args);
20
+ if (result.isError) return result;
21
+ let target;
22
+ if (options.resolveTarget) {
23
+ target = options.resolveTarget({ toolName: options.toolName, args, result });
24
+ } else {
25
+ target = { kind: options.kind, screenId: options.screenId };
26
+ }
27
+ if (!target) return result;
28
+ fancyAutoCommon.emitActivity({
29
+ agentId: options.agent.id,
30
+ agentName: options.agent.name,
31
+ agentColor: options.agent.color,
32
+ target: { ...target, kind: target.kind ?? options.kind, screenId: target.screenId ?? options.screenId },
33
+ action: options.toolName,
34
+ timestamp: Date.now(),
35
+ meta: extractMeta(result),
36
+ ttlMs: options.ttlMs
37
+ });
38
+ return result;
39
+ };
40
+ }
41
+ function extractMeta(result) {
42
+ const sc = result.structuredContent;
43
+ if (sc && typeof sc === "object" && !Array.isArray(sc)) {
44
+ return sc;
45
+ }
46
+ return void 0;
47
+ }
48
+
49
+ // src/undo/undo-tools.ts
50
+ var installedHosts = /* @__PURE__ */ new WeakSet();
51
+ function ensureUndoToolsRegistered(host, options = {}) {
52
+ if (installedHosts.has(host)) return;
53
+ installedHosts.add(host);
54
+ registerUndoTools(host, options);
55
+ }
56
+ function registerUndoTools(host, options = {}) {
57
+ const defaultAgent = options.defaultAgentId ?? "agent";
58
+ const disposers = [];
59
+ const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
60
+ disposers.push(
61
+ host.registerTool(
62
+ {
63
+ name: "agent_undo",
64
+ description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: { agentId: { type: "string" } },
68
+ additionalProperties: false
69
+ }
70
+ },
71
+ async (args) => {
72
+ const entry = await fancyAutoCommon.undoOne(agentOf(args));
73
+ if (!entry) return errorResult("Nothing to undo.");
74
+ return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
75
+ }
76
+ )
77
+ );
78
+ disposers.push(
79
+ host.registerTool(
80
+ {
81
+ name: "agent_redo",
82
+ description: "Redo the most recently undone action.",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: { agentId: { type: "string" } },
86
+ additionalProperties: false
87
+ }
88
+ },
89
+ async (args) => {
90
+ const entry = await fancyAutoCommon.redoOne(agentOf(args));
91
+ if (!entry) return errorResult("Nothing to redo.");
92
+ return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
93
+ }
94
+ )
95
+ );
96
+ disposers.push(
97
+ host.registerTool(
98
+ {
99
+ name: "agent_history",
100
+ description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: { agentId: { type: "string" } },
104
+ additionalProperties: false
105
+ }
106
+ },
107
+ async (args) => {
108
+ const history = fancyAutoCommon.readHistory(agentOf(args)).map(serialize);
109
+ const text = history.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
110
+ return textResult(text || "(empty)", history);
111
+ }
112
+ )
113
+ );
114
+ return () => disposers.forEach((d) => d());
115
+ }
116
+ function serialize(entry) {
117
+ return {
118
+ timestamp: entry.timestamp,
119
+ bridgeId: entry.bridgeId,
120
+ action: entry.action,
121
+ label: entry.label
122
+ };
123
+ }
124
+
125
+ // src/bridges/map.ts
126
+ var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#7c3aed" };
127
+ var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : 0;
128
+ var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
129
+ var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
130
+ function registerMapBridge(host, options) {
131
+ const { adapter } = options;
132
+ const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
133
+ const disposers = [];
134
+ ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
135
+ const mTarget = (args, result) => ({
136
+ kind: "map",
137
+ elementId: result?.structuredContent?.id ?? args?.id
138
+ });
139
+ const reg = (name, description, properties, required, handler, resolveTarget) => {
140
+ const wrapped = async (args) => {
141
+ try {
142
+ return await handler(args);
143
+ } catch (e) {
144
+ return errorResult(e instanceof Error ? e.message : String(e));
145
+ }
146
+ };
147
+ const final = resolveTarget ? wrapToolWithActivity(wrapped, {
148
+ toolName: name,
149
+ agent: { id: agent.id, name: agent.name, color: agent.color },
150
+ kind: "map",
151
+ resolveTarget: ({ args, result }) => resolveTarget(args, result)
152
+ }) : wrapped;
153
+ disposers.push(
154
+ host.registerTool(
155
+ {
156
+ name,
157
+ description,
158
+ inputSchema: {
159
+ type: "object",
160
+ properties,
161
+ required,
162
+ additionalProperties: false
163
+ }
164
+ },
165
+ final
166
+ )
167
+ );
168
+ };
169
+ reg("map_get_state", "Get the map's camera, markers, and current selection.", {}, [], () => {
170
+ const state = {
171
+ view: adapter.getView(),
172
+ markers: adapter.getMarkers(),
173
+ selectedId: adapter.getSelected()
174
+ };
175
+ return textResult(JSON.stringify(state, null, 2), state);
176
+ });
177
+ reg(
178
+ "map_set_view",
179
+ "Move the camera to an absolute position. Any omitted field is left unchanged.",
180
+ {
181
+ lat: { type: "number", description: "New center latitude." },
182
+ lng: { type: "number", description: "New center longitude." },
183
+ zoom: { type: "number", description: "New zoom level (higher = closer)." }
184
+ },
185
+ [],
186
+ (args) => {
187
+ const prev = adapter.getView();
188
+ const next = {
189
+ center: {
190
+ lat: args.lat !== void 0 ? num(args.lat) : prev.center.lat,
191
+ lng: args.lng !== void 0 ? num(args.lng) : prev.center.lng
192
+ },
193
+ zoom: args.zoom !== void 0 ? num(args.zoom) : prev.zoom
194
+ };
195
+ adapter.setView(next);
196
+ fancyAutoCommon.pushUndoEntry(agent.id, {
197
+ timestamp: Date.now(),
198
+ bridgeId: "map",
199
+ action: "map_set_view",
200
+ label: `Moved camera to ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)} z${next.zoom}`,
201
+ undo: () => adapter.setView(prev),
202
+ redo: () => adapter.setView(next)
203
+ });
204
+ return textResult(`Camera \u2192 ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)} z${next.zoom}`, next);
205
+ },
206
+ mTarget
207
+ );
208
+ reg(
209
+ "map_pan",
210
+ "Pan the camera by a relative offset in degrees (positive dLat = north, positive dLng = east).",
211
+ { dLat: { type: "number" }, dLng: { type: "number" } },
212
+ [],
213
+ (args) => {
214
+ const prev = adapter.getView();
215
+ const next = {
216
+ ...prev,
217
+ center: { lat: prev.center.lat + num(args.dLat), lng: prev.center.lng + num(args.dLng) }
218
+ };
219
+ adapter.setView(next);
220
+ fancyAutoCommon.pushUndoEntry(agent.id, {
221
+ timestamp: Date.now(),
222
+ bridgeId: "map",
223
+ action: "map_pan",
224
+ label: "Panned camera",
225
+ undo: () => adapter.setView(prev),
226
+ redo: () => adapter.setView(next)
227
+ });
228
+ return textResult(`Panned to ${next.center.lat.toFixed(4)},${next.center.lng.toFixed(4)}`, next);
229
+ },
230
+ mTarget
231
+ );
232
+ reg(
233
+ "map_zoom",
234
+ "Zoom the camera by a relative delta (e.g. +1 zooms in one level, -2 out two).",
235
+ { delta: { type: "number" } },
236
+ ["delta"],
237
+ (args) => {
238
+ const prev = adapter.getView();
239
+ const next = { ...prev, zoom: prev.zoom + num(args.delta) };
240
+ adapter.setView(next);
241
+ fancyAutoCommon.pushUndoEntry(agent.id, {
242
+ timestamp: Date.now(),
243
+ bridgeId: "map",
244
+ action: "map_zoom",
245
+ label: `Zoom \u2192 ${next.zoom}`,
246
+ undo: () => adapter.setView(prev),
247
+ redo: () => adapter.setView(next)
248
+ });
249
+ return textResult(`Zoom \u2192 ${next.zoom}`, next);
250
+ }
251
+ );
252
+ reg(
253
+ "map_fit_bounds",
254
+ "Fit the camera to enclose points. Pass `points`, or omit to fit all current markers.",
255
+ {
256
+ points: {
257
+ type: "array",
258
+ description: "Points to enclose, e.g. [{lat,lng},\u2026].",
259
+ items: {
260
+ type: "object",
261
+ properties: { lat: { type: "number" }, lng: { type: "number" } },
262
+ required: ["lat", "lng"]
263
+ }
264
+ },
265
+ padding: { type: "number", description: "Padding in pixels (default 40)." }
266
+ },
267
+ [],
268
+ (args) => {
269
+ if (!adapter.fitBounds) {
270
+ return errorResult("Host did not wire fitBounds (pass the <Map onReady> handle's fitBounds).");
271
+ }
272
+ const points = Array.isArray(args.points) ? args.points.map((p) => ({ lat: num(p?.lat), lng: num(p?.lng) })) : adapter.getMarkers().map((m) => m.position);
273
+ if (!points.length) {
274
+ return errorResult("No points to fit (map has no markers).");
275
+ }
276
+ adapter.fitBounds(points, args.padding !== void 0 ? num(args.padding) : void 0);
277
+ return textResult(`Fit camera to ${points.length} point(s).`);
278
+ }
279
+ );
280
+ reg(
281
+ "map_add_marker",
282
+ "Drop a marker. `id` is optional (auto-generated); `icon` is an emoji or 1\u20132 chars.",
283
+ {
284
+ id: { type: "string" },
285
+ lat: { type: "number" },
286
+ lng: { type: "number" },
287
+ label: { type: "string" },
288
+ color: { type: "string", description: "CSS color, e.g. #2563eb." },
289
+ icon: { type: "string", description: "Emoji / short text in the pin." },
290
+ draggable: { type: "boolean" },
291
+ data: { type: "object", description: "Arbitrary JSON payload carried with the marker." }
292
+ },
293
+ ["lat", "lng"],
294
+ (args) => {
295
+ const id = args.id ? str(args.id) : newId("m");
296
+ if (adapter.getMarkers().some((m) => m.id === id)) {
297
+ return errorResult(`A marker with id ${id} already exists \u2014 use map_update_marker.`);
298
+ }
299
+ const marker = {
300
+ id,
301
+ position: { lat: num(args.lat), lng: num(args.lng) },
302
+ ...args.label !== void 0 ? { label: str(args.label) } : {},
303
+ ...args.color !== void 0 ? { color: str(args.color) } : {},
304
+ ...args.icon !== void 0 ? { icon: str(args.icon) } : {},
305
+ ...args.draggable !== void 0 ? { draggable: !!args.draggable } : {},
306
+ ...args.data && typeof args.data === "object" ? { data: args.data } : {}
307
+ };
308
+ adapter.setMarkers((all) => [...all, marker]);
309
+ fancyAutoCommon.pushUndoEntry(agent.id, {
310
+ timestamp: Date.now(),
311
+ bridgeId: "map",
312
+ action: "map_add_marker",
313
+ label: `Added marker ${id}`,
314
+ undo: () => adapter.setMarkers((all) => all.filter((m) => m.id !== id)),
315
+ redo: () => adapter.setMarkers((all) => [...all, marker])
316
+ });
317
+ return textResult(`Added marker ${id} at ${marker.position.lat.toFixed(4)},${marker.position.lng.toFixed(4)}`, marker);
318
+ },
319
+ mTarget
320
+ );
321
+ reg(
322
+ "map_update_marker",
323
+ "Update a marker. Only provided fields change \u2014 update lat/lng repeatedly to drive a live track.",
324
+ {
325
+ id: { type: "string" },
326
+ lat: { type: "number" },
327
+ lng: { type: "number" },
328
+ label: { type: "string" },
329
+ color: { type: "string" },
330
+ icon: { type: "string" },
331
+ draggable: { type: "boolean" }
332
+ },
333
+ ["id"],
334
+ (args) => {
335
+ const id = str(args.id);
336
+ const prev = adapter.getMarkers().find((m) => m.id === id);
337
+ if (!prev) {
338
+ return errorResult(`No marker with id ${id}`);
339
+ }
340
+ const next = {
341
+ ...prev,
342
+ position: {
343
+ lat: args.lat !== void 0 ? num(args.lat) : prev.position.lat,
344
+ lng: args.lng !== void 0 ? num(args.lng) : prev.position.lng
345
+ },
346
+ ...args.label !== void 0 ? { label: str(args.label) } : {},
347
+ ...args.color !== void 0 ? { color: str(args.color) } : {},
348
+ ...args.icon !== void 0 ? { icon: str(args.icon) } : {},
349
+ ...args.draggable !== void 0 ? { draggable: !!args.draggable } : {}
350
+ };
351
+ adapter.setMarkers((all) => all.map((m) => m.id === id ? next : m));
352
+ fancyAutoCommon.pushUndoEntry(agent.id, {
353
+ timestamp: Date.now(),
354
+ bridgeId: "map",
355
+ action: "map_update_marker",
356
+ label: `Updated marker ${id}`,
357
+ undo: () => adapter.setMarkers((all) => all.map((m) => m.id === id ? prev : m)),
358
+ redo: () => adapter.setMarkers((all) => all.map((m) => m.id === id ? next : m))
359
+ });
360
+ return textResult(`Updated marker ${id}`, next);
361
+ },
362
+ mTarget
363
+ );
364
+ reg(
365
+ "map_remove_marker",
366
+ "Remove a marker by id.",
367
+ { id: { type: "string" } },
368
+ ["id"],
369
+ (args) => {
370
+ const id = str(args.id);
371
+ const prev = adapter.getMarkers().find((m) => m.id === id);
372
+ if (!prev) {
373
+ return errorResult(`No marker with id ${id}`);
374
+ }
375
+ adapter.setMarkers((all) => all.filter((m) => m.id !== id));
376
+ fancyAutoCommon.pushUndoEntry(agent.id, {
377
+ timestamp: Date.now(),
378
+ bridgeId: "map",
379
+ action: "map_remove_marker",
380
+ label: `Removed marker ${id}`,
381
+ undo: () => adapter.setMarkers((all) => [...all, prev]),
382
+ redo: () => adapter.setMarkers((all) => all.filter((m) => m.id !== id))
383
+ });
384
+ return textResult(`Removed marker ${id}`);
385
+ },
386
+ mTarget
387
+ );
388
+ reg(
389
+ "map_select",
390
+ "Select a marker (or pass no id / null to clear the selection).",
391
+ { id: { type: "string" } },
392
+ [],
393
+ (args) => {
394
+ const prev = adapter.getSelected();
395
+ const id = args.id === void 0 || args.id === null ? null : str(args.id);
396
+ if (id !== null && !adapter.getMarkers().some((m) => m.id === id)) {
397
+ return errorResult(`No marker with id ${id}`);
398
+ }
399
+ adapter.setSelected(id);
400
+ fancyAutoCommon.pushUndoEntry(agent.id, {
401
+ timestamp: Date.now(),
402
+ bridgeId: "map",
403
+ action: "map_select",
404
+ label: id ? `Selected ${id}` : "Cleared selection",
405
+ undo: () => adapter.setSelected(prev),
406
+ redo: () => adapter.setSelected(id)
407
+ });
408
+ return textResult(id ? `Selected marker ${id}` : "Cleared selection", { selectedId: id });
409
+ },
410
+ mTarget
411
+ );
412
+ reg(
413
+ "map_start_track",
414
+ "Make the camera follow a marker as its position updates (live tracking). Requires the host to wire setFollow.",
415
+ { id: { type: "string" } },
416
+ ["id"],
417
+ (args) => {
418
+ if (!adapter.setFollow) {
419
+ return errorResult("Host did not wire setFollow (pass the map's `follow` setter).");
420
+ }
421
+ const id = str(args.id);
422
+ if (!adapter.getMarkers().some((m) => m.id === id)) {
423
+ return errorResult(`No marker with id ${id}`);
424
+ }
425
+ adapter.setFollow(id);
426
+ return textResult(`Camera now following ${id}`, { follow: id });
427
+ },
428
+ mTarget
429
+ );
430
+ reg(
431
+ "map_stop_track",
432
+ "Stop following any marker (releases the camera). Requires the host to wire setFollow.",
433
+ {},
434
+ [],
435
+ () => {
436
+ if (!adapter.setFollow) {
437
+ return errorResult("Host did not wire setFollow.");
438
+ }
439
+ adapter.setFollow(null);
440
+ return textResult("Camera released (no longer following).", { follow: null });
441
+ }
442
+ );
443
+ return {
444
+ id: "map",
445
+ title: "Map",
446
+ dispose: () => {
447
+ for (const d of disposers) d();
448
+ }
449
+ };
450
+ }
451
+
452
+ exports.registerMapBridge = registerMapBridge;
453
+ //# sourceMappingURL=bridges-map.cjs.map
454
+ //# sourceMappingURL=bridges-map.cjs.map