@particle-academy/agent-integrations 0.47.1 → 0.48.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/CHANGELOG.md CHANGED
@@ -11,6 +11,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.48.0] - 2026-10-24
15
+
16
+ ### Added
17
+
18
+ - **Scoped screen-content bridge:** import `registerScreenDocBridge`,
19
+ `SCREEN_TEXT_NODE_TYPE`, `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode`
20
+ and `ScreenDocOp` from `@particle-academy/agent-integrations/bridges/screen-doc`.
21
+ Both ESM and CommonJS builds include their type declarations. This complements
22
+ `bridges/screens`, which navigates between screens rather than editing their
23
+ contents. **Nothing to do: the root import still works.** The other 22 bridge
24
+ modules already have scoped exports; no additional missing entry was found.
25
+ - Built-package coverage for every discovered bridge: Node loads all scoped
26
+ entries through both ESM and CommonJS, and TypeScript checks ESM/CommonJS
27
+ declaration consumers. `npm test` builds fresh output first, so stale `dist`
28
+ files cannot hide a missing export.
29
+
30
+ ### Security
31
+
32
+ - **Dev toolchain only — no runtime change, nothing for consumers to do.** The
33
+ lockfile now resolves `vite` 8.3.5 (was 8.2.2), whose bundled
34
+ `launch-editor` fixes GHSA-587h-7wmj-h5pm (HIGH, `fs.existsSync()` check
35
+ bypassable on Windows), GHSA-698v-4r3x-3vcv (`escapeCmdArgs()` omits `%`
36
+ escaping on Windows) and GHSA-r825-w8mm-5pxw (editor CLI flag
37
+ misinterpretation). `vite` is only reached through the test/dev toolchain
38
+ and the published package does not ship our lockfile, so the exposure was
39
+ this repo's own dev server.
40
+
14
41
  ## [0.47.1] - 2026-10-21
15
42
 
16
43
  **What to do — nothing, unless you call `DomNavigationAgent.read()` yourself**
package/README.md CHANGED
@@ -87,12 +87,20 @@ Every bridge has a matching subpath if you'd rather import surgically:
87
87
  | charts | `…/bridges/charts` |
88
88
  | scene | `…/bridges/scene` |
89
89
  | screens | `…/bridges/screens` |
90
+ | screen contents | `…/bridges/screen-doc` |
90
91
  | passkeys | `…/bridges/passkeys` |
91
92
  | sheets adapter hook | `…/sheets-adapter` |
92
93
  | presence | `…/presence` |
93
94
  | undo | `…/undo` |
94
95
  | sharing | `…/sharing` |
95
96
 
97
+ `registerScreensBridge` handles navigation between screens.
98
+ `registerScreenDocBridge` edits the nodes inside one screen and exports its
99
+ `DocAdapter`, `ScreenDocBridgeOptions`, `ScreenDocNode` and `ScreenDocOp` types
100
+ from `@particle-academy/agent-integrations/bridges/screen-doc`. The existing
101
+ root import still works; nothing to do for existing consumers. Use a distinct
102
+ `surface` tool prefix for each concurrently bridged document.
103
+
96
104
  ## When you don't need an MCP server
97
105
 
98
106
  If your agent runs in the same JS process as the surfaces it drives (an embedded chat widget, an in-page worker, an SSR-time tool loop), you don't need any of MCP's JSON-RPC framing. Use **`ToolRegistry`** — a plain in-memory tool host that the bridges register against. Agents drive the surface with `host.callTool("sheet_set_cell", { … })` directly.
