opensteer 0.4.13 → 0.5.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.
@@ -2,6 +2,232 @@ import {
2
2
  flattenExtractionDataToFieldPlan
3
3
  } from "./chunk-3H5RRIMZ.js";
4
4
 
5
+ // src/error-normalization.ts
6
+ function extractErrorMessage(error, fallback = "Unknown error.") {
7
+ if (error instanceof Error) {
8
+ const message = error.message.trim();
9
+ if (message) return message;
10
+ const name = error.name.trim();
11
+ if (name) return name;
12
+ }
13
+ if (typeof error === "string" && error.trim()) {
14
+ return error.trim();
15
+ }
16
+ const record = asRecord(error);
17
+ const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
18
+ if (recordMessage) {
19
+ return recordMessage;
20
+ }
21
+ return fallback;
22
+ }
23
+ function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
24
+ const seen = /* @__PURE__ */ new WeakSet();
25
+ return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
26
+ }
27
+ function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
28
+ const record = asRecord(error);
29
+ if (record) {
30
+ if (seen.has(record)) {
31
+ return {
32
+ message: extractErrorMessage(error, fallback)
33
+ };
34
+ }
35
+ seen.add(record);
36
+ }
37
+ const message = extractErrorMessage(error, fallback);
38
+ const code = extractCode(error);
39
+ const name = extractName(error);
40
+ const details = extractDetails(error);
41
+ if (depthRemaining <= 0) {
42
+ return compactErrorInfo({
43
+ message,
44
+ ...code ? { code } : {},
45
+ ...name ? { name } : {},
46
+ ...details ? { details } : {}
47
+ });
48
+ }
49
+ const cause = extractCause(error);
50
+ if (!cause) {
51
+ return compactErrorInfo({
52
+ message,
53
+ ...code ? { code } : {},
54
+ ...name ? { name } : {},
55
+ ...details ? { details } : {}
56
+ });
57
+ }
58
+ const normalizedCause = normalizeErrorInternal(
59
+ cause,
60
+ "Caused by an unknown error.",
61
+ depthRemaining - 1,
62
+ seen
63
+ );
64
+ return compactErrorInfo({
65
+ message,
66
+ ...code ? { code } : {},
67
+ ...name ? { name } : {},
68
+ ...details ? { details } : {},
69
+ cause: normalizedCause
70
+ });
71
+ }
72
+ function compactErrorInfo(info) {
73
+ const safeDetails = toJsonSafeRecord(info.details);
74
+ return {
75
+ message: info.message,
76
+ ...info.code ? { code: info.code } : {},
77
+ ...info.name ? { name: info.name } : {},
78
+ ...safeDetails ? { details: safeDetails } : {},
79
+ ...info.cause ? { cause: info.cause } : {}
80
+ };
81
+ }
82
+ function extractCode(error) {
83
+ const record = asRecord(error);
84
+ const raw = record?.code;
85
+ if (typeof raw === "string" && raw.trim()) {
86
+ return raw.trim();
87
+ }
88
+ if (typeof raw === "number" && Number.isFinite(raw)) {
89
+ return String(raw);
90
+ }
91
+ return void 0;
92
+ }
93
+ function extractName(error) {
94
+ if (error instanceof Error && error.name.trim()) {
95
+ return error.name.trim();
96
+ }
97
+ const record = asRecord(error);
98
+ return toNonEmptyString(record?.name);
99
+ }
100
+ function extractDetails(error) {
101
+ const record = asRecord(error);
102
+ if (!record) return void 0;
103
+ const details = {};
104
+ const rawDetails = asRecord(record.details);
105
+ if (rawDetails) {
106
+ Object.assign(details, rawDetails);
107
+ }
108
+ const action = toNonEmptyString(record.action);
109
+ if (action) {
110
+ details.action = action;
111
+ }
112
+ const selectorUsed = toNonEmptyString(record.selectorUsed);
113
+ if (selectorUsed) {
114
+ details.selectorUsed = selectorUsed;
115
+ }
116
+ if (typeof record.status === "number" && Number.isFinite(record.status)) {
117
+ details.status = record.status;
118
+ }
119
+ const failure = asRecord(record.failure);
120
+ if (failure) {
121
+ const failureCode = toNonEmptyString(failure.code);
122
+ const classificationSource = toNonEmptyString(
123
+ failure.classificationSource
124
+ );
125
+ const failureDetails = asRecord(failure.details);
126
+ if (failureCode || classificationSource || failureDetails) {
127
+ details.actionFailure = {
128
+ ...failureCode ? { code: failureCode } : {},
129
+ ...classificationSource ? { classificationSource } : {},
130
+ ...failureDetails ? { details: failureDetails } : {}
131
+ };
132
+ }
133
+ }
134
+ return Object.keys(details).length ? details : void 0;
135
+ }
136
+ function extractCause(error) {
137
+ if (error instanceof Error) {
138
+ return error.cause;
139
+ }
140
+ const record = asRecord(error);
141
+ return record?.cause;
142
+ }
143
+ function asRecord(value) {
144
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
145
+ return null;
146
+ }
147
+ return value;
148
+ }
149
+ function toNonEmptyString(value) {
150
+ if (typeof value !== "string") return void 0;
151
+ const normalized = value.trim();
152
+ return normalized.length ? normalized : void 0;
153
+ }
154
+ function toJsonSafeRecord(value) {
155
+ if (!value) return void 0;
156
+ const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
157
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
158
+ return void 0;
159
+ }
160
+ const record = sanitized;
161
+ return Object.keys(record).length > 0 ? record : void 0;
162
+ }
163
+ function toJsonSafeValue(value, seen) {
164
+ if (value === null) return null;
165
+ if (typeof value === "string" || typeof value === "boolean") {
166
+ return value;
167
+ }
168
+ if (typeof value === "number") {
169
+ return Number.isFinite(value) ? value : null;
170
+ }
171
+ if (typeof value === "bigint") {
172
+ return value.toString();
173
+ }
174
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
175
+ return void 0;
176
+ }
177
+ if (value instanceof Date) {
178
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
179
+ }
180
+ if (Array.isArray(value)) {
181
+ if (seen.has(value)) return "[Circular]";
182
+ seen.add(value);
183
+ const output = value.map((item) => {
184
+ const next = toJsonSafeValue(item, seen);
185
+ return next === void 0 ? null : next;
186
+ });
187
+ seen.delete(value);
188
+ return output;
189
+ }
190
+ if (value instanceof Set) {
191
+ if (seen.has(value)) return "[Circular]";
192
+ seen.add(value);
193
+ const output = Array.from(value, (item) => {
194
+ const next = toJsonSafeValue(item, seen);
195
+ return next === void 0 ? null : next;
196
+ });
197
+ seen.delete(value);
198
+ return output;
199
+ }
200
+ if (value instanceof Map) {
201
+ if (seen.has(value)) return "[Circular]";
202
+ seen.add(value);
203
+ const output = {};
204
+ for (const [key, item] of value.entries()) {
205
+ const normalizedKey = String(key);
206
+ const next = toJsonSafeValue(item, seen);
207
+ if (next !== void 0) {
208
+ output[normalizedKey] = next;
209
+ }
210
+ }
211
+ seen.delete(value);
212
+ return output;
213
+ }
214
+ if (typeof value === "object") {
215
+ const objectValue = value;
216
+ if (seen.has(objectValue)) return "[Circular]";
217
+ seen.add(objectValue);
218
+ const output = {};
219
+ for (const [key, item] of Object.entries(objectValue)) {
220
+ const next = toJsonSafeValue(item, seen);
221
+ if (next !== void 0) {
222
+ output[key] = next;
223
+ }
224
+ }
225
+ seen.delete(objectValue);
226
+ return output;
227
+ }
228
+ return void 0;
229
+ }
230
+
5
231
  // src/storage/namespace.ts
6
232
  import path from "path";
7
233
  var DEFAULT_NAMESPACE = "default";
