@silbercue/chrome 0.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/build/cache/a11y-tree.d.ts +252 -0
  4. package/build/cache/a11y-tree.js +1956 -0
  5. package/build/cache/index.d.ts +8 -0
  6. package/build/cache/index.js +4 -0
  7. package/build/cache/selector-cache.d.ts +47 -0
  8. package/build/cache/selector-cache.js +119 -0
  9. package/build/cache/session-defaults.d.ts +27 -0
  10. package/build/cache/session-defaults.js +130 -0
  11. package/build/cache/tab-state-cache.d.ts +39 -0
  12. package/build/cache/tab-state-cache.js +171 -0
  13. package/build/cdp/cdp-client.d.ts +25 -0
  14. package/build/cdp/cdp-client.js +146 -0
  15. package/build/cdp/chrome-launcher.d.ts +85 -0
  16. package/build/cdp/chrome-launcher.js +502 -0
  17. package/build/cdp/console-collector.d.ts +53 -0
  18. package/build/cdp/console-collector.js +147 -0
  19. package/build/cdp/debug.d.ts +1 -0
  20. package/build/cdp/debug.js +6 -0
  21. package/build/cdp/dialog-handler.d.ts +54 -0
  22. package/build/cdp/dialog-handler.js +129 -0
  23. package/build/cdp/dom-watcher.d.ts +45 -0
  24. package/build/cdp/dom-watcher.js +195 -0
  25. package/build/cdp/emulation.d.ts +12 -0
  26. package/build/cdp/emulation.js +17 -0
  27. package/build/cdp/index.d.ts +11 -0
  28. package/build/cdp/index.js +6 -0
  29. package/build/cdp/network-collector.d.ts +77 -0
  30. package/build/cdp/network-collector.js +257 -0
  31. package/build/cdp/protocol.d.ts +20 -0
  32. package/build/cdp/protocol.js +1 -0
  33. package/build/cdp/session-manager.d.ts +62 -0
  34. package/build/cdp/session-manager.js +205 -0
  35. package/build/cdp/settle.d.ts +16 -0
  36. package/build/cdp/settle.js +71 -0
  37. package/build/cli/license-commands.d.ts +19 -0
  38. package/build/cli/license-commands.js +199 -0
  39. package/build/cli/top-level-commands.d.ts +49 -0
  40. package/build/cli/top-level-commands.js +222 -0
  41. package/build/hooks/index.d.ts +2 -0
  42. package/build/hooks/index.js +1 -0
  43. package/build/hooks/pro-hooks.d.ts +126 -0
  44. package/build/hooks/pro-hooks.js +17 -0
  45. package/build/index.d.ts +4 -0
  46. package/build/index.js +86 -0
  47. package/build/license/free-tier-config.d.ts +14 -0
  48. package/build/license/free-tier-config.js +18 -0
  49. package/build/license/index.d.ts +4 -0
  50. package/build/license/index.js +2 -0
  51. package/build/license/license-status.d.ts +15 -0
  52. package/build/license/license-status.js +9 -0
  53. package/build/overlay/session-overlay.d.ts +22 -0
  54. package/build/overlay/session-overlay.js +372 -0
  55. package/build/plan/index.d.ts +7 -0
  56. package/build/plan/index.js +4 -0
  57. package/build/plan/plan-conditions.d.ts +12 -0
  58. package/build/plan/plan-conditions.js +242 -0
  59. package/build/plan/plan-executor.d.ts +49 -0
  60. package/build/plan/plan-executor.js +259 -0
  61. package/build/plan/plan-state-store.d.ts +24 -0
  62. package/build/plan/plan-state-store.js +43 -0
  63. package/build/plan/plan-variables.d.ts +16 -0
  64. package/build/plan/plan-variables.js +71 -0
  65. package/build/registry.d.ts +124 -0
  66. package/build/registry.js +884 -0
  67. package/build/server.d.ts +1 -0
  68. package/build/server.js +245 -0
  69. package/build/tools/click.d.ts +34 -0
  70. package/build/tools/click.js +293 -0
  71. package/build/tools/configure-session.d.ts +15 -0
  72. package/build/tools/configure-session.js +45 -0
  73. package/build/tools/console-logs.d.ts +18 -0
  74. package/build/tools/console-logs.js +44 -0
  75. package/build/tools/dom-snapshot.d.ts +13 -0
  76. package/build/tools/dom-snapshot.js +259 -0
  77. package/build/tools/element-utils.d.ts +23 -0
  78. package/build/tools/element-utils.js +133 -0
  79. package/build/tools/error-utils.d.ts +8 -0
  80. package/build/tools/error-utils.js +27 -0
  81. package/build/tools/evaluate.d.ts +34 -0
  82. package/build/tools/evaluate.js +217 -0
  83. package/build/tools/file-upload.d.ts +20 -0
  84. package/build/tools/file-upload.js +174 -0
  85. package/build/tools/fill-form.d.ts +39 -0
  86. package/build/tools/fill-form.js +256 -0
  87. package/build/tools/handle-dialog.d.ts +15 -0
  88. package/build/tools/handle-dialog.js +48 -0
  89. package/build/tools/index.d.ts +35 -0
  90. package/build/tools/index.js +18 -0
  91. package/build/tools/navigate.d.ts +18 -0
  92. package/build/tools/navigate.js +111 -0
  93. package/build/tools/network-monitor.d.ts +18 -0
  94. package/build/tools/network-monitor.js +66 -0
  95. package/build/tools/observe.d.ts +44 -0
  96. package/build/tools/observe.js +339 -0
  97. package/build/tools/press-key.d.ts +33 -0
  98. package/build/tools/press-key.js +155 -0
  99. package/build/tools/read-page.d.ts +22 -0
  100. package/build/tools/read-page.js +100 -0
  101. package/build/tools/run-plan.d.ts +205 -0
  102. package/build/tools/run-plan.js +215 -0
  103. package/build/tools/screenshot.d.ts +16 -0
  104. package/build/tools/screenshot.js +283 -0
  105. package/build/tools/scroll.d.ts +28 -0
  106. package/build/tools/scroll.js +143 -0
  107. package/build/tools/switch-tab.d.ts +26 -0
  108. package/build/tools/switch-tab.js +355 -0
  109. package/build/tools/tab-status.d.ts +7 -0
  110. package/build/tools/tab-status.js +50 -0
  111. package/build/tools/type.d.ts +31 -0
  112. package/build/tools/type.js +247 -0
  113. package/build/tools/virtual-desk.d.ts +7 -0
  114. package/build/tools/virtual-desk.js +108 -0
  115. package/build/tools/visual-constants.d.ts +3 -0
  116. package/build/tools/visual-constants.js +10 -0
  117. package/build/tools/wait-for.d.ts +26 -0
  118. package/build/tools/wait-for.js +323 -0
  119. package/build/transport/index.d.ts +3 -0
  120. package/build/transport/index.js +2 -0
  121. package/build/transport/pipe-transport.d.ts +18 -0
  122. package/build/transport/pipe-transport.js +63 -0
  123. package/build/transport/transport.d.ts +8 -0
  124. package/build/transport/transport.js +1 -0
  125. package/build/transport/websocket-transport.d.ts +22 -0
  126. package/build/transport/websocket-transport.js +200 -0
  127. package/build/types.d.ts +21 -0
  128. package/build/types.js +1 -0
  129. package/package.json +62 -0
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ export const consoleLogsSchema = z.object({
3
+ level: z.enum(["info", "warning", "error", "debug"])
4
+ .optional()
5
+ .describe("Filter by log level"),
6
+ pattern: z.string()
7
+ .optional()
8
+ .describe("Regex pattern to match against log text"),
9
+ clear: z.boolean()
10
+ .optional()
11
+ .default(false)
12
+ .describe("Clear the log buffer after returning results"),
13
+ });
14
+ export async function consoleLogsHandler(params, consoleCollector) {
15
+ const start = performance.now();
16
+ // 1. Retrieve logs (filtered or all)
17
+ let logs;
18
+ try {
19
+ logs = (params.level || params.pattern)
20
+ ? consoleCollector.getFiltered(params.level, params.pattern)
21
+ : consoleCollector.getAll();
22
+ }
23
+ catch (err) {
24
+ // Invalid regex pattern
25
+ return {
26
+ content: [{ type: "text", text: `Invalid regex pattern: ${err.message}` }],
27
+ isError: true,
28
+ _meta: { elapsedMs: Math.round(performance.now() - start), method: "console_logs" },
29
+ };
30
+ }
31
+ // 2. Clear buffer after retrieval if requested
32
+ if (params.clear) {
33
+ consoleCollector.clear();
34
+ }
35
+ // 3. Return logs as JSON array
36
+ return {
37
+ content: [{ type: "text", text: JSON.stringify(logs) }],
38
+ _meta: {
39
+ elapsedMs: Math.round(performance.now() - start),
40
+ method: "console_logs",
41
+ count: logs.length,
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import type { CdpClient } from "../cdp/cdp-client.js";
3
+ import type { SessionManager } from "../cdp/session-manager.js";
4
+ import type { ToolResponse } from "../types.js";
5
+ export declare const domSnapshotSchema: z.ZodObject<{
6
+ ref: z.ZodOptional<z.ZodString>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ ref?: string | undefined;
9
+ }, {
10
+ ref?: string | undefined;
11
+ }>;
12
+ export type DomSnapshotParams = z.infer<typeof domSnapshotSchema>;
13
+ export declare function domSnapshotHandler(params: DomSnapshotParams, cdpClient: CdpClient, sessionId?: string, sessionManager?: SessionManager): Promise<ToolResponse>;
@@ -0,0 +1,259 @@
1
+ import { z } from "zod";
2
+ import { a11yTree } from "../cache/a11y-tree.js";
3
+ import { wrapCdpError } from "./error-utils.js";
4
+ import { EMULATED_WIDTH, EMULATED_HEIGHT } from "../cdp/emulation.js";
5
+ import { CLICKABLE_TAGS, CLICKABLE_ROLES, COMPUTED_STYLES } from "./visual-constants.js";
6
+ // --- Schema ---
7
+ export const domSnapshotSchema = z.object({
8
+ ref: z
9
+ .string()
10
+ .optional()
11
+ .describe("Element ref (e.g. 'e42') to get subtree snapshot for"),
12
+ });
13
+ // --- Constants ---
14
+ const INTERACTIVE_TAGS = new Set([
15
+ "A", "BUTTON", "INPUT", "SELECT", "TEXTAREA",
16
+ "IMG", "H1", "H2", "H3", "H4", "H5", "H6", "LABEL",
17
+ ]);
18
+ const INTERACTIVE_ROLES = new Set([
19
+ "button", "link", "checkbox", "tab", "menuitem",
20
+ "radio", "switch", "slider", "option", "treeitem",
21
+ "textbox", "searchbox", "combobox", "spinbutton",
22
+ "menuitemcheckbox", "menuitemradio",
23
+ ]);
24
+ const MAX_ELEMENTS = 150;
25
+ const MIN_SIZE = 10;
26
+ // --- Helpers ---
27
+ function getStringAt(strings, index) {
28
+ if (index < 0 || index >= strings.length)
29
+ return "";
30
+ return strings[index];
31
+ }
32
+ function extractRole(attributes, strings) {
33
+ // attributes = [nameIdx, valueIdx, nameIdx, valueIdx, ...]
34
+ for (let i = 0; i < attributes.length - 1; i += 2) {
35
+ if (getStringAt(strings, attributes[i]) === "role") {
36
+ return getStringAt(strings, attributes[i + 1]);
37
+ }
38
+ }
39
+ return "";
40
+ }
41
+ function hasExplicitRole(attributes, strings) {
42
+ for (let i = 0; i < attributes.length - 1; i += 2) {
43
+ if (getStringAt(strings, attributes[i]) === "role")
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ // --- Handler ---
49
+ export async function domSnapshotHandler(params, cdpClient, sessionId, sessionManager) {
50
+ const start = performance.now();
51
+ const method = "dom_snapshot";
52
+ try {
53
+ // Ensure A11y refs exist — only trigger getTree if it has never been loaded
54
+ if (!a11yTree.hasRefs()) {
55
+ try {
56
+ await a11yTree.getTree(cdpClient, sessionId, {}, sessionManager);
57
+ }
58
+ catch {
59
+ // Non-fatal: proceed without refs if a11y tree fetch fails
60
+ }
61
+ }
62
+ // CDP call
63
+ const snapshot = await cdpClient.send("DOMSnapshot.captureSnapshot", {
64
+ computedStyles: [...COMPUTED_STYLES],
65
+ includeDOMRects: true,
66
+ includeBlendedBackgroundColors: true,
67
+ includePaintOrder: true,
68
+ }, sessionId);
69
+ if (!snapshot.documents || snapshot.documents.length === 0) {
70
+ const elapsedMs = Math.round(performance.now() - start);
71
+ return {
72
+ content: [{ type: "text", text: "[]" }],
73
+ _meta: { elapsedMs, method, elementCount: 0, filteredFrom: 0 },
74
+ };
75
+ }
76
+ const doc = snapshot.documents[0];
77
+ const strings = snapshot.strings;
78
+ const totalNodes = doc.nodes.backendNodeId.length;
79
+ // Resolve subtree constraint if ref is given
80
+ let subtreeAncestorIndex;
81
+ let subtreeDescendants;
82
+ if (params.ref) {
83
+ const targetBackendNodeId = a11yTree.resolveRef(params.ref);
84
+ if (targetBackendNodeId === undefined) {
85
+ const elapsedMs = Math.round(performance.now() - start);
86
+ return {
87
+ content: [{ type: "text", text: `Element ${params.ref} not found.` }],
88
+ isError: true,
89
+ _meta: { elapsedMs, method },
90
+ };
91
+ }
92
+ // Find the node index for this backendNodeId
93
+ for (let i = 0; i < doc.nodes.backendNodeId.length; i++) {
94
+ if (doc.nodes.backendNodeId[i] === targetBackendNodeId) {
95
+ subtreeAncestorIndex = i;
96
+ break;
97
+ }
98
+ }
99
+ if (subtreeAncestorIndex === undefined) {
100
+ const elapsedMs = Math.round(performance.now() - start);
101
+ return {
102
+ content: [{ type: "text", text: `Element ${params.ref} not found in DOM snapshot.` }],
103
+ isError: true,
104
+ _meta: { elapsedMs, method },
105
+ };
106
+ }
107
+ // Collect all descendants via parent→children index, then BFS
108
+ const childrenOf = new Map();
109
+ for (let i = 0; i < doc.nodes.parentIndex.length; i++) {
110
+ const parent = doc.nodes.parentIndex[i];
111
+ if (parent < 0)
112
+ continue;
113
+ let children = childrenOf.get(parent);
114
+ if (!children) {
115
+ children = [];
116
+ childrenOf.set(parent, children);
117
+ }
118
+ children.push(i);
119
+ }
120
+ subtreeDescendants = new Set();
121
+ const queue = [subtreeAncestorIndex];
122
+ while (queue.length > 0) {
123
+ const idx = queue.pop();
124
+ subtreeDescendants.add(idx);
125
+ const kids = childrenOf.get(idx);
126
+ if (kids) {
127
+ for (const kid of kids)
128
+ queue.push(kid);
129
+ }
130
+ }
131
+ }
132
+ // Build layout index map: nodeIndex → layoutIndex
133
+ const layoutMap = new Map();
134
+ for (let li = 0; li < doc.layout.nodeIndex.length; li++) {
135
+ layoutMap.set(doc.layout.nodeIndex[li], li);
136
+ }
137
+ // --- 6-Stage Filter Pipeline ---
138
+ const pipeline = [];
139
+ for (let ni = 0; ni < totalNodes; ni++) {
140
+ // Subtree filter: skip nodes not in subtree
141
+ if (subtreeDescendants && !subtreeDescendants.has(ni))
142
+ continue;
143
+ // Stage 1: Simplification — tag whitelist + explicit role
144
+ const tag = getStringAt(strings, doc.nodes.nodeName[ni]);
145
+ const attrs = doc.nodes.attributes[ni] ?? [];
146
+ const role = extractRole(attrs, strings);
147
+ if (!INTERACTIVE_TAGS.has(tag) && !hasExplicitRole(attrs, strings) && !INTERACTIVE_ROLES.has(role)) {
148
+ continue;
149
+ }
150
+ // Must have layout data
151
+ const li = layoutMap.get(ni);
152
+ if (li === undefined)
153
+ continue;
154
+ // Stage 2: Visibility Filter
155
+ const styleProps = doc.layout.styles[li] ?? [];
156
+ // computedStyles order matches COMPUTED_STYLES: display, visibility, color, bg-color, font-size, position, z-index
157
+ const displayVal = getStringAt(strings, styleProps[0]);
158
+ const visibilityVal = getStringAt(strings, styleProps[1]);
159
+ if (displayVal === "none" || visibilityVal === "hidden")
160
+ continue;
161
+ // Stage 3: BBox Filter
162
+ const boundsArr = doc.layout.bounds[li];
163
+ if (!boundsArr || boundsArr.length < 4)
164
+ continue;
165
+ const [x, y, w, h] = boundsArr;
166
+ if (w < 1 || h < 1)
167
+ continue;
168
+ if (x + w < 0 || y + h < 0 || x > EMULATED_WIDTH || y > EMULATED_HEIGHT)
169
+ continue;
170
+ // Stage 4: Size Filter (skip for explicitly requested ref subtree root)
171
+ const isSubtreeRoot = subtreeAncestorIndex !== undefined && ni === subtreeAncestorIndex;
172
+ if (!isSubtreeRoot && (w < MIN_SIZE || h < MIN_SIZE))
173
+ continue;
174
+ // Stage 5: Ref assignment
175
+ const backendNodeId = doc.nodes.backendNodeId[ni];
176
+ const ref = a11yTree.getRefForBackendNodeId(backendNodeId);
177
+ if (!ref)
178
+ continue;
179
+ // Get a11y node info for name
180
+ const nodeInfo = a11yTree.getNodeInfo(backendNodeId);
181
+ // Collect styles
182
+ const colorVal = getStringAt(strings, styleProps[2]);
183
+ const bgColorVal = getStringAt(strings, styleProps[3]);
184
+ const fontSizeVal = getStringAt(strings, styleProps[4]);
185
+ // styleProps[5] = position, styleProps[6] = z-index
186
+ const zIndexVal = getStringAt(strings, styleProps[6]);
187
+ const styles = {};
188
+ if (colorVal)
189
+ styles.color = colorVal;
190
+ if (bgColorVal)
191
+ styles.bg = bgColorVal;
192
+ if (fontSizeVal)
193
+ styles.fontSize = fontSizeVal;
194
+ // isClickable heuristic (Task 4)
195
+ const isClickable = CLICKABLE_TAGS.has(tag) || CLICKABLE_ROLES.has(role);
196
+ const paintOrder = doc.layout.paintOrders[li] ?? 0;
197
+ const zIndex = (zIndexVal && zIndexVal !== "auto") ? parseInt(zIndexVal, 10) : null;
198
+ pipeline.push({
199
+ nodeIndex: ni,
200
+ layoutIndex: li,
201
+ tag: tag.toLowerCase(),
202
+ role: role || (nodeInfo?.role ?? ""),
203
+ name: nodeInfo?.name ?? "",
204
+ ref,
205
+ bounds: {
206
+ x: Math.round(x),
207
+ y: Math.round(y),
208
+ w: Math.round(w),
209
+ h: Math.round(h),
210
+ },
211
+ styles,
212
+ isClickable,
213
+ paintOrder,
214
+ zIndex,
215
+ });
216
+ }
217
+ // Stage 6: Token Budget Guard
218
+ let elements = pipeline;
219
+ if (elements.length > MAX_ELEMENTS) {
220
+ // Prioritize interactive elements, then by paintOrder
221
+ elements.sort((a, b) => {
222
+ if (a.isClickable !== b.isClickable)
223
+ return a.isClickable ? -1 : 1;
224
+ return b.paintOrder - a.paintOrder;
225
+ });
226
+ elements = elements.slice(0, MAX_ELEMENTS);
227
+ }
228
+ // Format compact output
229
+ const output = elements.map((el) => ({
230
+ ref: el.ref,
231
+ tag: el.tag,
232
+ role: el.role,
233
+ name: el.name,
234
+ bounds: el.bounds,
235
+ styles: el.styles,
236
+ isClickable: el.isClickable,
237
+ paintOrder: el.paintOrder,
238
+ zIndex: el.zIndex,
239
+ }));
240
+ const elapsedMs = Math.round(performance.now() - start);
241
+ return {
242
+ content: [{ type: "text", text: JSON.stringify(output) }],
243
+ _meta: {
244
+ elapsedMs,
245
+ method,
246
+ elementCount: output.length,
247
+ filteredFrom: totalNodes,
248
+ },
249
+ };
250
+ }
251
+ catch (err) {
252
+ const elapsedMs = Math.round(performance.now() - start);
253
+ return {
254
+ content: [{ type: "text", text: wrapCdpError(err, "dom_snapshot") }],
255
+ isError: true,
256
+ _meta: { elapsedMs, method },
257
+ };
258
+ }
259
+ }
@@ -0,0 +1,23 @@
1
+ import type { CdpClient } from "../cdp/cdp-client.js";
2
+ import type { SessionManager } from "../cdp/session-manager.js";
3
+ export interface ResolvedElement {
4
+ backendNodeId: number;
5
+ objectId: string;
6
+ role: string;
7
+ name: string;
8
+ resolvedVia: "ref" | "css";
9
+ resolvedSessionId: string;
10
+ }
11
+ export interface ElementTarget {
12
+ ref?: string;
13
+ selector?: string;
14
+ }
15
+ /**
16
+ * Resolve an element target (ref or CSS selector) to a ResolvedElement.
17
+ * When both ref and selector are given, ref takes priority.
18
+ * Throws RefNotFoundError when a ref cannot be resolved.
19
+ * When sessionManager is provided, routes to the correct OOPIF session.
20
+ */
21
+ export declare function resolveElement(cdpClient: CdpClient, sessionId: string, target: ElementTarget, sessionManager?: SessionManager): Promise<ResolvedElement>;
22
+ export declare function buildRefNotFoundError(ref: string, roleFilter?: Set<string>): string;
23
+ export { RefNotFoundError } from "../cache/a11y-tree.js";
@@ -0,0 +1,133 @@
1
+ import { a11yTree, RefNotFoundError } from "../cache/a11y-tree.js";
2
+ import { selectorCache } from "../cache/selector-cache.js";
3
+ import { wrapCdpError } from "./error-utils.js";
4
+ import { debug } from "../cdp/debug.js";
5
+ // --- Element Resolution ---
6
+ /**
7
+ * Resolve an element target (ref or CSS selector) to a ResolvedElement.
8
+ * When both ref and selector are given, ref takes priority.
9
+ * Throws RefNotFoundError when a ref cannot be resolved.
10
+ * When sessionManager is provided, routes to the correct OOPIF session.
11
+ */
12
+ export async function resolveElement(cdpClient, sessionId, target, sessionManager) {
13
+ // Ref path (preferred)
14
+ if (target.ref) {
15
+ // --- Selector-Cache Check (Story 7.5) ---
16
+ const cached = selectorCache.get(target.ref);
17
+ if (cached) {
18
+ // M1 fix: Verify cached sessionId still matches current session
19
+ const currentSessionForNode = sessionManager?.getSessionForNode(cached.backendNodeId) ?? sessionId;
20
+ const sessionMatch = cached.sessionId === currentSessionForNode;
21
+ if (!sessionMatch) {
22
+ debug("SelectorCache: session mismatch for %s (cached=%s, current=%s), treating as miss", target.ref, cached.sessionId, currentSessionForNode);
23
+ // Fall through to normal resolution — session changed
24
+ }
25
+ else {
26
+ try {
27
+ const resolved = await cdpClient.send("DOM.resolveNode", { backendNodeId: cached.backendNodeId }, currentSessionForNode);
28
+ const info = a11yTree.getNodeInfo(cached.backendNodeId);
29
+ debug("SelectorCache: hit for %s (backendNodeId=%d)", target.ref, cached.backendNodeId);
30
+ return {
31
+ backendNodeId: cached.backendNodeId,
32
+ objectId: resolved.object.objectId,
33
+ role: info?.role ?? "",
34
+ name: info?.name ?? "",
35
+ resolvedVia: "ref",
36
+ resolvedSessionId: currentSessionForNode,
37
+ };
38
+ }
39
+ catch {
40
+ // Stale cache entry — node no longer in DOM. Remove and fall through.
41
+ debug("SelectorCache: stale entry for %s, invalidating", target.ref);
42
+ selectorCache.invalidate();
43
+ }
44
+ }
45
+ }
46
+ // --- Normal Ref Resolution ---
47
+ const backendNodeId = a11yTree.resolveRef(target.ref);
48
+ if (backendNodeId === undefined) {
49
+ throw new RefNotFoundError(`Element ${target.ref} not found.`);
50
+ }
51
+ // Determine the correct session for this node (main or OOPIF)
52
+ const targetSessionId = sessionManager?.getSessionForNode(backendNodeId) ?? sessionId;
53
+ // Safety net: ensure DOM domain is enabled before resolveNode.
54
+ // DOM.getDocument implicitly enables DOM and is idempotent.
55
+ try {
56
+ await cdpClient.send("DOM.getDocument", { depth: 0 }, targetSessionId);
57
+ }
58
+ catch {
59
+ // Best-effort — resolveNode may still work with backendNodeId
60
+ }
61
+ // Get objectId via DOM.resolveNode — may fail for stale refs (node removed from DOM)
62
+ let resolved;
63
+ try {
64
+ resolved = await cdpClient.send("DOM.resolveNode", { backendNodeId }, targetSessionId);
65
+ }
66
+ catch (err) {
67
+ // M1: Distinguish CDP connection errors from stale refs
68
+ const wrapped = wrapCdpError(err, "resolveElement");
69
+ if (wrapped.startsWith("CDP connection lost")) {
70
+ throw new Error(wrapped);
71
+ }
72
+ // Distinguish "DOM not enabled" from actual stale refs
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ if (msg.includes("DOM agent needs to be enabled")) {
75
+ throw new Error(`DOM domain not enabled for session — this is a server bug, not a stale ref. Try calling read_page first or report this issue.`);
76
+ }
77
+ throw new RefNotFoundError(`Element ${target.ref} not found (stale ref — node no longer in DOM).`);
78
+ }
79
+ // Get role/name directly from nodeInfoMap via backendNodeId
80
+ const info = a11yTree.getNodeInfo(backendNodeId);
81
+ // Cache the resolved ref for future lookups (Story 7.5)
82
+ // H1 fix: Pass URL + nodeCount so set() can compute on-the-fly fingerprint
83
+ // when no fingerprint is active yet (first resolution after navigation)
84
+ selectorCache.set(target.ref, backendNodeId, targetSessionId, a11yTree.currentUrl, a11yTree.refCount);
85
+ return {
86
+ backendNodeId,
87
+ objectId: resolved.object.objectId,
88
+ role: info?.role ?? "",
89
+ name: info?.name ?? "",
90
+ resolvedVia: "ref",
91
+ resolvedSessionId: targetSessionId,
92
+ };
93
+ }
94
+ // CSS path — always main frame (CSS selectors don't work cross-frame)
95
+ const doc = await cdpClient.send("DOM.getDocument", { depth: 0 }, sessionId);
96
+ const queryResult = await cdpClient.send("DOM.querySelector", { nodeId: doc.root.nodeId, selector: target.selector }, sessionId);
97
+ if (queryResult.nodeId === 0) {
98
+ throw new Error(`Element not found for selector '${target.selector}'`);
99
+ }
100
+ const desc = await cdpClient.send("DOM.describeNode", { nodeId: queryResult.nodeId }, sessionId);
101
+ // Get objectId
102
+ const resolved = await cdpClient.send("DOM.resolveNode", { backendNodeId: desc.node.backendNodeId }, sessionId);
103
+ return {
104
+ backendNodeId: desc.node.backendNodeId,
105
+ objectId: resolved.object.objectId,
106
+ role: "", // role not reliably available via CSS path
107
+ name: "", // name not reliably available via CSS path
108
+ resolvedVia: "css",
109
+ resolvedSessionId: sessionId,
110
+ };
111
+ }
112
+ // --- Contextual Error Messages ---
113
+ /**
114
+ * Build a contextual "did you mean?" error message for a missing ref.
115
+ * When roleFilter is provided, only suggests elements matching those roles.
116
+ */
117
+ // Roles that are useless as suggestions when their name is empty (BUG-013)
118
+ const CONTAINER_ROLES = new Set(["generic", "group", "none", "Section", "div"]);
119
+ export function buildRefNotFoundError(ref, roleFilter) {
120
+ const suggestion = a11yTree.findClosestRef(ref, roleFilter);
121
+ // FR-004 + BUG-013: Detect stale / useless suggestions.
122
+ // — no suggestion at all
123
+ // — suggestion ref equals the requested ref (safety-net)
124
+ // — suggestion is an unnamed container (e.g. generic '') — not actionable
125
+ const isUseless = !suggestion ||
126
+ suggestion.ref === ref ||
127
+ (!suggestion.name && CONTAINER_ROLES.has(suggestion.role));
128
+ if (isUseless) {
129
+ return `Element ${ref} not found — refs may be stale after page navigation or DOM changes. Use read_page to get fresh refs, or use a CSS selector instead.`;
130
+ }
131
+ return `Element ${ref} not found. Did you mean ${suggestion.ref} (${suggestion.role} '${suggestion.name}')?`;
132
+ }
133
+ export { RefNotFoundError } from "../cache/a11y-tree.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Wrap CDP connection errors into user-friendly error messages.
3
+ * Used across all tools to provide consistent error messages during reconnect scenarios.
4
+ *
5
+ * @param elementHint - Optional identifier for the element involved (e.g. ref "e5" or selector "#btn").
6
+ * Used to enrich "not visible" errors so the LLM knows which element failed.
7
+ */
8
+ export declare function wrapCdpError(err: unknown, toolName: string, elementHint?: string): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wrap CDP connection errors into user-friendly error messages.
3
+ * Used across all tools to provide consistent error messages during reconnect scenarios.
4
+ *
5
+ * @param elementHint - Optional identifier for the element involved (e.g. ref "e5" or selector "#btn").
6
+ * Used to enrich "not visible" errors so the LLM knows which element failed.
7
+ */
8
+ export function wrapCdpError(err, toolName, elementHint) {
9
+ const message = err instanceof Error ? err.message : String(err);
10
+ if (message.includes("CdpClient is closed") ||
11
+ message.includes("CdpClient closed") ||
12
+ message.includes("Transport is not connected") ||
13
+ message.includes("Transport closed unexpectedly")) {
14
+ return "CDP connection lost. The server is attempting to reconnect. Retry your request in a few seconds.";
15
+ }
16
+ if (message.includes("Session with given id not found") ||
17
+ message.includes("No target with given id found")) {
18
+ return `${toolName} failed: ${message}. Use virtual_desk to discover available tabs and reconnect.`;
19
+ }
20
+ // FR-003: Element exists in DOM but has no visual layout (display:none, hidden tab, etc.)
21
+ if (message.includes("Node does not have a layout object") ||
22
+ message.includes("Could not compute content quads")) {
23
+ const elem = elementHint ?? "target element";
24
+ return `${toolName} failed: Element ${elem} exists in the DOM but is not visible (no layout — likely display:none, hidden, or inside an inactive tab/panel). Use a CSS selector targeting the visible instance, or check if the element needs to be revealed first (e.g. switch tab, expand section).`;
25
+ }
26
+ return `${toolName} failed: ${message}`;
27
+ }
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+ import type { CdpClient } from "../cdp/cdp-client.js";
3
+ import type { ToolResponse } from "../types.js";
4
+ /**
5
+ * Detects top-level const/let/class declarations and wraps the expression in
6
+ * an IIFE to avoid "Identifier has already been declared" errors across
7
+ * repeated Runtime.evaluate calls (which share the global scope).
8
+ *
9
+ * The last ExpressionStatement is automatically returned so callers still
10
+ * get the evaluation result.
11
+ */
12
+ export declare function wrapInIIFE(expression: string): string;
13
+ export declare const evaluateSchema: z.ZodObject<{
14
+ expression: z.ZodString;
15
+ await_promise: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ expression: string;
18
+ await_promise: boolean;
19
+ }, {
20
+ expression: string;
21
+ await_promise?: boolean | undefined;
22
+ }>;
23
+ /**
24
+ * FR-024: Detect common evaluate anti-patterns where a dedicated tool would be better.
25
+ * Returns a hint string appended to the evaluate result, or null if no pattern matched.
26
+ * Intent: "what you did isn't wrong, but there's a more reliable tool for this".
27
+ *
28
+ * Design: be specific enough to avoid false positives on legitimate DOM work
29
+ * (e.g. `document.querySelector('.card').style.width = '200px'` is NOT an
30
+ * anti-pattern — it's a style edit that no other tool covers).
31
+ */
32
+ export declare function detectEvaluateAntiPattern(expression: string): string | null;
33
+ export type EvaluateParams = z.infer<typeof evaluateSchema>;
34
+ export declare function evaluateHandler(params: EvaluateParams, cdpClient: CdpClient, sessionId?: string): Promise<ToolResponse>;