nodality 1.1.14 → 1.2.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/bin/mcp-server.mjs +41 -5
- package/bin/nodality.js +61 -0
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +60 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +60 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +60 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +60 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +60 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +60 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +1 -1
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +6 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +13 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/agent-surface.js +347 -0
- package/lib/card-getter.js +1 -1
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +22 -1
- package/lib/element-mapper.js +2 -1
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +47 -8
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +196 -32
- package/lib/webmcp-adapter.js +307 -0
- package/package.json +1 -1
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.2.0
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*!
|
|
8
|
+
* webmcp-adapter.js — the derived surface, made callable.
|
|
9
|
+
*
|
|
10
|
+
* Everything protocol-shaped in this library lives HERE, in one file,
|
|
11
|
+
* on purpose. WebMCP is six months old, is a Draft Community Group
|
|
12
|
+
* Report rather than a standard, is available only in an origin trial,
|
|
13
|
+
* and has ALREADY moved its entry point once (`navigator.modelContext`
|
|
14
|
+
* → `document.modelContext`). A library that scattered those facts
|
|
15
|
+
* through the designer and the prerenderer would need archaeology every
|
|
16
|
+
* time the draft moved; here the next move is one file and a failing
|
|
17
|
+
* test.
|
|
18
|
+
*
|
|
19
|
+
* The registration itself is a REQUEST, not a guarantee — exactly the
|
|
20
|
+
* posture the live raster backend takes toward HTML-in-Canvas. Where
|
|
21
|
+
* the API is absent (every stock browser today, and jsdom at build
|
|
22
|
+
* time) nothing registers, nothing warns, and the page behaves
|
|
23
|
+
* identically. What still happens everywhere is the DECLARATION: the
|
|
24
|
+
* manifest is written into the document as data, so a crawler, an
|
|
25
|
+
* indexer, or an agent deciding whether to visit at all can read the
|
|
26
|
+
* site's capabilities without executing anything and without the API
|
|
27
|
+
* existing. That half needs no browser support and no standard.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { deriveSurface } from "./agent-surface.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The draft this adapter is written against. Asserted in ONE place so
|
|
34
|
+
* that a spec move is a single-file change with a failing test, and so
|
|
35
|
+
* that every emitted manifest records what it was derived against
|
|
36
|
+
* rather than implying a stability the spec does not yet have.
|
|
37
|
+
*/
|
|
38
|
+
export const SPEC_DRAFT = "2026-07-21";
|
|
39
|
+
|
|
40
|
+
/** Where the declaration lives in the document, JSON-LD style. */
|
|
41
|
+
export const MANIFEST_ID = "nodality-agent-manifest";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The registry, wherever this browser currently keeps it.
|
|
45
|
+
*
|
|
46
|
+
* Both spellings are accepted because the trial spans the rename: the
|
|
47
|
+
* draft moved to `document.modelContext`, Chrome 150 deprecated the
|
|
48
|
+
* older location, and pages served to both are the normal case for the
|
|
49
|
+
* length of an origin trial.
|
|
50
|
+
*/
|
|
51
|
+
function registry() {
|
|
52
|
+
if (typeof document !== "undefined" && document.modelContext) return document.modelContext;
|
|
53
|
+
if (typeof navigator !== "undefined" && navigator.modelContext) return navigator.modelContext;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `CSS.escape`, or a serviceable stand-in.
|
|
59
|
+
*
|
|
60
|
+
* jsdom has no `CSS` global, and the build-time pass runs there — the
|
|
61
|
+
* unguarded call threw, was swallowed by the designer's try/catch, and
|
|
62
|
+
* every prerendered page silently shipped without its declaration. The
|
|
63
|
+
* fallback escapes what an id may legally contain and a selector may
|
|
64
|
+
* not; ids here come from descriptors the author wrote, not from user
|
|
65
|
+
* input, so this is about correctness on ordinary hyphens and dots
|
|
66
|
+
* rather than about hostile strings.
|
|
67
|
+
*/
|
|
68
|
+
const esc = (s) => {
|
|
69
|
+
const str = String(s);
|
|
70
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(str);
|
|
71
|
+
return str.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Text an agent can read, from the subtree that is actually on screen. */
|
|
75
|
+
function readSubtree(root) {
|
|
76
|
+
if (!root) return { text: "", actions: [] };
|
|
77
|
+
const visible = (el) => {
|
|
78
|
+
const r = el.getBoundingClientRect();
|
|
79
|
+
if (!(r.width > 0 && r.height > 0)) return false;
|
|
80
|
+
const cs = getComputedStyle(el);
|
|
81
|
+
return cs.visibility !== "hidden" && cs.display !== "none" && cs.opacity !== "0";
|
|
82
|
+
};
|
|
83
|
+
const lines = [];
|
|
84
|
+
for (const el of root.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li")) {
|
|
85
|
+
if (!visible(el)) continue;
|
|
86
|
+
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
87
|
+
if (!t) continue;
|
|
88
|
+
const tag = el.tagName.toLowerCase();
|
|
89
|
+
lines.push(/^h[1-6]$/.test(tag) ? `${"#".repeat(+tag[1])} ${t}` : t);
|
|
90
|
+
}
|
|
91
|
+
const actions = [];
|
|
92
|
+
for (const el of root.querySelectorAll("a,button,[role=button]")) {
|
|
93
|
+
if (!visible(el)) continue;
|
|
94
|
+
const t = (el.textContent || "").replace(/\s+/g, " ").trim();
|
|
95
|
+
if (t && !actions.includes(t)) actions.push(t);
|
|
96
|
+
}
|
|
97
|
+
return { text: lines.join("\n"), actions };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A structured refusal, in the shape every other report in this library uses. */
|
|
101
|
+
const refuse = (code, got, valid) => ({ ok: false, code, got, valid });
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Give a derived surface its handlers and offer it to the browser.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} args
|
|
107
|
+
* @param {Element} args.mount the rendered mount
|
|
108
|
+
* @param {Array} args.elements E
|
|
109
|
+
* @param {Array} args.nodes N
|
|
110
|
+
* @param {Array} args.morphs handles returned by applyMorphNodes
|
|
111
|
+
* @returns {{manifest: object, tools: object[], registered: boolean,
|
|
112
|
+
* destroy: function}|null}
|
|
113
|
+
*/
|
|
114
|
+
export function installAgentSurface({ mount, elements, nodes, morphs }) {
|
|
115
|
+
const derived = deriveSurface(elements, nodes, { specDraft: SPEC_DRAFT });
|
|
116
|
+
if (!derived) return null; // opt-in: no node, no surface
|
|
117
|
+
|
|
118
|
+
const { tools, manifest } = derived;
|
|
119
|
+
const handles = Array.isArray(morphs) ? morphs.filter(Boolean) : [];
|
|
120
|
+
const graphFor = (id) => handles.find((h) => h.states && h.states.has(id)) || null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The rendered form a descriptor id names, wherever it currently is.
|
|
124
|
+
*
|
|
125
|
+
* A morph state that has not been visited is DETACHED — held by the
|
|
126
|
+
* controller, absent from the document — so a form inside one cannot
|
|
127
|
+
* be found by querying the mount. It is reachable through the state
|
|
128
|
+
* elements the controller holds, and stamping or filling it there
|
|
129
|
+
* works exactly as well: the attributes and values travel with the
|
|
130
|
+
* element when the transition inserts it.
|
|
131
|
+
*/
|
|
132
|
+
const findForm = (formId) => {
|
|
133
|
+
const dig = (root) => {
|
|
134
|
+
if (!root || typeof root.querySelector !== "function") return null;
|
|
135
|
+
if (root.id === formId) {
|
|
136
|
+
return root.tagName === "FORM" ? root : root.querySelector("form");
|
|
137
|
+
}
|
|
138
|
+
const host = root.querySelector(`#${esc(formId)}`);
|
|
139
|
+
if (!host) return null;
|
|
140
|
+
return host.tagName === "FORM" ? host : host.querySelector("form");
|
|
141
|
+
};
|
|
142
|
+
const here = dig(mount);
|
|
143
|
+
if (here) return here;
|
|
144
|
+
for (const h of handles) {
|
|
145
|
+
if (!h.states) continue;
|
|
146
|
+
for (const el of h.states.values()) {
|
|
147
|
+
const found = dig(el);
|
|
148
|
+
if (found) return found;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ── the declaration, written wherever we are ─────────────────────
|
|
155
|
+
//
|
|
156
|
+
// In jsdom this ends up inside the prerendered HTML, which is the
|
|
157
|
+
// whole point: the static declaration exists because the same code
|
|
158
|
+
// ran at build time, not because a second emitter was written for
|
|
159
|
+
// it. In a browser it is simply inert data in the page.
|
|
160
|
+
if (typeof document !== "undefined") {
|
|
161
|
+
const prev = document.getElementById(MANIFEST_ID);
|
|
162
|
+
if (prev) prev.remove();
|
|
163
|
+
const tag = document.createElement("script");
|
|
164
|
+
tag.type = "application/json";
|
|
165
|
+
tag.id = MANIFEST_ID;
|
|
166
|
+
tag.textContent = JSON.stringify(manifest, null, 2);
|
|
167
|
+
(document.body || document.documentElement).appendChild(tag);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── the spec's own form annotations ──────────────────────────────
|
|
171
|
+
//
|
|
172
|
+
// Emitted rather than resisted. The draft's declarative path derives
|
|
173
|
+
// a tool from `toolname`/`tooldescription` on a form, and Lighthouse
|
|
174
|
+
// already audits for them; stamping the SAME forms we derive from
|
|
175
|
+
// means the annotated surface and the derived surface describe one
|
|
176
|
+
// thing by construction, and a browser that only implements the
|
|
177
|
+
// declarative path still gets the form.
|
|
178
|
+
for (const t of tools) {
|
|
179
|
+
if (t.kind !== "submit" || !mount) continue;
|
|
180
|
+
const el = findForm(t.formId);
|
|
181
|
+
if (!el) continue;
|
|
182
|
+
el.setAttribute("toolname", t.name);
|
|
183
|
+
el.setAttribute("tooldescription", t.description);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── handlers ─────────────────────────────────────────────────────
|
|
187
|
+
//
|
|
188
|
+
// Traversals are SERIALISED, and this is an agent-specific hazard
|
|
189
|
+
// rather than a controller bug: a human cannot click twice inside
|
|
190
|
+
// one tick, so the controller's interrupt handling has never had to
|
|
191
|
+
// survive two transitions starting before either has built its
|
|
192
|
+
// pipeline. An agent does it trivially — two executeTool calls, no
|
|
193
|
+
// await — and the observed result was an interface with NOTHING on
|
|
194
|
+
// screen. Queuing here means the second call runs from the state the
|
|
195
|
+
// first actually landed in, which may make it unreachable; that
|
|
196
|
+
// returns a constructive refusal, which is a coherent answer. The
|
|
197
|
+
// controller is left exactly as it is, because for its own callers
|
|
198
|
+
// it is exactly right.
|
|
199
|
+
let queue = Promise.resolve();
|
|
200
|
+
const serial = (fn) => {
|
|
201
|
+
const run = queue.then(fn, fn);
|
|
202
|
+
queue = run.then(() => {}, () => {});
|
|
203
|
+
return run;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const execute = {
|
|
207
|
+
navigate: ({ destination } = {}) => serial(async () => {
|
|
208
|
+
const h = graphFor(destination);
|
|
209
|
+
if (!h) {
|
|
210
|
+
return refuse("UNKNOWN_VIEW", destination,
|
|
211
|
+
handles.flatMap((x) => [...x.states.keys()]));
|
|
212
|
+
}
|
|
213
|
+
// Reachability is a property of WHERE THE GRAPH IS, not of
|
|
214
|
+
// the schema, so it is answered here and not by the enum: a
|
|
215
|
+
// destination valid from `work` is invalid from `home`, and
|
|
216
|
+
// the honest answer names what is reachable right now.
|
|
217
|
+
const from = h.current;
|
|
218
|
+
await h.goToState(destination);
|
|
219
|
+
if (h.current !== destination) {
|
|
220
|
+
const edge = [...h.states.keys()];
|
|
221
|
+
return refuse("UNREACHABLE_FROM_HERE", destination,
|
|
222
|
+
edge.filter((id) => id !== from));
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, view: h.current, ...readSubtree(h.stage || mount) };
|
|
225
|
+
}),
|
|
226
|
+
|
|
227
|
+
back: () => serial(async () => {
|
|
228
|
+
const h = handles.find((x) => x.history && x.history.length);
|
|
229
|
+
if (!h) return refuse("NO_HISTORY", null, []);
|
|
230
|
+
await h.goBack();
|
|
231
|
+
return { ok: true, view: h.current, ...readSubtree(h.stage || mount) };
|
|
232
|
+
}),
|
|
233
|
+
|
|
234
|
+
read: async () => ({ ok: true, ...readSubtree(mount) }),
|
|
235
|
+
|
|
236
|
+
submit: async (args = {}, tool) => {
|
|
237
|
+
const form = findForm(tool.formId);
|
|
238
|
+
if (!form) return refuse("NO_FORM", tool.formId, []);
|
|
239
|
+
|
|
240
|
+
const props = (tool.inputSchema && tool.inputSchema.properties) || {};
|
|
241
|
+
const required = (tool.inputSchema && tool.inputSchema.required) || [];
|
|
242
|
+
const missing = required.filter(
|
|
243
|
+
(k) => args[k] === undefined || args[k] === "" || args[k] === null);
|
|
244
|
+
if (missing.length) {
|
|
245
|
+
// The constraint, not a boolean. A generator repairs from
|
|
246
|
+
// this in one turn; "false" costs it a guess.
|
|
247
|
+
return refuse("MISSING_REQUIRED", missing, Object.keys(props));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const filled = [];
|
|
251
|
+
for (const key of Object.keys(props)) {
|
|
252
|
+
if (args[key] === undefined) continue;
|
|
253
|
+
const field = form.querySelector(
|
|
254
|
+
`[name="${key}"], #${CSS.escape(key)}`);
|
|
255
|
+
if (!field) continue;
|
|
256
|
+
if (field.type === "checkbox") field.checked = !!args[key];
|
|
257
|
+
else field.value = String(args[key]);
|
|
258
|
+
// Both, because component state may listen for either.
|
|
259
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
260
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
261
|
+
filled.push(key);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Submitted through the form's OWN path, so its listeners,
|
|
265
|
+
// its validation attributes and the browser's constraint
|
|
266
|
+
// checking all apply. An agent submit is a user submit.
|
|
267
|
+
if (typeof form.requestSubmit === "function") form.requestSubmit();
|
|
268
|
+
else form.submit();
|
|
269
|
+
return { ok: true, submitted: tool.formId, filled };
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// ── registration, if this browser has anywhere to register ───────
|
|
274
|
+
const reg = registry();
|
|
275
|
+
let registered = false;
|
|
276
|
+
if (reg && typeof reg.registerTool === "function") {
|
|
277
|
+
for (const t of tools) {
|
|
278
|
+
try {
|
|
279
|
+
reg.registerTool({
|
|
280
|
+
name: t.name,
|
|
281
|
+
description: t.description,
|
|
282
|
+
inputSchema: t.inputSchema,
|
|
283
|
+
execute: (args) => execute[t.kind](args, t),
|
|
284
|
+
});
|
|
285
|
+
registered = true;
|
|
286
|
+
} catch (e) {
|
|
287
|
+
// A failed registration must never take the page down.
|
|
288
|
+
console.warn("[nodality] agent surface: tool rejected", t.name, e);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
manifest, tools, registered,
|
|
295
|
+
destroy() {
|
|
296
|
+
if (reg && typeof reg.unregisterTool === "function") {
|
|
297
|
+
for (const t of tools) {
|
|
298
|
+
try { reg.unregisterTool(t.name); } catch (e) { /* already gone */ }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (typeof document !== "undefined") {
|
|
302
|
+
const tag = document.getElementById(MANIFEST_ID);
|
|
303
|
+
if (tag) tag.remove();
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|