@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,542 @@
1
+ import {
2
+ composedChildren,
3
+ composedParent,
4
+ computeNameInfo,
5
+ computeRole,
6
+ computeState,
7
+ createIdentity,
8
+ createScrollableIdentity,
9
+ findNearestHeadingText,
10
+ getElementScrollability,
11
+ isActionableRole,
12
+ isContextRole,
13
+ isElementInViewport,
14
+ isElementRendered,
15
+ normalizeText,
16
+ walkComposedElements,
17
+ } from "./dom.js";
18
+ import type { ElementScrollability, NameSource } from "./dom.js";
19
+ import type {
20
+ PageSnapshot,
21
+ SemanticState,
22
+ SnapshotBundle,
23
+ SnapshotLimits,
24
+ SnapshotRegistry,
25
+ SnapshotScope,
26
+ } from "./types.js";
27
+
28
+ interface RenderNode {
29
+ element?: Element;
30
+ role: string;
31
+ name: string;
32
+ normalizedName: string;
33
+ nameSource: NameSource;
34
+ state: SemanticState;
35
+ actionable: boolean;
36
+ scrollable?: ElementScrollability;
37
+ children: RenderNode[];
38
+ }
39
+
40
+ interface WalkContext {
41
+ scope: SnapshotScope;
42
+ activeModal?: Element;
43
+ allowedPortalRoots: Set<Element>;
44
+ styles: WeakMap<Element, CSSStyleDeclaration>;
45
+ rects: WeakMap<Element, DOMRect>;
46
+ visitedNodes: number;
47
+ maxVisitedNodes: number;
48
+ truncated: boolean;
49
+ execution?: SnapshotExecutionContext;
50
+ }
51
+
52
+ interface RenderContext {
53
+ refs: SnapshotRegistry["refs"];
54
+ maxChars: number;
55
+ usedChars: number;
56
+ lines: string[];
57
+ truncated: boolean;
58
+ renderedNodes: number;
59
+ execution?: SnapshotExecutionContext;
60
+ }
61
+
62
+ export interface SnapshotExecutionContext {
63
+ deadline: number;
64
+ signal: AbortSignal;
65
+ }
66
+
67
+ const DEFAULT_LIMITS: SnapshotLimits = {
68
+ viewportMaxChars: 18_000,
69
+ viewportMaxVisitedNodes: 12_000,
70
+ fullMaxChars: 50_000,
71
+ fullMaxVisitedNodes: 30_000,
72
+ };
73
+
74
+ const STRUCTURAL_ROLES = new Set([
75
+ "alert",
76
+ "alertdialog",
77
+ "dialog",
78
+ "form",
79
+ "heading",
80
+ "iframe",
81
+ "list",
82
+ "listbox",
83
+ "menu",
84
+ "navigation",
85
+ "progressbar",
86
+ "region",
87
+ "row",
88
+ "rowgroup",
89
+ "status",
90
+ "table",
91
+ "tooltip",
92
+ "tree",
93
+ "listitem",
94
+ ]);
95
+
96
+ const PORTAL_ROLES = new Set([
97
+ "alert",
98
+ "listbox",
99
+ "menu",
100
+ "progressbar",
101
+ "status",
102
+ "tooltip",
103
+ "tree",
104
+ ]);
105
+
106
+ const REDUNDANT_WRAPPER_ROLES = new Set(["group", "listitem"]);
107
+
108
+ function checkExecution(context?: SnapshotExecutionContext): void {
109
+ if (!context) return;
110
+ context.signal.throwIfAborted();
111
+ if (performance.now() >= context.deadline) {
112
+ throw new DOMException(
113
+ "Snapshot collection exceeded the executeCode deadline.",
114
+ "TimeoutError",
115
+ );
116
+ }
117
+ }
118
+
119
+ function nextAnimationFrame(
120
+ context?: SnapshotExecutionContext,
121
+ ): Promise<void> {
122
+ checkExecution(context);
123
+ if (!context) {
124
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
125
+ }
126
+ return new Promise((resolve, reject) => {
127
+ let settled = false;
128
+ let frame = 0;
129
+ const remaining = Math.max(0, context.deadline - performance.now());
130
+ const cleanup = (): void => {
131
+ window.clearTimeout(timer);
132
+ context.signal.removeEventListener("abort", onAbort);
133
+ };
134
+ const finish = (error?: Error): void => {
135
+ if (settled) return;
136
+ settled = true;
137
+ cleanup();
138
+ if (error) reject(error);
139
+ else resolve();
140
+ };
141
+ const onAbort = (): void => finish(
142
+ context.signal.reason instanceof Error
143
+ ? context.signal.reason
144
+ : new DOMException("Execution cancelled.", "AbortError"),
145
+ );
146
+ const timer = window.setTimeout(() => finish(new DOMException(
147
+ "Snapshot collection exceeded the executeCode deadline.",
148
+ "TimeoutError",
149
+ )), remaining);
150
+ context.signal.addEventListener("abort", onAbort, { once: true });
151
+ frame = requestAnimationFrame(() => finish());
152
+ if (settled && typeof cancelAnimationFrame === "function") {
153
+ cancelAnimationFrame(frame);
154
+ }
155
+ });
156
+ }
157
+
158
+ function cachedStyle(
159
+ element: Element,
160
+ cache: WeakMap<Element, CSSStyleDeclaration>,
161
+ ): CSSStyleDeclaration {
162
+ const existing = cache.get(element);
163
+ if (existing) return existing;
164
+ const style = getComputedStyle(element);
165
+ cache.set(element, style);
166
+ return style;
167
+ }
168
+
169
+ function cachedRect(
170
+ element: Element,
171
+ cache: WeakMap<Element, DOMRect>,
172
+ ): DOMRect {
173
+ const existing = cache.get(element);
174
+ if (existing) return existing;
175
+ const rect = element.getBoundingClientRect();
176
+ cache.set(element, rect);
177
+ return rect;
178
+ }
179
+
180
+ function isTextInScope(node: Text, context: WalkContext): boolean {
181
+ if (!normalizeText(node.data)) return false;
182
+ const parent = node.parentElement;
183
+ if (!parent) return false;
184
+ const style = cachedStyle(parent, context.styles);
185
+ if (!isElementRendered(parent, style)) return false;
186
+ if (context.scope === "full") return true;
187
+ const parentRect = cachedRect(parent, context.rects);
188
+ if (!isElementInViewport(parent, parentRect)) return false;
189
+ const range = document.createRange();
190
+ range.selectNodeContents(node);
191
+ const rect = range.getBoundingClientRect();
192
+ range.detach();
193
+ return (
194
+ rect.width > 0 &&
195
+ rect.height > 0 &&
196
+ rect.right > 0 &&
197
+ rect.bottom > 0 &&
198
+ rect.left < window.innerWidth &&
199
+ rect.top < window.innerHeight
200
+ );
201
+ }
202
+
203
+ function isElementInScope(element: Element, context: WalkContext): boolean {
204
+ const style = cachedStyle(element, context.styles);
205
+ if (!isElementRendered(element, style)) return false;
206
+ if (context.scope === "full") {
207
+ const rect = cachedRect(element, context.rects);
208
+ return rect.width > 0 && rect.height > 0;
209
+ }
210
+ return isElementInViewport(element, cachedRect(element, context.rects));
211
+ }
212
+
213
+ function isDescendantOrSelf(element: Element, ancestor: Element): boolean {
214
+ if (element === ancestor) return true;
215
+ let current: Element | null = element;
216
+ while (current) {
217
+ if (current === ancestor) return true;
218
+ current = composedParent(current);
219
+ }
220
+ return false;
221
+ }
222
+
223
+ function isAllowedByModal(element: Element, context: WalkContext): boolean {
224
+ if (!context.activeModal) return true;
225
+ if (isDescendantOrSelf(element, context.activeModal)) return true;
226
+ for (const root of context.allowedPortalRoots) {
227
+ if (isDescendantOrSelf(element, root)) return true;
228
+ }
229
+ return false;
230
+ }
231
+
232
+ function normalizeNodeChildren(node: RenderNode): void {
233
+ const merged: RenderNode[] = [];
234
+ for (const child of node.children) {
235
+ const previous = merged.at(-1);
236
+ if (
237
+ child.role === "text" &&
238
+ previous?.role === "text" &&
239
+ !child.element &&
240
+ !previous.element
241
+ ) {
242
+ previous.name = normalizeText(`${previous.name} ${child.name}`);
243
+ previous.normalizedName = previous.name;
244
+ continue;
245
+ }
246
+ merged.push(child);
247
+ }
248
+
249
+ const directText = normalizeText(
250
+ merged
251
+ .filter((child) => child.role === "text")
252
+ .map((child) => child.name)
253
+ .join(" "),
254
+ );
255
+ node.children =
256
+ node.normalizedName &&
257
+ directText &&
258
+ node.normalizedName === directText
259
+ ? merged.filter((child) => child.role !== "text")
260
+ : merged;
261
+
262
+ const onlyChild = node.children.length === 1 ? node.children[0] : undefined;
263
+ if (
264
+ REDUNDANT_WRAPPER_ROLES.has(node.role) &&
265
+ node.nameSource === "content" &&
266
+ !node.actionable &&
267
+ Object.keys(node.state).length === 0 &&
268
+ onlyChild &&
269
+ onlyChild.role !== "text" &&
270
+ node.normalizedName === onlyChild.normalizedName
271
+ ) {
272
+ node.name = "";
273
+ node.normalizedName = "";
274
+ }
275
+ }
276
+
277
+ function visitNode(node: Node, context: WalkContext): RenderNode[] {
278
+ context.visitedNodes += 1;
279
+ if (context.visitedNodes % 128 === 0) checkExecution(context.execution);
280
+ if (context.visitedNodes > context.maxVisitedNodes) {
281
+ context.truncated = true;
282
+ return [];
283
+ }
284
+
285
+ if (node instanceof Text) {
286
+ if (!isTextInScope(node, context)) return [];
287
+ const parent = node.parentElement;
288
+ if (parent && !isAllowedByModal(parent, context)) return [];
289
+ return [{
290
+ role: "text",
291
+ name: normalizeText(node.data),
292
+ normalizedName: normalizeText(node.data),
293
+ nameSource: "content",
294
+ state: {},
295
+ actionable: false,
296
+ children: [],
297
+ }];
298
+ }
299
+
300
+ if (!(node instanceof Element)) return [];
301
+ const tag = node.tagName.toLowerCase();
302
+ if (["script", "style", "template", "meta", "link", "noscript"].includes(tag)) {
303
+ return [];
304
+ }
305
+
306
+ const style = cachedStyle(node, context.styles);
307
+ if (!isElementRendered(node, style)) return [];
308
+
309
+ const children = composedChildren(node).flatMap((child) =>
310
+ visitNode(child, context),
311
+ );
312
+ const inScope = isElementInScope(node, context);
313
+ const allowed = isAllowedByModal(node, context);
314
+ const role = computeRole(node);
315
+ const state = computeState(node);
316
+ const inferredActionable = isActionableRole(role);
317
+ const focusOnlyActionable =
318
+ role === "actionable" &&
319
+ node instanceof HTMLElement &&
320
+ node.tabIndex >= 0 &&
321
+ node.onclick === null &&
322
+ !node.hasAttribute("onclick") &&
323
+ !node.isContentEditable;
324
+ const scrollable = inScope && (!inferredActionable || focusOnlyActionable)
325
+ ? getElementScrollability(node, style)
326
+ : undefined;
327
+ const actionable = inferredActionable && !scrollable;
328
+ const hasState = Object.keys(state).length > 0;
329
+ const keep =
330
+ allowed &&
331
+ ((inScope && (actionable || scrollable || STRUCTURAL_ROLES.has(role) || hasState)) ||
332
+ (children.length > 0 && (isContextRole(role) || role === "table" || role === "list")));
333
+
334
+ if (!keep) return children;
335
+
336
+ const computedName = computeNameInfo(node);
337
+ const scrollableName =
338
+ scrollable && computedName.source === "content"
339
+ ? findNearestHeadingText(node)
340
+ : computedName.value || (scrollable ? findNearestHeadingText(node) : "");
341
+ const renderNode: RenderNode = {
342
+ element: node,
343
+ role: scrollable && (!role || role === "actionable")
344
+ ? "scrollable"
345
+ : role || "group",
346
+ name: scrollableName,
347
+ normalizedName: normalizeText(scrollableName),
348
+ nameSource: computedName.source,
349
+ state,
350
+ actionable,
351
+ ...(scrollable ? { scrollable } : {}),
352
+ children,
353
+ };
354
+ normalizeNodeChildren(renderNode);
355
+ return [renderNode];
356
+ }
357
+
358
+ function isVisibleModal(element: Element): boolean {
359
+ const role = computeRole(element);
360
+ const ariaModal =
361
+ role === "dialog" && element.getAttribute("aria-modal") === "true";
362
+ const alertDialog = role === "alertdialog";
363
+ let nativeModal = false;
364
+ if (element instanceof HTMLDialogElement && element.open) {
365
+ try {
366
+ nativeModal = element.matches(":modal");
367
+ } catch {
368
+ nativeModal = element.open;
369
+ }
370
+ }
371
+ return (ariaModal || alertDialog || nativeModal) && isElementInViewport(element);
372
+ }
373
+
374
+ function findActiveModal(
375
+ execution?: SnapshotExecutionContext,
376
+ ): Element | undefined {
377
+ let activeModal: Element | undefined;
378
+ let visited = 0;
379
+ for (const element of walkComposedElements()) {
380
+ visited += 1;
381
+ if (visited % 128 === 0) checkExecution(execution);
382
+ if (isVisibleModal(element)) activeModal = element;
383
+ }
384
+ return activeModal;
385
+ }
386
+
387
+ function findPortalRoots(
388
+ activeModal: Element,
389
+ execution?: SnapshotExecutionContext,
390
+ ): Set<Element> {
391
+ const roots = new Set<Element>();
392
+ let visited = 0;
393
+ for (const element of walkComposedElements()) {
394
+ visited += 1;
395
+ if (visited % 128 === 0) checkExecution(execution);
396
+ if (isDescendantOrSelf(element, activeModal)) continue;
397
+ const role = computeRole(element);
398
+ if (
399
+ (PORTAL_ROLES.has(role) || element.hasAttribute("aria-live")) &&
400
+ isElementInViewport(element)
401
+ ) {
402
+ roots.add(element);
403
+ }
404
+ }
405
+ return roots;
406
+ }
407
+
408
+ function serializeState(state: SemanticState): string {
409
+ const entries = Object.entries(state);
410
+ if (entries.length === 0) return "";
411
+ return ` [${entries
412
+ .map(([key, value]) =>
413
+ typeof value === "boolean"
414
+ ? value
415
+ ? key
416
+ : `${key}=false`
417
+ : `${key}=${JSON.stringify(value)}`,
418
+ )
419
+ .join(" ")}]`;
420
+ }
421
+
422
+ function serializeScrollability(value: ElementScrollability | undefined): string {
423
+ if (!value) return "";
424
+ const axis = value.x && value.y ? "xy" : value.y ? "y" : "x";
425
+ const metrics = value.y
426
+ ? `scrollTop=${value.scrollTop} scrollHeight=${value.scrollHeight} clientHeight=${value.clientHeight}`
427
+ : `scrollLeft=${value.scrollLeft} scrollWidth=${value.scrollWidth} clientWidth=${value.clientWidth}`;
428
+ return ` [axis=${axis} ${metrics}]`;
429
+ }
430
+
431
+ function renderTree(nodes: RenderNode[], context: RenderContext): void {
432
+ const renderNode = (node: RenderNode, depth: number): void => {
433
+ if (context.truncated) return;
434
+ context.renderedNodes += 1;
435
+ if (context.renderedNodes % 128 === 0) {
436
+ checkExecution(context.execution);
437
+ }
438
+ const indent = " ".repeat(Math.min(depth, 6));
439
+ const name = node.name ? ` ${JSON.stringify(normalizeText(node.name))}` : "";
440
+ const state = serializeState(node.state);
441
+ const scrollability = serializeScrollability(node.scrollable);
442
+ const nextRef = node.actionable || node.scrollable
443
+ ? `@${context.refs.size + 1}`
444
+ : undefined;
445
+ const refText = nextRef ? ` [ref=${nextRef}]` : "";
446
+ const prefix = node.role === "text" ? `${indent}- text:` : `${indent}- ${node.role}`;
447
+ const line =
448
+ node.role === "text"
449
+ ? `${prefix} ${JSON.stringify(normalizeText(node.name))}`
450
+ : `${prefix}${name}${state}${scrollability}${refText}`;
451
+
452
+ const added = line.length + (context.lines.length > 0 ? 1 : 0);
453
+ if (context.usedChars + added > context.maxChars) {
454
+ context.truncated = true;
455
+ return;
456
+ }
457
+
458
+ context.lines.push(line);
459
+ context.usedChars += added;
460
+ if (nextRef && node.element) {
461
+ context.refs.set(nextRef, {
462
+ element: node.element,
463
+ identity: node.scrollable
464
+ ? createScrollableIdentity(node.element)
465
+ : createIdentity(node.element),
466
+ kind: node.scrollable ? "scrollable" : "element",
467
+ });
468
+ }
469
+
470
+ for (const child of node.children) renderNode(child, depth + 1);
471
+ };
472
+
473
+ for (const node of nodes) renderNode(node, 0);
474
+ }
475
+
476
+ export async function createSnapshot(
477
+ scope: SnapshotScope = "viewport",
478
+ limits: Partial<SnapshotLimits> = {},
479
+ execution?: SnapshotExecutionContext,
480
+ ): Promise<SnapshotBundle> {
481
+ await nextAnimationFrame(execution);
482
+ checkExecution(execution);
483
+ const resolvedLimits = { ...DEFAULT_LIMITS, ...limits };
484
+ const activeModal = findActiveModal(execution);
485
+ const walkContext: WalkContext = {
486
+ scope,
487
+ ...(activeModal ? { activeModal } : {}),
488
+ allowedPortalRoots: activeModal
489
+ ? findPortalRoots(activeModal, execution)
490
+ : new Set(),
491
+ styles: new WeakMap(),
492
+ rects: new WeakMap(),
493
+ visitedNodes: 0,
494
+ maxVisitedNodes:
495
+ scope === "viewport"
496
+ ? resolvedLimits.viewportMaxVisitedNodes
497
+ : resolvedLimits.fullMaxVisitedNodes,
498
+ truncated: false,
499
+ ...(execution ? { execution } : {}),
500
+ };
501
+
502
+ const tree = composedChildren(document.body).flatMap((node) =>
503
+ visitNode(node, walkContext),
504
+ );
505
+ const snapshotId = `s-${crypto.randomUUID()}`;
506
+ const registry: SnapshotRegistry = {
507
+ snapshotId,
508
+ refs: new Map(),
509
+ };
510
+ const renderContext: RenderContext = {
511
+ refs: registry.refs,
512
+ maxChars:
513
+ scope === "viewport"
514
+ ? resolvedLimits.viewportMaxChars
515
+ : resolvedLimits.fullMaxChars,
516
+ usedChars: 0,
517
+ lines: [],
518
+ truncated: false,
519
+ renderedNodes: 0,
520
+ ...(execution ? { execution } : {}),
521
+ };
522
+ renderTree(tree, renderContext);
523
+ checkExecution(execution);
524
+
525
+ const snapshot: PageSnapshot = {
526
+ snapshotId,
527
+ scope,
528
+ url: location.href,
529
+ title: document.title,
530
+ content: renderContext.lines.join("\n"),
531
+ truncated: walkContext.truncated || renderContext.truncated,
532
+ };
533
+
534
+ return { snapshot, registry };
535
+ }
536
+
537
+ export async function getSnapshot(
538
+ scope: SnapshotScope = "viewport",
539
+ limits: Partial<SnapshotLimits> = {},
540
+ ): Promise<PageSnapshot> {
541
+ return (await createSnapshot(scope, limits)).snapshot;
542
+ }