@@ -0,0 +1,47 @@
1
+ import { T as ToolHost } from '../tool-host-D67N2a2h.cjs';
2
+ import { B as Bridge } from '../types-DP5yQZQU.cjs';
3
+ import { DocAdapter } from './doc.cjs';
4
+ import { DocNode, StagePolicy, TreeOp } from '@particle-academy/fancy-doc-commons';
5
+ import '../types-gzIpsUDE.cjs';
6
+
7
+ /**
8
+ * Reserved node type for a literal string child. Must match fancy-screens'
9
+ * `TEXT_NODE_TYPE`; duplicated rather than imported to keep this bridge free of
10
+ * a fancy-screens dependency.
11
+ */
12
+ declare const SCREEN_TEXT_NODE_TYPE = "#text";
13
+ /** A screen document node — a `DocNode` plus the minted-id flag. */
14
+ interface ScreenDocNode extends DocNode {
15
+ /**
16
+ * True when fancy-screens minted this id because the author supplied none.
17
+ * A synthetic id is derived from position, so it is not a durable handle.
18
+ */
19
+ synthetic?: boolean;
20
+ }
21
+ type ScreenDocOp = TreeOp<ScreenDocNode>;
22
+ interface ScreenDocBridgeOptions {
23
+ adapter: DocAdapter<ScreenDocNode>;
24
+ /**
25
+ * Tool-name prefix. Default `"screen"` → `screen_add`, `screen_set_text`, …
26
+ *
27
+ * Tool names are global, so a host wiring more than one doc-driven screen at
28
+ * once must give each a distinct prefix (`"screen_checkout"`).
29
+ */
30
+ surface?: string;
31
+ agent?: {
32
+ id: string;
33
+ name?: string;
34
+ color?: string;
35
+ };
36
+ /** Auto-apply vs. confirm per op (trust-but-verify). Default: auto. */
37
+ stagePolicy?: StagePolicy<ScreenDocOp>;
38
+ onPending?: (staged: {
39
+ id: string;
40
+ op: ScreenDocOp;
41
+ label: string;
42
+ }) => void;
43
+ }
44
+ /** Register the screen-content bridge (prefix `screen_`) over a screen's `DocTree`. */
45
+ declare function registerScreenDocBridge(host: ToolHost, options: ScreenDocBridgeOptions): Bridge;
46
+
47
+ export { DocAdapter, SCREEN_TEXT_NODE_TYPE, type ScreenDocBridgeOptions, type ScreenDocNode, type ScreenDocOp, registerScreenDocBridge };
@@ -0,0 +1,47 @@
1
+ import { T as ToolHost } from '../tool-host-LYfdPE8u.js';
2
+ import { B as Bridge } from '../types-VX_9je7w.js';
3
+ import { DocAdapter } from './doc.js';
4
+ import { DocNode, StagePolicy, TreeOp } from '@particle-academy/fancy-doc-commons';
5
+ import '../types-gzIpsUDE.js';
6
+
7
+ /**
8
+ * Reserved node type for a literal string child. Must match fancy-screens'
9
+ * `TEXT_NODE_TYPE`; duplicated rather than imported to keep this bridge free of
10
+ * a fancy-screens dependency.
11
+ */
12
+ declare const SCREEN_TEXT_NODE_TYPE = "#text";
13
+ /** A screen document node — a `DocNode` plus the minted-id flag. */
14
+ interface ScreenDocNode extends DocNode {
15
+ /**
16
+ * True when fancy-screens minted this id because the author supplied none.
17
+ * A synthetic id is derived from position, so it is not a durable handle.
18
+ */
19
+ synthetic?: boolean;
20
+ }
21
+ type ScreenDocOp = TreeOp<ScreenDocNode>;
22
+ interface ScreenDocBridgeOptions {
23
+ adapter: DocAdapter<ScreenDocNode>;
24
+ /**
25
+ * Tool-name prefix. Default `"screen"` → `screen_add`, `screen_set_text`, …
26
+ *
27
+ * Tool names are global, so a host wiring more than one doc-driven screen at
28
+ * once must give each a distinct prefix (`"screen_checkout"`).
29
+ */
30
+ surface?: string;
31
+ agent?: {
32
+ id: string;
33
+ name?: string;
34
+ color?: string;
35
+ };
36
+ /** Auto-apply vs. confirm per op (trust-but-verify). Default: auto. */
37
+ stagePolicy?: StagePolicy<ScreenDocOp>;
38
+ onPending?: (staged: {
39
+ id: string;
40
+ op: ScreenDocOp;
41
+ label: string;
42
+ }) => void;
43
+ }
44
+ /** Register the screen-content bridge (prefix `screen_`) over a screen's `DocTree`. */
45
+ declare function registerScreenDocBridge(host: ToolHost, options: ScreenDocBridgeOptions): Bridge;
46
+
47
+ export { DocAdapter, SCREEN_TEXT_NODE_TYPE, type ScreenDocBridgeOptions, type ScreenDocNode, type ScreenDocOp, registerScreenDocBridge };
@@ -0,0 +1,465 @@
1
+ 'use strict';
2
+
3
+ var fancyAutoCommon = require('@particle-academy/fancy-auto-common');
4
+ var fancyDocCommons = require('@particle-academy/fancy-doc-commons');
5
+
6
+ // src/mcp/server.ts
7
+ function textResult(text, structured) {
8
+ return {
9
+ content: [{ type: "text", text }],
10
+ ...structured !== void 0 ? { structuredContent: structured } : {}
11
+ };
12
+ }
13
+ function errorResult(text) {
14
+ return { content: [{ type: "text", text }], isError: true };
15
+ }
16
+
17
+ // src/presence/wrap-tool-with-activity.ts
18
+ function wrapToolWithActivity(handler, options) {
19
+ return async (args) => {
20
+ const result = await handler(args);
21
+ if (result.isError) return result;
22
+ let target;
23
+ if (options.resolveTarget) {
24
+ target = options.resolveTarget({ toolName: options.toolName, args, result });
25
+ } else {
26
+ target = { kind: options.kind, screenId: options.screenId };
27
+ }
28
+ if (!target) return result;
29
+ fancyAutoCommon.emitActivity({
30
+ agentId: options.agent.id,
31
+ agentName: options.agent.name,
32
+ agentColor: options.agent.color,
33
+ target: { ...target, kind: target.kind ?? options.kind, screenId: target.screenId ?? options.screenId },
34
+ action: options.toolName,
35
+ timestamp: Date.now(),
36
+ meta: options.buildMeta ? options.buildMeta(result) : extractMeta(result),
37
+ ttlMs: options.ttlMs
38
+ });
39
+ return result;
40
+ };
41
+ }
42
+ function extractMeta(result) {
43
+ const sc = result.structuredContent;
44
+ if (sc && typeof sc === "object" && !Array.isArray(sc)) {
45
+ return sc;
46
+ }
47
+ return void 0;
48
+ }
49
+
50
+ // src/undo/undo-tools.ts
51
+ var installedHosts = /* @__PURE__ */ new WeakSet();
52
+ function ensureUndoToolsRegistered(host, options = {}) {
53
+ if (installedHosts.has(host)) return;
54
+ installedHosts.add(host);
55
+ registerUndoTools(host, options);
56
+ }
57
+ function registerUndoTools(host, options = {}) {
58
+ const defaultAgent = options.defaultAgentId ?? "agent";
59
+ const disposers = [];
60
+ const agentOf = (args) => typeof args?.agentId === "string" ? args.agentId : defaultAgent;
61
+ disposers.push(
62
+ host.registerTool(
63
+ {
64
+ name: "agent_undo",
65
+ description: "Undo the most recent action on the agent's stack. Optional agentId targets a specific agent.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: { agentId: { type: "string" } },
69
+ additionalProperties: false
70
+ }
71
+ },
72
+ async (args) => {
73
+ const entry = await fancyAutoCommon.undoOne(agentOf(args));
74
+ if (!entry) return errorResult("Nothing to undo.");
75
+ return textResult(`Undid: ${entry.label}`, { entry: serialize(entry) });
76
+ }
77
+ )
78
+ );
79
+ disposers.push(
80
+ host.registerTool(
81
+ {
82
+ name: "agent_redo",
83
+ description: "Redo the most recently undone action.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: { agentId: { type: "string" } },
87
+ additionalProperties: false
88
+ }
89
+ },
90
+ async (args) => {
91
+ const entry = await fancyAutoCommon.redoOne(agentOf(args));
92
+ if (!entry) return errorResult("Nothing to redo.");
93
+ return textResult(`Redid: ${entry.label}`, { entry: serialize(entry) });
94
+ }
95
+ )
96
+ );
97
+ disposers.push(
98
+ host.registerTool(
99
+ {
100
+ name: "agent_history",
101
+ description: "List the agent's undo stack (oldest first). Useful for understanding what's reversible.",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: { agentId: { type: "string" } },
105
+ additionalProperties: false
106
+ }
107
+ },
108
+ async (args) => {
109
+ const history = fancyAutoCommon.readHistory(agentOf(args)).map(serialize);
110
+ const text = history.map((e) => `${new Date(e.timestamp).toISOString()} ${e.bridgeId} ${e.action}: ${e.label}`).join("\n");
111
+ return textResult(text || "(empty)", history);
112
+ }
113
+ )
114
+ );
115
+ return () => disposers.forEach((d) => d());
116
+ }
117
+ function serialize(entry) {
118
+ return {
119
+ timestamp: entry.timestamp,
120
+ bridgeId: entry.bridgeId,
121
+ action: entry.action,
122
+ label: entry.label
123
+ };
124
+ }
125
+ var DEFAULT_AGENT = { id: "agent", name: "Agent", color: "#a855f7" };
126
+ function registerDocBridge(host, options) {
127
+ const { adapter, surface } = options;
128
+ const agent = { ...DEFAULT_AGENT, ...options.agent ?? {} };
129
+ const reducer = options.reducer ?? fancyDocCommons.treeReducer();
130
+ const stage = options.stagePolicy ?? fancyDocCommons.autoApply;
131
+ const makeNode = options.makeNode ?? (({ id, type, parent, order }, props) => ({ id, type, parent, order, props }));
132
+ const describeNode = options.describeNode ?? ((n) => ({ id: n.id, type: n.type, parent: n.parent, props: Object.keys(n.props ?? {}) }));
133
+ const disposers = [];
134
+ ensureUndoToolsRegistered(host, { defaultAgentId: agent.id });
135
+ let idSeq = 0;
136
+ const mintId = () => {
137
+ if (adapter.newId) return adapter.newId();
138
+ const taken = adapter.get().nodes;
139
+ let id;
140
+ do
141
+ id = `${surface}-${idSeq += 1}`;
142
+ while (taken[id]);
143
+ return id;
144
+ };
145
+ const target = (elementId) => ({ kind: surface, screenId: adapter.screenId, elementId });
146
+ const pending = /* @__PURE__ */ new Map();
147
+ function commit(op, label, targetId) {
148
+ const prev = adapter.get();
149
+ const next = reducer.reduce(prev, op);
150
+ adapter.set(next);
151
+ fancyAutoCommon.pushUndoEntry(agent.id, {
152
+ timestamp: Date.now(),
153
+ bridgeId: surface,
154
+ action: label,
155
+ label,
156
+ undo: () => adapter.set(prev),
157
+ redo: () => adapter.set(next)
158
+ });
159
+ return { ok: true, id: targetId };
160
+ }
161
+ function submit(op, label, targetId) {
162
+ if (stage(op) === "confirm") {
163
+ const pendingId = `${surface}-pending-${idSeq += 1}`;
164
+ pending.set(pendingId, { op, label, targetId });
165
+ options.onPending?.({ id: pendingId, op, label });
166
+ return { staged: true, id: targetId, pendingId };
167
+ }
168
+ commit(op, label, targetId);
169
+ return { staged: false, id: targetId };
170
+ }
171
+ const reg = (verb, description, properties, required, handler, isMutation, resolveTarget) => {
172
+ const name = `${surface}_${verb}`;
173
+ const wrapped = async (args) => {
174
+ try {
175
+ const { _text, ...structured } = await handler(args);
176
+ return textResult(typeof _text === "string" ? _text : name, structured);
177
+ } catch (e) {
178
+ return errorResult(e instanceof Error ? e.message : String(e));
179
+ }
180
+ };
181
+ const final = isMutation ? wrapToolWithActivity(wrapped, {
182
+ toolName: name,
183
+ agent,
184
+ kind: surface,
185
+ screenId: adapter.screenId,
186
+ resolveTarget: ({ args, result }) => {
187
+ if (result.structuredContent?.staged) return null;
188
+ return resolveTarget?.(args, result.structuredContent ?? {}) ?? target();
189
+ }
190
+ }) : wrapped;
191
+ disposers.push(
192
+ host.registerTool(
193
+ { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } },
194
+ final
195
+ )
196
+ );
197
+ };
198
+ reg(
199
+ "describe",
200
+ `Summarize the ${surface} document: node count, roots, and the active surface. Call before reading or writing.`,
201
+ {},
202
+ [],
203
+ () => {
204
+ const tree = adapter.get();
205
+ const ids = Object.keys(tree.nodes);
206
+ return { nodeCount: ids.length, roots: fancyDocCommons.childrenOf(tree, null).map((n) => n.id), _text: `${ids.length} nodes` };
207
+ },
208
+ false
209
+ );
210
+ reg(
211
+ "get",
212
+ `Get the whole ${surface} document tree as JSON.`,
213
+ {},
214
+ [],
215
+ () => ({ tree: adapter.get(), _text: "full tree" }),
216
+ false
217
+ );
218
+ reg(
219
+ "list",
220
+ "List nodes (optionally the children of `parent`), in order.",
221
+ { parent: { type: ["string", "null"], description: "Parent id, or null/omitted for roots." } },
222
+ [],
223
+ (args) => {
224
+ const tree = adapter.get();
225
+ const parent = typeof args.parent === "string" ? args.parent : null;
226
+ return { nodes: fancyDocCommons.childrenOf(tree, parent).map(describeNode), _text: `children of ${parent ?? "root"}` };
227
+ },
228
+ false
229
+ );
230
+ reg(
231
+ "get_node",
232
+ "Get one node by id.",
233
+ { id: { type: "string" } },
234
+ ["id"],
235
+ (args) => {
236
+ const node = adapter.get().nodes[String(args.id)];
237
+ if (!node) throw new Error(`No node ${args.id}`);
238
+ return { node, _text: `node ${node.id}` };
239
+ },
240
+ false
241
+ );
242
+ reg(
243
+ "add",
244
+ "Insert a node. Returns its id. Appends under `parent` (or roots) unless `order` is given.",
245
+ {
246
+ type: { type: "string", description: "Node type discriminant." },
247
+ parent: { type: ["string", "null"], description: "Parent id, or null for a root." },
248
+ props: { type: "object", description: "JSON props bag." },
249
+ order: { type: "string", description: "Fractional order key (default: append)." },
250
+ id: {
251
+ type: "string",
252
+ description: "Id for the new node. Supply a meaningful one if you intend to address this node later; omitted means one is minted for you. Must not already exist."
253
+ }
254
+ },
255
+ ["type"],
256
+ (args) => {
257
+ const tree = adapter.get();
258
+ const parent = typeof args.parent === "string" ? args.parent : null;
259
+ const requested = typeof args.id === "string" ? args.id : void 0;
260
+ if (requested !== void 0 && tree.nodes[requested]) {
261
+ throw new Error(`Node ${requested} already exists \u2014 pick a different id or update it instead`);
262
+ }
263
+ const id = requested ?? mintId();
264
+ const order = typeof args.order === "string" ? args.order : fancyDocCommons.appendOrder(tree, parent);
265
+ const props = args.props && typeof args.props === "object" ? args.props : {};
266
+ const node = makeNode({ id, type: String(args.type), parent, order }, props);
267
+ const res = submit({ t: "insert", node }, `add ${args.type} ${id}`, id);
268
+ return { ...res, _text: res.staged ? `staged add ${id} \u2014 confirm ${res.pendingId}` : `added ${id}` };
269
+ },
270
+ true,
271
+ (_args, result) => target(result.id)
272
+ );
273
+ reg(
274
+ "update",
275
+ "Merge a props patch into a node.",
276
+ { id: { type: "string" }, patch: { type: "object" } },
277
+ ["id", "patch"],
278
+ (args) => {
279
+ const id = String(args.id);
280
+ if (!adapter.get().nodes[id]) throw new Error(`No node ${id}`);
281
+ const patch = args.patch && typeof args.patch === "object" ? args.patch : {};
282
+ const res = submit({ t: "set_props", id, patch }, `update ${id}`, id);
283
+ return { ...res, id, _text: res.staged ? `staged update ${id} \u2014 confirm ${res.pendingId}` : `updated ${id}` };
284
+ },
285
+ true,
286
+ (args) => target(String(args.id))
287
+ );
288
+ reg(
289
+ "remove",
290
+ "Remove a node and its subtree.",
291
+ { id: { type: "string" } },
292
+ ["id"],
293
+ (args) => {
294
+ const id = String(args.id);
295
+ const tree = adapter.get();
296
+ if (!tree.nodes[id]) throw new Error(`No node ${id}`);
297
+ const count = fancyDocCommons.descendantsOf(tree, id).length + 1;
298
+ const res = submit({ t: "remove", id }, `remove ${id} (${count} node${count === 1 ? "" : "s"})`, id);
299
+ return { ...res, id, removed: count, _text: res.staged ? `staged remove ${id} \u2014 confirm ${res.pendingId}` : `removed ${id}` };
300
+ },
301
+ true,
302
+ (args) => target(String(args.id))
303
+ );
304
+ reg(
305
+ "move",
306
+ "Reparent + reorder a node.",
307
+ { id: { type: "string" }, parent: { type: ["string", "null"] }, order: { type: "string", description: "Fractional order key (default: append under the new parent)." } },
308
+ ["id"],
309
+ (args) => {
310
+ const id = String(args.id);
311
+ const tree = adapter.get();
312
+ if (!tree.nodes[id]) throw new Error(`No node ${id}`);
313
+ const parent = args.parent === null ? null : typeof args.parent === "string" ? args.parent : tree.nodes[id].parent;
314
+ const order = typeof args.order === "string" ? args.order : fancyDocCommons.appendOrder(tree, parent);
315
+ const res = submit({ t: "move", id, parent, order }, `move ${id}`, id);
316
+ return { ...res, id, _text: res.staged ? `staged move ${id} \u2014 confirm ${res.pendingId}` : `moved ${id}` };
317
+ },
318
+ true,
319
+ (args) => target(String(args.id))
320
+ );
321
+ for (const t of options.opTools ?? []) {
322
+ reg(
323
+ t.verb,
324
+ t.description,
325
+ t.properties,
326
+ t.required,
327
+ (args) => {
328
+ const built = t.build(args, adapter.get());
329
+ if (built && typeof built === "object" && "error" in built) throw new Error(built.error);
330
+ const tid = t.targetId?.(args);
331
+ const res = submit(built, `${t.verb} ${tid ?? ""}`.trim(), tid);
332
+ return { ...res, id: tid, _text: res.staged ? `staged ${t.verb} \u2014 confirm ${res.pendingId}` : t.verb };
333
+ },
334
+ true,
335
+ (args) => target(t.targetId?.(args))
336
+ );
337
+ }
338
+ reg(
339
+ "confirm",
340
+ "Apply a staged op. Pass the `pendingId` a staged mutation returned \u2014 not the node `id` it also returns.",
341
+ { id: { type: "string", description: "The `pendingId` from a staged mutation." } },
342
+ ["id"],
343
+ (args) => {
344
+ const id = String(args.id);
345
+ const p = pending.get(id);
346
+ if (!p) throw new Error(`No pending op ${id}`);
347
+ pending.delete(id);
348
+ commit(p.op, p.label, p.targetId);
349
+ return { ok: true, applied: p.label, id: p.targetId, _text: `applied ${p.label}` };
350
+ },
351
+ true,
352
+ (_args, result) => target(result.id)
353
+ );
354
+ reg(
355
+ "reject",
356
+ "Discard a staged op. Pass the `pendingId` a staged mutation returned \u2014 not the node `id` it also returns.",
357
+ { id: { type: "string", description: "The `pendingId` from a staged mutation." } },
358
+ ["id"],
359
+ (args) => {
360
+ const id = String(args.id);
361
+ const p = pending.get(id);
362
+ if (!p) throw new Error(`No pending op ${id}`);
363
+ pending.delete(id);
364
+ return { ok: true, rejected: p.label, _text: `rejected ${p.label}` };
365
+ },
366
+ false
367
+ );
368
+ return {
369
+ id: surface,
370
+ title: surface,
371
+ dispose: () => {
372
+ for (const d of disposers) d();
373
+ pending.clear();
374
+ }
375
+ };
376
+ }
377
+ var SCREEN_TEXT_NODE_TYPE = "#text";
378
+ function registerScreenDocBridge(host, options) {
379
+ const { adapter } = options;
380
+ const surface = options.surface ?? "screen";
381
+ const setText = {
382
+ verb: "set_text",
383
+ description: "Replace the literal text of an element node \u2014 the natural way to change a label, heading or paragraph. Pass the ELEMENT's id, not the text node's.",
384
+ properties: {
385
+ id: { type: "string", description: "Id of the element whose text to set." },
386
+ text: { type: "string" }
387
+ },
388
+ required: ["id", "text"],
389
+ build: (args, tree) => {
390
+ const id = String(args.id);
391
+ const node = tree.nodes[id];
392
+ if (!node) return { error: `No node ${id}` };
393
+ if (node.type === SCREEN_TEXT_NODE_TYPE) {
394
+ return { t: "set_props", id, patch: { value: String(args.text) } };
395
+ }
396
+ const texts = fancyDocCommons.childrenOf(tree, id).filter((c) => c.type === SCREEN_TEXT_NODE_TYPE);
397
+ if (texts.length === 1) {
398
+ return { t: "set_props", id: texts[0].id, patch: { value: String(args.text) } };
399
+ }
400
+ if (texts.length === 0) {
401
+ return {
402
+ t: "insert",
403
+ node: {
404
+ id: `${id}-text`,
405
+ type: SCREEN_TEXT_NODE_TYPE,
406
+ parent: id,
407
+ order: fancyDocCommons.appendOrder(tree, id),
408
+ props: { value: String(args.text) }
409
+ }
410
+ };
411
+ }
412
+ return {
413
+ error: `Node ${id} has ${texts.length} text children (${texts.map((t) => t.id).join(", ")}) \u2014 set_text on the one you mean`
414
+ };
415
+ },
416
+ targetId: (args) => String(args.id)
417
+ };
418
+ const bridge = registerDocBridge(host, {
419
+ adapter,
420
+ surface,
421
+ agent: options.agent,
422
+ stagePolicy: options.stagePolicy,
423
+ onPending: options.onPending,
424
+ // Anything the AGENT adds is authored by definition — it chose or was given
425
+ // the id — so it must not inherit the synthetic flag from its neighbours.
426
+ makeNode: ({ id, type, parent, order }, props) => ({ id, type, parent, order, props }),
427
+ describeNode: (n) => ({
428
+ id: n.id,
429
+ type: n.type,
430
+ parent: n.parent,
431
+ ...n.synthetic ? { synthetic: true } : {},
432
+ ...n.type === SCREEN_TEXT_NODE_TYPE ? { text: String(n.props?.value ?? "") } : { props: Object.keys(n.props ?? {}) }
433
+ }),
434
+ opTools: [setText]
435
+ });
436
+ const disposeAddressable = host.registerTool(
437
+ {
438
+ name: `${surface}_addressable`,
439
+ description: "List the node ids that are safe to remember and use later. Ids NOT listed here were minted from a node's position: they resolve today and silently point at a different node once a sibling is inserted above them. Address those in the same turn you read them, and never store one.",
440
+ inputSchema: { type: "object", properties: {}, required: [], additionalProperties: false }
441
+ },
442
+ (async () => {
443
+ const nodes = Object.values(adapter.get().nodes);
444
+ const authored = nodes.filter((n) => !n.synthetic).map((n) => ({ id: n.id, type: n.type }));
445
+ const syntheticCount = nodes.length - authored.length;
446
+ return textResult(
447
+ authored.length ? `${authored.length} addressable, ${syntheticCount} positional` : `no addressable ids \u2014 all ${syntheticCount} nodes are positional`,
448
+ { addressable: authored, syntheticCount }
449
+ );
450
+ })
451
+ );
452
+ return {
453
+ id: surface,
454
+ title: "Screen content",
455
+ dispose: () => {
456
+ disposeAddressable();
457
+ bridge.dispose();
458
+ }
459
+ };
460
+ }
461
+
462
+ exports.SCREEN_TEXT_NODE_TYPE = SCREEN_TEXT_NODE_TYPE;
463
+ exports.registerScreenDocBridge = registerScreenDocBridge;
464
+ //# sourceMappingURL=bridges-screen-doc.cjs.map
465
+ //# sourceMappingURL=bridges-screen-doc.cjs.map