@cyberstrike-io/cyberstrike 1.1.16-beta.2 → 1.1.16-beta.3

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.
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
8
  var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
9
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
10
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
11
  for (let key of __getOwnPropNames(mod))
24
12
  if (!__hasOwnProp.call(to, key))
25
13
  __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
14
+ get: () => mod[key],
27
15
  enumerable: true
28
16
  });
29
- if (canCache)
30
- cache.set(mod, to);
31
17
  return to;
32
18
  };
33
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
- var __returnValue = (v) => v;
35
- function __exportSetter(name, newValue) {
36
- this[name] = __returnValue.bind(null, newValue);
37
- }
38
20
  var __export = (target, all) => {
39
21
  for (var name in all)
40
22
  __defProp(target, name, {
41
23
  get: all[name],
42
24
  enumerable: true,
43
25
  configurable: true,
44
- set: __exportSetter.bind(all, name)
26
+ set: (newValue) => all[name] = () => newValue
45
27
  });
46
28
  };
47
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -19343,7 +19325,7 @@ var require_tracestate_impl = __commonJS((exports) => {
19343
19325
  const value = listMember.slice(i2 + 1, part.length);
19344
19326
  if ((0, tracestate_validators_1.validateKey)(key) && (0, tracestate_validators_1.validateValue)(value)) {
19345
19327
  agg.set(key, value);
19346
- }
19328
+ } else {}
19347
19329
  }
19348
19330
  return agg;
19349
19331
  }, new Map);