@@ -607,9 +833,11 @@ import path2 from "path";
607
833
  var LocalSelectorStorage = class {
608
834
  rootDir;
609
835
  namespace;
610
- constructor(rootDir, namespace) {
836
+ debug;
837
+ constructor(rootDir, namespace, options = {}) {
611
838
  this.rootDir = rootDir;
612
839
  this.namespace = normalizeNamespace(namespace);
840
+ this.debug = options.debug === true;
613
841
  }
614
842
  getRootDir() {
615
843
  return this.rootDir;
@@ -643,7 +871,16 @@ var LocalSelectorStorage = class {
643
871
  try {
644
872
  const raw = fs.readFileSync(file, "utf8");
645
873
  return JSON.parse(raw);
646
- } catch {
874
+ } catch (error) {
875
+ const message = extractErrorMessage(
876
+ error,
877
+ "Unable to parse selector registry JSON."
878
+ );
879
+ if (this.debug) {
880
+ console.warn(
881
+ `[opensteer] failed to read selector registry "${file}": ${message}`
882
+ );
883
+ }
647
884
  return createEmptyRegistry(this.namespace);
648
885
  }
649
886
  }
@@ -660,7 +897,16 @@ var LocalSelectorStorage = class {
660
897
  try {
661
898
  const raw = fs.readFileSync(file, "utf8");
662
899
  return JSON.parse(raw);
663
- } catch {
900
+ } catch (error) {
901
+ const message = extractErrorMessage(
902
+ error,
903
+ "Unable to parse selector file JSON."
904
+ );
905
+ if (this.debug) {
906
+ console.warn(
907
+ `[opensteer] failed to read selector file "${file}": ${message}`
908
+ );
909
+ }
664
910
  return null;
665
911
  }
666
912
  }
@@ -946,9 +1192,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
946
1192
  `;
947
1193
  var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
948
1194
  var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
949
- var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
950
- var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
951
- var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
952
1195
 
953
1196
  // src/element-path/build.ts
954
1197
  var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
@@ -2739,567 +2982,179 @@ function cleanForAction(html) {
2739
2982
  return compactHtml(htmlOut);
2740
2983
  }
2741
2984
 
2742
- // src/extract-value-normalization.ts
2743
- var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
2744
- function normalizeExtractedValue(raw, attribute) {
2745
- if (raw == null) return null;
2746
- const rawText = String(raw);
2747
- if (!rawText.trim()) return null;
2748
- const normalizedAttribute = String(attribute || "").trim().toLowerCase();
2749
- if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
2750
- const singleValue = pickSingleListAttributeValue(
2751
- normalizedAttribute,
2752
- rawText
2753
- ).trim();
2754
- return singleValue || null;
2985
+ // src/html/pipeline.ts
2986
+ import * as cheerio3 from "cheerio";
2987
+ function applyCleaner(mode, html) {
2988
+ switch (mode) {
2989
+ case "clickable":
2990
+ return cleanForClickable(html);
2991
+ case "scrollable":
2992
+ return cleanForScrollable(html);
2993
+ case "extraction":
2994
+ return cleanForExtraction(html);
2995
+ case "full":
2996
+ return cleanForFull(html);
2997
+ case "action":
2998
+ default:
2999
+ return cleanForAction(html);
2755
3000
  }
2756
- const text = rawText.replace(/\s+/g, " ").trim();
2757
- return text || null;
2758
3001
  }
2759
- function pickSingleListAttributeValue(attribute, raw) {
2760
- if (attribute === "ping") {
2761
- const firstUrl = raw.trim().split(/\s+/)[0] || "";
2762
- return firstUrl.trim();
2763
- }
2764
- if (attribute === "srcset" || attribute === "imagesrcset") {
2765
- const picked = pickBestSrcsetCandidate(raw);
2766
- if (picked) return picked;
2767
- return pickFirstSrcsetToken(raw) || "";
3002
+ async function assignCounters(page, html, nodePaths, nodeMeta) {
3003
+ const $ = cheerio3.load(html, { xmlMode: false });
3004
+ const counterIndex = /* @__PURE__ */ new Map();
3005
+ let nextCounter = 1;
3006
+ const assignedByNodeId = /* @__PURE__ */ new Map();
3007
+ $("*").each(function() {
3008
+ const el = $(this);
3009
+ const nodeId = el.attr(OS_NODE_ID_ATTR);
3010
+ if (!nodeId) return;
3011
+ const counter = nextCounter++;
3012
+ assignedByNodeId.set(nodeId, counter);
3013
+ const path5 = nodePaths.get(nodeId);
3014
+ el.attr("c", String(counter));
3015
+ el.removeAttr(OS_NODE_ID_ATTR);
3016
+ if (path5) {
3017
+ counterIndex.set(counter, cloneElementPath(path5));
3018
+ }
3019
+ });
3020
+ try {
3021
+ await syncLiveCounters(page, nodeMeta, assignedByNodeId);
3022
+ } catch (error) {
3023
+ await clearLiveCounters(page);
3024
+ throw error;
2768
3025
  }
2769
- return raw.trim();
3026
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
3027
+ return {
3028
+ html: $.html(),
3029
+ counterIndex
3030
+ };
2770
3031
  }
2771
- function pickBestSrcsetCandidate(raw) {
2772
- const candidates = parseSrcsetCandidates(raw);
2773
- if (!candidates.length) return null;
2774
- const widthCandidates = candidates.filter(
2775
- (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
2776
- );
2777
- if (widthCandidates.length) {
2778
- return widthCandidates.reduce(
2779
- (best, candidate) => candidate.width > best.width ? candidate : best
2780
- ).url;
2781
- }
2782
- const densityCandidates = candidates.filter(
2783
- (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
2784
- );
2785
- if (densityCandidates.length) {
2786
- return densityCandidates.reduce(
2787
- (best, candidate) => candidate.density > best.density ? candidate : best
2788
- ).url;
3032
+ async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
3033
+ await clearLiveCounters(page);
3034
+ if (!assignedByNodeId.size) return;
3035
+ const groupedByFrame = /* @__PURE__ */ new Map();
3036
+ for (const [nodeId, counter] of assignedByNodeId.entries()) {
3037
+ const meta = nodeMeta.get(nodeId);
3038
+ if (!meta?.frameToken) continue;
3039
+ const list = groupedByFrame.get(meta.frameToken) || [];
3040
+ list.push({
3041
+ nodeId,
3042
+ counter
3043
+ });
3044
+ groupedByFrame.set(meta.frameToken, list);
2789
3045
  }
2790
- return candidates[0]?.url || null;
2791
- }
2792
- function parseSrcsetCandidates(raw) {
2793
- const text = String(raw || "").trim();
2794
- if (!text) return [];
2795
- const out = [];
2796
- let index = 0;
2797
- while (index < text.length) {
2798
- index = skipSeparators(text, index);
2799
- if (index >= text.length) break;
2800
- const urlToken = readUrlToken(text, index);
2801
- index = urlToken.nextIndex;
2802
- const url = urlToken.value.trim();
2803
- if (!url) continue;
2804
- index = skipWhitespace(text, index);
2805
- const descriptors = [];
2806
- while (index < text.length && text[index] !== ",") {
2807
- const descriptorToken = readDescriptorToken(text, index);
2808
- if (!descriptorToken.value) {
2809
- index = descriptorToken.nextIndex;
2810
- continue;
3046
+ if (!groupedByFrame.size) return;
3047
+ const failures = [];
3048
+ const framesByToken = await mapFramesByToken(page);
3049
+ for (const [frameToken, entries] of groupedByFrame.entries()) {
3050
+ const frame = framesByToken.get(frameToken);
3051
+ if (!frame) {
3052
+ for (const entry of entries) {
3053
+ failures.push({
3054
+ nodeId: entry.nodeId,
3055
+ counter: entry.counter,
3056
+ frameToken,
3057
+ reason: "frame_missing"
3058
+ });
2811
3059
  }
2812
- descriptors.push(descriptorToken.value);
2813
- index = descriptorToken.nextIndex;
2814
- index = skipWhitespace(text, index);
2815
- }
2816
- if (index < text.length && text[index] === ",") {
2817
- index += 1;
3060
+ continue;
2818
3061
  }
2819
- let width = null;
2820
- let density = null;
2821
- for (const descriptor of descriptors) {
2822
- const token = descriptor.trim().toLowerCase();
2823
- if (!token) continue;
2824
- const widthMatch = token.match(/^(\d+)w$/);
2825
- if (widthMatch) {
2826
- const parsed = Number.parseInt(widthMatch[1], 10);
2827
- if (Number.isFinite(parsed)) {
2828
- width = parsed;
3062
+ try {
3063
+ const unresolved = await frame.evaluate(
3064
+ ({ entries: entries2, nodeAttr }) => {
3065
+ const index = /* @__PURE__ */ new Map();
3066
+ const unresolved2 = [];
3067
+ const walk = (root) => {
3068
+ const children = Array.from(root.children);
3069
+ for (const child of children) {
3070
+ const nodeId = child.getAttribute(nodeAttr);
3071
+ if (nodeId) {
3072
+ const list = index.get(nodeId) || [];
3073
+ list.push(child);
3074
+ index.set(nodeId, list);
3075
+ }
3076
+ walk(child);
3077
+ if (child.shadowRoot) {
3078
+ walk(child.shadowRoot);
3079
+ }
3080
+ }
3081
+ };
3082
+ walk(document);
3083
+ for (const entry of entries2) {
3084
+ const matches = index.get(entry.nodeId) || [];
3085
+ if (matches.length !== 1) {
3086
+ unresolved2.push({
3087
+ nodeId: entry.nodeId,
3088
+ counter: entry.counter,
3089
+ matches: matches.length
3090
+ });
3091
+ continue;
3092
+ }
3093
+ matches[0].setAttribute("c", String(entry.counter));
3094
+ }
3095
+ return unresolved2;
3096
+ },
3097
+ {
3098
+ entries,
3099
+ nodeAttr: OS_NODE_ID_ATTR
2829
3100
  }
2830
- continue;
3101
+ );
3102
+ for (const entry of unresolved) {
3103
+ failures.push({
3104
+ nodeId: entry.nodeId,
3105
+ counter: entry.counter,
3106
+ frameToken,
3107
+ reason: "match_count",
3108
+ matches: entry.matches
3109
+ });
2831
3110
  }
2832
- const densityMatch = token.match(/^(\d*\.?\d+)x$/);
2833
- if (densityMatch) {
2834
- const parsed = Number.parseFloat(densityMatch[1]);
2835
- if (Number.isFinite(parsed)) {
2836
- density = parsed;
2837
- }
3111
+ } catch {
3112
+ for (const entry of entries) {
3113
+ failures.push({
3114
+ nodeId: entry.nodeId,
3115
+ counter: entry.counter,
3116
+ frameToken,
3117
+ reason: "frame_unavailable"
3118
+ });
2838
3119
  }
2839
3120
  }
2840
- out.push({
2841
- url,
2842
- width,
2843
- density
3121
+ }
3122
+ if (failures.length) {
3123
+ const preview = failures.slice(0, 3).map((failure) => {
3124
+ const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
3125
+ if (failure.reason === "frame_missing") {
3126
+ return `${base} could not be synchronized because the frame is missing.`;
3127
+ }
3128
+ if (failure.reason === "frame_unavailable") {
3129
+ return `${base} could not be synchronized because frame evaluation failed.`;
3130
+ }
3131
+ return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
2844
3132
  });
3133
+ const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
3134
+ throw new Error(
3135
+ `Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
3136
+ );
2845
3137
  }
2846
- return out;
2847
3138
  }
