@predicatelabs/sdk 0.99.9

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 (302) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +252 -0
  3. package/dist/actions.d.ts +185 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +1120 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-runtime.d.ts +352 -0
  8. package/dist/agent-runtime.d.ts.map +1 -0
  9. package/dist/agent-runtime.js +1170 -0
  10. package/dist/agent-runtime.js.map +1 -0
  11. package/dist/agent.d.ts +164 -0
  12. package/dist/agent.d.ts.map +1 -0
  13. package/dist/agent.js +408 -0
  14. package/dist/agent.js.map +1 -0
  15. package/dist/asserts/expect.d.ts +159 -0
  16. package/dist/asserts/expect.d.ts.map +1 -0
  17. package/dist/asserts/expect.js +547 -0
  18. package/dist/asserts/expect.js.map +1 -0
  19. package/dist/asserts/index.d.ts +58 -0
  20. package/dist/asserts/index.d.ts.map +1 -0
  21. package/dist/asserts/index.js +70 -0
  22. package/dist/asserts/index.js.map +1 -0
  23. package/dist/asserts/query.d.ts +199 -0
  24. package/dist/asserts/query.d.ts.map +1 -0
  25. package/dist/asserts/query.js +288 -0
  26. package/dist/asserts/query.js.map +1 -0
  27. package/dist/backends/actions.d.ts +119 -0
  28. package/dist/backends/actions.d.ts.map +1 -0
  29. package/dist/backends/actions.js +291 -0
  30. package/dist/backends/actions.js.map +1 -0
  31. package/dist/backends/browser-use-adapter.d.ts +131 -0
  32. package/dist/backends/browser-use-adapter.d.ts.map +1 -0
  33. package/dist/backends/browser-use-adapter.js +219 -0
  34. package/dist/backends/browser-use-adapter.js.map +1 -0
  35. package/dist/backends/cdp-backend.d.ts +66 -0
  36. package/dist/backends/cdp-backend.d.ts.map +1 -0
  37. package/dist/backends/cdp-backend.js +273 -0
  38. package/dist/backends/cdp-backend.js.map +1 -0
  39. package/dist/backends/index.d.ts +80 -0
  40. package/dist/backends/index.d.ts.map +1 -0
  41. package/dist/backends/index.js +101 -0
  42. package/dist/backends/index.js.map +1 -0
  43. package/dist/backends/protocol.d.ts +156 -0
  44. package/dist/backends/protocol.d.ts.map +1 -0
  45. package/dist/backends/protocol.js +16 -0
  46. package/dist/backends/protocol.js.map +1 -0
  47. package/dist/backends/sentience-context.d.ts +143 -0
  48. package/dist/backends/sentience-context.d.ts.map +1 -0
  49. package/dist/backends/sentience-context.js +359 -0
  50. package/dist/backends/sentience-context.js.map +1 -0
  51. package/dist/backends/snapshot.d.ts +188 -0
  52. package/dist/backends/snapshot.d.ts.map +1 -0
  53. package/dist/backends/snapshot.js +360 -0
  54. package/dist/backends/snapshot.js.map +1 -0
  55. package/dist/browser.d.ts +154 -0
  56. package/dist/browser.d.ts.map +1 -0
  57. package/dist/browser.js +920 -0
  58. package/dist/browser.js.map +1 -0
  59. package/dist/canonicalization.d.ts +126 -0
  60. package/dist/canonicalization.d.ts.map +1 -0
  61. package/dist/canonicalization.js +161 -0
  62. package/dist/canonicalization.js.map +1 -0
  63. package/dist/captcha/strategies.d.ts +12 -0
  64. package/dist/captcha/strategies.d.ts.map +1 -0
  65. package/dist/captcha/strategies.js +43 -0
  66. package/dist/captcha/strategies.js.map +1 -0
  67. package/dist/captcha/types.d.ts +45 -0
  68. package/dist/captcha/types.d.ts.map +1 -0
  69. package/dist/captcha/types.js +12 -0
  70. package/dist/captcha/types.js.map +1 -0
  71. package/dist/cli.d.ts +5 -0
  72. package/dist/cli.d.ts.map +1 -0
  73. package/dist/cli.js +422 -0
  74. package/dist/cli.js.map +1 -0
  75. package/dist/conversational-agent.d.ts +123 -0
  76. package/dist/conversational-agent.d.ts.map +1 -0
  77. package/dist/conversational-agent.js +341 -0
  78. package/dist/conversational-agent.js.map +1 -0
  79. package/dist/cursor-policy.d.ts +41 -0
  80. package/dist/cursor-policy.d.ts.map +1 -0
  81. package/dist/cursor-policy.js +81 -0
  82. package/dist/cursor-policy.js.map +1 -0
  83. package/dist/debugger.d.ts +28 -0
  84. package/dist/debugger.d.ts.map +1 -0
  85. package/dist/debugger.js +107 -0
  86. package/dist/debugger.js.map +1 -0
  87. package/dist/expect.d.ts +16 -0
  88. package/dist/expect.d.ts.map +1 -0
  89. package/dist/expect.js +67 -0
  90. package/dist/expect.js.map +1 -0
  91. package/dist/failure-artifacts.d.ts +95 -0
  92. package/dist/failure-artifacts.d.ts.map +1 -0
  93. package/dist/failure-artifacts.js +805 -0
  94. package/dist/failure-artifacts.js.map +1 -0
  95. package/dist/generator.d.ts +16 -0
  96. package/dist/generator.d.ts.map +1 -0
  97. package/dist/generator.js +205 -0
  98. package/dist/generator.js.map +1 -0
  99. package/dist/index.d.ts +37 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +160 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/inspector.d.ts +13 -0
  104. package/dist/inspector.d.ts.map +1 -0
  105. package/dist/inspector.js +153 -0
  106. package/dist/inspector.js.map +1 -0
  107. package/dist/llm-provider.d.ts +144 -0
  108. package/dist/llm-provider.d.ts.map +1 -0
  109. package/dist/llm-provider.js +460 -0
  110. package/dist/llm-provider.js.map +1 -0
  111. package/dist/ordinal.d.ts +90 -0
  112. package/dist/ordinal.d.ts.map +1 -0
  113. package/dist/ordinal.js +249 -0
  114. package/dist/ordinal.js.map +1 -0
  115. package/dist/overlay.d.ts +63 -0
  116. package/dist/overlay.d.ts.map +1 -0
  117. package/dist/overlay.js +102 -0
  118. package/dist/overlay.js.map +1 -0
  119. package/dist/protocols/browser-protocol.d.ts +79 -0
  120. package/dist/protocols/browser-protocol.d.ts.map +1 -0
  121. package/dist/protocols/browser-protocol.js +9 -0
  122. package/dist/protocols/browser-protocol.js.map +1 -0
  123. package/dist/query.d.ts +66 -0
  124. package/dist/query.d.ts.map +1 -0
  125. package/dist/query.js +482 -0
  126. package/dist/query.js.map +1 -0
  127. package/dist/read.d.ts +47 -0
  128. package/dist/read.d.ts.map +1 -0
  129. package/dist/read.js +128 -0
  130. package/dist/read.js.map +1 -0
  131. package/dist/recorder.d.ts +44 -0
  132. package/dist/recorder.d.ts.map +1 -0
  133. package/dist/recorder.js +262 -0
  134. package/dist/recorder.js.map +1 -0
  135. package/dist/runtime-agent.d.ts +72 -0
  136. package/dist/runtime-agent.d.ts.map +1 -0
  137. package/dist/runtime-agent.js +357 -0
  138. package/dist/runtime-agent.js.map +1 -0
  139. package/dist/screenshot.d.ts +17 -0
  140. package/dist/screenshot.d.ts.map +1 -0
  141. package/dist/screenshot.js +40 -0
  142. package/dist/screenshot.js.map +1 -0
  143. package/dist/snapshot-diff.d.ts +23 -0
  144. package/dist/snapshot-diff.d.ts.map +1 -0
  145. package/dist/snapshot-diff.js +119 -0
  146. package/dist/snapshot-diff.js.map +1 -0
  147. package/dist/snapshot.d.ts +47 -0
  148. package/dist/snapshot.d.ts.map +1 -0
  149. package/dist/snapshot.js +358 -0
  150. package/dist/snapshot.js.map +1 -0
  151. package/dist/textSearch.d.ts +64 -0
  152. package/dist/textSearch.d.ts.map +1 -0
  153. package/dist/textSearch.js +113 -0
  154. package/dist/textSearch.js.map +1 -0
  155. package/dist/tools/context.d.ts +18 -0
  156. package/dist/tools/context.d.ts.map +1 -0
  157. package/dist/tools/context.js +40 -0
  158. package/dist/tools/context.js.map +1 -0
  159. package/dist/tools/defaults.d.ts +5 -0
  160. package/dist/tools/defaults.d.ts.map +1 -0
  161. package/dist/tools/defaults.js +368 -0
  162. package/dist/tools/defaults.js.map +1 -0
  163. package/dist/tools/filesystem.d.ts +12 -0
  164. package/dist/tools/filesystem.d.ts.map +1 -0
  165. package/dist/tools/filesystem.js +137 -0
  166. package/dist/tools/filesystem.js.map +1 -0
  167. package/dist/tools/index.d.ts +5 -0
  168. package/dist/tools/index.d.ts.map +1 -0
  169. package/dist/tools/index.js +15 -0
  170. package/dist/tools/index.js.map +1 -0
  171. package/dist/tools/registry.d.ts +38 -0
  172. package/dist/tools/registry.d.ts.map +1 -0
  173. package/dist/tools/registry.js +100 -0
  174. package/dist/tools/registry.js.map +1 -0
  175. package/dist/tracing/cloud-sink.d.ts +189 -0
  176. package/dist/tracing/cloud-sink.d.ts.map +1 -0
  177. package/dist/tracing/cloud-sink.js +1067 -0
  178. package/dist/tracing/cloud-sink.js.map +1 -0
  179. package/dist/tracing/index-schema.d.ts +231 -0
  180. package/dist/tracing/index-schema.d.ts.map +1 -0
  181. package/dist/tracing/index-schema.js +235 -0
  182. package/dist/tracing/index-schema.js.map +1 -0
  183. package/dist/tracing/index.d.ts +12 -0
  184. package/dist/tracing/index.d.ts.map +1 -0
  185. package/dist/tracing/index.js +28 -0
  186. package/dist/tracing/index.js.map +1 -0
  187. package/dist/tracing/indexer.d.ts +20 -0
  188. package/dist/tracing/indexer.d.ts.map +1 -0
  189. package/dist/tracing/indexer.js +347 -0
  190. package/dist/tracing/indexer.js.map +1 -0
  191. package/dist/tracing/jsonl-sink.d.ts +51 -0
  192. package/dist/tracing/jsonl-sink.d.ts.map +1 -0
  193. package/dist/tracing/jsonl-sink.js +329 -0
  194. package/dist/tracing/jsonl-sink.js.map +1 -0
  195. package/dist/tracing/sink.d.ts +25 -0
  196. package/dist/tracing/sink.d.ts.map +1 -0
  197. package/dist/tracing/sink.js +15 -0
  198. package/dist/tracing/sink.js.map +1 -0
  199. package/dist/tracing/tracer-factory.d.ts +102 -0
  200. package/dist/tracing/tracer-factory.d.ts.map +1 -0
  201. package/dist/tracing/tracer-factory.js +375 -0
  202. package/dist/tracing/tracer-factory.js.map +1 -0
  203. package/dist/tracing/tracer.d.ts +140 -0
  204. package/dist/tracing/tracer.d.ts.map +1 -0
  205. package/dist/tracing/tracer.js +336 -0
  206. package/dist/tracing/tracer.js.map +1 -0
  207. package/dist/tracing/types.d.ts +203 -0
  208. package/dist/tracing/types.d.ts.map +1 -0
  209. package/dist/tracing/types.js +8 -0
  210. package/dist/tracing/types.js.map +1 -0
  211. package/dist/types.d.ts +422 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +6 -0
  214. package/dist/types.js.map +1 -0
  215. package/dist/utils/action-executor.d.ts +25 -0
  216. package/dist/utils/action-executor.d.ts.map +1 -0
  217. package/dist/utils/action-executor.js +121 -0
  218. package/dist/utils/action-executor.js.map +1 -0
  219. package/dist/utils/browser-evaluator.d.ts +76 -0
  220. package/dist/utils/browser-evaluator.d.ts.map +1 -0
  221. package/dist/utils/browser-evaluator.js +130 -0
  222. package/dist/utils/browser-evaluator.js.map +1 -0
  223. package/dist/utils/browser.d.ts +30 -0
  224. package/dist/utils/browser.d.ts.map +1 -0
  225. package/dist/utils/browser.js +75 -0
  226. package/dist/utils/browser.js.map +1 -0
  227. package/dist/utils/element-filter.d.ts +76 -0
  228. package/dist/utils/element-filter.d.ts.map +1 -0
  229. package/dist/utils/element-filter.js +195 -0
  230. package/dist/utils/element-filter.js.map +1 -0
  231. package/dist/utils/grid-utils.d.ts +37 -0
  232. package/dist/utils/grid-utils.d.ts.map +1 -0
  233. package/dist/utils/grid-utils.js +283 -0
  234. package/dist/utils/grid-utils.js.map +1 -0
  235. package/dist/utils/llm-interaction-handler.d.ts +41 -0
  236. package/dist/utils/llm-interaction-handler.d.ts.map +1 -0
  237. package/dist/utils/llm-interaction-handler.js +171 -0
  238. package/dist/utils/llm-interaction-handler.js.map +1 -0
  239. package/dist/utils/llm-response-builder.d.ts +56 -0
  240. package/dist/utils/llm-response-builder.d.ts.map +1 -0
  241. package/dist/utils/llm-response-builder.js +130 -0
  242. package/dist/utils/llm-response-builder.js.map +1 -0
  243. package/dist/utils/selector-utils.d.ts +12 -0
  244. package/dist/utils/selector-utils.d.ts.map +1 -0
  245. package/dist/utils/selector-utils.js +32 -0
  246. package/dist/utils/selector-utils.js.map +1 -0
  247. package/dist/utils/snapshot-event-builder.d.ts +28 -0
  248. package/dist/utils/snapshot-event-builder.d.ts.map +1 -0
  249. package/dist/utils/snapshot-event-builder.js +88 -0
  250. package/dist/utils/snapshot-event-builder.js.map +1 -0
  251. package/dist/utils/snapshot-processor.d.ts +27 -0
  252. package/dist/utils/snapshot-processor.d.ts.map +1 -0
  253. package/dist/utils/snapshot-processor.js +47 -0
  254. package/dist/utils/snapshot-processor.js.map +1 -0
  255. package/dist/utils/trace-event-builder.d.ts +122 -0
  256. package/dist/utils/trace-event-builder.d.ts.map +1 -0
  257. package/dist/utils/trace-event-builder.js +365 -0
  258. package/dist/utils/trace-event-builder.js.map +1 -0
  259. package/dist/utils/trace-file-manager.d.ts +70 -0
  260. package/dist/utils/trace-file-manager.d.ts.map +1 -0
  261. package/dist/utils/trace-file-manager.js +194 -0
  262. package/dist/utils/trace-file-manager.js.map +1 -0
  263. package/dist/utils/zod.d.ts +5 -0
  264. package/dist/utils/zod.d.ts.map +1 -0
  265. package/dist/utils/zod.js +80 -0
  266. package/dist/utils/zod.js.map +1 -0
  267. package/dist/utils.d.ts +8 -0
  268. package/dist/utils.d.ts.map +1 -0
  269. package/dist/utils.js +13 -0
  270. package/dist/utils.js.map +1 -0
  271. package/dist/verification.d.ts +194 -0
  272. package/dist/verification.d.ts.map +1 -0
  273. package/dist/verification.js +530 -0
  274. package/dist/verification.js.map +1 -0
  275. package/dist/vision-executor.d.ts +18 -0
  276. package/dist/vision-executor.d.ts.map +1 -0
  277. package/dist/vision-executor.js +60 -0
  278. package/dist/vision-executor.js.map +1 -0
  279. package/dist/visual-agent.d.ts +120 -0
  280. package/dist/visual-agent.d.ts.map +1 -0
  281. package/dist/visual-agent.js +796 -0
  282. package/dist/visual-agent.js.map +1 -0
  283. package/dist/wait.d.ts +35 -0
  284. package/dist/wait.d.ts.map +1 -0
  285. package/dist/wait.js +76 -0
  286. package/dist/wait.js.map +1 -0
  287. package/package.json +94 -0
  288. package/spec/README.md +72 -0
  289. package/spec/SNAPSHOT_V1.md +208 -0
  290. package/spec/sdk-types.md +259 -0
  291. package/spec/snapshot.schema.json +148 -0
  292. package/src/extension/background.js +104 -0
  293. package/src/extension/content.js +162 -0
  294. package/src/extension/injected_api.js +1399 -0
  295. package/src/extension/manifest.json +36 -0
  296. package/src/extension/pkg/README.md +1340 -0
  297. package/src/extension/pkg/package.json +15 -0
  298. package/src/extension/pkg/sentience_core.d.ts +51 -0
  299. package/src/extension/pkg/sentience_core.js +371 -0
  300. package/src/extension/pkg/sentience_core_bg.wasm +0 -0
  301. package/src/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  302. package/src/extension/release.json +116 -0