@@ -30852,7 +30834,7 @@ class JSONSchemaGenerator {
30852
30834
  if (val === undefined) {
30853
30835
  if (this.unrepresentable === "throw") {
30854
30836
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
30855
- }
30837
+ } else {}
30856
30838
  } else if (typeof val === "bigint") {
30857
30839
  if (this.unrepresentable === "throw") {
30858
30840
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -87530,7 +87512,7 @@ function finalize(ctx, schema) {
87530
87512
  result.$schema = "http://json-schema.org/draft-07/schema#";
87531
87513
  } else if (ctx.target === "draft-04") {
87532
87514
  result.$schema = "http://json-schema.org/draft-04/schema#";
87533
- } else if (ctx.target === "openapi-3.0") {}
87515
+ } else if (ctx.target === "openapi-3.0") {} else {}
87534
87516
  if (ctx.external?.uri) {
87535
87517
  const id = ctx.external.registry.get(schema)?.id;
87536
87518
  if (!id)
@@ -87777,7 +87759,7 @@ var literalProcessor = (schema, ctx, json2, _params) => {
87777
87759
  if (val === undefined) {
87778
87760
  if (ctx.unrepresentable === "throw") {
87779
87761
  throw new Error("Literal `undefined` cannot be represented in JSON Schema");
87780
- }
87762
+ } else {}
87781
87763
  } else if (typeof val === "bigint") {
87782
87764
  if (ctx.unrepresentable === "throw") {
87783
87765
  throw new Error("BigInt literals cannot be represented in JSON Schema");
@@ -115538,6 +115520,9 @@ A JSON snapshot with:
115538
115520
  - url: current page URL
115539
115521
  - viewportCenterBlocked: true if an overlay/modal/sidebar is open
115540
115522
  - elements: interactive elements with id, role, label, type, value, placeholder, href, options
115523
+ - occludedBy: present when this element is physically covered by an overlay and
115524
+ cannot be clicked until that overlay is cleared. The value is the overlay's
115525
+ visible text (which usually includes its own dismiss control's label).
115541
115526
  - outOfScope (optional): array of labels the user has excluded — never plan tasks matching these
115542
115527
  - pendingMutations (optional): mutation keywords awaiting a triggering task.
115543
115528
  If a task you plan would trigger one of these, set \`triggersMutation\` on that
@@ -115639,6 +115624,25 @@ Also include:
115639
115624
  - Product/item cards — include max 2-3 (the API pattern repeats)
115640
115625
  - CRUD buttons (Edit, Delete, View, Export, etc.) — one per action type is enough
115641
115626
 
115627
+ ### Modal/dialog you opened to fill (viewportCenterBlocked + form fields)
115628
+ When viewportCenterBlocked is true because a MODAL/DIALOG containing form fields is
115629
+ open (you likely just opened it via a "+ Add" / "New" / "Create" / "Edit" button),
115630
+ that modal IS the content to explore: FILL its fields and SUBMIT via its primary
115631
+ action button (e.g. Kaydet / Save / Submit / Create). Do NOT plan a click on its
115632
+ close / X / Cancel control — closing it discards the form before it is submitted.
115633
+ Only dismiss a centered overlay that has NO form to complete (a banner, tour, cookie
115634
+ or notification notice covering the content behind it).
115635
+
115636
+ ### Occluded elements (occludedBy present)
115637
+ An element with an \`occludedBy\` field is physically covered by an overlay and
115638
+ cannot be clicked until that overlay is cleared. \`occludedBy\` is the overlay's
115639
+ visible text. BEFORE planning any task on an occluded element, plan a click that
115640
+ dismisses the overlay FIRST (make it the first task). Identify the dismiss control
115641
+ among the listed elements using the \`occludedBy\` text, and choose the LEAST-COMMITTAL
115642
+ option — one that closes or postpones without granting permissions, subscribing,
115643
+ accepting terms, or changing account state. If no such dismiss control is present,
115644
+ simply omit the occluded element (do not invent a control, do not click a destructive one).
115645
+
115642
115646
  ### Skip these (do NOT include as tasks):
115643
115647
  - Language / locale selectors (labels: "EN", "English", "DE", "🌐")
115644
115648
  - Logout / sign-out / delete account (system handles logout separately)
@@ -115830,8 +115834,11 @@ async function planPage(snapshot, model, usageAcc) {
115830
115834
  model,
115831
115835
  system: systemPrompt,
115832
115836
  messages: [{ role: "user", content: userMessage }],
115833
- maxOutputTokens: 2048,
115834
- temperature: 0
115837
+ maxOutputTokens: 16384,
115838
+ temperature: 0,
115839
+ providerOptions: {
115840
+ openai: { reasoningEffort: "low" }
115841
+ }
115835
115842
  });
115836
115843
  if (usageAcc) {
115837
115844
  usageAcc.inputTokens += result.usage.inputTokens ?? 0;
@@ -115884,8 +115891,11 @@ async function planUnexploredElements(snapshot, unexploredLabels, model, usageAc
115884
115891
  model,
115885
115892
  system: systemPrompt,
115886
115893
  messages: [{ role: "user", content: userMessage }],
115887
- maxOutputTokens: 2048,
115888
- temperature: 0
115894
+ maxOutputTokens: 16384,
115895
+ temperature: 0,
115896
+ providerOptions: {
115897
+ openai: { reasoningEffort: "low" }
115898
+ }
115889
115899
  });
115890
115900
  if (usageAcc) {
115891
115901
  usageAcc.inputTokens += result.usage.inputTokens ?? 0;
@@ -116123,12 +116133,20 @@ function elementToPrompt(el) {
116123
116133
  }
116124
116134
  return result;
116125
116135
  }
116126
- function buildPlannerSnapshot(url3, elements, globalState, credId, viewportCenterBlocked) {
116136
+ function buildPlannerSnapshot(url3, elements, globalState, credId, viewportCenterBlocked, occlusions) {
116137
+ const promptElements = elements.map(elementToPrompt);
116138
+ if (occlusions && occlusions.length > 0) {
116139
+ for (const occ of occlusions) {
116140
+ const match = promptElements.find((e2) => e2.label === occ.label && e2.role === occ.role);
116141
+ if (match)
116142
+ match.occludedBy = occ.occluderText;
116143
+ }
116144
+ }
116127
116145
  const snapshot = {
116128
116146
  url: url3,
116129
116147
  viewportCenterBlocked,
116130
116148
  totalPagesVisited: globalState.visitedPages.size,
116131
- elements: elements.map(elementToPrompt)
116149
+ elements: promptElements
116132
116150
  };
116133
116151
  if (globalState.outOfScope.length > 0) {
116134
116152
  snapshot.outOfScope = [...globalState.outOfScope];
@@ -116299,7 +116317,7 @@ async function collectInteractiveElements(page) {
116299
116317
  return true;
116300
116318
  return /^(mui-\d|radix-|headlessui-|react-aria-|rc-_?\d|ember\d)/i.test(id);
116301
116319
  }
116302
- function buildCSSSelector(el) {
116320
+ function buildCSSSelectorCandidate(el) {
116303
116321
  const tag = el.tagName.toLowerCase();
116304
116322
  const id = el.getAttribute("id");
116305
116323
  if (id && !isEphemeralId(id))
@@ -116347,6 +116365,35 @@ async function collectInteractiveElements(page) {
116347
116365
  }
116348
116366
  return tag;
116349
116367
  }
116368
+ function uniquePositionalPath(el) {
116369
+ const segs = [];
116370
+ let cur = el;
116371
+ while (cur && cur !== document.documentElement) {
116372
+ const tag = cur.tagName.toLowerCase();
116373
+ const id = cur.getAttribute("id");
116374
+ if (id && !isEphemeralId(id)) {
116375
+ segs.unshift(`${tag}#${CSS.escape(id)}`);
116376
+ break;
116377
+ }
116378
+ const parent = cur.parentElement;
116379
+ if (!parent) {
116380
+ segs.unshift(tag);
116381
+ break;
116382
+ }
116383
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
116384
+ segs.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${sameTag.indexOf(cur) + 1})` : tag);
116385
+ cur = parent;
116386
+ }
116387
+ return segs.join(" > ");
116388
+ }
116389
+ function buildCSSSelector(el) {
116390
+ const candidate = buildCSSSelectorCandidate(el);
116391
+ try {
116392
+ if (document.querySelectorAll(candidate).length === 1)
116393
+ return candidate;
116394
+ } catch {}
116395
+ return uniquePositionalPath(el);
116396
+ }
116350
116397
  const INTERACTIVE_SELECTORS = [
116351
116398
  "button",
116352
116399
  "a[href]",
@@ -116438,7 +116485,8 @@ async function collectInteractiveElements(page) {
116438
116485
  const disambiguatedLabel = count > 1 ? `${label} (${count})` : label;
116439
116486
  const ariaLabelRaw = (el.getAttribute("aria-label") || "").trim();
116440
116487
  const safeAriaLabel = ariaLabelRaw.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
116441
- const selectorRole = syntheticRole || count > 1 ? "" : safeAriaLabel ? `role=${role}[name="${safeAriaLabel}"]` : `role=${role}`;
116488
+ const isFileInput = el.matches("input[type=file]");
116489
+ const selectorRole = syntheticRole || count > 1 || isFileInput ? "" : safeAriaLabel ? `role=${role}[name="${safeAriaLabel}"]` : `role=${role}`;
116442
116490
  const selectorCSS = buildCSSSelector(el);
116443
116491
  const inChrome = !!el.closest("nav, header, footer, aside, [role=navigation], [role=banner], [role=contentinfo], [role=complementary]");
116444
116492
  const roleCount = (seenRoleSelectors.get(selectorRole) ?? 0) + 1;
@@ -116573,8 +116621,17 @@ function assignIds(browserElements, startId) {
116573
116621
  }
116574
116622
  async function collectElements(page) {
116575
116623
  const browserElements = await collectInteractiveElements(page);
116576
- const sampled = sampleTemplates(browserElements);
116577
- return assignIds(sampled, 1).slice(0, MAX_ELEMENTS);
116624
+ const sampled = capElements(sampleTemplates(browserElements));
116625
+ return assignIds(sampled, 1);
116626
+ }
116627
+ var ACTION_ROLES_CAP = new Set(["button", "link", "menuitem", "tab"]);
116628
+ function capElements(els) {
116629
+ if (els.length <= MAX_ELEMENTS)
116630
+ return els;
116631
+ const actions = els.filter((e2) => ACTION_ROLES_CAP.has(e2.role));
116632
+ const rest = els.filter((e2) => !ACTION_ROLES_CAP.has(e2.role));
116633
+ const kept = new Set([...actions, ...rest].slice(0, MAX_ELEMENTS));
116634
+ return els.filter((e2) => kept.has(e2));
116578
116635
  }
116579
116636
  function sampleTemplates(elements) {
116580
116637
  const clusterCount = new Map;
@@ -116703,6 +116760,48 @@ function evalIsViewportCenterBlocked(page) {
116703
116760
  return false;
116704
116761
  });
116705
116762
  }
116763
+ async function probeClickPoint(page, selector) {
116764
+ try {
116765
+ return await page.evaluate((sel) => {
116766
+ const el = document.querySelector(sel);
116767
+ if (!el)
116768
+ return { status: "clickable" };
116769
+ const r2 = el.getBoundingClientRect();
116770
+ if (r2.width === 0 || r2.height === 0)
116771
+ return { status: "clickable" };
116772
+ const cx = r2.left + r2.width / 2;
116773
+ const cy = r2.top + r2.height / 2;
116774
+ if (cx < 0 || cy < 0 || cx > window.innerWidth || cy > window.innerHeight) {
116775
+ return { status: "offscreen" };
116776
+ }
116777
+ const top = document.elementFromPoint(cx, cy);
116778
+ if (!top)
116779
+ return { status: "offscreen" };
116780
+ if (top === el || el.contains(top) || top.contains(el))
116781
+ return { status: "clickable" };
116782
+ if (top.tagName === "IFRAME")
116783
+ return { status: "clickable" };
116784
+ let node = el;
116785
+ while (node) {
116786
+ const root = node.getRootNode();
116787
+ if (root instanceof ShadowRoot) {
116788
+ const host = root.host;
116789
+ if (host === top || top.contains(host))
116790
+ return { status: "clickable" };
116791
+ node = host;
116792
+ } else
116793
+ break;
116794
+ }
116795
+ const text2 = (top.innerText || top.getAttribute("aria-label") || "").trim().replace(/\s+/g, " ").slice(0, 80);
116796
+ return {
116797
+ status: "occluded",
116798
+ occluder: { tag: top.tagName.toLowerCase(), role: top.getAttribute("role") || "", text: text2 }
116799
+ };
116800
+ }, selector);
116801
+ } catch {
116802
+ return { status: "clickable" };
116803
+ }
116804
+ }
116706
116805
  function filterVisitedLinks(elements, currentUrl, visitedPages) {
116707
116806
  let currentPath;
116708
116807
  try {
@@ -116728,6 +116827,59 @@ function filterVisitedLinks(elements, currentUrl, visitedPages) {
116728
116827
  });
116729
116828
  }
116730
116829
 
116830
+ // ../hackbrowser/src/upload-samples.ts
116831
+ var nodeBuffer = globalThis.Buffer;
116832
+ var b64 = (s2) => nodeBuffer.from(s2, "base64");
116833
+ var txt = (s2) => nodeBuffer.from(s2, "utf8");
116834
+ var SAMPLES = [
116835
+ {
116836
+ name: "sample.png",
116837
+ ext: "png",
116838
+ mimeType: "image/png",
116839
+ buffer: b64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGJgAQAAAAUAAaX2RUAAAAAASUVORK5CYII=")
116840
+ },
116841
+ {
116842
+ name: "sample.jpg",
116843
+ ext: "jpg",
116844
+ mimeType: "image/jpeg",
116845
+ buffer: b64("/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AfwD/2Q==")
116846
+ },
116847
+ {
116848
+ name: "sample.pdf",
116849
+ ext: "pdf",
116850
+ mimeType: "application/pdf",
116851
+ buffer: b64("JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBvYmo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlwZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgMjAwIDIwMF0+PmVuZG9iagp0cmFpbGVyPDwvUm9vdCAxIDAgUj4+CiUlRU9GCg==")
116852
+ },
116853
+ { name: "sample.csv", ext: "csv", mimeType: "text/csv", buffer: txt(`name,email
116854
+ Test User,test@example.com
116855
+ `) },
116856
+ { name: "sample.txt", ext: "txt", mimeType: "text/plain", buffer: txt(`sample text file
116857
+ `) },
116858
+ { name: "sample.json", ext: "json", mimeType: "application/json", buffer: txt(`{"sample":true,"value":123}
116859
+ `) }
116860
+ ];
116861
+ var DEFAULT_SAMPLE = SAMPLES[0];
116862
+ function tokenMatches(sample, token) {
116863
+ const t2 = token.trim().toLowerCase();
116864
+ if (!t2)
116865
+ return false;
116866
+ if (t2.startsWith("."))
116867
+ return t2.slice(1) === sample.ext;
116868
+ if (t2.endsWith("/*"))
116869
+ return sample.mimeType.startsWith(t2.slice(0, -1));
116870
+ return t2 === sample.mimeType;
116871
+ }
116872
+ function pickSample(accept) {
116873
+ if (!accept || !accept.trim())
116874
+ return DEFAULT_SAMPLE;
116875
+ const tokens = accept.split(",");
116876
+ for (const sample of SAMPLES) {
116877
+ if (tokens.some((tok) => tokenMatches(sample, tok)))
116878
+ return sample;
116879
+ }
116880
+ return DEFAULT_SAMPLE;
116881
+ }
116882
+
116731
116883
  // ../hackbrowser/src/executor.ts
116732
116884
  var log6 = Log.create({ service: "hackbrowser:executor" });
116733
116885
  var CLICK_TIMEOUT = 2000;
@@ -116773,6 +116925,9 @@ async function executeFill(page, element, value) {
116773
116925
  if (element.role === "slider" || element.type === "range") {
116774
116926
  return executeSliderFill(page, selector || "role=slider", value);
116775
116927
  }
116928
+ if (element.type === "file") {
116929
+ return executeFileSelect(page, selector);
116930
+ }
116776
116931
  const fillErr = await page.fill(selector, value, { timeout: FILL_TIMEOUT }).then(() => null).catch((e2) => e2.message.split(`
116777
116932
  `)[0]);
116778
116933
  if (!fillErr)
@@ -116784,6 +116939,18 @@ async function executeFill(page, element, value) {
116784
116939
  log6.warn("fill failed", { selector, error: fillErr });
116785
116940
  return fillErr;
116786
116941
  }
116942
+ async function executeFileSelect(page, selector) {
116943
+ const accept = await page.getAttribute(selector, "accept").catch(() => null);
116944
+ const sample = pickSample(accept);
116945
+ const err = await page.setInputFiles(selector, { name: sample.name, mimeType: sample.mimeType, buffer: sample.buffer }, { timeout: FILL_TIMEOUT }).then(() => null).catch((e2) => e2.message.split(`
116946
+ `)[0]);
116947
+ if (err) {
116948
+ log6.warn("file select failed", { selector, accept, error: err });
116949
+ return err;
116950
+ }
116951
+ log6.info("file selected", { selector, file: sample.name });
116952
+ return;
116953
+ }
116787
116954
  async function executeSliderFill(page, selector, value) {
116788
116955
  const numericValue = parseInt(value);
116789
116956
  if (isNaN(numericValue) || numericValue <= 0) {
@@ -127843,7 +128010,8 @@ function normalizeScope(input) {
127843
128010
  s2 = s2.replace(/^https?:\/\//, "");
127844
128011
  s2 = s2.split(/[/?#]/)[0] ?? "";
127845
128012
  s2 = s2.replace(/:\d+$/, "");
127846
- s2 = s2.replace(/\.+$/, "");
128013
+ while (s2.endsWith("."))
128014
+ s2 = s2.slice(0, -1);
127847
128015
  return s2;
127848
128016
  }
127849
128017
  function deriveScope(targetUrl) {
@@ -127872,8 +128040,10 @@ var log7 = Log.create({ service: "hackbrowser:agent" });
127872
128040
  var MAX_STEPS_PER_PAGE = 30;
127873
128041
  var MAX_UNPLANNED_ITERATIONS = 2;
127874
128042
  var OVERLAY_ESCAPE_WAIT = 400;
128043
+ var OVERLAY_CLEAR_WAIT = 800;
127875
128044
  var SPA_RENDER_RETRY_WAIT = 600;
127876
128045
  var POST_GOTO_WAIT = 400;
128046
+ var MAX_INLINE_DEPTH = 2;
127877
128047
  var LOGIN_SUCCESS_PATTERN = /POST\s+.*\/(login|signin|authenticate)\S*\s+\[200\]/i;
127878
128048
  function triggerReDiscovery(globalState) {
127879
128049
  if (globalState.authPhase === "authenticated")
@@ -128036,6 +128206,15 @@ function attachDialogAutoAccept(page) {
128036
128206
  dialog.accept().catch(() => {});
128037
128207
  });
128038
128208
  }
128209
+ function attachFileChooserAutoFill(page) {
128210
+ page.on("filechooser", async (chooser) => {
128211
+ try {
128212
+ const accept = await chooser.element().getAttribute("accept").catch(() => null);
128213
+ const sample = pickSample(accept);
128214
+ await chooser.setFiles({ name: sample.name, mimeType: sample.mimeType, buffer: sample.buffer });
128215
+ } catch {}
128216
+ });
128217
+ }
128039
128218
  function setupRequestInterceptor(page, inScope, onCapture, credentialId = SINGLE_CRED) {
128040
128219
  let pendingUIContext = null;
128041
128220
  let pendingTrigger = null;
@@ -128185,9 +128364,10 @@ function getPathPatternKey(url3) {
128185
128364
  return null;
128186
128365
  }
128187
128366
  }
128188
- async function explorePageWithAI(page, pageUrl, interceptor, model, globalState, inScope, credentialId, maxPages, usageAcc) {
128367
+ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState, inScope, credentialId, maxPages, usageAcc, nav = { explore: async () => {}, depth: MAX_INLINE_DEPTH }) {
128189
128368
  const linksToEnqueue = [];
128190
128369
  const semanticActionsDone = new Set;
128370
+ const occ = { pending: [], signaled: new Set };
128191
128371
  csEmit(page, {
128192
128372
  type: "page-change",
128193
128373
  url: pageUrl,
@@ -128246,9 +128426,9 @@ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState,
128246
128426
  remaining: taskQueue.length
128247
128427
  });
128248
128428
  if (task.type === "form") {
128249
- await executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope);
128429
+ await executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav);
128250
128430
  } else {
128251
- await executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope);
128431
+ await executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav);
128252
128432
  }
128253
128433
  const postActionElements = filterVisitedLinks(await collectElements(page), pageUrl, globalState.visitedPages);
128254
128434
  if (task.type === "click" && task.role === "combobox") {
@@ -128259,9 +128439,10 @@ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState,
128259
128439
  }
128260
128440
  const hasNewElements = discoverNewElements(postActionElements, seenKeys);
128261
128441
  const hasStaleTargets = queueHasStaleTargets(taskQueue, postActionElements);
128262
- if (hasNewElements || hasStaleTargets) {
128442
+ if (hasNewElements || hasStaleTargets || occ.pending.length > 0) {
128263
128443
  const freshElements = filterVisitedLinks(await collectElements(page), pageUrl, globalState.visitedPages);
128264
- const snapshot2 = buildPlannerSnapshot(pageUrl, freshElements, globalState, credentialId, await isViewportCenterBlocked(page));
128444
+ const snapshot2 = buildPlannerSnapshot(pageUrl, freshElements, globalState, credentialId, await isViewportCenterBlocked(page), occ.pending);
128445
+ occ.pending = [];
128265
128446
  csEmit(page, {
128266
128447
  type: "llm-thinking",
128267
128448
  reason: "replan",
@@ -128367,9 +128548,9 @@ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState,
128367
128548
  remaining: additionalQueue.length
128368
128549
  });
128369
128550
  if (task.type === "form") {
128370
- await executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope);
128551
+ await executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav);
128371
128552
  } else {
128372
- await executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope);
128553
+ await executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav);
128373
128554
  }
128374
128555
  const postActionElements = filterVisitedLinks(await collectElements(page), pageUrl, globalState.visitedPages);
128375
128556
  if (task.type === "click" && task.role === "combobox") {
@@ -128379,9 +128560,10 @@ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState,
128379
128560
  }
128380
128561
  const hasNewElements = discoverNewElements(postActionElements, seenKeys);
128381
128562
  const hasStaleTargets = queueHasStaleTargets(additionalQueue, postActionElements);
128382
- if (hasNewElements || hasStaleTargets) {
128563
+ if (hasNewElements || hasStaleTargets || occ.pending.length > 0) {
128383
128564
  const freshElements = filterVisitedLinks(await collectElements(page), pageUrl, globalState.visitedPages);
128384
- const freshSnap = buildPlannerSnapshot(pageUrl, freshElements, globalState, credentialId, await isViewportCenterBlocked(page));
128565
+ const freshSnap = buildPlannerSnapshot(pageUrl, freshElements, globalState, credentialId, await isViewportCenterBlocked(page), occ.pending);
128566
+ occ.pending = [];
128385
128567
  csEmit(page, {
128386
128568
  type: "llm-thinking",
128387
128569
  reason: "replan",
@@ -128431,7 +128613,45 @@ async function explorePageWithAI(page, pageUrl, interceptor, model, globalState,
128431
128613
  }
128432
128614
  return linksToEnqueue;
128433
128615
  }
128434
- async function executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope) {
128616
+ async function recoverFromOcclusion(page, el, original, retry) {
128617
+ if (original.success)
128618
+ return original;
128619
+ if ((await probeClickPoint(page, el.selector)).status !== "occluded")
128620
+ return original;
128621
+ log7.debug("click occluded — clearing overlay + waiting, then retrying once", { label: el.label });
128622
+ await closeOverlay(page);
128623
+ await page.waitForTimeout(OVERLAY_CLEAR_WAIT);
128624
+ return retry();
128625
+ }
128626
+ async function diagnoseOcclusion(page, el, occ) {
128627
+ const probe = await probeClickPoint(page, el.selector);
128628
+ if (probe.status === "occluded" && !occ.signaled.has(probe.occluder.text)) {
128629
+ occ.signaled.add(probe.occluder.text);
128630
+ occ.pending.push({ label: el.label, role: el.role, occluderText: probe.occluder.text });
128631
+ log7.debug("action target occluded — signalling planner to dismiss overlay first", {
128632
+ label: el.label,
128633
+ occluder: probe.occluder.text
128634
+ });
128635
+ return true;
128636
+ }
128637
+ return false;
128638
+ }
128639
+ async function handleNavigation(page, parentUrl, linksToEnqueue, inScope, globalState, nav) {
128640
+ const cur = page.url();
128641
+ const curNorm = normalizeUrl(cur);
128642
+ if (curNorm !== normalizeUrl(parentUrl) && isInScope(cur, inScope)) {
128643
+ if (!globalState.visitedPages.has(curNorm) && nav.depth < MAX_INLINE_DEPTH) {
128644
+ globalState.visitedPages.add(curNorm);
128645
+ await nav.explore(page, cur, nav.depth + 1);
128646
+ } else {
128647
+ linksToEnqueue.push(cur);
128648
+ }
128649
+ }
128650
+ await page.goto(parentUrl, { waitUntil: "domcontentloaded", timeout: 15000 }).catch(() => {});
128651
+ await page.waitForTimeout(POST_GOTO_WAIT);
128652
+ await page.waitForLoadState("domcontentloaded", { timeout: 5000 }).catch(() => {});
128653
+ }
128654
+ async function executeFormTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav) {
128435
128655
  for (const field of task.fields) {
128436
128656
  let el = resolveElement(elements, field.role, field.label);
128437
128657
  if (!el) {
@@ -128483,23 +128703,19 @@ async function executeFormTask(task, page, elements, interceptor, semanticAction
128483
128703
  targetSelector: submitEl.selector,
128484
128704
  credential: credentialId
128485
128705
  });
128486
- const result = await execute(page, submitEl, "click", undefined, interceptor.setPendingUI, elements.length);
128706
+ let result = await execute(page, submitEl, "click", undefined, interceptor.setPendingUI, elements.length);
128707
+ result = await recoverFromOcclusion(page, submitEl, result, () => execute(page, submitEl, "click", undefined, interceptor.setPendingUI, elements.length));
128708
+ const occluded = !result.success && await diagnoseOcclusion(page, submitEl, occ);
128487
128709
  interceptor.clearPendingTrigger();
128488
128710
  trackResult(result, interceptor, globalState, credentialId, task.triggersMutation, page);
128489
128711
  csEmit(page, { type: "action-end", ok: result.success, credential: credentialId });
128490
- semanticActionsDone.add(submitKey);
128712
+ if (!occluded)
128713
+ semanticActionsDone.add(submitKey);
128491
128714
  globalState.totalSteps++;
128492
- if (result.navigated) {
128493
- const cur = page.url();
128494
- if (cur !== pageUrl && isInScope(cur, inScope))
128495
- linksToEnqueue.push(cur);
128496
- log7.debug("reloading after form submit", { pageUrl });
128497
- await page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 15000 }).catch(() => {});
128498
- await page.waitForTimeout(POST_GOTO_WAIT);
128499
- await page.waitForLoadState("domcontentloaded", { timeout: 5000 }).catch(() => {});
128500
- }
128715
+ if (result.navigated)
128716
+ await handleNavigation(page, pageUrl, linksToEnqueue, inScope, globalState, nav);
128501
128717
  }
128502
- async function executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope) {
128718
+ async function executeClickTask(task, page, elements, interceptor, semanticActionsDone, globalState, credentialId, linksToEnqueue, pageUrl, inScope, occ, nav) {
128503
128719
  const el = resolveElement(elements, task.role, task.label);
128504
128720
  if (!el) {
128505
128721
  log7.debug("click target not found", { role: task.role, label: task.label });
@@ -128518,15 +128734,13 @@ async function executeClickTask(task, page, elements, interceptor, semanticActio
128518
128734
  credential: credentialId
128519
128735
  });
128520
128736
  let result = await execute(page, el, "click", undefined, interceptor.setPendingUI, elements.length);
128521
- if (!result.success && await isViewportCenterBlocked(page)) {
128522
- log7.debug("click blocked by a stray overlay — closing it and retrying once", { label: el.label });
128523
- await closeOverlay(page);
128524
- result = await execute(page, el, "click", undefined, interceptor.setPendingUI, elements.length);
128525
- }
128737
+ result = await recoverFromOcclusion(page, el, result, () => execute(page, el, "click", undefined, interceptor.setPendingUI, elements.length));
128738
+ const occluded = !result.success && await diagnoseOcclusion(page, el, occ);
128526
128739
  interceptor.clearPendingTrigger();
128527
128740
  trackResult(result, interceptor, globalState, credentialId, task.triggersMutation, page);
128528
128741
  csEmit(page, { type: "action-end", ok: result.success, credential: credentialId });
128529
- semanticActionsDone.add(key);
128742
+ if (!occluded)
128743
+ semanticActionsDone.add(key);
128530
128744
  globalState.totalSteps++;
128531
128745
  if (el.role === "option") {
128532
128746
  const parentCombobox = elements.find((e2) => e2.role === "combobox");
@@ -128535,12 +128749,7 @@ async function executeClickTask(task, page, elements, interceptor, semanticActio
128535
128749
  }
128536
128750
  }
128537
128751
  if (result.navigated) {
128538
- const cur = page.url();
128539
- if (cur !== pageUrl && isInScope(cur, inScope))
128540
- linksToEnqueue.push(cur);
128541
- await page.goto(pageUrl, { waitUntil: "domcontentloaded", timeout: 15000 }).catch(() => {});
128542
- await page.waitForTimeout(POST_GOTO_WAIT);
128543
- await page.waitForLoadState("domcontentloaded", { timeout: 5000 }).catch(() => {});
128752
+ await handleNavigation(page, pageUrl, linksToEnqueue, inScope, globalState, nav);
128544
128753
  return;
128545
128754
  }
128546
128755
  if (el.role === "combobox")
@@ -128870,6 +129079,7 @@ async function runMultiCredential(config3, credentials) {
128870
129079
  await browserContext.addInitScript(PANEL_INIT_SCRIPT);
128871
129080
  const page = await browserContext.newPage();
128872
129081
  attachDialogAutoAccept(page);
129082
+ attachFileChooserAutoFill(page);
128873
129083
  await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
128874
129084
  csEmit(page, {
128875
129085
  type: "init",
@@ -128975,11 +129185,14 @@ async function runMultiCredential(config3, credentials) {
128975
129185
  });
128976
129186
  continue;
128977
129187
  }
128978
- log7.info("redirect to different page, will explore actual URL", {
129188
+ const landed = normalizeUrl(r2.ctx.page.url());
129189
+ log7.info("redirect to different page, enqueueing landed URL", {
128979
129190
  credential: r2.ctx.id,
128980
129191
  intended: entry.url,
128981
- actual: r2.ctx.page.url()
129192
+ landed
128982
129193
  });
129194
+ enqueueWithContext(landed, r2.ctx.id, pageQueue, visitedPages, inScope, pathPatternCounts);
129195
+ continue;
128983
129196
  }
128984
129197
  visitableContexts.push(r2.ctx);
128985
129198
  }
@@ -129022,7 +129235,12 @@ async function runMultiCredential(config3, credentials) {
129022
129235
  }
129023
129236
  for (const ctx of visitableContexts) {
129024
129237
  log7.info("exploring", { credential: ctx.id, url: entry.url });
129025
- const discovered = await explorePageWithAI(ctx.page, entry.url, ctx.interceptor, model, globalState, inScope, ctx.id, maxPages, usageAcc);
129238
+ const exploreInline = async (p, url3, depth) => {
129239
+ const found = await explorePageWithAI(p, url3, ctx.interceptor, model, globalState, inScope, ctx.id, maxPages, usageAcc, { explore: exploreInline, depth });
129240
+ for (const u of found)
129241
+ enqueueWithContext(u, ctx.id, pageQueue, visitedPages, inScope, pathPatternCounts);
129242
+ };
129243
+ const discovered = await explorePageWithAI(ctx.page, entry.url, ctx.interceptor, model, globalState, inScope, ctx.id, maxPages, usageAcc, { explore: exploreInline, depth: 0 });
129026
129244
  for (const url3 of discovered) {
129027
129245
  enqueueWithContext(url3, ctx.id, pageQueue, visitedPages, inScope, pathPatternCounts);
129028
129246
  }
@@ -129115,6 +129333,7 @@ async function run(config3) {
129115
129333
  await context.addInitScript(PANEL_INIT_SCRIPT);
129116
129334
  const page = await context.newPage();
129117
129335
  attachDialogAutoAccept(page);
129336
+ attachFileChooserAutoFill(page);
129118
129337
  if (config3.auth.sessionFile) {
129119
129338
  await loadSession(context, config3.auth.sessionFile);
129120
129339
  }
@@ -129207,6 +129426,10 @@ async function run(config3) {
129207
129426
  continue;
129208
129427
  }
129209
129428
  const currentUrl = normalizeUrl(page.url());
129429
+ if (currentUrl !== normalizeUrl(nextUrl) && globalState.visitedPages.has(currentUrl)) {
129430
+ log7.info("redirect to already-explored page, skipping", { requested: normalizeUrl(nextUrl), landed: currentUrl });
129431
+ continue;
129432
+ }
129210
129433
  globalState.visitedPages.add(currentUrl);
129211
129434
  log7.info("exploring page", { page: `${pagesExplored}/${maxPages}`, url: currentUrl });
129212
129435
  log7.debug("state", {
@@ -129258,7 +129481,12 @@ async function run(config3) {
129258
129481
  }
129259
129482
  log7.info("page changed after auth, re-exploring", { url: currentUrl });
129260
129483
  }
129261
- const discovered = await explorePageWithAI(page, currentUrl, interceptor, model, globalState, inScope, SINGLE_CRED, maxPages, usageAcc);
129484
+ const exploreInline = async (p, url3, depth) => {
129485
+ const found = await explorePageWithAI(p, url3, interceptor, model, globalState, inScope, SINGLE_CRED, maxPages, usageAcc, { explore: exploreInline, depth });
129486
+ for (const u of found)
129487
+ enqueueUrl(u, globalState, inScope);
129488
+ };
129489
+ const discovered = await explorePageWithAI(page, currentUrl, interceptor, model, globalState, inScope, SINGLE_CRED, maxPages, usageAcc, { explore: exploreInline, depth: 0 });
129262
129490
  let newEnqueued = 0;
129263
129491
  for (const url3 of discovered) {
129264
129492
  if (enqueueUrl(url3, globalState, inScope))