@nexface/tools 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/browser/controller.d.ts +30 -0
  2. package/dist/browser/controller.js +783 -0
  3. package/dist/browser/controller.js.map +1 -0
  4. package/dist/browser/dom.d.ts +43 -0
  5. package/dist/browser/dom.js +438 -0
  6. package/dist/browser/dom.js.map +1 -0
  7. package/dist/browser/feedback.d.ts +4 -0
  8. package/dist/browser/feedback.js +5 -0
  9. package/dist/browser/feedback.js.map +1 -0
  10. package/dist/browser/locator.d.ts +19 -0
  11. package/dist/browser/locator.js +277 -0
  12. package/dist/browser/locator.js.map +1 -0
  13. package/dist/browser/prompt.d.ts +5 -0
  14. package/dist/browser/prompt.generated.d.ts +1 -0
  15. package/dist/browser/prompt.generated.js +50 -0
  16. package/dist/browser/prompt.generated.js.map +1 -0
  17. package/dist/browser/prompt.js +9 -0
  18. package/dist/browser/prompt.js.map +1 -0
  19. package/dist/browser/snapshot.d.ts +7 -0
  20. package/dist/browser/snapshot.js +424 -0
  21. package/dist/browser/snapshot.js.map +1 -0
  22. package/dist/browser/toolset.d.ts +35 -0
  23. package/dist/browser/toolset.js +383 -0
  24. package/dist/browser/toolset.js.map +1 -0
  25. package/dist/browser/types.d.ts +129 -0
  26. package/dist/browser/types.js +2 -0
  27. package/dist/browser/types.js.map +1 -0
  28. package/dist/browser/worker-runner.d.ts +11 -0
  29. package/dist/browser/worker-runner.js +400 -0
  30. package/dist/browser/worker-runner.js.map +1 -0
  31. package/dist/definition.d.ts +16 -0
  32. package/dist/definition.js +72 -0
  33. package/dist/definition.js.map +1 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +4 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/internal-react.d.ts +12 -0
  38. package/dist/internal-react.js +23 -0
  39. package/dist/internal-react.js.map +1 -0
  40. package/dist/internal-types.d.ts +11 -0
  41. package/dist/internal-types.js +2 -0
  42. package/dist/internal-types.js.map +1 -0
  43. package/dist/internal.d.ts +9 -0
  44. package/dist/internal.js +5 -0
  45. package/dist/internal.js.map +1 -0
  46. package/dist/react.d.ts +25 -0
  47. package/dist/react.js +258 -0
  48. package/dist/react.js.map +1 -0
  49. package/dist/runtime.d.ts +93 -0
  50. package/dist/runtime.js +492 -0
  51. package/dist/runtime.js.map +1 -0
  52. package/dist/schema.d.ts +2 -0
  53. package/dist/schema.js +46 -0
  54. package/dist/schema.js.map +1 -0
  55. package/dist/types.d.ts +80 -0
  56. package/dist/types.js +2 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/webmcp.d.ts +2 -0
  59. package/dist/webmcp.js +73 -0
  60. package/dist/webmcp.js.map +1 -0
  61. package/package.json +26 -0
  62. package/src/browser/controller.ts +984 -0
  63. package/src/browser/dom.ts +507 -0
  64. package/src/browser/feedback.ts +5 -0
  65. package/src/browser/locator.ts +383 -0
  66. package/src/browser/prompt.generated.ts +50 -0
  67. package/src/browser/prompt.ts +10 -0
  68. package/src/browser/snapshot.ts +542 -0
  69. package/src/browser/toolset.ts +522 -0
  70. package/src/browser/types.ts +172 -0
  71. package/src/browser/worker-runner.ts +468 -0
  72. package/src/definition.ts +115 -0
  73. package/src/index.ts +35 -0
  74. package/src/internal-react.tsx +62 -0
  75. package/src/internal-types.ts +12 -0
  76. package/src/internal.ts +36 -0
  77. package/src/react.tsx +397 -0
  78. package/src/runtime.ts +722 -0
  79. package/src/schema.ts +52 -0
  80. package/src/types.ts +123 -0
  81. package/src/webmcp.ts +102 -0
  82. package/tsconfig.json +8 -0