@@ -0,0 +1,1120 @@
1
+ "use strict";
2
+ /**
3
+ * Actions v1 - click, type, press
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.click = click;
7
+ exports.typeText = typeText;
8
+ exports.clear = clear;
9
+ exports.check = check;
10
+ exports.uncheck = uncheck;
11
+ exports.selectOption = selectOption;
12
+ exports.uploadFile = uploadFile;
13
+ exports.submit = submit;
14
+ exports.back = back;
15
+ exports.scrollTo = scrollTo;
16
+ exports.press = press;
17
+ exports.sendKeys = sendKeys;
18
+ exports.search = search;
19
+ exports.clickRect = clickRect;
20
+ const snapshot_1 = require("./snapshot");
21
+ const browser_evaluator_1 = require("./utils/browser-evaluator");
22
+ const cursor_policy_1 = require("./cursor-policy");
23
+ const cursorPosByPage = new WeakMap();
24
+ async function humanMoveIfEnabled(page, target, cursorPolicy) {
25
+ if (!cursorPolicy || cursorPolicy.mode !== 'human')
26
+ return undefined;
27
+ const prev = cursorPosByPage.get(page);
28
+ let from;
29
+ if (prev) {
30
+ from = prev;
31
+ }
32
+ else {
33
+ const vp = page.viewportSize ? page.viewportSize() : null;
34
+ from = vp ? { x: vp.width / 2, y: vp.height / 2 } : { x: 0, y: 0 };
35
+ }
36
+ const meta = (0, cursor_policy_1.buildHumanCursorPath)([from.x, from.y], [target.x, target.y], cursorPolicy);
37
+ const pts = meta.path || [];
38
+ const durationMs = meta.duration_ms || 0;
39
+ const perStepMs = durationMs > 0 ? durationMs / Math.max(1, pts.length) : 0;
40
+ for (const p of pts) {
41
+ await page.mouse.move(p.x, p.y);
42
+ if (perStepMs > 0)
43
+ await page.waitForTimeout(perStepMs);
44
+ }
45
+ if (meta.pause_before_click_ms > 0) {
46
+ await page.waitForTimeout(meta.pause_before_click_ms);
47
+ }
48
+ cursorPosByPage.set(page, { x: target.x, y: target.y });
49
+ return meta;
50
+ }
51
+ /**
52
+ * Highlight a rectangle with a red border overlay
53
+ */
54
+ async function highlightRect(browser, rect, durationSec = 2.0) {
55
+ const page = browser.getPage();
56
+ if (!page) {
57
+ throw new Error('Browser not started. Call start() first.');
58
+ }
59
+ const highlightId = `sentience_highlight_${Date.now()}`;
60
+ // Combine all arguments into a single object for Playwright
61
+ const args = {
62
+ rect: {
63
+ x: rect.x,
64
+ y: rect.y,
65
+ w: rect.w || rect.width || 0,
66
+ h: rect.h || rect.height || 0,
67
+ },
68
+ highlightId,
69
+ durationSec,
70
+ };
71
+ await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
72
+ const { rect, highlightId, durationSec } = args;
73
+ // Create overlay div
74
+ const overlay = document.createElement('div');
75
+ overlay.id = highlightId;
76
+ overlay.style.position = 'fixed';
77
+ overlay.style.left = `${rect.x}px`;
78
+ overlay.style.top = `${rect.y}px`;
79
+ overlay.style.width = `${rect.w}px`;
80
+ overlay.style.height = `${rect.h}px`;
81
+ overlay.style.border = '3px solid red';
82
+ overlay.style.borderRadius = '2px';
83
+ overlay.style.boxSizing = 'border-box';
84
+ overlay.style.pointerEvents = 'none';
85
+ overlay.style.zIndex = '999999';
86
+ overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
87
+ overlay.style.transition = 'opacity 0.3s ease-out';
88
+ document.body.appendChild(overlay);
89
+ // Remove after duration
90
+ setTimeout(() => {
91
+ overlay.style.opacity = '0';
92
+ setTimeout(() => {
93
+ if (overlay.parentNode) {
94
+ overlay.parentNode.removeChild(overlay);
95
+ }
96
+ }, 300); // Wait for fade-out transition
97
+ }, durationSec * 1000);
98
+ }, args);
99
+ }
100
+ /**
101
+ * Click an element by its ID
102
+ *
103
+ * Uses a hybrid approach: gets element bounding box from snapshot and calculates center,
104
+ * then uses Playwright's native mouse.click() for realistic event simulation.
105
+ * Falls back to JavaScript click if element not found in snapshot.
106
+ *
107
+ * @param browser - SentienceBrowser instance
108
+ * @param elementId - Element ID from snapshot
109
+ * @param useMouse - Use mouse simulation (default: true). If false, uses JavaScript click.
110
+ * @param takeSnapshot - Take snapshot after action (default: false)
111
+ * @returns ActionResult with success status, outcome, duration, and optional snapshot
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const snap = await snapshot(browser);
116
+ * const button = find(snap, 'role=button');
117
+ * if (button) {
118
+ * const result = await click(browser, button.id);
119
+ * console.log(`Click ${result.success ? 'succeeded' : 'failed'}`);
120
+ * }
121
+ * ```
122
+ */
123
+ async function click(browser, elementId, useMouse = true, takeSnapshot = false, cursorPolicy) {
124
+ const page = browser.getPage();
125
+ if (!page) {
126
+ throw new Error('Browser not started. Call start() first.');
127
+ }
128
+ const startTime = Date.now();
129
+ const urlBefore = page.url();
130
+ let success;
131
+ let cursorMeta;
132
+ if (useMouse) {
133
+ // Hybrid approach: Get element bbox from snapshot, calculate center, use mouse.click()
134
+ try {
135
+ const snap = await (0, snapshot_1.snapshot)(browser);
136
+ const element = snap.elements.find(el => el.id === elementId);
137
+ if (element) {
138
+ // Calculate center of element bbox
139
+ const centerX = element.bbox.x + element.bbox.width / 2;
140
+ const centerY = element.bbox.y + element.bbox.height / 2;
141
+ cursorMeta = await humanMoveIfEnabled(page, { x: centerX, y: centerY }, cursorPolicy);
142
+ // Use Playwright's native mouse click for realistic simulation
143
+ await page.mouse.click(centerX, centerY);
144
+ success = true;
145
+ // Keep cursor position even when not in human mode (for future moves)
146
+ cursorPosByPage.set(page, { x: centerX, y: centerY });
147
+ }
148
+ else {
149
+ // Fallback to JS click if element not found in snapshot
150
+ success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
151
+ );
152
+ }
153
+ }
154
+ catch {
155
+ // Fallback to JS click on error
156
+ success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
157
+ );
158
+ }
159
+ }
160
+ else {
161
+ // Legacy JS-based click
162
+ success = await browser_evaluator_1.BrowserEvaluator.evaluateWithNavigationFallback(page, id => window.sentience.click(id), elementId, true // Assume success if navigation destroyed context
163
+ );
164
+ }
165
+ // Wait a bit for navigation/DOM updates
166
+ try {
167
+ await page.waitForTimeout(500);
168
+ }
169
+ catch {
170
+ // Navigation might have happened, context destroyed
171
+ }
172
+ const durationMs = Date.now() - startTime;
173
+ // Check if URL changed (handle navigation gracefully)
174
+ let urlAfter;
175
+ let urlChanged;
176
+ try {
177
+ urlAfter = page.url();
178
+ urlChanged = urlBefore !== urlAfter;
179
+ }
180
+ catch {
181
+ // Context destroyed due to navigation - assume URL changed
182
+ urlAfter = urlBefore;
183
+ urlChanged = true;
184
+ }
185
+ // Determine outcome
186
+ let outcome;
187
+ if (urlChanged) {
188
+ outcome = 'navigated';
189
+ }
190
+ else if (success) {
191
+ outcome = 'dom_updated';
192
+ }
193
+ else {
194
+ outcome = 'error';
195
+ }
196
+ // Optional snapshot after
197
+ let snapshotAfter;
198
+ if (takeSnapshot) {
199
+ try {
200
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
201
+ }
202
+ catch {
203
+ // Navigation might have destroyed context
204
+ }
205
+ }
206
+ return {
207
+ success,
208
+ duration_ms: durationMs,
209
+ outcome,
210
+ url_changed: urlChanged,
211
+ snapshot_after: snapshotAfter,
212
+ cursor: cursorMeta,
213
+ error: success
214
+ ? undefined
215
+ : { code: 'click_failed', reason: 'Element not found or not clickable' },
216
+ };
217
+ }
218
+ /**
219
+ * Type text into an input element
220
+ *
221
+ * Focuses the element first, then types the text using Playwright's keyboard simulation.
222
+ *
223
+ * @param browser - SentienceBrowser instance
224
+ * @param elementId - Element ID from snapshot (must be a text input element)
225
+ * @param text - Text to type
226
+ * @param takeSnapshot - Take snapshot after action (default: false)
227
+ * @param delayMs - Delay between keystrokes in milliseconds for human-like typing (default: 0)
228
+ * @returns ActionResult with success status, outcome, duration, and optional snapshot
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const snap = await snapshot(browser);
233
+ * const searchBox = find(snap, 'role=searchbox');
234
+ * if (searchBox) {
235
+ * // Type instantly (default behavior)
236
+ * await typeText(browser, searchBox.id, 'Hello World');
237
+ *
238
+ * // Type with human-like delay (~10ms between keystrokes)
239
+ * await typeText(browser, searchBox.id, 'Hello World', false, 10);
240
+ * }
241
+ * ```
242
+ */
243
+ async function typeText(browser, elementId, text, takeSnapshot = false, delayMs = 0) {
244
+ const page = browser.getPage();
245
+ if (!page) {
246
+ throw new Error('Browser not started. Call start() first.');
247
+ }
248
+ const startTime = Date.now();
249
+ const urlBefore = page.url();
250
+ // Focus element first
251
+ const focused = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
252
+ const el = window.sentience_registry[id];
253
+ if (el) {
254
+ el.focus();
255
+ return true;
256
+ }
257
+ return false;
258
+ }, elementId);
259
+ if (!focused) {
260
+ return {
261
+ success: false,
262
+ duration_ms: Date.now() - startTime,
263
+ outcome: 'error',
264
+ error: { code: 'focus_failed', reason: 'Element not found' },
265
+ };
266
+ }
267
+ // Type using Playwright keyboard with optional delay between keystrokes
268
+ await page.keyboard.type(text, { delay: delayMs });
269
+ const durationMs = Date.now() - startTime;
270
+ const urlAfter = page.url();
271
+ const urlChanged = urlBefore !== urlAfter;
272
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
273
+ let snapshotAfter;
274
+ if (takeSnapshot) {
275
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
276
+ }
277
+ return {
278
+ success: true,
279
+ duration_ms: durationMs,
280
+ outcome,
281
+ url_changed: urlChanged,
282
+ snapshot_after: snapshotAfter,
283
+ };
284
+ }
285
+ /**
286
+ * Clear the value of an input/textarea element (best-effort).
287
+ */
288
+ async function clear(browser, elementId, takeSnapshot = false) {
289
+ const page = browser.getPage();
290
+ if (!page)
291
+ throw new Error('Browser not started. Call start() first.');
292
+ const startTime = Date.now();
293
+ const urlBefore = page.url();
294
+ const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
295
+ const el = window.sentience_registry?.[id];
296
+ if (!el)
297
+ return false;
298
+ try {
299
+ el.focus?.();
300
+ }
301
+ catch {
302
+ /* ignore */
303
+ }
304
+ if ('value' in el) {
305
+ el.value = '';
306
+ el.dispatchEvent(new Event('input', { bubbles: true }));
307
+ el.dispatchEvent(new Event('change', { bubbles: true }));
308
+ return true;
309
+ }
310
+ return false;
311
+ }, elementId);
312
+ if (!ok) {
313
+ return {
314
+ success: false,
315
+ duration_ms: Date.now() - startTime,
316
+ outcome: 'error',
317
+ error: { code: 'clear_failed', reason: 'Element not found or not clearable' },
318
+ };
319
+ }
320
+ try {
321
+ await page.waitForTimeout(250);
322
+ }
323
+ catch {
324
+ /* ignore */
325
+ }
326
+ const durationMs = Date.now() - startTime;
327
+ const urlAfter = page.url();
328
+ const urlChanged = urlBefore !== urlAfter;
329
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
330
+ let snapshotAfter;
331
+ if (takeSnapshot) {
332
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
333
+ }
334
+ return {
335
+ success: true,
336
+ duration_ms: durationMs,
337
+ outcome,
338
+ url_changed: urlChanged,
339
+ snapshot_after: snapshotAfter,
340
+ };
341
+ }
342
+ /**
343
+ * Ensure a checkbox/radio is checked (best-effort).
344
+ */
345
+ async function check(browser, elementId, takeSnapshot = false) {
346
+ const page = browser.getPage();
347
+ if (!page)
348
+ throw new Error('Browser not started. Call start() first.');
349
+ const startTime = Date.now();
350
+ const urlBefore = page.url();
351
+ const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
352
+ const el = window.sentience_registry?.[id];
353
+ if (!el)
354
+ return false;
355
+ try {
356
+ el.focus?.();
357
+ }
358
+ catch {
359
+ /* ignore */
360
+ }
361
+ if (!('checked' in el))
362
+ return false;
363
+ if (el.checked === true)
364
+ return true;
365
+ try {
366
+ el.click();
367
+ }
368
+ catch {
369
+ return false;
370
+ }
371
+ return true;
372
+ }, elementId);
373
+ if (!ok) {
374
+ return {
375
+ success: false,
376
+ duration_ms: Date.now() - startTime,
377
+ outcome: 'error',
378
+ error: { code: 'check_failed', reason: 'Element not found or not checkable' },
379
+ };
380
+ }
381
+ try {
382
+ await page.waitForTimeout(250);
383
+ }
384
+ catch {
385
+ /* ignore */
386
+ }
387
+ const durationMs = Date.now() - startTime;
388
+ const urlAfter = page.url();
389
+ const urlChanged = urlBefore !== urlAfter;
390
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
391
+ let snapshotAfter;
392
+ if (takeSnapshot)
393
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
394
+ return {
395
+ success: true,
396
+ duration_ms: durationMs,
397
+ outcome,
398
+ url_changed: urlChanged,
399
+ snapshot_after: snapshotAfter,
400
+ };
401
+ }
402
+ /**
403
+ * Ensure a checkbox/radio is unchecked (best-effort).
404
+ */
405
+ async function uncheck(browser, elementId, takeSnapshot = false) {
406
+ const page = browser.getPage();
407
+ if (!page)
408
+ throw new Error('Browser not started. Call start() first.');
409
+ const startTime = Date.now();
410
+ const urlBefore = page.url();
411
+ const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
412
+ const el = window.sentience_registry?.[id];
413
+ if (!el)
414
+ return false;
415
+ try {
416
+ el.focus?.();
417
+ }
418
+ catch {
419
+ /* ignore */
420
+ }
421
+ if (!('checked' in el))
422
+ return false;
423
+ if (el.checked === false)
424
+ return true;
425
+ try {
426
+ el.click();
427
+ }
428
+ catch {
429
+ return false;
430
+ }
431
+ return true;
432
+ }, elementId);
433
+ if (!ok) {
434
+ return {
435
+ success: false,
436
+ duration_ms: Date.now() - startTime,
437
+ outcome: 'error',
438
+ error: { code: 'uncheck_failed', reason: 'Element not found or not uncheckable' },
439
+ };
440
+ }
441
+ try {
442
+ await page.waitForTimeout(250);
443
+ }
444
+ catch {
445
+ /* ignore */
446
+ }
447
+ const durationMs = Date.now() - startTime;
448
+ const urlAfter = page.url();
449
+ const urlChanged = urlBefore !== urlAfter;
450
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
451
+ let snapshotAfter;
452
+ if (takeSnapshot)
453
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
454
+ return {
455
+ success: true,
456
+ duration_ms: durationMs,
457
+ outcome,
458
+ url_changed: urlChanged,
459
+ snapshot_after: snapshotAfter,
460
+ };
461
+ }
462
+ /**
463
+ * Select an option in a <select> element by matching option value or label (best-effort).
464
+ */
465
+ async function selectOption(browser, elementId, option, takeSnapshot = false) {
466
+ const page = browser.getPage();
467
+ if (!page)
468
+ throw new Error('Browser not started. Call start() first.');
469
+ const startTime = Date.now();
470
+ const urlBefore = page.url();
471
+ const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
472
+ const el = window.sentience_registry?.[args.id];
473
+ if (!el)
474
+ return false;
475
+ const tag = String(el.tagName || '').toUpperCase();
476
+ if (tag !== 'SELECT')
477
+ return false;
478
+ const needle = String(args.option ?? '');
479
+ const opts = Array.from(el.options || []);
480
+ let chosen = null;
481
+ for (const o of opts) {
482
+ const oo = o;
483
+ if (String(oo.value) === needle || String(oo.text) === needle) {
484
+ chosen = o;
485
+ break;
486
+ }
487
+ }
488
+ if (!chosen) {
489
+ for (const o of opts) {
490
+ const oo = o;
491
+ if (String(oo.text || '').includes(needle)) {
492
+ chosen = o;
493
+ break;
494
+ }
495
+ }
496
+ }
497
+ if (!chosen)
498
+ return false;
499
+ el.value = chosen.value;
500
+ el.dispatchEvent(new Event('input', { bubbles: true }));
501
+ el.dispatchEvent(new Event('change', { bubbles: true }));
502
+ return true;
503
+ }, { id: elementId, option });
504
+ if (!ok) {
505
+ return {
506
+ success: false,
507
+ duration_ms: Date.now() - startTime,
508
+ outcome: 'error',
509
+ error: { code: 'select_failed', reason: 'Element not found or option not found' },
510
+ };
511
+ }
512
+ try {
513
+ await page.waitForTimeout(250);
514
+ }
515
+ catch {
516
+ /* ignore */
517
+ }
518
+ const durationMs = Date.now() - startTime;
519
+ const urlAfter = page.url();
520
+ const urlChanged = urlBefore !== urlAfter;
521
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
522
+ let snapshotAfter;
523
+ if (takeSnapshot)
524
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
525
+ return {
526
+ success: true,
527
+ duration_ms: durationMs,
528
+ outcome,
529
+ url_changed: urlChanged,
530
+ snapshot_after: snapshotAfter,
531
+ };
532
+ }
533
+ /**
534
+ * Upload a local file via an <input type="file"> element (best-effort).
535
+ */
536
+ async function uploadFile(browser, elementId, filePath, takeSnapshot = false) {
537
+ const page = browser.getPage();
538
+ if (!page)
539
+ throw new Error('Browser not started. Call start() first.');
540
+ const startTime = Date.now();
541
+ const urlBefore = page.url();
542
+ let success = false;
543
+ let errorMsg;
544
+ try {
545
+ // First try: grab the exact element handle from the sentience registry.
546
+ try {
547
+ const handle = await page.evaluateHandle('(id) => (window.sentience_registry && window.sentience_registry[id]) || null', elementId);
548
+ const el = handle.asElement?.() ?? null;
549
+ if (!el)
550
+ throw new Error('Element not found');
551
+ await el.setInputFiles(filePath);
552
+ success = true;
553
+ }
554
+ catch {
555
+ // Fallback: resolve a selector from the element's attributes and use page.setInputFiles().
556
+ const attrs = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
557
+ const el = window.sentience_registry?.[id];
558
+ if (!el)
559
+ return null;
560
+ const tag = String(el.tagName || '').toUpperCase();
561
+ const type = String(el.type || '').toLowerCase();
562
+ const idAttr = el.id ? String(el.id) : null;
563
+ const nameAttr = el.name ? String(el.name) : null;
564
+ return { tag, type, id: idAttr, name: nameAttr };
565
+ }, elementId);
566
+ let selector = null;
567
+ if (attrs && attrs.tag === 'INPUT' && attrs.type === 'file') {
568
+ if (attrs.id)
569
+ selector = `input#${attrs.id}`;
570
+ else if (attrs.name)
571
+ selector = `input[name="${String(attrs.name).replace(/"/g, '\\"')}"]`;
572
+ }
573
+ if (!selector)
574
+ throw new Error('Element not found');
575
+ await page.setInputFiles(selector, filePath);
576
+ success = true;
577
+ }
578
+ }
579
+ catch (e) {
580
+ success = false;
581
+ errorMsg = String(e?.message ?? e);
582
+ }
583
+ try {
584
+ await page.waitForTimeout(250);
585
+ }
586
+ catch {
587
+ /* ignore */
588
+ }
589
+ const durationMs = Date.now() - startTime;
590
+ const urlAfter = page.url();
591
+ const urlChanged = urlBefore !== urlAfter;
592
+ const outcome = urlChanged ? 'navigated' : success ? 'dom_updated' : 'error';
593
+ let snapshotAfter;
594
+ if (takeSnapshot) {
595
+ try {
596
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
597
+ }
598
+ catch {
599
+ /* ignore */
600
+ }
601
+ }
602
+ return {
603
+ success,
604
+ duration_ms: durationMs,
605
+ outcome,
606
+ url_changed: urlChanged,
607
+ snapshot_after: snapshotAfter,
608
+ error: success ? undefined : { code: 'upload_failed', reason: errorMsg ?? 'upload failed' },
609
+ };
610
+ }
611
+ /**
612
+ * Submit a form (best-effort) by clicking a submit control or calling requestSubmit().
613
+ */
614
+ async function submit(browser, elementId, takeSnapshot = false) {
615
+ const page = browser.getPage();
616
+ if (!page)
617
+ throw new Error('Browser not started. Call start() first.');
618
+ const startTime = Date.now();
619
+ const urlBefore = page.url();
620
+ const ok = await browser_evaluator_1.BrowserEvaluator.evaluate(page, id => {
621
+ const el = window.sentience_registry?.[id];
622
+ if (!el)
623
+ return false;
624
+ try {
625
+ el.focus?.();
626
+ }
627
+ catch {
628
+ /* ignore */
629
+ }
630
+ const tag = String(el.tagName || '').toUpperCase();
631
+ if (tag === 'FORM') {
632
+ if (typeof el.requestSubmit === 'function') {
633
+ el.requestSubmit();
634
+ return true;
635
+ }
636
+ try {
637
+ el.submit();
638
+ return true;
639
+ }
640
+ catch {
641
+ return false;
642
+ }
643
+ }
644
+ const form = el.form;
645
+ if (form && typeof form.requestSubmit === 'function') {
646
+ form.requestSubmit();
647
+ return true;
648
+ }
649
+ try {
650
+ el.click();
651
+ return true;
652
+ }
653
+ catch {
654
+ return false;
655
+ }
656
+ }, elementId);
657
+ if (!ok) {
658
+ return {
659
+ success: false,
660
+ duration_ms: Date.now() - startTime,
661
+ outcome: 'error',
662
+ error: { code: 'submit_failed', reason: 'Element not found or not submittable' },
663
+ };
664
+ }
665
+ try {
666
+ await page.waitForTimeout(500);
667
+ }
668
+ catch {
669
+ /* ignore */
670
+ }
671
+ const durationMs = Date.now() - startTime;
672
+ const urlAfter = page.url();
673
+ const urlChanged = urlBefore !== urlAfter;
674
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
675
+ let snapshotAfter;
676
+ if (takeSnapshot) {
677
+ try {
678
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
679
+ }
680
+ catch {
681
+ /* ignore */
682
+ }
683
+ }
684
+ return {
685
+ success: true,
686
+ duration_ms: durationMs,
687
+ outcome,
688
+ url_changed: urlChanged,
689
+ snapshot_after: snapshotAfter,
690
+ };
691
+ }
692
+ /**
693
+ * Navigate back in history (best-effort).
694
+ */
695
+ async function back(browser, takeSnapshot = false) {
696
+ const page = browser.getPage();
697
+ if (!page)
698
+ throw new Error('Browser not started. Call start() first.');
699
+ const startTime = Date.now();
700
+ const urlBefore = page.url();
701
+ let success = false;
702
+ let errorMsg;
703
+ try {
704
+ await page.goBack();
705
+ success = true;
706
+ }
707
+ catch (e) {
708
+ success = false;
709
+ errorMsg = String(e?.message ?? e);
710
+ }
711
+ try {
712
+ await page.waitForTimeout(500);
713
+ }
714
+ catch {
715
+ /* ignore */
716
+ }
717
+ const durationMs = Date.now() - startTime;
718
+ let urlChanged = false;
719
+ try {
720
+ urlChanged = urlBefore !== page.url();
721
+ }
722
+ catch {
723
+ urlChanged = true;
724
+ }
725
+ const outcome = urlChanged ? 'navigated' : success ? 'dom_updated' : 'error';
726
+ let snapshotAfter;
727
+ if (takeSnapshot) {
728
+ try {
729
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
730
+ }
731
+ catch {
732
+ /* ignore */
733
+ }
734
+ }
735
+ return {
736
+ success,
737
+ duration_ms: durationMs,
738
+ outcome,
739
+ url_changed: urlChanged,
740
+ snapshot_after: snapshotAfter,
741
+ error: success ? undefined : { code: 'back_failed', reason: errorMsg ?? 'back failed' },
742
+ };
743
+ }
744
+ /**
745
+ * Scroll an element into view
746
+ *
747
+ * Scrolls the page so that the specified element is visible in the viewport.
748
+ * Uses the element registry to find the element and scrollIntoView() to scroll it.
749
+ *
750
+ * @param browser - SentienceBrowser instance
751
+ * @param elementId - Element ID from snapshot to scroll into view
752
+ * @param behavior - Scroll behavior: 'smooth' for animated scroll, 'instant' for immediate (default: 'smooth')
753
+ * @param block - Vertical alignment: 'start', 'center', 'end', 'nearest' (default: 'center')
754
+ * @param takeSnapshot - Take snapshot after action (default: false)
755
+ * @returns ActionResult with success status, outcome, duration, and optional snapshot
756
+ *
757
+ * @example
758
+ * ```typescript
759
+ * const snap = await snapshot(browser);
760
+ * const button = find(snap, 'role=button[name="Submit"]');
761
+ * if (button) {
762
+ * // Scroll element into view with smooth animation
763
+ * await scrollTo(browser, button.id);
764
+ *
765
+ * // Scroll instantly to top of viewport
766
+ * await scrollTo(browser, button.id, 'instant', 'start');
767
+ * }
768
+ * ```
769
+ */
770
+ async function scrollTo(browser, elementId, behavior = 'smooth', block = 'center', takeSnapshot = false) {
771
+ const page = browser.getPage();
772
+ if (!page) {
773
+ throw new Error('Browser not started. Call start() first.');
774
+ }
775
+ const startTime = Date.now();
776
+ const urlBefore = page.url();
777
+ // Scroll element into view using the element registry
778
+ const scrolled = await browser_evaluator_1.BrowserEvaluator.evaluate(page, (args) => {
779
+ const el = window.sentience_registry[args.id];
780
+ if (el && el.scrollIntoView) {
781
+ el.scrollIntoView({
782
+ behavior: args.behavior,
783
+ block: args.block,
784
+ inline: 'nearest',
785
+ });
786
+ return true;
787
+ }
788
+ return false;
789
+ }, { id: elementId, behavior, block });
790
+ if (!scrolled) {
791
+ return {
792
+ success: false,
793
+ duration_ms: Date.now() - startTime,
794
+ outcome: 'error',
795
+ error: { code: 'scroll_failed', reason: 'Element not found or not scrollable' },
796
+ };
797
+ }
798
+ // Wait a bit for scroll to complete (especially for smooth scrolling)
799
+ await page.waitForTimeout(behavior === 'smooth' ? 500 : 100);
800
+ const durationMs = Date.now() - startTime;
801
+ const urlAfter = page.url();
802
+ const urlChanged = urlBefore !== urlAfter;
803
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
804
+ let snapshotAfter;
805
+ if (takeSnapshot) {
806
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
807
+ }
808
+ return {
809
+ success: true,
810
+ duration_ms: durationMs,
811
+ outcome,
812
+ url_changed: urlChanged,
813
+ snapshot_after: snapshotAfter,
814
+ };
815
+ }
816
+ /**
817
+ * Press a keyboard key
818
+ *
819
+ * Simulates pressing a key using Playwright's keyboard API.
820
+ * Common keys: 'Enter', 'Escape', 'Tab', 'ArrowUp', 'ArrowDown', etc.
821
+ *
822
+ * @param browser - SentienceBrowser instance
823
+ * @param key - Key to press (e.g., 'Enter', 'Escape', 'Tab')
824
+ * @param takeSnapshot - Take snapshot after action (default: false)
825
+ * @returns ActionResult with success status, outcome, duration, and optional snapshot
826
+ *
827
+ * @example
828
+ * ```typescript
829
+ * // Press Enter after typing
830
+ * await typeText(browser, elementId, 'search query');
831
+ * await press(browser, 'Enter');
832
+ * ```
833
+ */
834
+ async function press(browser, key, takeSnapshot = false) {
835
+ const page = browser.getPage();
836
+ if (!page) {
837
+ throw new Error('Browser not started. Call start() first.');
838
+ }
839
+ const startTime = Date.now();
840
+ const urlBefore = page.url();
841
+ // Press key using Playwright
842
+ await page.keyboard.press(key);
843
+ // Wait a bit for navigation/DOM updates
844
+ await page.waitForTimeout(500);
845
+ const durationMs = Date.now() - startTime;
846
+ const urlAfter = page.url();
847
+ const urlChanged = urlBefore !== urlAfter;
848
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
849
+ let snapshotAfter;
850
+ if (takeSnapshot) {
851
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
852
+ }
853
+ return {
854
+ success: true,
855
+ duration_ms: durationMs,
856
+ outcome,
857
+ url_changed: urlChanged,
858
+ snapshot_after: snapshotAfter,
859
+ };
860
+ }
861
+ function normalizeKeyToken(token) {
862
+ const lookup = {
863
+ CMD: 'Meta',
864
+ COMMAND: 'Meta',
865
+ CTRL: 'Control',
866
+ CONTROL: 'Control',
867
+ ALT: 'Alt',
868
+ OPTION: 'Alt',
869
+ SHIFT: 'Shift',
870
+ ESC: 'Escape',
871
+ ESCAPE: 'Escape',
872
+ ENTER: 'Enter',
873
+ RETURN: 'Enter',
874
+ TAB: 'Tab',
875
+ SPACE: 'Space',
876
+ };
877
+ const upper = token.trim().toUpperCase();
878
+ return lookup[upper] ?? token.trim();
879
+ }
880
+ function parseKeySequence(sequence) {
881
+ const parts = [];
882
+ for (const rawPart of sequence.replace(/,/g, ' ').split(/\s+/)) {
883
+ let raw = rawPart.trim();
884
+ if (!raw)
885
+ continue;
886
+ if (raw.startsWith('{') && raw.endsWith('}')) {
887
+ raw = raw.slice(1, -1);
888
+ }
889
+ if (raw.includes('+')) {
890
+ const combo = raw
891
+ .split('+')
892
+ .filter(Boolean)
893
+ .map(token => normalizeKeyToken(token))
894
+ .join('+');
895
+ parts.push(combo);
896
+ }
897
+ else {
898
+ parts.push(normalizeKeyToken(raw));
899
+ }
900
+ }
901
+ return parts;
902
+ }
903
+ /**
904
+ * Send a sequence of key presses (e.g., "CMD+H", "CTRL+SHIFT+P").
905
+ */
906
+ async function sendKeys(browser, sequence, takeSnapshot = false, delayMs = 50) {
907
+ const page = browser.getPage();
908
+ if (!page) {
909
+ throw new Error('Browser not started. Call start() first.');
910
+ }
911
+ const startTime = Date.now();
912
+ const urlBefore = page.url();
913
+ const keys = parseKeySequence(sequence);
914
+ if (keys.length === 0) {
915
+ throw new Error('send_keys sequence is empty');
916
+ }
917
+ for (const key of keys) {
918
+ await page.keyboard.press(key);
919
+ if (delayMs > 0) {
920
+ await page.waitForTimeout(delayMs);
921
+ }
922
+ }
923
+ const durationMs = Date.now() - startTime;
924
+ const urlAfter = page.url();
925
+ const urlChanged = urlBefore !== urlAfter;
926
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
927
+ let snapshotAfter;
928
+ if (takeSnapshot) {
929
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
930
+ }
931
+ return {
932
+ success: true,
933
+ duration_ms: durationMs,
934
+ outcome,
935
+ url_changed: urlChanged,
936
+ snapshot_after: snapshotAfter,
937
+ };
938
+ }
939
+ function buildSearchUrl(query, engine) {
940
+ const q = encodeURIComponent(query).replace(/%20/g, '+');
941
+ const key = engine.trim().toLowerCase();
942
+ if (key === 'duckduckgo' || key === 'ddg') {
943
+ return `https://duckduckgo.com/?q=${q}`;
944
+ }
945
+ if (key === 'google.com' || key === 'google') {
946
+ return `https://www.google.com/search?q=${q}`;
947
+ }
948
+ if (key === 'bing') {
949
+ return `https://www.bing.com/search?q=${q}`;
950
+ }
951
+ throw new Error(`unsupported search engine: ${engine}`);
952
+ }
953
+ /**
954
+ * Navigate to a search results page for the given query.
955
+ */
956
+ async function search(browser, query, engine = 'duckduckgo', takeSnapshot = false, snapshotOptions = undefined) {
957
+ const page = browser.getPage();
958
+ if (!page) {
959
+ throw new Error('Browser not started. Call start() first.');
960
+ }
961
+ if (!query.trim()) {
962
+ throw new Error('search query is empty');
963
+ }
964
+ const startTime = Date.now();
965
+ const urlBefore = page.url();
966
+ const url = buildSearchUrl(query, engine);
967
+ await browser.goto(url);
968
+ // Use lightweight readiness checks instead of networkidle.
969
+ // On Windows CI and some search engines, networkidle can remain pending due to
970
+ // long-lived background requests and cause flaky timeouts.
971
+ try {
972
+ await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
973
+ await page.waitForLoadState('load', { timeout: 5000 });
974
+ }
975
+ catch {
976
+ // best-effort only; URL/outcome assertions do not require full idle
977
+ }
978
+ const durationMs = Date.now() - startTime;
979
+ const urlAfter = page.url();
980
+ const urlChanged = urlBefore !== urlAfter;
981
+ const outcome = urlChanged ? 'navigated' : 'dom_updated';
982
+ let snapshotAfter;
983
+ if (takeSnapshot) {
984
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser, snapshotOptions);
985
+ }
986
+ return {
987
+ success: true,
988
+ duration_ms: durationMs,
989
+ outcome,
990
+ url_changed: urlChanged,
991
+ snapshot_after: snapshotAfter,
992
+ };
993
+ }
994
+ /**
995
+ * Click at the center of a rectangle using Playwright's native mouse simulation.
996
+ * This uses a hybrid approach: calculates center coordinates and uses mouse.click()
997
+ * for realistic event simulation (triggers hover, focus, mousedown, mouseup).
998
+ *
999
+ * @param browser - SentienceBrowser instance
1000
+ * @param rect - Rectangle with x, y, w (or width), h (or height) keys, or BBox object
1001
+ * @param highlight - Whether to show a red border highlight when clicking (default: true)
1002
+ * @param highlightDuration - How long to show the highlight in seconds (default: 2.0)
1003
+ * @param takeSnapshot - Whether to take snapshot after action
1004
+ * @returns ActionResult
1005
+ *
1006
+ * @example
1007
+ * ```typescript
1008
+ * // Click using rect object
1009
+ * await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 });
1010
+ *
1011
+ * // Click using BBox from element
1012
+ * const snap = await snapshot(browser);
1013
+ * const element = find(snap, "role=button");
1014
+ * if (element) {
1015
+ * await clickRect(browser, {
1016
+ * x: element.bbox.x,
1017
+ * y: element.bbox.y,
1018
+ * w: element.bbox.width,
1019
+ * h: element.bbox.height
1020
+ * });
1021
+ * }
1022
+ *
1023
+ * // Without highlight
1024
+ * await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 }, false);
1025
+ *
1026
+ * // Custom highlight duration
1027
+ * await clickRect(browser, { x: 100, y: 200, w: 50, h: 30 }, true, 3.0);
1028
+ * ```
1029
+ */
1030
+ async function clickRect(browser, rect, highlight = true, highlightDuration = 2.0, takeSnapshot = false, cursorPolicy) {
1031
+ const page = browser.getPage();
1032
+ if (!page) {
1033
+ throw new Error('Browser not started. Call start() first.');
1034
+ }
1035
+ const startTime = Date.now();
1036
+ const urlBefore = page.url();
1037
+ // Handle BBox object or ClickRect dict
1038
+ let x, y, w, h;
1039
+ if ('width' in rect && 'height' in rect && !('w' in rect) && !('h' in rect)) {
1040
+ // BBox object (width and height are required in BBox)
1041
+ const bbox = rect;
1042
+ x = bbox.x;
1043
+ y = bbox.y;
1044
+ w = bbox.width;
1045
+ h = bbox.height;
1046
+ }
1047
+ else {
1048
+ // ClickRect dict
1049
+ const clickRect = rect;
1050
+ x = clickRect.x;
1051
+ y = clickRect.y;
1052
+ w = clickRect.w || clickRect.width || 0;
1053
+ h = clickRect.h || clickRect.height || 0;
1054
+ }
1055
+ if (w <= 0 || h <= 0) {
1056
+ return {
1057
+ success: false,
1058
+ duration_ms: 0,
1059
+ outcome: 'error',
1060
+ error: {
1061
+ code: 'invalid_rect',
1062
+ reason: 'Rectangle width and height must be positive',
1063
+ },
1064
+ };
1065
+ }
1066
+ // Calculate center of rectangle
1067
+ const centerX = x + w / 2;
1068
+ const centerY = y + h / 2;
1069
+ let cursorMeta;
1070
+ // Show highlight before clicking (if enabled)
1071
+ if (highlight) {
1072
+ await highlightRect(browser, { x, y, w, h }, highlightDuration);
1073
+ // Small delay to ensure highlight is visible
1074
+ await page.waitForTimeout(50);
1075
+ }
1076
+ // Use Playwright's native mouse click for realistic simulation
1077
+ let success;
1078
+ let errorMsg;
1079
+ try {
1080
+ cursorMeta = await humanMoveIfEnabled(page, { x: centerX, y: centerY }, cursorPolicy);
1081
+ await page.mouse.click(centerX, centerY);
1082
+ success = true;
1083
+ cursorPosByPage.set(page, { x: centerX, y: centerY });
1084
+ }
1085
+ catch (error) {
1086
+ success = false;
1087
+ errorMsg = error instanceof Error ? error.message : String(error);
1088
+ }
1089
+ // Wait a bit for navigation/DOM updates
1090
+ await page.waitForTimeout(500);
1091
+ const durationMs = Date.now() - startTime;
1092
+ const urlAfter = page.url();
1093
+ const urlChanged = urlBefore !== urlAfter;
1094
+ // Determine outcome
1095
+ let outcome;
1096
+ if (urlChanged) {
1097
+ outcome = 'navigated';
1098
+ }
1099
+ else if (success) {
1100
+ outcome = 'dom_updated';
1101
+ }
1102
+ else {
1103
+ outcome = 'error';
1104
+ }
1105
+ // Optional snapshot after
1106
+ let snapshotAfter;
1107
+ if (takeSnapshot) {
1108
+ snapshotAfter = await (0, snapshot_1.snapshot)(browser);
1109
+ }
1110
+ return {
1111
+ success,
1112
+ duration_ms: durationMs,
1113
+ outcome,
1114
+ url_changed: urlChanged,
1115
+ snapshot_after: snapshotAfter,
1116
+ cursor: cursorMeta,
1117
+ error: success ? undefined : { code: 'click_failed', reason: errorMsg || 'Click failed' },
1118
+ };
1119
+ }
1120
+ //# sourceMappingURL=actions.js.map