@everydaydevopsio/pilot 0.5.2 → 0.6.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 (280) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CLAUDE.md +7 -7
  3. package/README.md +108 -53
  4. package/dist/browser/browser-manager.d.ts +60 -0
  5. package/dist/browser/browser-manager.d.ts.map +1 -0
  6. package/dist/browser/browser-manager.js +348 -0
  7. package/dist/browser/browser-manager.js.map +1 -0
  8. package/dist/browser/chrome-launcher.d.ts +30 -0
  9. package/dist/browser/chrome-launcher.d.ts.map +1 -0
  10. package/dist/browser/chrome-launcher.js +217 -0
  11. package/dist/browser/chrome-launcher.js.map +1 -0
  12. package/dist/browser/connection.d.ts +23 -0
  13. package/dist/browser/connection.d.ts.map +1 -0
  14. package/dist/browser/connection.js +53 -0
  15. package/dist/browser/connection.js.map +1 -0
  16. package/dist/browser/events.d.ts +9 -0
  17. package/dist/browser/events.d.ts.map +1 -0
  18. package/dist/browser/events.js +99 -0
  19. package/dist/browser/events.js.map +1 -0
  20. package/dist/browser/index.d.ts +4 -0
  21. package/dist/browser/index.d.ts.map +1 -0
  22. package/dist/browser/index.js +4 -0
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/inspect/element-ref.d.ts +23 -0
  25. package/dist/browser/inspect/element-ref.d.ts.map +1 -0
  26. package/dist/browser/inspect/element-ref.js +52 -0
  27. package/dist/browser/inspect/element-ref.js.map +1 -0
  28. package/dist/browser/inspect/snapshot.d.ts +11 -0
  29. package/dist/browser/inspect/snapshot.d.ts.map +1 -0
  30. package/dist/browser/inspect/snapshot.js +282 -0
  31. package/dist/browser/inspect/snapshot.js.map +1 -0
  32. package/dist/browser/inspect/styles.d.ts +45 -0
  33. package/dist/browser/inspect/styles.d.ts.map +1 -0
  34. package/dist/browser/inspect/styles.js +190 -0
  35. package/dist/browser/inspect/styles.js.map +1 -0
  36. package/dist/browser/inspect/types.d.ts +35 -0
  37. package/dist/browser/inspect/types.d.ts.map +1 -0
  38. package/dist/browser/inspect/types.js +2 -0
  39. package/dist/browser/inspect/types.js.map +1 -0
  40. package/dist/browser/interaction/dialogs.d.ts +18 -0
  41. package/dist/browser/interaction/dialogs.d.ts.map +1 -0
  42. package/dist/browser/interaction/dialogs.js +35 -0
  43. package/dist/browser/interaction/dialogs.js.map +1 -0
  44. package/dist/browser/interaction/downloads.d.ts +23 -0
  45. package/dist/browser/interaction/downloads.d.ts.map +1 -0
  46. package/dist/browser/interaction/downloads.js +87 -0
  47. package/dist/browser/interaction/downloads.js.map +1 -0
  48. package/dist/browser/interaction/events.d.ts +17 -0
  49. package/dist/browser/interaction/events.d.ts.map +1 -0
  50. package/dist/browser/interaction/events.js +66 -0
  51. package/dist/browser/interaction/events.js.map +1 -0
  52. package/dist/browser/interaction/ref-resolver.d.ts +18 -0
  53. package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
  54. package/dist/browser/interaction/ref-resolver.js +32 -0
  55. package/dist/browser/interaction/ref-resolver.js.map +1 -0
  56. package/dist/browser/interaction/uploads.d.ts +4 -0
  57. package/dist/browser/interaction/uploads.d.ts.map +1 -0
  58. package/dist/browser/interaction/uploads.js +43 -0
  59. package/dist/browser/interaction/uploads.js.map +1 -0
  60. package/dist/browser/network/network-buffer.d.ts +19 -0
  61. package/dist/browser/network/network-buffer.d.ts.map +1 -0
  62. package/dist/browser/network/network-buffer.js +118 -0
  63. package/dist/browser/network/network-buffer.js.map +1 -0
  64. package/dist/browser/network/network-monitor.d.ts +8 -0
  65. package/dist/browser/network/network-monitor.d.ts.map +1 -0
  66. package/dist/browser/network/network-monitor.js +85 -0
  67. package/dist/browser/network/network-monitor.js.map +1 -0
  68. package/dist/browser/network/redaction.d.ts +3 -0
  69. package/dist/browser/network/redaction.d.ts.map +1 -0
  70. package/dist/browser/network/redaction.js +36 -0
  71. package/dist/browser/network/redaction.js.map +1 -0
  72. package/dist/browser/network/types.d.ts +45 -0
  73. package/dist/browser/network/types.d.ts.map +1 -0
  74. package/dist/browser/network/types.js +2 -0
  75. package/dist/browser/network/types.js.map +1 -0
  76. package/dist/browser/performance/analyze.d.ts +5 -0
  77. package/dist/browser/performance/analyze.d.ts.map +1 -0
  78. package/dist/browser/performance/analyze.js +230 -0
  79. package/dist/browser/performance/analyze.js.map +1 -0
  80. package/dist/browser/performance/trace.d.ts +10 -0
  81. package/dist/browser/performance/trace.d.ts.map +1 -0
  82. package/dist/browser/performance/trace.js +38 -0
  83. package/dist/browser/performance/trace.js.map +1 -0
  84. package/dist/browser/performance/types.d.ts +31 -0
  85. package/dist/browser/performance/types.d.ts.map +1 -0
  86. package/dist/browser/performance/types.js +2 -0
  87. package/dist/browser/performance/types.js.map +1 -0
  88. package/dist/browser/security/origins.d.ts +18 -0
  89. package/dist/browser/security/origins.d.ts.map +1 -0
  90. package/dist/browser/security/origins.js +84 -0
  91. package/dist/browser/security/origins.js.map +1 -0
  92. package/dist/browser/tabs.d.ts +15 -0
  93. package/dist/browser/tabs.d.ts.map +1 -0
  94. package/dist/browser/tabs.js +71 -0
  95. package/dist/browser/tabs.js.map +1 -0
  96. package/dist/browser/types.d.ts +31 -0
  97. package/dist/browser/types.d.ts.map +1 -0
  98. package/dist/browser/types.js +2 -0
  99. package/dist/browser/types.js.map +1 -0
  100. package/dist/browser.d.ts +2 -99
  101. package/dist/browser.d.ts.map +1 -1
  102. package/dist/browser.js +3 -653
  103. package/dist/browser.js.map +1 -1
  104. package/dist/commands/check.d.ts +17 -0
  105. package/dist/commands/check.d.ts.map +1 -0
  106. package/dist/commands/check.js +38 -0
  107. package/dist/commands/check.js.map +1 -0
  108. package/dist/commands/click.d.ts +3 -1
  109. package/dist/commands/click.d.ts.map +1 -1
  110. package/dist/commands/click.js +16 -4
  111. package/dist/commands/click.js.map +1 -1
  112. package/dist/commands/connect.d.ts +14 -0
  113. package/dist/commands/connect.d.ts.map +1 -0
  114. package/dist/commands/connect.js +19 -0
  115. package/dist/commands/connect.js.map +1 -0
  116. package/dist/commands/dialog.d.ts +19 -0
  117. package/dist/commands/dialog.d.ts.map +1 -0
  118. package/dist/commands/dialog.js +51 -0
  119. package/dist/commands/dialog.js.map +1 -0
  120. package/dist/commands/downloads.d.ts +30 -0
  121. package/dist/commands/downloads.d.ts.map +1 -0
  122. package/dist/commands/downloads.js +66 -0
  123. package/dist/commands/downloads.js.map +1 -0
  124. package/dist/commands/fill.d.ts +16 -0
  125. package/dist/commands/fill.d.ts.map +1 -0
  126. package/dist/commands/fill.js +15 -0
  127. package/dist/commands/fill.js.map +1 -0
  128. package/dist/commands/find.d.ts +14 -0
  129. package/dist/commands/find.d.ts.map +1 -0
  130. package/dist/commands/find.js +26 -0
  131. package/dist/commands/find.js.map +1 -0
  132. package/dist/commands/hover.d.ts +13 -0
  133. package/dist/commands/hover.d.ts.map +1 -0
  134. package/dist/commands/hover.js +24 -0
  135. package/dist/commands/hover.js.map +1 -0
  136. package/dist/commands/navigate.d.ts.map +1 -1
  137. package/dist/commands/navigate.js +13 -0
  138. package/dist/commands/navigate.js.map +1 -1
  139. package/dist/commands/network.d.ts +33 -0
  140. package/dist/commands/network.d.ts.map +1 -0
  141. package/dist/commands/network.js +84 -0
  142. package/dist/commands/network.js.map +1 -0
  143. package/dist/commands/performance.d.ts +12 -0
  144. package/dist/commands/performance.d.ts.map +1 -0
  145. package/dist/commands/performance.js +26 -0
  146. package/dist/commands/performance.js.map +1 -0
  147. package/dist/commands/press_key.d.ts +10 -0
  148. package/dist/commands/press_key.d.ts.map +1 -0
  149. package/dist/commands/press_key.js +103 -0
  150. package/dist/commands/press_key.js.map +1 -0
  151. package/dist/commands/scroll.d.ts +21 -0
  152. package/dist/commands/scroll.d.ts.map +1 -0
  153. package/dist/commands/scroll.js +54 -0
  154. package/dist/commands/scroll.js.map +1 -0
  155. package/dist/commands/select.d.ts +18 -0
  156. package/dist/commands/select.d.ts.map +1 -0
  157. package/dist/commands/select.js +53 -0
  158. package/dist/commands/select.js.map +1 -0
  159. package/dist/commands/snapshot.d.ts +9 -0
  160. package/dist/commands/snapshot.d.ts.map +1 -0
  161. package/dist/commands/snapshot.js +7 -0
  162. package/dist/commands/snapshot.js.map +1 -0
  163. package/dist/commands/styles.d.ts +13 -0
  164. package/dist/commands/styles.d.ts.map +1 -0
  165. package/dist/commands/styles.js +7 -0
  166. package/dist/commands/styles.js.map +1 -0
  167. package/dist/commands/type.d.ts +3 -1
  168. package/dist/commands/type.d.ts.map +1 -1
  169. package/dist/commands/type.js +11 -2
  170. package/dist/commands/type.js.map +1 -1
  171. package/dist/commands/upload.d.ts +12 -0
  172. package/dist/commands/upload.d.ts.map +1 -0
  173. package/dist/commands/upload.js +13 -0
  174. package/dist/commands/upload.js.map +1 -0
  175. package/dist/mcp/console-buffer.d.ts +9 -0
  176. package/dist/mcp/console-buffer.d.ts.map +1 -1
  177. package/dist/mcp/console-buffer.js.map +1 -1
  178. package/dist/mcp/server.d.ts +10 -0
  179. package/dist/mcp/server.d.ts.map +1 -1
  180. package/dist/mcp/server.js +63 -2
  181. package/dist/mcp/server.js.map +1 -1
  182. package/dist/mcp/tools/browser.d.ts.map +1 -1
  183. package/dist/mcp/tools/browser.js +99 -8
  184. package/dist/mcp/tools/browser.js.map +1 -1
  185. package/dist/mcp/tools/connect.d.ts +5 -0
  186. package/dist/mcp/tools/connect.d.ts.map +1 -0
  187. package/dist/mcp/tools/connect.js +48 -0
  188. package/dist/mcp/tools/connect.js.map +1 -0
  189. package/dist/mcp/tools/files.d.ts +4 -0
  190. package/dist/mcp/tools/files.d.ts.map +1 -0
  191. package/dist/mcp/tools/files.js +85 -0
  192. package/dist/mcp/tools/files.js.map +1 -0
  193. package/dist/mcp/tools/interaction.d.ts +4 -0
  194. package/dist/mcp/tools/interaction.d.ts.map +1 -0
  195. package/dist/mcp/tools/interaction.js +176 -0
  196. package/dist/mcp/tools/interaction.js.map +1 -0
  197. package/dist/mcp/tools/names.d.ts +1 -1
  198. package/dist/mcp/tools/names.d.ts.map +1 -1
  199. package/dist/mcp/tools/names.js +19 -1
  200. package/dist/mcp/tools/names.js.map +1 -1
  201. package/dist/mcp/tools/network.d.ts +4 -0
  202. package/dist/mcp/tools/network.d.ts.map +1 -0
  203. package/dist/mcp/tools/network.js +68 -0
  204. package/dist/mcp/tools/network.js.map +1 -0
  205. package/dist/mcp/tools/performance.d.ts +4 -0
  206. package/dist/mcp/tools/performance.d.ts.map +1 -0
  207. package/dist/mcp/tools/performance.js +26 -0
  208. package/dist/mcp/tools/performance.js.map +1 -0
  209. package/dist/mcp/tools/runtime.d.ts +4 -0
  210. package/dist/mcp/tools/runtime.d.ts.map +1 -0
  211. package/dist/mcp/tools/runtime.js +128 -0
  212. package/dist/mcp/tools/runtime.js.map +1 -0
  213. package/dist/mcp/tools/snapshot.d.ts +4 -0
  214. package/dist/mcp/tools/snapshot.d.ts.map +1 -0
  215. package/dist/mcp/tools/snapshot.js +42 -0
  216. package/dist/mcp/tools/snapshot.js.map +1 -0
  217. package/dist/mcp/tools/styles.d.ts +4 -0
  218. package/dist/mcp/tools/styles.d.ts.map +1 -0
  219. package/dist/mcp/tools/styles.js +27 -0
  220. package/dist/mcp/tools/styles.js.map +1 -0
  221. package/dist/util/config.d.ts.map +1 -1
  222. package/dist/util/config.js +3 -2
  223. package/dist/util/config.js.map +1 -1
  224. package/package.json +3 -3
  225. package/src/browser/browser-manager.ts +457 -0
  226. package/src/browser/chrome-launcher.ts +271 -0
  227. package/src/browser/connection.ts +84 -0
  228. package/src/browser/events.ts +121 -0
  229. package/src/browser/index.ts +20 -0
  230. package/src/browser/inspect/element-ref.ts +68 -0
  231. package/src/browser/inspect/snapshot.ts +348 -0
  232. package/src/browser/inspect/styles.ts +258 -0
  233. package/src/browser/inspect/types.ts +37 -0
  234. package/src/browser/interaction/dialogs.ts +54 -0
  235. package/src/browser/interaction/downloads.ts +116 -0
  236. package/src/browser/interaction/events.ts +79 -0
  237. package/src/browser/interaction/ref-resolver.ts +53 -0
  238. package/src/browser/interaction/uploads.ts +60 -0
  239. package/src/browser/network/network-buffer.ts +133 -0
  240. package/src/browser/network/network-monitor.ts +107 -0
  241. package/src/browser/network/redaction.ts +42 -0
  242. package/src/browser/network/types.ts +47 -0
  243. package/src/browser/performance/analyze.ts +298 -0
  244. package/src/browser/performance/trace.ts +60 -0
  245. package/src/browser/performance/types.ts +34 -0
  246. package/src/browser/security/origins.ts +102 -0
  247. package/src/browser/tabs.ts +110 -0
  248. package/src/browser/types.ts +31 -0
  249. package/src/browser.ts +22 -798
  250. package/src/commands/check.ts +59 -0
  251. package/src/commands/click.ts +18 -4
  252. package/src/commands/connect.ts +40 -0
  253. package/src/commands/dialog.ts +79 -0
  254. package/src/commands/downloads.ts +111 -0
  255. package/src/commands/fill.ts +34 -0
  256. package/src/commands/find.ts +52 -0
  257. package/src/commands/hover.ts +42 -0
  258. package/src/commands/navigate.ts +20 -0
  259. package/src/commands/network.ts +141 -0
  260. package/src/commands/performance.ts +51 -0
  261. package/src/commands/press_key.ts +134 -0
  262. package/src/commands/scroll.ts +79 -0
  263. package/src/commands/select.ts +78 -0
  264. package/src/commands/snapshot.ts +18 -0
  265. package/src/commands/styles.ts +24 -0
  266. package/src/commands/type.ts +12 -2
  267. package/src/commands/upload.ts +36 -0
  268. package/src/mcp/console-buffer.ts +10 -0
  269. package/src/mcp/server.ts +74 -2
  270. package/src/mcp/tools/browser.ts +113 -7
  271. package/src/mcp/tools/connect.ts +62 -0
  272. package/src/mcp/tools/files.ts +120 -0
  273. package/src/mcp/tools/interaction.ts +232 -0
  274. package/src/mcp/tools/names.ts +19 -1
  275. package/src/mcp/tools/network.ts +86 -0
  276. package/src/mcp/tools/performance.ts +40 -0
  277. package/src/mcp/tools/runtime.ts +164 -0
  278. package/src/mcp/tools/snapshot.ts +64 -0
  279. package/src/mcp/tools/styles.ts +41 -0
  280. package/src/util/config.ts +3 -2