@@ -0,0 +1,507 @@
1
+ import type {
2
+ RegexValue,
3
+ SemanticState,
4
+ TextMatcher,
5
+ } from "./types.js";
6
+
7
+ const ROLE_BY_TAG: Record<string, string> = {
8
+ A: "link",
9
+ BUTTON: "button",
10
+ DIALOG: "dialog",
11
+ FORM: "form",
12
+ H1: "heading",
13
+ H2: "heading",
14
+ H3: "heading",
15
+ H4: "heading",
16
+ H5: "heading",
17
+ H6: "heading",
18
+ IFRAME: "iframe",
19
+ LI: "listitem",
20
+ NAV: "navigation",
21
+ OL: "list",
22
+ OPTION: "option",
23
+ PROGRESS: "progressbar",
24
+ SELECT: "combobox",
25
+ TABLE: "table",
26
+ TBODY: "rowgroup",
27
+ TD: "cell",
28
+ TEXTAREA: "textbox",
29
+ TH: "columnheader",
30
+ TR: "row",
31
+ UL: "list",
32
+ };
33
+
34
+ const ACTIONABLE_ROLES = new Set([
35
+ "button",
36
+ "checkbox",
37
+ "combobox",
38
+ "link",
39
+ "menuitem",
40
+ "option",
41
+ "radio",
42
+ "searchbox",
43
+ "slider",
44
+ "spinbutton",
45
+ "switch",
46
+ "tab",
47
+ "textbox",
48
+ "treeitem",
49
+ ]);
50
+
51
+ const CONTEXT_ROLES = new Set(["dialog", "row", "listitem"]);
52
+ const ALLOWED_EXPLICIT_ROLES = new Set([
53
+ ...ACTIONABLE_ROLES,
54
+ ...CONTEXT_ROLES,
55
+ "alert",
56
+ "alertdialog",
57
+ "cell",
58
+ "columnheader",
59
+ "form",
60
+ "group",
61
+ "heading",
62
+ "iframe",
63
+ "list",
64
+ "listbox",
65
+ "main",
66
+ "menu",
67
+ "navigation",
68
+ "progressbar",
69
+ "region",
70
+ "rowgroup",
71
+ "status",
72
+ "table",
73
+ "tooltip",
74
+ "tree",
75
+ ]);
76
+
77
+ const SENSITIVE_INPUT_TYPES = new Set(["file", "hidden", "password"]);
78
+
79
+ export function normalizeText(value: string | null | undefined): string {
80
+ return (value ?? "").replace(/\s+/g, " ").trim();
81
+ }
82
+
83
+ export function composedParent(element: Element): Element | null {
84
+ if (element.assignedSlot) return element.assignedSlot;
85
+ if (element.parentElement) return element.parentElement;
86
+ const root = element.getRootNode();
87
+ return root instanceof ShadowRoot ? root.host : null;
88
+ }
89
+
90
+ export function isAgentIgnored(element: Element): boolean {
91
+ let current: Element | null = element;
92
+ while (current) {
93
+ if (current.hasAttribute("data-nexface-ignore")) return true;
94
+ current = composedParent(current);
95
+ }
96
+ return false;
97
+ }
98
+
99
+ export function composedChildren(node: Element | Document | ShadowRoot): Node[] {
100
+ if (node instanceof HTMLSlotElement) {
101
+ const assigned = node.assignedNodes({ flatten: true });
102
+ if (assigned.length > 0) return assigned;
103
+ }
104
+ if (node instanceof Element && node.shadowRoot) {
105
+ return Array.from(node.shadowRoot.childNodes);
106
+ }
107
+ return Array.from(node.childNodes);
108
+ }
109
+
110
+ export function* walkComposedElements(
111
+ root: Element | Document | ShadowRoot = document,
112
+ ): Generator<Element> {
113
+ for (const child of composedChildren(root)) {
114
+ if (!(child instanceof Element)) continue;
115
+ yield child;
116
+ yield* walkComposedElements(child);
117
+ }
118
+ }
119
+
120
+ function hasGenericInteractionEvidence(element: Element): boolean {
121
+ if (!(element instanceof HTMLElement)) return false;
122
+ return (
123
+ element.tabIndex >= 0 ||
124
+ element.hasAttribute("onclick") ||
125
+ element.isContentEditable
126
+ );
127
+ }
128
+
129
+ export function computeRole(element: Element): string {
130
+ const explicit = normalizeText(element.getAttribute("role"))
131
+ .split(" ")
132
+ .find((role) => ALLOWED_EXPLICIT_ROLES.has(role));
133
+ if (explicit) return explicit;
134
+
135
+ if (element instanceof HTMLInputElement) {
136
+ const type = element.type.toLowerCase();
137
+ if (type === "checkbox") return "checkbox";
138
+ if (type === "radio") return "radio";
139
+ if (["button", "submit", "reset"].includes(type)) return "button";
140
+ if (type === "range") return "slider";
141
+ if (type === "number") return "spinbutton";
142
+ if (type === "search") return "searchbox";
143
+ if (type === "hidden") return "";
144
+ return "textbox";
145
+ }
146
+
147
+ if (element instanceof HTMLAnchorElement && !element.hasAttribute("href")) {
148
+ return hasGenericInteractionEvidence(element) ? "actionable" : "";
149
+ }
150
+
151
+ const implicit = ROLE_BY_TAG[element.tagName] ?? "";
152
+ if (implicit) return implicit;
153
+ return hasGenericInteractionEvidence(element) ? "actionable" : "";
154
+ }
155
+
156
+ function referencedText(element: Element, attribute: string): string {
157
+ const root = element.getRootNode();
158
+ const ids = normalizeText(element.getAttribute(attribute))
159
+ .split(" ")
160
+ .filter(Boolean);
161
+ return normalizeText(
162
+ ids
163
+ .map((id) => {
164
+ if (root instanceof Document || root instanceof ShadowRoot) {
165
+ return root.getElementById(id)?.textContent ?? "";
166
+ }
167
+ return document.getElementById(id)?.textContent ?? "";
168
+ })
169
+ .join(" "),
170
+ );
171
+ }
172
+
173
+ export type NameSource = "explicit" | "content" | "fallback";
174
+
175
+ export interface ComputedName {
176
+ value: string;
177
+ source: NameSource;
178
+ }
179
+
180
+ export function computeNameInfo(element: Element): ComputedName {
181
+ const labelledBy = referencedText(element, "aria-labelledby");
182
+ if (labelledBy) return { value: labelledBy, source: "explicit" };
183
+
184
+ const ariaLabel = normalizeText(element.getAttribute("aria-label"));
185
+ if (ariaLabel) return { value: ariaLabel, source: "explicit" };
186
+
187
+ if (
188
+ element instanceof HTMLInputElement ||
189
+ element instanceof HTMLTextAreaElement ||
190
+ element instanceof HTMLSelectElement
191
+ ) {
192
+ const labels = Array.from(element.labels ?? []);
193
+ const label = normalizeText(
194
+ labels.map((item) => item.textContent ?? "").join(" "),
195
+ );
196
+ if (label) return { value: label, source: "explicit" };
197
+ }
198
+
199
+ const legend = element.querySelector(":scope > legend");
200
+ if (legend) {
201
+ const text = normalizeText(legend.textContent);
202
+ if (text) return { value: text, source: "explicit" };
203
+ }
204
+
205
+ const alt = normalizeText(element.getAttribute("alt"));
206
+ if (alt) return { value: alt, source: "explicit" };
207
+
208
+ if (element instanceof HTMLTableElement) {
209
+ const caption = normalizeText(element.caption?.textContent);
210
+ if (caption) return { value: caption, source: "explicit" };
211
+ }
212
+
213
+ if (
214
+ element instanceof HTMLInputElement &&
215
+ ["button", "submit", "reset"].includes(element.type)
216
+ ) {
217
+ const value = normalizeText(element.value);
218
+ if (value) return { value, source: "fallback" };
219
+ }
220
+
221
+ const role = computeRole(element);
222
+ if (
223
+ isActionableRole(role) ||
224
+ [
225
+ "cell",
226
+ "columnheader",
227
+ "heading",
228
+ "listitem",
229
+ "option",
230
+ "row",
231
+ "tab",
232
+ "treeitem",
233
+ ].includes(role)
234
+ ) {
235
+ const content = normalizeText(element.textContent);
236
+ if (content) return { value: content, source: "content" };
237
+ }
238
+
239
+ const title = normalizeText(element.getAttribute("title"));
240
+ if (title) return { value: title, source: "fallback" };
241
+
242
+ if (
243
+ element instanceof HTMLInputElement ||
244
+ element instanceof HTMLTextAreaElement
245
+ ) {
246
+ return {
247
+ value: normalizeText(element.placeholder),
248
+ source: "fallback",
249
+ };
250
+ }
251
+
252
+ return { value: "", source: "fallback" };
253
+ }
254
+
255
+ export function computeName(element: Element): string {
256
+ return computeNameInfo(element).value;
257
+ }
258
+
259
+ export interface ElementScrollability {
260
+ x: boolean;
261
+ y: boolean;
262
+ scrollLeft: number;
263
+ scrollTop: number;
264
+ scrollWidth: number;
265
+ scrollHeight: number;
266
+ clientWidth: number;
267
+ clientHeight: number;
268
+ }
269
+
270
+ const SCROLLABLE_OVERFLOW = new Set(["auto", "scroll", "overlay", "hidden"]);
271
+
272
+ export function getElementScrollability(
273
+ element: Element,
274
+ style = getComputedStyle(element),
275
+ ): ElementScrollability | undefined {
276
+ if (!(element instanceof HTMLElement)) return undefined;
277
+
278
+ const x =
279
+ SCROLLABLE_OVERFLOW.has(style.overflowX) &&
280
+ element.scrollWidth > element.clientWidth + 2;
281
+ const y =
282
+ SCROLLABLE_OVERFLOW.has(style.overflowY) &&
283
+ element.scrollHeight > element.clientHeight + 2;
284
+ if (!x && !y) return undefined;
285
+
286
+ return {
287
+ x,
288
+ y,
289
+ scrollLeft: element.scrollLeft,
290
+ scrollTop: element.scrollTop,
291
+ scrollWidth: element.scrollWidth,
292
+ scrollHeight: element.scrollHeight,
293
+ clientWidth: element.clientWidth,
294
+ clientHeight: element.clientHeight,
295
+ };
296
+ }
297
+
298
+ export function findNearestHeadingText(element: Element): string {
299
+ let current = composedParent(element);
300
+ while (current && current !== document.body) {
301
+ for (const child of composedChildren(current)) {
302
+ if (!(child instanceof Element) || computeRole(child) !== "heading") {
303
+ continue;
304
+ }
305
+ const name = normalizeText(computeName(child));
306
+ if (name) return name;
307
+ }
308
+ current = composedParent(current);
309
+ }
310
+ return "";
311
+ }
312
+
313
+ export function createScrollableIdentity(element: Element): string {
314
+ const name = computeNameInfo(element);
315
+ return JSON.stringify([
316
+ element.tagName.toLowerCase(),
317
+ element.id,
318
+ element.getAttribute("aria-label") ?? "",
319
+ element.getAttribute("aria-labelledby") ?? "",
320
+ name.source === "content" ? "" : normalizeText(name.value),
321
+ findNearestHeadingText(element),
322
+ ]);
323
+ }
324
+
325
+ export function isDisabled(element: Element): boolean {
326
+ let current: Element | null = element;
327
+ while (current) {
328
+ if (current.getAttribute("aria-disabled") === "true") return true;
329
+ current = composedParent(current);
330
+ }
331
+ return element.matches(":disabled");
332
+ }
333
+
334
+ function exposeInputValue(element: HTMLInputElement): boolean {
335
+ return !SENSITIVE_INPUT_TYPES.has(element.type.toLowerCase());
336
+ }
337
+
338
+ export function computeState(element: Element): SemanticState {
339
+ const state: SemanticState = {};
340
+ let active: Element | null = document.activeElement;
341
+ while (active?.shadowRoot?.activeElement) {
342
+ active = active.shadowRoot.activeElement;
343
+ }
344
+ if (active === element) state.focused = true;
345
+ if (isDisabled(element)) state.disabled = true;
346
+ if (element.getAttribute("aria-expanded") !== null) {
347
+ state.expanded = element.getAttribute("aria-expanded") === "true";
348
+ }
349
+ if (element.getAttribute("aria-checked") === "true") state.checked = true;
350
+ if (element.getAttribute("aria-selected") === "true") state.selected = true;
351
+ if (element.getAttribute("aria-invalid") === "true") state.invalid = true;
352
+ if (element.getAttribute("aria-required") === "true") state.required = true;
353
+
354
+ if (element instanceof HTMLInputElement) {
355
+ if (["checkbox", "radio"].includes(element.type) && element.checked) {
356
+ state.checked = true;
357
+ }
358
+ if (element.required) state.required = true;
359
+ if (!element.validity.valid) state.invalid = true;
360
+ if (
361
+ exposeInputValue(element) &&
362
+ !["checkbox", "radio", "button", "submit", "reset"].includes(element.type) &&
363
+ element.value
364
+ ) {
365
+ state.value = element.value;
366
+ }
367
+ }
368
+
369
+ if (element instanceof HTMLTextAreaElement) {
370
+ if (element.required) state.required = true;
371
+ if (!element.validity.valid) state.invalid = true;
372
+ if (element.value) state.value = element.value;
373
+ }
374
+
375
+ if (element instanceof HTMLSelectElement && element.value) {
376
+ state.value = element.value;
377
+ }
378
+
379
+ return state;
380
+ }
381
+
382
+ export function isActionableRole(role: string): boolean {
383
+ return role === "actionable" || ACTIONABLE_ROLES.has(role);
384
+ }
385
+
386
+ export function isContextRole(role: string): boolean {
387
+ return CONTEXT_ROLES.has(role);
388
+ }
389
+
390
+ export function findSemanticContext(element: Element): {
391
+ element?: Element;
392
+ role?: "dialog" | "row" | "listitem";
393
+ text?: string;
394
+ } {
395
+ let current = composedParent(element);
396
+ while (current && current !== document.body) {
397
+ const role = computeRole(current);
398
+ if (role === "row" || role === "listitem") {
399
+ return {
400
+ element: current,
401
+ role,
402
+ text: computeName(current),
403
+ };
404
+ }
405
+ if (role === "dialog" || role === "alertdialog") {
406
+ return {
407
+ element: current,
408
+ role: "dialog",
409
+ text: computeName(current),
410
+ };
411
+ }
412
+ current = composedParent(current);
413
+ }
414
+ return {};
415
+ }
416
+
417
+ export function createIdentity(element: Element): string {
418
+ const context = findSemanticContext(element);
419
+ return JSON.stringify([
420
+ computeRole(element),
421
+ normalizeText(computeName(element)),
422
+ context.role ?? "",
423
+ normalizeText(context.text),
424
+ ]);
425
+ }
426
+
427
+ export function isElementRendered(
428
+ element: Element,
429
+ style = getComputedStyle(element),
430
+ ): boolean {
431
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
432
+ return false;
433
+ }
434
+ if (isAgentIgnored(element)) return false;
435
+ if (
436
+ element.hasAttribute("hidden") ||
437
+ element.hasAttribute("inert") ||
438
+ element.getAttribute("aria-hidden") === "true"
439
+ ) {
440
+ return false;
441
+ }
442
+ return style.display !== "none" && style.visibility !== "hidden";
443
+ }
444
+
445
+ export function isElementTreeRendered(element: Element): boolean {
446
+ let current: Element | null = element;
447
+ while (current) {
448
+ if (!isElementRendered(current)) return false;
449
+ current = composedParent(current);
450
+ }
451
+ return true;
452
+ }
453
+
454
+ export function isElementInViewport(
455
+ element: Element,
456
+ rect = element.getBoundingClientRect(),
457
+ ): boolean {
458
+ if (rect.width <= 0 || rect.height <= 0) return false;
459
+ return (
460
+ rect.right > 0 &&
461
+ rect.bottom > 0 &&
462
+ rect.left < window.innerWidth &&
463
+ rect.top < window.innerHeight
464
+ );
465
+ }
466
+
467
+ export function isVisible(element: Element): boolean {
468
+ if (!isElementTreeRendered(element)) return false;
469
+ return isElementInViewport(element);
470
+ }
471
+
472
+ export function describeElement(element: Element): string {
473
+ const role = computeRole(element) || element.tagName.toLowerCase();
474
+ const name = computeName(element).slice(0, 120);
475
+ const context = findSemanticContext(element);
476
+ return [
477
+ context.role && context.text
478
+ ? `${context.role} ${JSON.stringify(context.text.slice(0, 160))}`
479
+ : "",
480
+ name ? `${role} ${JSON.stringify(name)}` : role,
481
+ ]
482
+ .filter(Boolean)
483
+ .join(" > ");
484
+ }
485
+
486
+ function toRegExp(value: RegexValue): RegExp | undefined {
487
+ try {
488
+ return new RegExp(value.source, value.flags);
489
+ } catch {
490
+ return undefined;
491
+ }
492
+ }
493
+
494
+ export function matchesText(
495
+ value: string,
496
+ matcher: TextMatcher | undefined,
497
+ exact = false,
498
+ ): boolean {
499
+ if (matcher === undefined) return true;
500
+ const normalized = normalizeText(value);
501
+ if (typeof matcher !== "string") {
502
+ const regex = toRegExp(matcher);
503
+ return regex ? regex.test(normalized) : false;
504
+ }
505
+ const expected = normalizeText(matcher);
506
+ return exact ? normalized === expected : normalized.includes(expected);
507
+ }
@@ -0,0 +1,5 @@
1
+ export class ActionFeedback {
2
+ before(_action: string, _element: Element): void {}
3
+
4
+ after(_action: string): void {}
5
+ }