2848
- function pickFirstSrcsetToken(raw) {
2849
- const candidate = parseSrcsetCandidates(raw)[0];
2850
- if (candidate?.url) {
2851
- return candidate.url;
3139
+ async function clearLiveCounters(page) {
3140
+ for (const frame of page.frames()) {
3141
+ try {
3142
+ await frame.evaluate(() => {
3143
+ const walk = (root) => {
3144
+ const children = Array.from(root.children);
3145
+ for (const child of children) {
3146
+ child.removeAttribute("c");
3147
+ walk(child);
3148
+ if (child.shadowRoot) {
3149
+ walk(child.shadowRoot);
3150
+ }
3151
+ }
3152
+ };
3153
+ walk(document);
3154
+ });
3155
+ } catch {
3156
+ }
2852
3157
  }
2853
- const text = String(raw || "");
2854
- const start = skipSeparators(text, 0);
2855
- if (start >= text.length) return null;
2856
- const firstToken = readUrlToken(text, start).value.trim();
2857
- return firstToken || null;
2858
- }
2859
- function skipWhitespace(value, index) {
2860
- let cursor = index;
2861
- while (cursor < value.length && /\s/.test(value[cursor])) {
2862
- cursor += 1;
2863
- }
2864
- return cursor;
2865
- }
2866
- function skipSeparators(value, index) {
2867
- let cursor = skipWhitespace(value, index);
2868
- while (cursor < value.length && value[cursor] === ",") {
2869
- cursor += 1;
2870
- cursor = skipWhitespace(value, cursor);
2871
- }
2872
- return cursor;
2873
- }
2874
- function readUrlToken(value, index) {
2875
- let cursor = index;
2876
- let out = "";
2877
- const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
2878
- while (cursor < value.length) {
2879
- const char = value[cursor];
2880
- if (/\s/.test(char)) {
2881
- break;
2882
- }
2883
- if (char === "," && !isDataUrl) {
2884
- break;
2885
- }
2886
- out += char;
2887
- cursor += 1;
2888
- }
2889
- if (isDataUrl && out.endsWith(",") && cursor < value.length) {
2890
- out = out.slice(0, -1);
2891
- }
2892
- return {
2893
- value: out,
2894
- nextIndex: cursor
2895
- };
2896
- }
2897
- function readDescriptorToken(value, index) {
2898
- let cursor = skipWhitespace(value, index);
2899
- let out = "";
2900
- while (cursor < value.length) {
2901
- const char = value[cursor];
2902
- if (char === "," || /\s/.test(char)) {
2903
- break;
2904
- }
2905
- out += char;
2906
- cursor += 1;
2907
- }
2908
- return {
2909
- value: out.trim(),
2910
- nextIndex: cursor
2911
- };
2912
- }
2913
-
2914
- // src/html/counter-runtime.ts
2915
- var CounterResolutionError = class extends Error {
2916
- code;
2917
- constructor(code, message) {
2918
- super(message);
2919
- this.name = "CounterResolutionError";
2920
- this.code = code;
2921
- }
2922
- };
2923
- async function ensureLiveCounters(page, nodeMeta, nodeIds) {
2924
- const out = /* @__PURE__ */ new Map();
2925
- if (!nodeIds.length) return out;
2926
- const grouped = /* @__PURE__ */ new Map();
2927
- for (const nodeId of nodeIds) {
2928
- const meta = nodeMeta.get(nodeId);
2929
- if (!meta) {
2930
- throw new CounterResolutionError(
2931
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
2932
- `Missing metadata for node ${nodeId}. Run snapshot() again.`
2933
- );
2934
- }
2935
- const list = grouped.get(meta.frameToken) || [];
2936
- list.push({
2937
- nodeId,
2938
- instanceToken: meta.instanceToken
2939
- });
2940
- grouped.set(meta.frameToken, list);
2941
- }
2942
- const framesByToken = await mapFramesByToken(page);
2943
- let nextCounter = await readGlobalNextCounter(page);
2944
- const usedCounters = /* @__PURE__ */ new Map();
2945
- for (const [frameToken, entries] of grouped.entries()) {
2946
- const frame = framesByToken.get(frameToken);
2947
- if (!frame) {
2948
- throw new CounterResolutionError(
2949
- "ERR_COUNTER_FRAME_UNAVAILABLE",
2950
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
2951
- );
2952
- }
2953
- const result = await frame.evaluate(
2954
- ({
2955
- entries: entries2,
2956
- nodeAttr,
2957
- instanceTokenKey,
2958
- counterOwnerKey,
2959
- counterValueKey,
2960
- startCounter
2961
- }) => {
2962
- const helpers = {
2963
- pushNode(map, node) {
2964
- const nodeId = node.getAttribute(nodeAttr);
2965
- if (!nodeId) return;
2966
- const list = map.get(nodeId) || [];
2967
- list.push(node);
2968
- map.set(nodeId, list);
2969
- },
2970
- walk(map, root) {
2971
- const children = Array.from(root.children);
2972
- for (const child of children) {
2973
- helpers.pushNode(map, child);
2974
- helpers.walk(map, child);
2975
- if (child.shadowRoot) {
2976
- helpers.walk(map, child.shadowRoot);
2977
- }
2978
- }
2979
- },
2980
- buildNodeIndex() {
2981
- const map = /* @__PURE__ */ new Map();
2982
- helpers.walk(map, document);
2983
- return map;
2984
- }
2985
- };
2986
- const index = helpers.buildNodeIndex();
2987
- const assigned = [];
2988
- const failures = [];
2989
- let next = Math.max(1, Number(startCounter || 1));
2990
- for (const entry of entries2) {
2991
- const matches = index.get(entry.nodeId) || [];
2992
- if (!matches.length) {
2993
- failures.push({
2994
- nodeId: entry.nodeId,
2995
- reason: "missing"
2996
- });
2997
- continue;
2998
- }
2999
- if (matches.length !== 1) {
3000
- failures.push({
3001
- nodeId: entry.nodeId,
3002
- reason: "ambiguous"
3003
- });
3004
- continue;
3005
- }
3006
- const target = matches[0];
3007
- if (target[instanceTokenKey] !== entry.instanceToken) {
3008
- failures.push({
3009
- nodeId: entry.nodeId,
3010
- reason: "instance_mismatch"
3011
- });
3012
- continue;
3013
- }
3014
- const owned = target[counterOwnerKey] === true;
3015
- const runtimeCounter = Number(target[counterValueKey] || 0);
3016
- if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
3017
- target.setAttribute("c", String(runtimeCounter));
3018
- assigned.push({
3019
- nodeId: entry.nodeId,
3020
- counter: runtimeCounter
3021
- });
3022
- continue;
3023
- }
3024
- const counter = next++;
3025
- target.setAttribute("c", String(counter));
3026
- Object.defineProperty(target, counterOwnerKey, {
3027
- value: true,
3028
- writable: true,
3029
- configurable: true
3030
- });
3031
- Object.defineProperty(target, counterValueKey, {
3032
- value: counter,
3033
- writable: true,
3034
- configurable: true
3035
- });
3036
- assigned.push({ nodeId: entry.nodeId, counter });
3037
- }
3038
- return {
3039
- assigned,
3040
- failures,
3041
- nextCounter: next
3042
- };
3043
- },
3044
- {
3045
- entries,
3046
- nodeAttr: OS_NODE_ID_ATTR,
3047
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
3048
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
3049
- counterValueKey: OS_COUNTER_VALUE_KEY,
3050
- startCounter: nextCounter
3051
- }
3052
- );
3053
- if (result.failures.length) {
3054
- const first = result.failures[0];
3055
- throw buildCounterFailureError(first.nodeId, first.reason);
3056
- }
3057
- nextCounter = result.nextCounter;
3058
- for (const item of result.assigned) {
3059
- const existingNode = usedCounters.get(item.counter);
3060
- if (existingNode && existingNode !== item.nodeId) {
3061
- throw new CounterResolutionError(
3062
- "ERR_COUNTER_AMBIGUOUS",
3063
- `Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
3064
- );
3065
- }
3066
- usedCounters.set(item.counter, item.nodeId);
3067
- out.set(item.nodeId, item.counter);
3068
- }
3069
- }
3070
- await writeGlobalNextCounter(page, nextCounter);
3071
- return out;
3072
- }
3073
- async function resolveCounterElement(page, snapshot, counter) {
3074
- const binding = readBinding(snapshot, counter);
3075
- const framesByToken = await mapFramesByToken(page);
3076
- const frame = framesByToken.get(binding.frameToken);
3077
- if (!frame) {
3078
- throw new CounterResolutionError(
3079
- "ERR_COUNTER_FRAME_UNAVAILABLE",
3080
- `Counter ${counter} frame is unavailable. Run snapshot() again.`
3081
- );
3082
- }
3083
- const status = await frame.evaluate(
3084
- ({
3085
- nodeId,
3086
- instanceToken,
3087
- counter: counter2,
3088
- nodeAttr,
3089
- instanceTokenKey,
3090
- counterOwnerKey,
3091
- counterValueKey
3092
- }) => {
3093
- const helpers = {
3094
- walk(map, root) {
3095
- const children = Array.from(root.children);
3096
- for (const child of children) {
3097
- const id = child.getAttribute(nodeAttr);
3098
- if (id) {
3099
- const list = map.get(id) || [];
3100
- list.push(child);
3101
- map.set(id, list);
3102
- }
3103
- helpers.walk(map, child);
3104
- if (child.shadowRoot) {
3105
- helpers.walk(map, child.shadowRoot);
3106
- }
3107
- }
3108
- },
3109
- buildNodeIndex() {
3110
- const map = /* @__PURE__ */ new Map();
3111
- helpers.walk(map, document);
3112
- return map;
3113
- }
3114
- };
3115
- const matches = helpers.buildNodeIndex().get(nodeId) || [];
3116
- if (!matches.length) return "missing";
3117
- if (matches.length !== 1) return "ambiguous";
3118
- const target = matches[0];
3119
- if (target[instanceTokenKey] !== instanceToken) {
3120
- return "instance_mismatch";
3121
- }
3122
- if (target[counterOwnerKey] !== true) {
3123
- return "instance_mismatch";
3124
- }
3125
- if (Number(target[counterValueKey] || 0) !== counter2) {
3126
- return "instance_mismatch";
3127
- }
3128
- if (target.getAttribute("c") !== String(counter2)) {
3129
- return "instance_mismatch";
3130
- }
3131
- return "ok";
3132
- },
3133
- {
3134
- nodeId: binding.nodeId,
3135
- instanceToken: binding.instanceToken,
3136
- counter,
3137
- nodeAttr: OS_NODE_ID_ATTR,
3138
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
3139
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
3140
- counterValueKey: OS_COUNTER_VALUE_KEY
3141
- }
3142
- );
3143
- if (status !== "ok") {
3144
- throw buildCounterFailureError(binding.nodeId, status);
3145
- }
3146
- const handle = await frame.evaluateHandle(
3147
- ({ nodeId, nodeAttr }) => {
3148
- const helpers = {
3149
- walk(matches, root) {
3150
- const children = Array.from(root.children);
3151
- for (const child of children) {
3152
- if (child.getAttribute(nodeAttr) === nodeId) {
3153
- matches.push(child);
3154
- }
3155
- helpers.walk(matches, child);
3156
- if (child.shadowRoot) {
3157
- helpers.walk(matches, child.shadowRoot);
3158
- }
3159
- }
3160
- },
3161
- findUniqueNode() {
3162
- const matches = [];
3163
- helpers.walk(matches, document);
3164
- if (matches.length !== 1) return null;
3165
- return matches[0];
3166
- }
3167
- };
3168
- return helpers.findUniqueNode();
3169
- },
3170
- {
3171
- nodeId: binding.nodeId,
3172
- nodeAttr: OS_NODE_ID_ATTR
3173
- }
3174
- );
3175
- const element = handle.asElement();
3176
- if (!element) {
3177
- await handle.dispose();
3178
- throw new CounterResolutionError(
3179
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
3180
- `Counter ${counter} became stale. Run snapshot() again.`
3181
- );
3182
- }
3183
- return element;
3184
- }
3185
- async function resolveCountersBatch(page, snapshot, requests) {
3186
- const out = {};
3187
- if (!requests.length) return out;
3188
- const grouped = /* @__PURE__ */ new Map();
3189
- for (const request of requests) {
3190
- const binding = readBinding(snapshot, request.counter);
3191
- const list = grouped.get(binding.frameToken) || [];
3192
- list.push({
3193
- ...request,
3194
- ...binding
3195
- });
3196
- grouped.set(binding.frameToken, list);
3197
- }
3198
- const framesByToken = await mapFramesByToken(page);
3199
- for (const [frameToken, entries] of grouped.entries()) {
3200
- const frame = framesByToken.get(frameToken);
3201
- if (!frame) {
3202
- throw new CounterResolutionError(
3203
- "ERR_COUNTER_FRAME_UNAVAILABLE",
3204
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
3205
- );
3206
- }
3207
- const result = await frame.evaluate(
3208
- ({
3209
- entries: entries2,
3210
- nodeAttr,
3211
- instanceTokenKey,
3212
- counterOwnerKey,
3213
- counterValueKey
3214
- }) => {
3215
- const values = [];
3216
- const failures = [];
3217
- const helpers = {
3218
- walk(map, root) {
3219
- const children = Array.from(root.children);
3220
- for (const child of children) {
3221
- const id = child.getAttribute(nodeAttr);
3222
- if (id) {
3223
- const list = map.get(id) || [];
3224
- list.push(child);
3225
- map.set(id, list);
3226
- }
3227
- helpers.walk(map, child);
3228
- if (child.shadowRoot) {
3229
- helpers.walk(map, child.shadowRoot);
3230
- }
3231
- }
3232
- },
3233
- buildNodeIndex() {
3234
- const map = /* @__PURE__ */ new Map();
3235
- helpers.walk(map, document);
3236
- return map;
3237
- },
3238
- readRawValue(element, attribute) {
3239
- if (attribute) {
3240
- return element.getAttribute(attribute);
3241
- }
3242
- return element.textContent;
3243
- }
3244
- };
3245
- const index = helpers.buildNodeIndex();
3246
- for (const entry of entries2) {
3247
- const matches = index.get(entry.nodeId) || [];
3248
- if (!matches.length) {
3249
- failures.push({
3250
- nodeId: entry.nodeId,
3251
- reason: "missing"
3252
- });
3253
- continue;
3254
- }
3255
- if (matches.length !== 1) {
3256
- failures.push({
3257
- nodeId: entry.nodeId,
3258
- reason: "ambiguous"
3259
- });
3260
- continue;
3261
- }
3262
- const target = matches[0];
3263
- if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
3264
- failures.push({
3265
- nodeId: entry.nodeId,
3266
- reason: "instance_mismatch"
3267
- });
3268
- continue;
3269
- }
3270
- values.push({
3271
- key: entry.key,
3272
- value: helpers.readRawValue(target, entry.attribute)
3273
- });
3274
- }
3275
- return {
3276
- values,
3277
- failures
3278
- };
3279
- },
3280
- {
3281
- entries,
3282
- nodeAttr: OS_NODE_ID_ATTR,
3283
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
3284
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
3285
- counterValueKey: OS_COUNTER_VALUE_KEY
3286
- }
3287
- );
3288
- if (result.failures.length) {
3289
- const first = result.failures[0];
3290
- throw buildCounterFailureError(first.nodeId, first.reason);
3291
- }
3292
- const attributeByKey = new Map(
3293
- entries.map((entry) => [entry.key, entry.attribute])
3294
- );
3295
- for (const item of result.values) {
3296
- out[item.key] = normalizeExtractedValue(
3297
- item.value,
3298
- attributeByKey.get(item.key)
3299
- );
3300
- }
3301
- }
3302
- return out;
3303
3158
  }
3304
3159
  async function mapFramesByToken(page) {
3305
3160
  const out = /* @__PURE__ */ new Map();
@@ -3321,182 +3176,6 @@ async function readFrameToken(frame) {
3321
3176
  return null;
3322
3177
  }
3323
3178
  }
3324
- async function readGlobalNextCounter(page) {
3325
- const current = await page.mainFrame().evaluate((counterNextKey) => {
3326
- const win = window;
3327
- return Number(win[counterNextKey] || 0);
3328
- }, OS_COUNTER_NEXT_KEY).catch(() => 0);
3329
- if (Number.isFinite(current) && current > 0) {
3330
- return current;
3331
- }
3332
- let max = 0;
3333
- for (const frame of page.frames()) {
3334
- try {
3335
- const frameMax = await frame.evaluate(
3336
- ({ nodeAttr, counterOwnerKey, counterValueKey }) => {
3337
- let localMax = 0;
3338
- const helpers = {
3339
- walk(root) {
3340
- const children = Array.from(
3341
- root.children
3342
- );
3343
- for (const child of children) {
3344
- const candidate = child;
3345
- const hasNodeId = child.hasAttribute(nodeAttr);
3346
- const owned = candidate[counterOwnerKey] === true;
3347
- if (hasNodeId && owned) {
3348
- const value = Number(
3349
- candidate[counterValueKey] || 0
3350
- );
3351
- if (Number.isFinite(value) && value > localMax) {
3352
- localMax = value;
3353
- }
3354
- }
3355
- helpers.walk(child);
3356
- if (child.shadowRoot) {
3357
- helpers.walk(child.shadowRoot);
3358
- }
3359
- }
3360
- }
3361
- };
3362
- helpers.walk(document);
3363
- return localMax;
3364
- },
3365
- {
3366
- nodeAttr: OS_NODE_ID_ATTR,
3367
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
3368
- counterValueKey: OS_COUNTER_VALUE_KEY
3369
- }
3370
- );
3371
- if (frameMax > max) {
3372
- max = frameMax;
3373
- }
3374
- } catch {
3375
- }
3376
- }
3377
- const next = max + 1;
3378
- await writeGlobalNextCounter(page, next);
3379
- return next;
3380
- }
3381
- async function writeGlobalNextCounter(page, nextCounter) {
3382
- await page.mainFrame().evaluate(
3383
- ({ counterNextKey, nextCounter: nextCounter2 }) => {
3384
- const win = window;
3385
- win[counterNextKey] = nextCounter2;
3386
- },
3387
- {
3388
- counterNextKey: OS_COUNTER_NEXT_KEY,
3389
- nextCounter
3390
- }
3391
- ).catch(() => void 0);
3392
- }
3393
- function readBinding(snapshot, counter) {
3394
- if (!snapshot.counterBindings) {
3395
- throw new CounterResolutionError(
3396
- "ERR_COUNTER_NOT_FOUND",
3397
- `Counter ${counter} is unavailable because this snapshot has no counter bindings. Run snapshot() with counters first.`
3398
- );
3399
- }
3400
- const binding = snapshot.counterBindings.get(counter);
3401
- if (!binding) {
3402
- throw new CounterResolutionError(
3403
- "ERR_COUNTER_NOT_FOUND",
3404
- `Counter ${counter} was not found in the current snapshot. Run snapshot() again.`
3405
- );
3406
- }
3407
- if (binding.sessionId !== snapshot.snapshotSessionId) {
3408
- throw new CounterResolutionError(
3409
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
3410
- `Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
3411
- );
3412
- }
3413
- return binding;
3414
- }
3415
- function buildCounterFailureError(nodeId, reason) {
3416
- if (reason === "ambiguous") {
3417
- return new CounterResolutionError(
3418
- "ERR_COUNTER_AMBIGUOUS",
3419
- `Counter target is ambiguous for node ${nodeId}. Run snapshot() again.`
3420
- );
3421
- }
3422
- return new CounterResolutionError(
3423
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
3424
- `Counter target is stale or missing for node ${nodeId}. Run snapshot() again.`
3425
- );
3426
- }
3427
-
3428
- // src/html/pipeline.ts
3429
- import * as cheerio3 from "cheerio";
3430
- import { randomUUID } from "crypto";
3431
- function applyCleaner(mode, html) {
3432
- switch (mode) {
3433
- case "clickable":
3434
- return cleanForClickable(html);
3435
- case "scrollable":
3436
- return cleanForScrollable(html);
3437
- case "extraction":
3438
- return cleanForExtraction(html);
3439
- case "full":
3440
- return cleanForFull(html);
3441
- case "action":
3442
- default:
3443
- return cleanForAction(html);
3444
- }
3445
- }
3446
- async function assignCounters(page, html, nodePaths, nodeMeta, snapshotSessionId) {
3447
- const $ = cheerio3.load(html, { xmlMode: false });
3448
- const counterIndex = /* @__PURE__ */ new Map();
3449
- const counterBindings = /* @__PURE__ */ new Map();
3450
- const orderedNodeIds = [];
3451
- $("*").each(function() {
3452
- const el = $(this);
3453
- const nodeId = el.attr(OS_NODE_ID_ATTR);
3454
- if (!nodeId) return;
3455
- orderedNodeIds.push(nodeId);
3456
- });
3457
- const countersByNodeId = await ensureLiveCounters(
3458
- page,
3459
- nodeMeta,
3460
- orderedNodeIds
3461
- );
3462
- $("*").each(function() {
3463
- const el = $(this);
3464
- const nodeId = el.attr(OS_NODE_ID_ATTR);
3465
- if (!nodeId) return;
3466
- const path5 = nodePaths.get(nodeId);
3467
- const meta = nodeMeta.get(nodeId);
3468
- const counter = countersByNodeId.get(nodeId);
3469
- if (counter == null || !Number.isFinite(counter)) {
3470
- throw new Error(
3471
- `Counter assignment failed for node ${nodeId}. Run snapshot() again.`
3472
- );
3473
- }
3474
- if (counterBindings.has(counter) && counterBindings.get(counter)?.nodeId !== nodeId) {
3475
- throw new Error(
3476
- `Counter ${counter} was assigned to multiple nodes. Run snapshot() again.`
3477
- );
3478
- }
3479
- el.attr("c", String(counter));
3480
- el.removeAttr(OS_NODE_ID_ATTR);
3481
- if (path5) {
3482
- counterIndex.set(counter, cloneElementPath(path5));
3483
- }
3484
- if (meta) {
3485
- counterBindings.set(counter, {
3486
- sessionId: snapshotSessionId,
3487
- frameToken: meta.frameToken,
3488
- nodeId,
3489
- instanceToken: meta.instanceToken
3490
- });
3491
- }
3492
- });
3493
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
3494
- return {
3495
- html: $.html(),
3496
- counterIndex,
3497
- counterBindings
3498
- };
3499
- }
3500
3179
  function stripNodeIds(html) {
3501
3180
  if (!html.includes(OS_NODE_ID_ATTR)) return html;
3502
3181
  const $ = cheerio3.load(html, { xmlMode: false });
@@ -3504,7 +3183,6 @@ function stripNodeIds(html) {
3504
3183
  return $.html();
3505
3184
  }
3506
3185
  async function prepareSnapshot(page, options = {}) {
3507
- const snapshotSessionId = randomUUID();
3508
3186
  const mode = options.mode ?? "action";
3509
3187
  const withCounters = options.withCounters ?? true;
3510
3188
  const shouldMarkInteractive = options.markInteractive ?? true;
@@ -3517,18 +3195,15 @@ async function prepareSnapshot(page, options = {}) {
3517
3195
  const reducedHtml = applyCleaner(mode, processedHtml);
3518
3196
  let cleanedHtml = reducedHtml;
3519
3197
  let counterIndex = null;
3520
- let counterBindings = null;
3521
3198
  if (withCounters) {
3522
3199
  const counted = await assignCounters(
3523
3200
  page,
3524
3201
  reducedHtml,
3525
3202
  serialized.nodePaths,
3526
- serialized.nodeMeta,
3527
- snapshotSessionId
3203
+ serialized.nodeMeta
3528
3204
  );
3529
3205
  cleanedHtml = counted.html;
3530
3206
  counterIndex = counted.counterIndex;
3531
- counterBindings = counted.counterBindings;
3532
3207
  } else {
3533
3208
  cleanedHtml = stripNodeIds(cleanedHtml);
3534
3209
  }
@@ -3537,15 +3212,13 @@ async function prepareSnapshot(page, options = {}) {
3537
3212
  cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
3538
3213
  }
3539
3214
  return {
3540
- snapshotSessionId,
3541
3215
  mode,
3542
3216
  url: page.url(),
3543
3217
  rawHtml,
3544
3218
  processedHtml,
3545
3219
  reducedHtml,
3546
3220
  cleanedHtml,
3547
- counterIndex,
3548
- counterBindings
3221
+ counterIndex
3549
3222
  };
3550
3223
  }
3551
3224
 
@@ -3674,13 +3347,41 @@ function buildTargetNotFoundMessage(domPath, diagnostics) {
3674
3347
  return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
3675
3348
  return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
3676
3349
  }
3677
- function selectInDocument(selectors) {
3350
+ function selectInDocument(selectors) {
3351
+ let fallback = null;
3352
+ for (const selector of selectors) {
3353
+ if (!selector) continue;
3354
+ let count = 0;
3355
+ try {
3356
+ count = document.querySelectorAll(selector).length;
3357
+ } catch {
3358
+ count = 0;
3359
+ }
3360
+ if (count === 1) {
3361
+ return {
3362
+ selector,
3363
+ count,
3364
+ mode: "unique"
3365
+ };
3366
+ }
3367
+ if (count > 1 && !fallback) {
3368
+ fallback = {
3369
+ selector,
3370
+ count,
3371
+ mode: "fallback"
3372
+ };
3373
+ }
3374
+ }
3375
+ return fallback;
3376
+ }
3377
+ function selectInRoot(root, selectors) {
3378
+ if (!(root instanceof ShadowRoot)) return null;
3678
3379
  let fallback = null;
3679
3380
  for (const selector of selectors) {
3680
3381
  if (!selector) continue;
3681
3382
  let count = 0;
3682
3383
  try {
3683
- count = document.querySelectorAll(selector).length;
3384
+ count = root.querySelectorAll(selector).length;
3684
3385
  } catch {
3685
3386
  count = 0;
3686
3387
  }
@@ -3701,9 +3402,23 @@ function selectInDocument(selectors) {
3701
3402
  }
3702
3403
  return fallback;
3703
3404
  }
3704
- function selectInRoot(root, selectors) {
3705
- if (!(root instanceof ShadowRoot)) return null;
3706
- let fallback = null;
3405
+ function countInDocument(selectors) {
3406
+ const out = [];
3407
+ for (const selector of selectors) {
3408
+ if (!selector) continue;
3409
+ let count = 0;
3410
+ try {
3411
+ count = document.querySelectorAll(selector).length;
3412
+ } catch {
3413
+ count = 0;
3414
+ }
3415
+ out.push({ selector, count });
3416
+ }
3417
+ return out;
3418
+ }
3419
+ function countInRoot(root, selectors) {
3420
+ if (!(root instanceof ShadowRoot)) return [];
3421
+ const out = [];
3707
3422
  for (const selector of selectors) {
3708
3423
  if (!selector) continue;
3709
3424
  let count = 0;
@@ -3712,73 +3427,432 @@ function selectInRoot(root, selectors) {
3712
3427
  } catch {
3713
3428
  count = 0;
3714
3429
  }
3715
- if (count === 1) {
3716
- return {
3717
- selector,
3718
- count,
3719
- mode: "unique"
3720
- };
3430
+ out.push({ selector, count });
3431
+ }
3432
+ return out;
3433
+ }
3434
+ function isPathDebugEnabled() {
3435
+ const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
3436
+ if (!value) return false;
3437
+ const normalized = value.trim().toLowerCase();
3438
+ return normalized === "1" || normalized === "true";
3439
+ }
3440
+ function debugPath(message, data) {
3441
+ if (!isPathDebugEnabled()) return;
3442
+ if (data !== void 0) {
3443
+ console.log(`[opensteer:path] ${message}`, data);
3444
+ } else {
3445
+ console.log(`[opensteer:path] ${message}`);
3446
+ }
3447
+ }
3448
+ async function disposeHandle(handle) {
3449
+ if (!handle) return;
3450
+ try {
3451
+ await handle.dispose();
3452
+ } catch {
3453
+ }
3454
+ }
3455
+
3456
+ // src/extract-value-normalization.ts
3457
+ var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
3458
+ function normalizeExtractedValue(raw, attribute) {
3459
+ if (raw == null) return null;
3460
+ const rawText = String(raw);
3461
+ if (!rawText.trim()) return null;
3462
+ const normalizedAttribute = String(attribute || "").trim().toLowerCase();
3463
+ if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
3464
+ const singleValue = pickSingleListAttributeValue(
3465
+ normalizedAttribute,
3466
+ rawText
3467
+ ).trim();
3468
+ return singleValue || null;
3469
+ }
3470
+ const text = rawText.replace(/\s+/g, " ").trim();
3471
+ return text || null;
3472
+ }
3473
+ function pickSingleListAttributeValue(attribute, raw) {
3474
+ if (attribute === "ping") {
3475
+ const firstUrl = raw.trim().split(/\s+/)[0] || "";
3476
+ return firstUrl.trim();
3477
+ }
3478
+ if (attribute === "srcset" || attribute === "imagesrcset") {
3479
+ const picked = pickBestSrcsetCandidate(raw);
3480
+ if (picked) return picked;
3481
+ return pickFirstSrcsetToken(raw) || "";
3482
+ }
3483
+ return raw.trim();
3484
+ }
3485
+ function pickBestSrcsetCandidate(raw) {
3486
+ const candidates = parseSrcsetCandidates(raw);
3487
+ if (!candidates.length) return null;
3488
+ const widthCandidates = candidates.filter(
3489
+ (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
3490
+ );
3491
+ if (widthCandidates.length) {
3492
+ return widthCandidates.reduce(
3493
+ (best, candidate) => candidate.width > best.width ? candidate : best
3494
+ ).url;
3495
+ }
3496
+ const densityCandidates = candidates.filter(
3497
+ (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
3498
+ );
3499
+ if (densityCandidates.length) {
3500
+ return densityCandidates.reduce(
3501
+ (best, candidate) => candidate.density > best.density ? candidate : best
3502
+ ).url;
3503
+ }
3504
+ return candidates[0]?.url || null;
3505
+ }
3506
+ function parseSrcsetCandidates(raw) {
3507
+ const text = String(raw || "").trim();
3508
+ if (!text) return [];
3509
+ const out = [];
3510
+ let index = 0;
3511
+ while (index < text.length) {
3512
+ index = skipSeparators(text, index);
3513
+ if (index >= text.length) break;
3514
+ const urlToken = readUrlToken(text, index);
3515
+ index = urlToken.nextIndex;
3516
+ const url = urlToken.value.trim();
3517
+ if (!url) continue;
3518
+ index = skipWhitespace(text, index);
3519
+ const descriptors = [];
3520
+ while (index < text.length && text[index] !== ",") {
3521
+ const descriptorToken = readDescriptorToken(text, index);
3522
+ if (!descriptorToken.value) {
3523
+ index = descriptorToken.nextIndex;
3524
+ continue;
3525
+ }
3526
+ descriptors.push(descriptorToken.value);
3527
+ index = descriptorToken.nextIndex;
3528
+ index = skipWhitespace(text, index);
3529
+ }
3530
+ if (index < text.length && text[index] === ",") {
3531
+ index += 1;
3532
+ }
3533
+ let width = null;
3534
+ let density = null;
3535
+ for (const descriptor of descriptors) {
3536
+ const token = descriptor.trim().toLowerCase();
3537
+ if (!token) continue;
3538
+ const widthMatch = token.match(/^(\d+)w$/);
3539
+ if (widthMatch) {
3540
+ const parsed = Number.parseInt(widthMatch[1], 10);
3541
+ if (Number.isFinite(parsed)) {
3542
+ width = parsed;
3543
+ }
3544
+ continue;
3545
+ }
3546
+ const densityMatch = token.match(/^(\d*\.?\d+)x$/);
3547
+ if (densityMatch) {
3548
+ const parsed = Number.parseFloat(densityMatch[1]);
3549
+ if (Number.isFinite(parsed)) {
3550
+ density = parsed;
3551
+ }
3552
+ }
3553
+ }
3554
+ out.push({
3555
+ url,
3556
+ width,
3557
+ density
3558
+ });
3559
+ }
3560
+ return out;
3561
+ }
3562
+ function pickFirstSrcsetToken(raw) {
3563
+ const candidate = parseSrcsetCandidates(raw)[0];
3564
+ if (candidate?.url) {
3565
+ return candidate.url;
3566
+ }
3567
+ const text = String(raw || "");
3568
+ const start = skipSeparators(text, 0);
3569
+ if (start >= text.length) return null;
3570
+ const firstToken = readUrlToken(text, start).value.trim();
3571
+ return firstToken || null;
3572
+ }
3573
+ function skipWhitespace(value, index) {
3574
+ let cursor = index;
3575
+ while (cursor < value.length && /\s/.test(value[cursor])) {
3576
+ cursor += 1;
3577
+ }
3578
+ return cursor;
3579
+ }
3580
+ function skipSeparators(value, index) {
3581
+ let cursor = skipWhitespace(value, index);
3582
+ while (cursor < value.length && value[cursor] === ",") {
3583
+ cursor += 1;
3584
+ cursor = skipWhitespace(value, cursor);
3585
+ }
3586
+ return cursor;
3587
+ }
3588
+ function readUrlToken(value, index) {
3589
+ let cursor = index;
3590
+ let out = "";
3591
+ const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
3592
+ while (cursor < value.length) {
3593
+ const char = value[cursor];
3594
+ if (/\s/.test(char)) {
3595
+ break;
3596
+ }
3597
+ if (char === "," && !isDataUrl) {
3598
+ break;
3599
+ }
3600
+ out += char;
3601
+ cursor += 1;
3602
+ }
3603
+ if (isDataUrl && out.endsWith(",") && cursor < value.length) {
3604
+ out = out.slice(0, -1);
3605
+ }
3606
+ return {
3607
+ value: out,
3608
+ nextIndex: cursor
3609
+ };
3610
+ }
3611
+ function readDescriptorToken(value, index) {
3612
+ let cursor = skipWhitespace(value, index);
3613
+ let out = "";
3614
+ while (cursor < value.length) {
3615
+ const char = value[cursor];
3616
+ if (char === "," || /\s/.test(char)) {
3617
+ break;
3618
+ }
3619
+ out += char;
3620
+ cursor += 1;
3621
+ }
3622
+ return {
3623
+ value: out.trim(),
3624
+ nextIndex: cursor
3625
+ };
3626
+ }
3627
+
3628
+ // src/html/counter-runtime.ts
3629
+ var CounterResolutionError = class extends Error {
3630
+ code;
3631
+ constructor(code, message) {
3632
+ super(message);
3633
+ this.name = "CounterResolutionError";
3634
+ this.code = code;
3635
+ }
3636
+ };
3637
+ async function resolveCounterElement(page, counter) {
3638
+ const normalized = normalizeCounter(counter);
3639
+ if (normalized == null) {
3640
+ throw buildCounterNotFoundError(counter);
3641
+ }
3642
+ const scan = await scanCounterOccurrences(page, [normalized]);
3643
+ const entry = scan.get(normalized);
3644
+ if (!entry || entry.count <= 0 || !entry.frame) {
3645
+ throw buildCounterNotFoundError(counter);
3646
+ }
3647
+ if (entry.count > 1) {
3648
+ throw buildCounterAmbiguousError(counter);
3649
+ }
3650
+ const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
3651
+ const element = handle.asElement();
3652
+ if (!element) {
3653
+ await handle.dispose();
3654
+ throw buildCounterNotFoundError(counter);
3655
+ }
3656
+ return element;
3657
+ }
3658
+ async function resolveCountersBatch(page, requests) {
3659
+ const out = {};
3660
+ if (!requests.length) return out;
3661
+ const counters = dedupeCounters(requests);
3662
+ const scan = await scanCounterOccurrences(page, counters);
3663
+ for (const counter of counters) {
3664
+ const entry = scan.get(counter);
3665
+ if (entry.count > 1) {
3666
+ throw buildCounterAmbiguousError(counter);
3667
+ }
3668
+ }
3669
+ const valueCache = /* @__PURE__ */ new Map();
3670
+ for (const request of requests) {
3671
+ const normalized = normalizeCounter(request.counter);
3672
+ if (normalized == null) {
3673
+ out[request.key] = null;
3674
+ continue;
3675
+ }
3676
+ const entry = scan.get(normalized);
3677
+ if (!entry || entry.count <= 0 || !entry.frame) {
3678
+ out[request.key] = null;
3679
+ continue;
3721
3680
  }
3722
- if (count > 1 && !fallback) {
3723
- fallback = {
3724
- selector,
3725
- count,
3726
- mode: "fallback"
3727
- };
3681
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
3682
+ if (valueCache.has(cacheKey)) {
3683
+ out[request.key] = valueCache.get(cacheKey);
3684
+ continue;
3685
+ }
3686
+ const read = await readCounterValueInFrame(
3687
+ entry.frame,
3688
+ normalized,
3689
+ request.attribute
3690
+ );
3691
+ if (read.status === "ambiguous") {
3692
+ throw buildCounterAmbiguousError(normalized);
3693
+ }
3694
+ if (read.status === "missing") {
3695
+ valueCache.set(cacheKey, null);
3696
+ out[request.key] = null;
3697
+ continue;
3728
3698
  }
3699
+ const normalizedValue = normalizeExtractedValue(
3700
+ read.value ?? null,
3701
+ request.attribute
3702
+ );
3703
+ valueCache.set(cacheKey, normalizedValue);
3704
+ out[request.key] = normalizedValue;
3729
3705
  }
3730
- return fallback;
3706
+ return out;
3731
3707
  }
3732
- function countInDocument(selectors) {
3708
+ function dedupeCounters(requests) {
3709
+ const seen = /* @__PURE__ */ new Set();
3733
3710
  const out = [];
3734
- for (const selector of selectors) {
3735
- if (!selector) continue;
3736
- let count = 0;
3737
- try {
3738
- count = document.querySelectorAll(selector).length;
3739
- } catch {
3740
- count = 0;
3741
- }
3742
- out.push({ selector, count });
3711
+ for (const request of requests) {
3712
+ const normalized = normalizeCounter(request.counter);
3713
+ if (normalized == null || seen.has(normalized)) continue;
3714
+ seen.add(normalized);
3715
+ out.push(normalized);
3743
3716
  }
3744
3717
  return out;
3745
3718
  }
3746
- function countInRoot(root, selectors) {
3747
- if (!(root instanceof ShadowRoot)) return [];
3748
- const out = [];
3749
- for (const selector of selectors) {
3750
- if (!selector) continue;
3751
- let count = 0;
3719
+ function normalizeCounter(counter) {
3720
+ if (!Number.isFinite(counter)) return null;
3721
+ if (!Number.isInteger(counter)) return null;
3722
+ if (counter <= 0) return null;
3723
+ return counter;
3724
+ }
3725
+ async function scanCounterOccurrences(page, counters) {
3726
+ const out = /* @__PURE__ */ new Map();
3727
+ for (const counter of counters) {
3728
+ out.set(counter, {
3729
+ count: 0,
3730
+ frame: null
3731
+ });
3732
+ }
3733
+ if (!counters.length) return out;
3734
+ for (const frame of page.frames()) {
3735
+ let frameCounts;
3752
3736
  try {
3753
- count = root.querySelectorAll(selector).length;
3737
+ frameCounts = await frame.evaluate((candidates) => {
3738
+ const keys = new Set(candidates.map((value) => String(value)));
3739
+ const counts = {};
3740
+ for (const key of keys) {
3741
+ counts[key] = 0;
3742
+ }
3743
+ const walk = (root) => {
3744
+ const children = Array.from(root.children);
3745
+ for (const child of children) {
3746
+ const value = child.getAttribute("c");
3747
+ if (value && keys.has(value)) {
3748
+ counts[value] = (counts[value] || 0) + 1;
3749
+ }
3750
+ walk(child);
3751
+ if (child.shadowRoot) {
3752
+ walk(child.shadowRoot);
3753
+ }
3754
+ }
3755
+ };
3756
+ walk(document);
3757
+ return counts;
3758
+ }, counters);
3754
3759
  } catch {
3755
- count = 0;
3760
+ continue;
3761
+ }
3762
+ for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
3763
+ const counter = Number.parseInt(rawCounter, 10);
3764
+ if (!Number.isFinite(counter)) continue;
3765
+ const count = Number(rawCount || 0);
3766
+ if (!Number.isFinite(count) || count <= 0) continue;
3767
+ const entry = out.get(counter);
3768
+ entry.count += count;
3769
+ if (!entry.frame) {
3770
+ entry.frame = frame;
3771
+ }
3756
3772
  }
3757
- out.push({ selector, count });
3758
3773
  }
3759
3774
  return out;
3760
3775
  }
3761
- function isPathDebugEnabled() {
3762
- const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
3763
- if (!value) return false;
3764
- const normalized = value.trim().toLowerCase();
3765
- return normalized === "1" || normalized === "true";
3766
- }
3767
- function debugPath(message, data) {
3768
- if (!isPathDebugEnabled()) return;
3769
- if (data !== void 0) {
3770
- console.log(`[opensteer:path] ${message}`, data);
3771
- } else {
3772
- console.log(`[opensteer:path] ${message}`);
3773
- }
3776
+ async function resolveUniqueHandleInFrame(frame, counter) {
3777
+ return frame.evaluateHandle((targetCounter) => {
3778
+ const matches = [];
3779
+ const walk = (root) => {
3780
+ const children = Array.from(root.children);
3781
+ for (const child of children) {
3782
+ if (child.getAttribute("c") === targetCounter) {
3783
+ matches.push(child);
3784
+ }
3785
+ walk(child);
3786
+ if (child.shadowRoot) {
3787
+ walk(child.shadowRoot);
3788
+ }
3789
+ }
3790
+ };
3791
+ walk(document);
3792
+ if (matches.length !== 1) {
3793
+ return null;
3794
+ }
3795
+ return matches[0];
3796
+ }, String(counter));
3774
3797
  }
3775
- async function disposeHandle(handle) {
3776
- if (!handle) return;
3798
+ async function readCounterValueInFrame(frame, counter, attribute) {
3777
3799
  try {
3778
- await handle.dispose();
3800
+ return await frame.evaluate(
3801
+ ({ targetCounter, attribute: attribute2 }) => {
3802
+ const matches = [];
3803
+ const walk = (root) => {
3804
+ const children = Array.from(root.children);
3805
+ for (const child of children) {
3806
+ if (child.getAttribute("c") === targetCounter) {
3807
+ matches.push(child);
3808
+ }
3809
+ walk(child);
3810
+ if (child.shadowRoot) {
3811
+ walk(child.shadowRoot);
3812
+ }
3813
+ }
3814
+ };
3815
+ walk(document);
3816
+ if (!matches.length) {
3817
+ return {
3818
+ status: "missing"
3819
+ };
3820
+ }
3821
+ if (matches.length > 1) {
3822
+ return {
3823
+ status: "ambiguous"
3824
+ };
3825
+ }
3826
+ const target = matches[0];
3827
+ const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
3828
+ return {
3829
+ status: "ok",
3830
+ value
3831
+ };
3832
+ },
3833
+ {
3834
+ targetCounter: String(counter),
3835
+ attribute
3836
+ }
3837
+ );
3779
3838
  } catch {
3839
+ return {
3840
+ status: "missing"
3841
+ };
3780
3842
  }
3781
3843
  }
3844
+ function buildCounterNotFoundError(counter) {
3845
+ return new CounterResolutionError(
3846
+ "ERR_COUNTER_NOT_FOUND",
3847
+ `Counter ${counter} was not found in the live DOM.`
3848
+ );
3849
+ }
3850
+ function buildCounterAmbiguousError(counter) {
3851
+ return new CounterResolutionError(
3852
+ "ERR_COUNTER_AMBIGUOUS",
3853
+ `Counter ${counter} matches multiple live elements.`
3854
+ );
3855
+ }
3782
3856
 
3783
3857
  // src/actions/actionability-probe.ts
3784
3858
  async function probeActionabilityState(element) {
@@ -3906,7 +3980,7 @@ function defaultActionFailureMessage(action) {
3906
3980
  function classifyActionFailure(input) {
3907
3981
  const typed = classifyTypedError(input.error);
3908
3982
  if (typed) return typed;
3909
- const message = extractErrorMessage(input.error, input.fallbackMessage);
3983
+ const message = extractErrorMessage2(input.error, input.fallbackMessage);
3910
3984
  const fromCallLog = classifyFromPlaywrightMessage(message, input.probe);
3911
3985
  if (fromCallLog) return fromCallLog;
3912
3986
  const fromProbe = classifyFromProbe(input.probe);
@@ -3915,7 +3989,7 @@ function classifyActionFailure(input) {
3915
3989
  if (fromHeuristic) return fromHeuristic;
3916
3990
  return buildFailure({
3917
3991
  code: "UNKNOWN",
3918
- message: ensureMessage(input.fallbackMessage, "Action failed."),
3992
+ message: ensureMessage(message, input.fallbackMessage),
3919
3993
  classificationSource: "unknown"
3920
3994
  });
3921
3995
  }
@@ -3971,13 +4045,6 @@ function classifyTypedError(error) {
3971
4045
  classificationSource: "typed_error"
3972
4046
  });
3973
4047
  }
3974
- if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
3975
- return buildFailure({
3976
- code: "TARGET_UNAVAILABLE",
3977
- message: error.message,
3978
- classificationSource: "typed_error"
3979
- });
3980
- }
3981
4048
  if (error.code === "ERR_COUNTER_AMBIGUOUS") {
3982
4049
  return buildFailure({
3983
4050
  code: "TARGET_AMBIGUOUS",
@@ -3985,13 +4052,6 @@ function classifyTypedError(error) {
3985
4052
  classificationSource: "typed_error"
3986
4053
  });
3987
4054
  }
3988
- if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
3989
- return buildFailure({
3990
- code: "TARGET_STALE",
3991
- message: error.message,
3992
- classificationSource: "typed_error"
3993
- });
3994
- }
3995
4055
  }
3996
4056
  return null;
3997
4057
  }
@@ -4175,13 +4235,22 @@ function defaultRetryableForCode(code) {
4175
4235
  return true;
4176
4236
  }
4177
4237
  }
4178
- function extractErrorMessage(error, fallbackMessage) {
4238
+ function extractErrorMessage2(error, fallbackMessage) {
4179
4239
  if (error instanceof Error && error.message.trim()) {
4180
4240
  return error.message;
4181
4241
  }
4182
4242
  if (typeof error === "string" && error.trim()) {
4183
4243
  return error.trim();
4184
4244
  }
4245
+ if (error && typeof error === "object" && !Array.isArray(error)) {
4246
+ const record = error;
4247
+ if (typeof record.message === "string" && record.message.trim()) {
4248
+ return record.message.trim();
4249
+ }
4250
+ if (typeof record.error === "string" && record.error.trim()) {
4251
+ return record.error.trim();
4252
+ }
4253
+ }
4185
4254
  return ensureMessage(fallbackMessage, "Action failed.");
4186
4255
  }
4187
4256
  function ensureMessage(value, fallback) {
@@ -5140,7 +5209,8 @@ function withTokenQuery(wsUrl, token) {
5140
5209
  // src/cloud/local-cache-sync.ts
5141
5210
  import fs2 from "fs";
5142
5211
  import path3 from "path";
5143
- function collectLocalSelectorCacheEntries(storage) {
5212
+ function collectLocalSelectorCacheEntries(storage, options = {}) {
5213
+ const debug = options.debug === true;
5144
5214
  const namespace = storage.getNamespace();
5145
5215
  const namespaceDir = storage.getNamespaceDir();
5146
5216
  if (!fs2.existsSync(namespaceDir)) return [];
@@ -5149,7 +5219,7 @@ function collectLocalSelectorCacheEntries(storage) {
5149
5219
  for (const fileName of fileNames) {
5150
5220
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
5151
5221
  const filePath = path3.join(namespaceDir, fileName);
5152
- const selector = readSelectorFile(filePath);
5222
+ const selector = readSelectorFile(filePath, debug);
5153
5223
  if (!selector) continue;
5154
5224
  const descriptionHash = normalizeDescriptionHash(selector.id);
5155
5225
  const method = normalizeMethod(selector.method);
@@ -5174,11 +5244,20 @@ function collectLocalSelectorCacheEntries(storage) {
5174
5244
  }
5175
5245
  return dedupeNewest(entries);
5176
5246
  }
5177
- function readSelectorFile(filePath) {
5247
+ function readSelectorFile(filePath, debug) {
5178
5248
  try {
5179
5249
  const raw = fs2.readFileSync(filePath, "utf8");
5180
5250
  return JSON.parse(raw);
5181
- } catch {
5251
+ } catch (error) {
5252
+ const message = extractErrorMessage(
5253
+ error,
5254
+ "Unable to parse selector cache file JSON."
5255
+ );
5256
+ if (debug) {
5257
+ console.warn(
5258
+ `[opensteer] failed to read local selector cache file "${filePath}": ${message}`
5259
+ );
5260
+ }
5182
5261
  return null;
5183
5262
  }
5184
5263
  }
@@ -7323,7 +7402,7 @@ function sleep3(ms) {
7323
7402
  }
7324
7403
 
7325
7404
  // src/opensteer.ts
7326
- import { createHash, randomUUID as randomUUID2 } from "crypto";
7405
+ import { createHash, randomUUID } from "crypto";
7327
7406
 
7328
7407
  // src/browser/pool.ts
7329
7408
  import {
@@ -7824,11 +7903,12 @@ function dotenvFileOrder(nodeEnv) {
7824
7903
  files.push(".env");
7825
7904
  return files;
7826
7905
  }
7827
- function loadDotenvValues(rootDir, baseEnv) {
7906
+ function loadDotenvValues(rootDir, baseEnv, options = {}) {
7828
7907
  const values = {};
7829
7908
  if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
7830
7909
  return values;
7831
7910
  }
7911
+ const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
7832
7912
  const baseDir = path4.resolve(rootDir);
7833
7913
  const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
7834
7914
  for (const filename of dotenvFileOrder(nodeEnv)) {
@@ -7842,15 +7922,24 @@ function loadDotenvValues(rootDir, baseEnv) {
7842
7922
  values[key] = value;
7843
7923
  }
7844
7924
  }
7845
- } catch {
7925
+ } catch (error) {
7926
+ const message = extractErrorMessage(
7927
+ error,
7928
+ "Unable to read or parse dotenv file."
7929
+ );
7930
+ if (debug) {
7931
+ console.warn(
7932
+ `[opensteer] failed to load dotenv file "${filePath}": ${message}`
7933
+ );
7934
+ }
7846
7935
  continue;
7847
7936
  }
7848
7937
  }
7849
7938
  return values;
7850
7939
  }
7851
- function resolveEnv(rootDir) {
7940
+ function resolveEnv(rootDir, options = {}) {
7852
7941
  const baseEnv = process.env;
7853
- const dotenvValues = loadDotenvValues(rootDir, baseEnv);
7942
+ const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
7854
7943
  return {
7855
7944
  ...dotenvValues,
7856
7945
  ...baseEnv
@@ -7894,13 +7983,22 @@ function assertNoLegacyRuntimeConfig(source, config) {
7894
7983
  );
7895
7984
  }
7896
7985
  }
7897
- function loadConfigFile(rootDir) {
7986
+ function loadConfigFile(rootDir, options = {}) {
7898
7987
  const configPath = path4.join(rootDir, ".opensteer", "config.json");
7899
7988
  if (!fs3.existsSync(configPath)) return {};
7900
7989
  try {
7901
7990
  const raw = fs3.readFileSync(configPath, "utf8");
7902
7991
  return JSON.parse(raw);
7903
- } catch {
7992
+ } catch (error) {
7993
+ const message = extractErrorMessage(
7994
+ error,
7995
+ "Unable to read or parse config file."
7996
+ );
7997
+ if (options.debug) {
7998
+ console.warn(
7999
+ `[opensteer] failed to load config file "${configPath}": ${message}`
8000
+ );
8001
+ }
7904
8002
  return {};
7905
8003
  }
7906
8004
  }
@@ -8032,6 +8130,8 @@ function resolveCloudSelection(config, env = process.env) {
8032
8130
  };
8033
8131
  }
8034
8132
  function resolveConfig(input = {}) {
8133
+ const processEnv = process.env;
8134
+ const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
8035
8135
  const initialRootDir = input.storage?.rootDir ?? process.cwd();
8036
8136
  const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
8037
8137
  storage: {
@@ -8040,12 +8140,16 @@ function resolveConfig(input = {}) {
8040
8140
  });
8041
8141
  assertNoLegacyAiConfig("Opensteer constructor config", input);
8042
8142
  assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
8043
- const fileConfig = loadConfigFile(initialRootDir);
8143
+ const fileConfig = loadConfigFile(initialRootDir, {
8144
+ debug: debugHint
8145
+ });
8044
8146
  assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
8045
8147
  assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
8046
8148
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
8047
8149
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
8048
- const env = resolveEnv(envRootDir);
8150
+ const env = resolveEnv(envRootDir, {
8151
+ debug: debugHint
8152
+ });
8049
8153
  if (env.OPENSTEER_AI_MODEL) {
8050
8154
  throw new Error(
8051
8155
  "OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
@@ -9579,7 +9683,9 @@ var Opensteer = class _Opensteer {
9579
9683
  this.aiExtract = this.createLazyExtractCallback(model);
9580
9684
  const rootDir = resolved.storage?.rootDir || process.cwd();
9581
9685
  this.namespace = resolveNamespace(resolved, rootDir);
9582
- this.storage = new LocalSelectorStorage(rootDir, this.namespace);
9686
+ this.storage = new LocalSelectorStorage(rootDir, this.namespace, {
9687
+ debug: Boolean(resolved.debug)
9688
+ });
9583
9689
  this.pool = new BrowserPool(resolved.browser || {});
9584
9690
  if (cloudSelection.cloud) {
9585
9691
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
@@ -9598,12 +9704,20 @@ var Opensteer = class _Opensteer {
9598
9704
  this.cloud = null;
9599
9705
  }
9600
9706
  }
9707
+ logDebugError(context, error) {
9708
+ if (!this.config.debug) return;
9709
+ const normalized = normalizeError(error, "Unknown error.");
9710
+ const codeSuffix = normalized.code && normalized.code.trim() ? ` [${normalized.code.trim()}]` : "";
9711
+ console.warn(
9712
+ `[opensteer] ${context}: ${normalized.message}${codeSuffix}`
9713
+ );
9714
+ }
9601
9715
  createLazyResolveCallback(model) {
9602
9716
  let resolverPromise = null;
9603
9717
  return async (...args) => {
9604
9718
  try {
9605
9719
  if (!resolverPromise) {
9606
- resolverPromise = import("./resolver-HVZJQZ32.js").then(
9720
+ resolverPromise = import("./resolver-ZREUOOTV.js").then(
9607
9721
  (m) => m.createResolveCallback(model)
9608
9722
  );
9609
9723
  }
@@ -9620,7 +9734,7 @@ var Opensteer = class _Opensteer {
9620
9734
  const extract = async (args) => {
9621
9735
  try {
9622
9736
  if (!extractorPromise) {
9623
- extractorPromise = import("./extractor-I6TJPTXV.js").then(
9737
+ extractorPromise = import("./extractor-CZFCFUME.js").then(
9624
9738
  (m) => m.createExtractCallback(model)
9625
9739
  );
9626
9740
  }
@@ -9688,7 +9802,8 @@ var Opensteer = class _Opensteer {
9688
9802
  let tabs;
9689
9803
  try {
9690
9804
  tabs = await this.invokeCloudAction("tabs", {});
9691
- } catch {
9805
+ } catch (error) {
9806
+ this.logDebugError("cloud page reference sync (tabs lookup) failed", error);
9692
9807
  return;
9693
9808
  }
9694
9809
  if (!tabs.length) {
@@ -9826,12 +9941,7 @@ var Opensteer = class _Opensteer {
9826
9941
  try {
9827
9942
  await this.syncLocalSelectorCacheToCloud();
9828
9943
  } catch (error) {
9829
- if (this.config.debug) {
9830
- const message = error instanceof Error ? error.message : String(error);
9831
- console.warn(
9832
- `[opensteer] cloud selector cache sync failed: ${message}`
9833
- );
9834
- }
9944
+ this.logDebugError("cloud selector cache sync failed", error);
9835
9945
  }
9836
9946
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
9837
9947
  this.cloud.localRunId = localRunId;
@@ -9863,7 +9973,12 @@ var Opensteer = class _Opensteer {
9863
9973
  this.cloud.actionClient = actionClient;
9864
9974
  this.cloud.sessionId = sessionId;
9865
9975
  this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
9866
- await this.syncCloudPageRef().catch(() => void 0);
9976
+ await this.syncCloudPageRef().catch((error) => {
9977
+ this.logDebugError(
9978
+ "cloud page reference sync after launch failed",
9979
+ error
9980
+ );
9981
+ });
9867
9982
  this.announceCloudSession({
9868
9983
  sessionId: session2.sessionId,
9869
9984
  workspaceId: session2.cloudSession.workspaceId,
@@ -9950,7 +10065,9 @@ var Opensteer = class _Opensteer {
9950
10065
  }
9951
10066
  async syncLocalSelectorCacheToCloud() {
9952
10067
  if (!this.cloud) return;
9953
- const entries = collectLocalSelectorCacheEntries(this.storage);
10068
+ const entries = collectLocalSelectorCacheEntries(this.storage, {
10069
+ debug: Boolean(this.config.debug)
10070
+ });
9954
10071
  if (!entries.length) return;
9955
10072
  await this.cloud.sessionClient.importSelectorCache({
9956
10073
  entries
@@ -9959,9 +10076,12 @@ var Opensteer = class _Opensteer {
9959
10076
  async goto(url, options) {
9960
10077
  if (this.cloud) {
9961
10078
  await this.invokeCloudActionAndResetCache("goto", { url, options });
9962
- await this.syncCloudPageRef({ expectedUrl: url }).catch(
9963
- () => void 0
9964
- );
10079
+ await this.syncCloudPageRef({ expectedUrl: url }).catch((error) => {
10080
+ this.logDebugError(
10081
+ "cloud page reference sync after goto failed",
10082
+ error
10083
+ );
10084
+ });
9965
10085
  return;
9966
10086
  }
9967
10087
  const { waitUntil = "domcontentloaded", ...rest } = options ?? {};
@@ -10062,7 +10182,7 @@ var Opensteer = class _Opensteer {
10062
10182
  let persistPath = null;
10063
10183
  try {
10064
10184
  if (storageKey && resolution.shouldPersist) {
10065
- persistPath = await this.buildPathFromResolvedHandle(
10185
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10066
10186
  handle,
10067
10187
  "hover",
10068
10188
  resolution.counter
@@ -10161,7 +10281,7 @@ var Opensteer = class _Opensteer {
10161
10281
  let persistPath = null;
10162
10282
  try {
10163
10283
  if (storageKey && resolution.shouldPersist) {
10164
- persistPath = await this.buildPathFromResolvedHandle(
10284
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10165
10285
  handle,
10166
10286
  "input",
10167
10287
  resolution.counter
@@ -10264,7 +10384,7 @@ var Opensteer = class _Opensteer {
10264
10384
  let persistPath = null;
10265
10385
  try {
10266
10386
  if (storageKey && resolution.shouldPersist) {
10267
- persistPath = await this.buildPathFromResolvedHandle(
10387
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10268
10388
  handle,
10269
10389
  "select",
10270
10390
  resolution.counter
@@ -10374,7 +10494,7 @@ var Opensteer = class _Opensteer {
10374
10494
  let persistPath = null;
10375
10495
  try {
10376
10496
  if (storageKey && resolution.shouldPersist) {
10377
- persistPath = await this.buildPathFromResolvedHandle(
10497
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10378
10498
  handle,
10379
10499
  "scroll",
10380
10500
  resolution.counter
@@ -10473,7 +10593,12 @@ var Opensteer = class _Opensteer {
10473
10593
  }
10474
10594
  );
10475
10595
  await this.syncCloudPageRef({ expectedUrl: result.url }).catch(
10476
- () => void 0
10596
+ (error) => {
10597
+ this.logDebugError(
10598
+ "cloud page reference sync after newTab failed",
10599
+ error
10600
+ );
10601
+ }
10477
10602
  );
10478
10603
  return result;
10479
10604
  }
@@ -10485,7 +10610,12 @@ var Opensteer = class _Opensteer {
10485
10610
  async switchTab(index) {
10486
10611
  if (this.cloud) {
10487
10612
  await this.invokeCloudActionAndResetCache("switchTab", { index });
10488
- await this.syncCloudPageRef().catch(() => void 0);
10613
+ await this.syncCloudPageRef().catch((error) => {
10614
+ this.logDebugError(
10615
+ "cloud page reference sync after switchTab failed",
10616
+ error
10617
+ );
10618
+ });
10489
10619
  return;
10490
10620
  }
10491
10621
  const page = await switchTab(this.context, index);
@@ -10495,7 +10625,12 @@ var Opensteer = class _Opensteer {
10495
10625
  async closeTab(index) {
10496
10626
  if (this.cloud) {
10497
10627
  await this.invokeCloudActionAndResetCache("closeTab", { index });
10498
- await this.syncCloudPageRef().catch(() => void 0);
10628
+ await this.syncCloudPageRef().catch((error) => {
10629
+ this.logDebugError(
10630
+ "cloud page reference sync after closeTab failed",
10631
+ error
10632
+ );
10633
+ });
10499
10634
  return;
10500
10635
  }
10501
10636
  const newPage = await closeTab(this.context, this.page, index);
@@ -10655,22 +10790,28 @@ var Opensteer = class _Opensteer {
10655
10790
  const handle = await this.resolveCounterHandle(resolution.counter);
10656
10791
  try {
10657
10792
  if (storageKey && resolution.shouldPersist) {
10658
- const persistPath = await this.buildPathFromResolvedHandle(
10793
+ const persistPath = await this.tryBuildPathFromResolvedHandle(
10659
10794
  handle,
10660
10795
  method,
10661
10796
  resolution.counter
10662
10797
  );
10663
- this.persistPath(
10664
- storageKey,
10665
- method,
10666
- options.description,
10667
- persistPath
10668
- );
10798
+ if (persistPath) {
10799
+ this.persistPath(
10800
+ storageKey,
10801
+ method,
10802
+ options.description,
10803
+ persistPath
10804
+ );
10805
+ }
10669
10806
  }
10670
10807
  return await counterFn(handle);
10671
10808
  } catch (err) {
10672
- const message = err instanceof Error ? err.message : `${method} failed.`;
10673
- throw new Error(message);
10809
+ if (err instanceof Error) {
10810
+ throw err;
10811
+ }
10812
+ throw new Error(
10813
+ `${method} failed. ${extractErrorMessage(err, "Unknown error.")}`
10814
+ );
10674
10815
  } finally {
10675
10816
  await handle.dispose();
10676
10817
  }
@@ -10699,7 +10840,7 @@ var Opensteer = class _Opensteer {
10699
10840
  let persistPath = null;
10700
10841
  try {
10701
10842
  if (storageKey && resolution.shouldPersist) {
10702
- persistPath = await this.buildPathFromResolvedHandle(
10843
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10703
10844
  handle,
10704
10845
  "uploadFile",
10705
10846
  resolution.counter
@@ -10797,6 +10938,9 @@ var Opensteer = class _Opensteer {
10797
10938
  if (this.cloud) {
10798
10939
  return await this.invokeCloudAction("extract", options);
10799
10940
  }
10941
+ if (options.schema !== void 0) {
10942
+ assertValidExtractSchemaRoot(options.schema);
10943
+ }
10800
10944
  const storageKey = this.resolveStorageKey(options.description);
10801
10945
  const schemaHash = options.schema ? computeSchemaHash(options.schema) : null;
10802
10946
  const stored = storageKey ? this.storage.readSelector(storageKey) : null;
@@ -10805,7 +10949,7 @@ var Opensteer = class _Opensteer {
10805
10949
  try {
10806
10950
  payload = normalizePersistedExtractPayload(stored.path);
10807
10951
  } catch (err) {
10808
- const message = err instanceof Error ? err.message : "Unknown error";
10952
+ const message = extractErrorMessage(err, "Unknown error.");
10809
10953
  const selectorFile = storageKey ? this.storage.getSelectorPath(storageKey) : "unknown selector file";
10810
10954
  throw new Error(
10811
10955
  `Cached extraction selector is invalid for the current schema at "${selectorFile}". Delete the cached selector and rerun extraction. ${message}`
@@ -10822,7 +10966,16 @@ var Opensteer = class _Opensteer {
10822
10966
  fields.push(...schemaFields);
10823
10967
  }
10824
10968
  if (!fields.length) {
10825
- const planResult = await this.parseAiExtractPlan(options);
10969
+ let planResult;
10970
+ try {
10971
+ planResult = await this.parseAiExtractPlan(options);
10972
+ } catch (error) {
10973
+ const message = extractErrorMessage(error, "Unknown error.");
10974
+ const contextMessage = options.schema ? "Schema extraction did not resolve deterministic field targets, so Opensteer attempted AI extraction planning." : "Opensteer attempted AI extraction planning.";
10975
+ throw new Error(`${contextMessage} ${message}`, {
10976
+ cause: error
10977
+ });
10978
+ }
10826
10979
  if (planResult.fields.length) {
10827
10980
  fields.push(...planResult.fields);
10828
10981
  } else if (planResult.data !== void 0) {
@@ -10963,7 +11116,7 @@ var Opensteer = class _Opensteer {
10963
11116
  let persistPath = null;
10964
11117
  try {
10965
11118
  if (storageKey && resolution.shouldPersist) {
10966
- persistPath = await this.buildPathFromResolvedHandle(
11119
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10967
11120
  handle,
10968
11121
  "click",
10969
11122
  resolution.counter
@@ -11059,17 +11212,6 @@ var Opensteer = class _Opensteer {
11059
11212
  }
11060
11213
  }
11061
11214
  if (options.element != null) {
11062
- const pathFromElement = await this.tryBuildPathFromCounter(
11063
- options.element
11064
- );
11065
- if (pathFromElement) {
11066
- return {
11067
- path: pathFromElement,
11068
- counter: null,
11069
- shouldPersist: Boolean(storageKey),
11070
- source: "element"
11071
- };
11072
- }
11073
11215
  return {
11074
11216
  path: null,
11075
11217
  counter: options.element,
@@ -11097,17 +11239,6 @@ var Opensteer = class _Opensteer {
11097
11239
  options.description
11098
11240
  );
11099
11241
  if (resolved?.counter != null) {
11100
- const pathFromAiCounter = await this.tryBuildPathFromCounter(
11101
- resolved.counter
11102
- );
11103
- if (pathFromAiCounter) {
11104
- return {
11105
- path: pathFromAiCounter,
11106
- counter: null,
11107
- shouldPersist: Boolean(storageKey),
11108
- source: "ai"
11109
- };
11110
- }
11111
11242
  return {
11112
11243
  path: null,
11113
11244
  counter: resolved.counter,
@@ -11189,23 +11320,22 @@ var Opensteer = class _Opensteer {
11189
11320
  try {
11190
11321
  const builtPath = await buildElementPathFromHandle(handle);
11191
11322
  if (builtPath) {
11192
- return this.withIndexedIframeContext(builtPath, indexedPath);
11323
+ const withFrameContext = await this.withHandleIframeContext(
11324
+ handle,
11325
+ builtPath
11326
+ );
11327
+ return this.withIndexedIframeContext(
11328
+ withFrameContext,
11329
+ indexedPath
11330
+ );
11193
11331
  }
11194
11332
  return indexedPath;
11195
11333
  } finally {
11196
11334
  await handle.dispose();
11197
11335
  }
11198
11336
  }
11199
- async tryBuildPathFromCounter(counter) {
11200
- try {
11201
- return await this.buildPathFromElement(counter);
11202
- } catch {
11203
- return null;
11204
- }
11205
- }
11206
11337
  async resolveCounterHandle(element) {
11207
- const snapshot = await this.ensureSnapshotWithCounters();
11208
- return resolveCounterElement(this.page, snapshot, element);
11338
+ return resolveCounterElement(this.page, element);
11209
11339
  }
11210
11340
  async resolveCounterHandleForAction(action, description, element) {
11211
11341
  try {
@@ -11229,8 +11359,12 @@ var Opensteer = class _Opensteer {
11229
11359
  const indexedPath = await this.readPathFromCounterIndex(counter);
11230
11360
  const builtPath = await buildElementPathFromHandle(handle);
11231
11361
  if (builtPath) {
11362
+ const withFrameContext = await this.withHandleIframeContext(
11363
+ handle,
11364
+ builtPath
11365
+ );
11232
11366
  const normalized = this.withIndexedIframeContext(
11233
- builtPath,
11367
+ withFrameContext,
11234
11368
  indexedPath
11235
11369
  );
11236
11370
  if (normalized.nodes.length) return normalized;
@@ -11240,15 +11374,34 @@ var Opensteer = class _Opensteer {
11240
11374
  `Unable to build element path from counter ${counter} during ${action}.`
11241
11375
  );
11242
11376
  }
11377
+ async tryBuildPathFromResolvedHandle(handle, action, counter) {
11378
+ try {
11379
+ return await this.buildPathFromResolvedHandle(handle, action, counter);
11380
+ } catch (error) {
11381
+ this.logDebugError(
11382
+ `path persistence skipped for ${action} counter ${counter}`,
11383
+ error
11384
+ );
11385
+ return null;
11386
+ }
11387
+ }
11243
11388
  withIndexedIframeContext(builtPath, indexedPath) {
11244
11389
  const normalizedBuilt = this.normalizePath(builtPath);
11245
11390
  if (!indexedPath) return normalizedBuilt;
11246
11391
  const iframePrefix = collectIframeContextPrefix(indexedPath);
11247
11392
  if (!iframePrefix.length) return normalizedBuilt;
11393
+ const builtContext = cloneContextHops(normalizedBuilt.context);
11394
+ const overlap = measureContextOverlap(iframePrefix, builtContext);
11395
+ const missingPrefix = cloneContextHops(
11396
+ iframePrefix.slice(0, iframePrefix.length - overlap)
11397
+ );
11398
+ if (!missingPrefix.length) {
11399
+ return normalizedBuilt;
11400
+ }
11248
11401
  const merged = {
11249
11402
  context: [
11250
- ...cloneContextHops(iframePrefix),
11251
- ...cloneContextHops(normalizedBuilt.context)
11403
+ ...missingPrefix,
11404
+ ...builtContext
11252
11405
  ],
11253
11406
  nodes: cloneElementPath(normalizedBuilt).nodes
11254
11407
  };
@@ -11258,9 +11411,48 @@ var Opensteer = class _Opensteer {
11258
11411
  if (fallback.nodes.length) return fallback;
11259
11412
  return normalizedBuilt;
11260
11413
  }
11414
+ async withHandleIframeContext(handle, path5) {
11415
+ const ownFrame = await handle.ownerFrame();
11416
+ if (!ownFrame) {
11417
+ return this.normalizePath(path5);
11418
+ }
11419
+ let frame = ownFrame;
11420
+ let prefix = [];
11421
+ while (frame && frame !== this.page.mainFrame()) {
11422
+ const parent = frame.parentFrame();
11423
+ if (!parent) break;
11424
+ const frameElement = await frame.frameElement().catch(() => null);
11425
+ if (!frameElement) break;
11426
+ try {
11427
+ const frameElementPath = await buildElementPathFromHandle(frameElement);
11428
+ if (frameElementPath?.nodes.length) {
11429
+ const segment = [
11430
+ ...cloneContextHops(frameElementPath.context),
11431
+ {
11432
+ kind: "iframe",
11433
+ host: cloneElementPath(frameElementPath).nodes
11434
+ }
11435
+ ];
11436
+ prefix = [...segment, ...prefix];
11437
+ }
11438
+ } finally {
11439
+ await frameElement.dispose().catch(() => void 0);
11440
+ }
11441
+ frame = parent;
11442
+ }
11443
+ if (!prefix.length) {
11444
+ return this.normalizePath(path5);
11445
+ }
11446
+ return this.normalizePath({
11447
+ context: [...prefix, ...cloneContextHops(path5.context)],
11448
+ nodes: cloneElementPath(path5).nodes
11449
+ });
11450
+ }
11261
11451
  async readPathFromCounterIndex(counter) {
11262
- const snapshot = await this.ensureSnapshotWithCounters();
11263
- const indexed = snapshot.counterIndex?.get(counter);
11452
+ if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
11453
+ return null;
11454
+ }
11455
+ const indexed = this.snapshotCache.counterIndex.get(counter);
11264
11456
  if (!indexed) return null;
11265
11457
  const normalized = this.normalizePath(indexed);
11266
11458
  if (!normalized.nodes.length) return null;
@@ -11271,15 +11463,6 @@ var Opensteer = class _Opensteer {
11271
11463
  if (!path5) return null;
11272
11464
  return this.normalizePath(path5);
11273
11465
  }
11274
- async ensureSnapshotWithCounters() {
11275
- if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
11276
- await this.snapshot({
11277
- mode: "full",
11278
- withCounters: true
11279
- });
11280
- }
11281
- return this.snapshotCache;
11282
- }
11283
11466
  persistPath(id, method, description, path5) {
11284
11467
  const now = Date.now();
11285
11468
  const safeFile = this.storage.getSelectorFileName(id);
@@ -11484,12 +11667,6 @@ var Opensteer = class _Opensteer {
11484
11667
  };
11485
11668
  }
11486
11669
  async buildFieldTargetsFromSchema(schema) {
11487
- if (!schema || typeof schema !== "object") {
11488
- return [];
11489
- }
11490
- if (Array.isArray(schema)) {
11491
- return [];
11492
- }
11493
11670
  const fields = [];
11494
11671
  await this.collectFieldTargetsFromSchemaObject(
11495
11672
  schema,
@@ -11535,17 +11712,6 @@ var Opensteer = class _Opensteer {
11535
11712
  return;
11536
11713
  }
11537
11714
  if (normalized.element != null) {
11538
- const path5 = await this.tryBuildPathFromCounter(
11539
- normalized.element
11540
- );
11541
- if (path5) {
11542
- fields.push({
11543
- key: fieldKey,
11544
- path: path5,
11545
- attribute: normalized.attribute
11546
- });
11547
- return;
11548
- }
11549
11715
  fields.push({
11550
11716
  key: fieldKey,
11551
11717
  counter: normalized.element,
@@ -11563,6 +11729,10 @@ var Opensteer = class _Opensteer {
11563
11729
  path: path5,
11564
11730
  attribute: normalized.attribute
11565
11731
  });
11732
+ } else {
11733
+ throw new Error(
11734
+ `Extraction schema field "${fieldKey}" uses selector "${normalized.selector}", but no matching element path could be built from the current page snapshot.`
11735
+ );
11566
11736
  }
11567
11737
  return;
11568
11738
  }
@@ -11586,15 +11756,6 @@ var Opensteer = class _Opensteer {
11586
11756
  continue;
11587
11757
  }
11588
11758
  if (fieldPlan.element != null) {
11589
- const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
11590
- if (path6) {
11591
- fields.push({
11592
- key,
11593
- path: path6,
11594
- attribute: fieldPlan.attribute
11595
- });
11596
- continue;
11597
- }
11598
11759
  fields.push({
11599
11760
  key,
11600
11761
  counter: fieldPlan.element,
@@ -11649,12 +11810,7 @@ var Opensteer = class _Opensteer {
11649
11810
  }
11650
11811
  }
11651
11812
  if (counterRequests.length) {
11652
- const snapshot = await this.ensureSnapshotWithCounters();
11653
- const counterValues = await resolveCountersBatch(
11654
- this.page,
11655
- snapshot,
11656
- counterRequests
11657
- );
11813
+ const counterValues = await resolveCountersBatch(this.page, counterRequests);
11658
11814
  Object.assign(result, counterValues);
11659
11815
  }
11660
11816
  if (pathFields.length) {
@@ -11684,7 +11840,7 @@ var Opensteer = class _Opensteer {
11684
11840
  const path5 = await this.buildPathFromElement(field.counter);
11685
11841
  if (!path5) {
11686
11842
  throw new Error(
11687
- `Unable to build element path from counter ${field.counter} for extraction field "${field.key}".`
11843
+ `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
11688
11844
  );
11689
11845
  }
11690
11846
  resolved.push({
@@ -11730,6 +11886,33 @@ function collectIframeContextPrefix(path5) {
11730
11886
  if (lastIframeIndex < 0) return [];
11731
11887
  return cloneContextHops(context.slice(0, lastIframeIndex + 1));
11732
11888
  }
11889
+ function measureContextOverlap(indexedPrefix, builtContext) {
11890
+ const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
11891
+ for (let size = maxOverlap; size > 0; size -= 1) {
11892
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
11893
+ return size;
11894
+ }
11895
+ }
11896
+ for (let size = maxOverlap; size > 0; size -= 1) {
11897
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
11898
+ return size;
11899
+ }
11900
+ }
11901
+ return 0;
11902
+ }
11903
+ function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
11904
+ for (let idx = 0; idx < size; idx += 1) {
11905
+ const left = indexedPrefix[indexedPrefix.length - size + idx];
11906
+ const right = builtContext[idx];
11907
+ if (left.kind !== right.kind) {
11908
+ return false;
11909
+ }
11910
+ if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
11911
+ return false;
11912
+ }
11913
+ }
11914
+ return true;
11915
+ }
11733
11916
  function normalizeSchemaValue(value) {
11734
11917
  if (!value) return null;
11735
11918
  if (typeof value !== "object" || Array.isArray(value)) {
@@ -11913,13 +12096,28 @@ function countNonNullLeaves(value) {
11913
12096
  function isPrimitiveLike(value) {
11914
12097
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
11915
12098
  }
12099
+ function assertValidExtractSchemaRoot(schema) {
12100
+ if (!schema || typeof schema !== "object") {
12101
+ throw new Error(
12102
+ "Invalid extraction schema: expected a JSON object at the top level."
12103
+ );
12104
+ }
12105
+ if (Array.isArray(schema)) {
12106
+ throw new Error(
12107
+ 'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
12108
+ );
12109
+ }
12110
+ }
11916
12111
  function parseAiExtractResponse(response) {
11917
12112
  if (typeof response === "string") {
11918
12113
  const trimmed = stripCodeFence(response);
11919
12114
  try {
11920
12115
  return JSON.parse(trimmed);
11921
12116
  } catch {
11922
- throw new Error("LLM extraction returned a non-JSON string.");
12117
+ const preview = summarizeForError(trimmed);
12118
+ throw new Error(
12119
+ `LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
12120
+ );
11923
12121
  }
11924
12122
  }
11925
12123
  if (response && typeof response === "object") {
@@ -11944,6 +12142,12 @@ function stripCodeFence(input) {
11944
12142
  if (lastFence === -1) return withoutHeader.trim();
11945
12143
  return withoutHeader.slice(0, lastFence).trim();
11946
12144
  }
12145
+ function summarizeForError(input, maxLength = 180) {
12146
+ const compact = input.replace(/\s+/g, " ").trim();
12147
+ if (!compact) return "";
12148
+ if (compact.length <= maxLength) return compact;
12149
+ return `${compact.slice(0, maxLength)}...`;
12150
+ }
11947
12151
  function getScrollDelta2(options) {
11948
12152
  const amount = typeof options.amount === "number" ? options.amount : 600;
11949
12153
  const absoluteAmount = Math.abs(amount);
@@ -11966,10 +12170,11 @@ function isInternalOrBlankPageUrl(url) {
11966
12170
  }
11967
12171
  function buildLocalRunId(namespace) {
11968
12172
  const normalized = namespace.trim() || "default";
11969
- return `${normalized}-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
12173
+ return `${normalized}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
11970
12174
  }
11971
12175
 
11972
12176
  export {
12177
+ normalizeError,
11973
12178
  normalizeNamespace,
11974
12179
  resolveNamespaceDir,
11975
12180
  waitForVisualStability,
@@ -11995,13 +12200,12 @@ export {
11995
12200
  cleanForClickable,
11996
12201
  cleanForScrollable,
11997
12202
  cleanForAction,
11998
- CounterResolutionError,
11999
- ensureLiveCounters,
12000
- resolveCounterElement,
12001
- resolveCountersBatch,
12002
12203
  prepareSnapshot,
12003
12204
  ElementPathError,
12004
12205
  resolveElementPath,
12206
+ CounterResolutionError,
12207
+ resolveCounterElement,
12208
+ resolveCountersBatch,
12005
12209
  performClick,
12006
12210
  performHover,
12007
12211
  performInput,