@@ -0,0 +1,348 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import type { ElementRefMap } from './element-ref.js';
3
+ import type { ElementInfo, ElementBounds, SnapshotResult } from './types.js';
4
+ import { getLogger } from '../../util/logger.js';
5
+
6
+ /**
7
+ * Roles that are always included in snapshots when they have
8
+ * a name or are interactive.
9
+ */
10
+ const INTERACTIVE_ROLES = new Set([
11
+ 'button',
12
+ 'link',
13
+ 'textbox',
14
+ 'searchbox',
15
+ 'checkbox',
16
+ 'radio',
17
+ 'combobox',
18
+ 'listbox',
19
+ 'option',
20
+ 'menuitem',
21
+ 'menuitemcheckbox',
22
+ 'menuitemradio',
23
+ 'tab',
24
+ 'switch',
25
+ 'slider',
26
+ 'spinbutton',
27
+ 'treeitem'
28
+ ]);
29
+
30
+ /**
31
+ * Roles included in snapshots when they have a name.
32
+ */
33
+ const SEMANTIC_ROLES = new Set([
34
+ 'heading',
35
+ 'img',
36
+ 'alert',
37
+ 'dialog',
38
+ 'alertdialog',
39
+ 'navigation',
40
+ 'main',
41
+ 'complementary',
42
+ 'contentinfo',
43
+ 'banner',
44
+ 'form',
45
+ 'region',
46
+ 'table',
47
+ 'cell',
48
+ 'row',
49
+ 'columnheader',
50
+ 'rowheader'
51
+ ]);
52
+
53
+ interface AXNode {
54
+ nodeId: string;
55
+ ignored: boolean;
56
+ role?: { type: string; value?: string };
57
+ name?: { type: string; value?: string };
58
+ description?: { type: string; value?: string };
59
+ value?: { type: string; value?: unknown };
60
+ properties?: Array<{
61
+ name: string;
62
+ value: { type: string; value?: unknown };
63
+ }>;
64
+ backendDOMNodeId?: number;
65
+ childIds?: string[];
66
+ parentId?: string;
67
+ }
68
+
69
+ function getAXProperty(node: AXNode, propName: string): unknown | undefined {
70
+ const prop = node.properties?.find((p) => p.name === propName);
71
+ return prop?.value?.value;
72
+ }
73
+
74
+ function isNodeVisible(node: AXNode): boolean {
75
+ const hidden = getAXProperty(node, 'hidden');
76
+ if (hidden === true) return false;
77
+ return true;
78
+ }
79
+
80
+ function shouldIncludeNode(node: AXNode): boolean {
81
+ if (node.ignored) return false;
82
+
83
+ const role = node.role?.value;
84
+ if (!role || role === 'none' || role === 'presentation') return false;
85
+
86
+ // Generic containers without names are noise
87
+ if (role === 'generic' || role === 'GenericContainer') {
88
+ return false;
89
+ }
90
+
91
+ const name = node.name?.value;
92
+
93
+ // Interactive roles are always included
94
+ if (INTERACTIVE_ROLES.has(role)) return true;
95
+
96
+ // Semantic roles are included when they have a name
97
+ if (SEMANTIC_ROLES.has(role) && name) return true;
98
+
99
+ // Static text nodes with content
100
+ if (role === 'StaticText' || role === 'text') return false;
101
+
102
+ // Include any other node that has an explicit name (ARIA-labeled)
103
+ if (name) return true;
104
+
105
+ return false;
106
+ }
107
+
108
+ async function getBoxModelSafe(
109
+ client: Client,
110
+ backendNodeId: number
111
+ ): Promise<ElementBounds | undefined> {
112
+ try {
113
+ const { model } = await client.DOM.getBoxModel({ backendNodeId });
114
+ // content quad: [x1,y1, x2,y2, x3,y3, x4,y4]
115
+ const content = model.content;
116
+ const x = Math.min(content[0], content[2], content[4], content[6]);
117
+ const y = Math.min(content[1], content[3], content[5], content[7]);
118
+ const maxX = Math.max(content[0], content[2], content[4], content[6]);
119
+ const maxY = Math.max(content[1], content[3], content[5], content[7]);
120
+ return {
121
+ x: Math.round(x),
122
+ y: Math.round(y),
123
+ width: Math.round(maxX - x),
124
+ height: Math.round(maxY - y)
125
+ };
126
+ } catch {
127
+ // Element may not have a box model (e.g. display:none, zero-size)
128
+ return undefined;
129
+ }
130
+ }
131
+
132
+ async function getNodeTag(
133
+ client: Client,
134
+ backendNodeId: number
135
+ ): Promise<{ tag: string; type?: string }> {
136
+ try {
137
+ const { node } = await client.DOM.describeNode({
138
+ backendNodeId,
139
+ depth: 0
140
+ });
141
+ const tag = node.nodeName?.toLowerCase() ?? '';
142
+ // Extract input type from attributes
143
+ let type: string | undefined;
144
+ if (tag === 'input' && node.attributes) {
145
+ for (let i = 0; i < node.attributes.length; i += 2) {
146
+ if (node.attributes[i] === 'type') {
147
+ type = node.attributes[i + 1];
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ return { tag, type };
153
+ } catch {
154
+ return { tag: '' };
155
+ }
156
+ }
157
+
158
+ export async function buildSnapshot(
159
+ client: Client,
160
+ refMap: ElementRefMap
161
+ ): Promise<SnapshotResult> {
162
+ const logger = getLogger();
163
+
164
+ // Clear refs for the new snapshot without incrementing generation.
165
+ // Generation only advances on navigation (via invalidate()).
166
+ refMap.resetRefs();
167
+
168
+ // Enable required domains
169
+ await Promise.all([client.Accessibility.enable(), client.DOM.enable({})]);
170
+
171
+ // Get the full accessibility tree
172
+ const { nodes } = await client.Accessibility.getFullAXTree({});
173
+
174
+ // Get page info
175
+ const { result: urlResult } = await client.Runtime.evaluate({
176
+ expression: 'document.URL',
177
+ returnByValue: true
178
+ });
179
+ const { result: titleResult } = await client.Runtime.evaluate({
180
+ expression: 'document.title',
181
+ returnByValue: true
182
+ });
183
+
184
+ const url = String(urlResult.value ?? '');
185
+ const title = String(titleResult.value ?? '');
186
+
187
+ // Filter to meaningful nodes
188
+ const candidates = (nodes as AXNode[]).filter(shouldIncludeNode);
189
+
190
+ logger.debug(
191
+ { totalNodes: nodes.length, candidates: candidates.length },
192
+ 'Building snapshot'
193
+ );
194
+
195
+ // Build element info for each candidate.
196
+ // Assign refs first, then fetch DOM details in parallel per node.
197
+ const validCandidates = candidates.filter((n) => n.backendDOMNodeId);
198
+ const refs = validCandidates.map((axNode) => {
199
+ const ref = refMap.nextRef();
200
+ refMap.set(ref, axNode.backendDOMNodeId!);
201
+ return ref;
202
+ });
203
+
204
+ const domDetails = await Promise.all(
205
+ validCandidates.map(async (axNode) => {
206
+ const id = axNode.backendDOMNodeId!;
207
+ const [tagInfo, bounds] = await Promise.all([
208
+ getNodeTag(client, id),
209
+ getBoxModelSafe(client, id)
210
+ ]);
211
+ return { tagInfo, bounds };
212
+ })
213
+ );
214
+
215
+ const elements: ElementInfo[] = [];
216
+
217
+ for (let i = 0; i < validCandidates.length; i++) {
218
+ const axNode = validCandidates[i];
219
+ const ref = refs[i];
220
+ const { tagInfo, bounds } = domDetails[i];
221
+
222
+ const role = axNode.role?.value ?? '';
223
+ const name = axNode.name?.value ?? '';
224
+
225
+ const { tag, type } = tagInfo;
226
+
227
+ // Extract properties
228
+ const checked = getAXProperty(axNode, 'checked');
229
+ const disabled = getAXProperty(axNode, 'disabled');
230
+ const required = getAXProperty(axNode, 'required');
231
+ const selected = getAXProperty(axNode, 'selected');
232
+ const expanded = getAXProperty(axNode, 'expanded');
233
+ const level = getAXProperty(axNode, 'level');
234
+
235
+ // Get value for form controls
236
+ let value: string | undefined;
237
+ if (axNode.value?.value !== undefined && axNode.value?.value !== '') {
238
+ value = String(axNode.value.value);
239
+ }
240
+
241
+ const visible = isNodeVisible(axNode) && bounds !== undefined;
242
+ const description = axNode.description?.value || undefined;
243
+
244
+ const element: ElementInfo = {
245
+ ref,
246
+ role,
247
+ name,
248
+ tag,
249
+ visible,
250
+ ...(type !== undefined && { type }),
251
+ ...(value !== undefined && { value }),
252
+ ...(checked !== undefined && {
253
+ checked: checked === true || checked === 'true'
254
+ }),
255
+ ...(disabled === true && { disabled: true }),
256
+ ...(required === true && { required: true }),
257
+ ...(selected === true && { selected: true }),
258
+ ...(expanded !== undefined && { expanded: expanded === true }),
259
+ ...(typeof level === 'number' && { level }),
260
+ ...(bounds && { bounds }),
261
+ ...(description && { description })
262
+ };
263
+
264
+ elements.push(element);
265
+ }
266
+
267
+ return {
268
+ url,
269
+ title,
270
+ timestamp: Date.now(),
271
+ elements
272
+ };
273
+ }
274
+
275
+ export function formatSnapshot(snapshot: SnapshotResult): string {
276
+ const lines: string[] = [];
277
+ lines.push(`Page: ${snapshot.title || snapshot.url}`);
278
+ lines.push(`URL: ${snapshot.url}`);
279
+ lines.push('');
280
+
281
+ if (snapshot.elements.length === 0) {
282
+ lines.push('No interactive elements found.');
283
+ return lines.join('\n');
284
+ }
285
+
286
+ lines.push(`Elements (${snapshot.elements.length}):`);
287
+ lines.push('');
288
+
289
+ for (const el of snapshot.elements) {
290
+ const parts: string[] = [`[${el.ref}]`, el.role];
291
+
292
+ if (el.name) {
293
+ parts.push(`"${el.name}"`);
294
+ }
295
+
296
+ const attrs: string[] = [];
297
+ if (el.tag && el.tag !== el.role) attrs.push(el.tag);
298
+ if (el.type) attrs.push(`type=${el.type}`);
299
+ if (el.value !== undefined) attrs.push(`value="${el.value}"`);
300
+ if (el.checked !== undefined)
301
+ attrs.push(el.checked ? 'checked' : 'unchecked');
302
+ if (el.disabled) attrs.push('disabled');
303
+ if (el.required) attrs.push('required');
304
+ if (el.selected) attrs.push('selected');
305
+ if (el.expanded !== undefined)
306
+ attrs.push(el.expanded ? 'expanded' : 'collapsed');
307
+ if (el.level !== undefined) attrs.push(`level=${el.level}`);
308
+ if (!el.visible) attrs.push('hidden');
309
+
310
+ if (attrs.length > 0) {
311
+ parts.push(`(${attrs.join(', ')})`);
312
+ }
313
+
314
+ lines.push(' ' + parts.join(' '));
315
+ }
316
+
317
+ return lines.join('\n');
318
+ }
319
+
320
+ export function findElements(
321
+ snapshot: SnapshotResult,
322
+ query: {
323
+ role?: string;
324
+ name?: string;
325
+ text?: string;
326
+ }
327
+ ): ElementInfo[] {
328
+ return snapshot.elements.filter((el) => {
329
+ if (query.role && el.role.toLowerCase() !== query.role.toLowerCase()) {
330
+ return false;
331
+ }
332
+ if (query.name) {
333
+ const nameMatch = el.name
334
+ .toLowerCase()
335
+ .includes(query.name.toLowerCase());
336
+ if (!nameMatch) return false;
337
+ }
338
+ if (query.text) {
339
+ const textLower = query.text.toLowerCase();
340
+ const nameMatch = el.name.toLowerCase().includes(textLower);
341
+ const valueMatch = el.value?.toLowerCase().includes(textLower) ?? false;
342
+ const descMatch =
343
+ el.description?.toLowerCase().includes(textLower) ?? false;
344
+ if (!nameMatch && !valueMatch && !descMatch) return false;
345
+ }
346
+ return true;
347
+ });
348
+ }
@@ -0,0 +1,258 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import type { ElementRefMap } from './element-ref.js';
3
+
4
+ /**
5
+ * Default diagnostic properties returned when no filter is specified.
6
+ * Covers the most common layout/visibility debugging needs.
7
+ */
8
+ const DEFAULT_PROPERTIES = new Set([
9
+ 'display',
10
+ 'visibility',
11
+ 'opacity',
12
+ 'position',
13
+ 'width',
14
+ 'height',
15
+ 'min-width',
16
+ 'min-height',
17
+ 'max-width',
18
+ 'max-height',
19
+ 'margin-top',
20
+ 'margin-right',
21
+ 'margin-bottom',
22
+ 'margin-left',
23
+ 'padding-top',
24
+ 'padding-right',
25
+ 'padding-bottom',
26
+ 'padding-left',
27
+ 'overflow',
28
+ 'overflow-x',
29
+ 'overflow-y',
30
+ 'z-index',
31
+ 'transform',
32
+ 'color',
33
+ 'background-color',
34
+ 'font-size',
35
+ 'font-weight',
36
+ 'text-align',
37
+ 'flex-direction',
38
+ 'flex-wrap',
39
+ 'justify-content',
40
+ 'align-items',
41
+ 'align-self',
42
+ 'flex-grow',
43
+ 'flex-shrink',
44
+ 'gap',
45
+ 'grid-template-columns',
46
+ 'grid-template-rows',
47
+ 'border',
48
+ 'border-radius',
49
+ 'box-sizing',
50
+ 'pointer-events',
51
+ 'cursor'
52
+ ]);
53
+
54
+ export interface BoxModelInfo {
55
+ content: { x: number; y: number; width: number; height: number };
56
+ padding: { top: number; right: number; bottom: number; left: number };
57
+ border: { top: number; right: number; bottom: number; left: number };
58
+ margin: { top: number; right: number; bottom: number; left: number };
59
+ width: number;
60
+ height: number;
61
+ }
62
+
63
+ export interface MatchedRule {
64
+ selector: string;
65
+ source: string;
66
+ properties: Record<string, string>;
67
+ }
68
+
69
+ export interface StylesResult {
70
+ ref: string;
71
+ computed: Record<string, string>;
72
+ matched: MatchedRule[];
73
+ boxModel?: BoxModelInfo;
74
+ }
75
+
76
+ let cssEnabledFor: WeakRef<Client> | null = null;
77
+
78
+ async function ensureCssEnabled(client: Client): Promise<void> {
79
+ if (cssEnabledFor?.deref() === client) return;
80
+ await client.CSS.enable();
81
+ cssEnabledFor = new WeakRef(client);
82
+ }
83
+
84
+ // Reset CSS enabled state (for reconnection)
85
+ export function resetCssState(): void {
86
+ cssEnabledFor = null;
87
+ }
88
+
89
+ function parseQuad(quad: number[]): {
90
+ x: number;
91
+ y: number;
92
+ width: number;
93
+ height: number;
94
+ } {
95
+ const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
96
+ const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
97
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
98
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
99
+ return {
100
+ x: Math.round(x),
101
+ y: Math.round(y),
102
+ width: Math.round(maxX - x),
103
+ height: Math.round(maxY - y)
104
+ };
105
+ }
106
+
107
+ function computeBoxEdges(
108
+ outer: number[],
109
+ inner: number[]
110
+ ): { top: number; right: number; bottom: number; left: number } {
111
+ const outerRect = parseQuad(outer);
112
+ const innerRect = parseQuad(inner);
113
+ return {
114
+ top: Math.round(innerRect.y - outerRect.y),
115
+ right: Math.round(
116
+ outerRect.x + outerRect.width - (innerRect.x + innerRect.width)
117
+ ),
118
+ bottom: Math.round(
119
+ outerRect.y + outerRect.height - (innerRect.y + innerRect.height)
120
+ ),
121
+ left: Math.round(innerRect.x - outerRect.x)
122
+ };
123
+ }
124
+
125
+ export async function getStyles(
126
+ client: Client,
127
+ refMap: ElementRefMap,
128
+ ref: string,
129
+ properties?: string[]
130
+ ): Promise<StylesResult> {
131
+ const backendNodeId = refMap.resolve(ref);
132
+
133
+ await ensureCssEnabled(client);
134
+ await client.DOM.enable({});
135
+
136
+ // We need a nodeId for CSS APIs — push the backend node to the frontend
137
+ const { nodeIds } = await client.DOM.pushNodesByBackendIdsToFrontend({
138
+ backendNodeIds: [backendNodeId]
139
+ });
140
+ const nodeId = nodeIds[0];
141
+ if (!nodeId) {
142
+ throw new Error(`Could not resolve ref "${ref}" to a DOM node`);
143
+ }
144
+
145
+ // Get computed styles
146
+ const { computedStyle } = await client.CSS.getComputedStyleForNode({
147
+ nodeId
148
+ });
149
+
150
+ const filterSet = properties
151
+ ? new Set(properties.map((p) => p.toLowerCase()))
152
+ : DEFAULT_PROPERTIES;
153
+
154
+ const computed: Record<string, string> = {};
155
+ for (const prop of computedStyle) {
156
+ if (filterSet.has(prop.name)) {
157
+ computed[prop.name] = prop.value;
158
+ }
159
+ }
160
+
161
+ // Get matched CSS rules
162
+ const matched: MatchedRule[] = [];
163
+ try {
164
+ const { matchedCSSRules } = await client.CSS.getMatchedStylesForNode({
165
+ nodeId
166
+ });
167
+ if (matchedCSSRules) {
168
+ for (const match of matchedCSSRules) {
169
+ const rule = match.rule;
170
+ const selector = rule.selectorList?.text ?? '';
171
+ const source = rule.origin ?? 'regular';
172
+ const ruleProps: Record<string, string> = {};
173
+ for (const prop of rule.style.cssProperties) {
174
+ if (filterSet.has(prop.name) && prop.value) {
175
+ ruleProps[prop.name] = prop.value;
176
+ }
177
+ }
178
+ if (Object.keys(ruleProps).length > 0) {
179
+ matched.push({ selector, source, properties: ruleProps });
180
+ }
181
+ }
182
+ }
183
+ } catch {
184
+ // getMatchedStylesForNode can fail for some node types
185
+ }
186
+
187
+ // Get box model
188
+ let boxModel: BoxModelInfo | undefined;
189
+ try {
190
+ const { model } = await client.DOM.getBoxModel({ backendNodeId });
191
+ boxModel = {
192
+ content: parseQuad(model.content),
193
+ padding: computeBoxEdges(model.padding, model.content),
194
+ border: computeBoxEdges(model.border, model.padding),
195
+ margin: computeBoxEdges(model.margin, model.border),
196
+ width: model.width,
197
+ height: model.height
198
+ };
199
+ } catch {
200
+ // Element may not have a box model (display:none)
201
+ }
202
+
203
+ return { ref, computed, matched, boxModel };
204
+ }
205
+
206
+ export function formatStyles(result: StylesResult): string {
207
+ const lines: string[] = [`Styles for ${result.ref}:\n`];
208
+
209
+ // Computed styles (sorted for stable output)
210
+ lines.push('Computed:');
211
+ const entries = Object.entries(result.computed).sort(([a], [b]) =>
212
+ a.localeCompare(b)
213
+ );
214
+ if (entries.length === 0) {
215
+ lines.push(' (none)');
216
+ } else {
217
+ for (const [name, value] of entries) {
218
+ lines.push(` ${name}: ${value}`);
219
+ }
220
+ }
221
+
222
+ // Box model
223
+ if (result.boxModel) {
224
+ const bm = result.boxModel;
225
+ lines.push('');
226
+ lines.push('Box Model:');
227
+ lines.push(
228
+ ` content: ${bm.content.width}x${bm.content.height} at (${bm.content.x}, ${bm.content.y})`
229
+ );
230
+ lines.push(
231
+ ` padding: ${bm.padding.top} ${bm.padding.right} ${bm.padding.bottom} ${bm.padding.left}`
232
+ );
233
+ lines.push(
234
+ ` border: ${bm.border.top} ${bm.border.right} ${bm.border.bottom} ${bm.border.left}`
235
+ );
236
+ lines.push(
237
+ ` margin: ${bm.margin.top} ${bm.margin.right} ${bm.margin.bottom} ${bm.margin.left}`
238
+ );
239
+ lines.push(` total: ${bm.width}x${bm.height}`);
240
+ }
241
+
242
+ // Matched rules
243
+ if (result.matched.length > 0) {
244
+ lines.push('');
245
+ lines.push('Matched Rules:');
246
+ for (const rule of result.matched) {
247
+ lines.push(` ${rule.selector} (${rule.source}):`);
248
+ const sortedProps = Object.entries(rule.properties).sort(([a], [b]) =>
249
+ a.localeCompare(b)
250
+ );
251
+ for (const [name, value] of sortedProps) {
252
+ lines.push(` ${name}: ${value}`);
253
+ }
254
+ }
255
+ }
256
+
257
+ return lines.join('\n');
258
+ }
@@ -0,0 +1,37 @@
1
+ export interface ElementBounds {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export interface ElementInfo {
9
+ ref: string;
10
+ role: string;
11
+ name: string;
12
+ tag: string;
13
+ type?: string;
14
+ value?: string;
15
+ checked?: boolean;
16
+ disabled?: boolean;
17
+ required?: boolean;
18
+ selected?: boolean;
19
+ expanded?: boolean;
20
+ level?: number;
21
+ visible: boolean;
22
+ bounds?: ElementBounds;
23
+ description?: string;
24
+ }
25
+
26
+ export interface SnapshotResult {
27
+ url: string;
28
+ title: string;
29
+ timestamp: number;
30
+ elements: ElementInfo[];
31
+ }
32
+
33
+ export interface StaleElementError {
34
+ code: 'STALE_ELEMENT_REFERENCE';
35
+ ref: string;
36
+ message: string;
37
+ }
@@ -0,0 +1,54 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ export interface PendingDialog {
4
+ type: string;
5
+ message: string;
6
+ defaultPrompt?: string;
7
+ timestamp: number;
8
+ }
9
+
10
+ export class DialogQueue {
11
+ private pending: PendingDialog[] = [];
12
+
13
+ push(dialog: PendingDialog): void {
14
+ this.pending.push(dialog);
15
+ }
16
+
17
+ list(): PendingDialog[] {
18
+ return [...this.pending];
19
+ }
20
+
21
+ shift(): PendingDialog | undefined {
22
+ return this.pending.shift();
23
+ }
24
+
25
+ size(): number {
26
+ return this.pending.length;
27
+ }
28
+
29
+ clear(): void {
30
+ this.pending = [];
31
+ }
32
+ }
33
+
34
+ export function attachDialogHandler(client: Client, queue: DialogQueue): void {
35
+ client.Page.javascriptDialogOpening((params) => {
36
+ queue.push({
37
+ type: params.type,
38
+ message: params.message,
39
+ defaultPrompt: params.defaultPrompt ?? undefined,
40
+ timestamp: Date.now()
41
+ });
42
+ });
43
+ }
44
+
45
+ export async function handleDialog(
46
+ client: Client,
47
+ accept: boolean,
48
+ promptText?: string
49
+ ): Promise<void> {
50
+ await client.Page.handleJavaScriptDialog({
51
+ accept,
52
+ promptText
53
+ });
54
+ }