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.
package/dist/index.cjs CHANGED
@@ -109,6 +109,15 @@ function resolveProviderInfo(modelStr) {
109
109
  return info;
110
110
  }
111
111
  }
112
+ const slash = modelStr.indexOf("/");
113
+ if (slash > 0) {
114
+ const provider = modelStr.slice(0, slash).trim().toLowerCase();
115
+ if (provider) {
116
+ throw new Error(
117
+ `Unsupported model provider prefix "${provider}" in model "${modelStr}". Use one of: openai, anthropic, google, xai, groq.`
118
+ );
119
+ }
120
+ }
112
121
  return { pkg: "@ai-sdk/openai", providerFn: "openai" };
113
122
  }
114
123
  function stripProviderPrefix(modelStr) {
@@ -396,7 +405,6 @@ __export(index_exports, {
396
405
  createExtractCallback: () => createExtractCallback,
397
406
  createResolveCallback: () => createResolveCallback,
398
407
  createTab: () => createTab,
399
- ensureLiveCounters: () => ensureLiveCounters,
400
408
  exportCookies: () => exportCookies,
401
409
  extractArrayRowsWithPaths: () => extractArrayRowsWithPaths,
402
410
  extractArrayWithPaths: () => extractArrayWithPaths,
@@ -436,7 +444,7 @@ __export(index_exports, {
436
444
  module.exports = __toCommonJS(index_exports);
437
445
 
438
446
  // src/opensteer.ts
439
- var import_crypto2 = require("crypto");
447
+ var import_crypto = require("crypto");
440
448
 
441
449
  // src/browser/pool.ts
442
450
  var import_playwright = require("playwright");
@@ -906,6 +914,232 @@ var import_path3 = __toESM(require("path"), 1);
906
914
  var import_url = require("url");
907
915
  var import_dotenv = require("dotenv");
908
916
 
917
+ // src/error-normalization.ts
918
+ function extractErrorMessage(error, fallback = "Unknown error.") {
919
+ if (error instanceof Error) {
920
+ const message = error.message.trim();
921
+ if (message) return message;
922
+ const name = error.name.trim();
923
+ if (name) return name;
924
+ }
925
+ if (typeof error === "string" && error.trim()) {
926
+ return error.trim();
927
+ }
928
+ const record = asRecord(error);
929
+ const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
930
+ if (recordMessage) {
931
+ return recordMessage;
932
+ }
933
+ return fallback;
934
+ }
935
+ function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
936
+ const seen = /* @__PURE__ */ new WeakSet();
937
+ return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
938
+ }
939
+ function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
940
+ const record = asRecord(error);
941
+ if (record) {
942
+ if (seen.has(record)) {
943
+ return {
944
+ message: extractErrorMessage(error, fallback)
945
+ };
946
+ }
947
+ seen.add(record);
948
+ }
949
+ const message = extractErrorMessage(error, fallback);
950
+ const code = extractCode(error);
951
+ const name = extractName(error);
952
+ const details = extractDetails(error);
953
+ if (depthRemaining <= 0) {
954
+ return compactErrorInfo({
955
+ message,
956
+ ...code ? { code } : {},
957
+ ...name ? { name } : {},
958
+ ...details ? { details } : {}
959
+ });
960
+ }
961
+ const cause = extractCause(error);
962
+ if (!cause) {
963
+ return compactErrorInfo({
964
+ message,
965
+ ...code ? { code } : {},
966
+ ...name ? { name } : {},
967
+ ...details ? { details } : {}
968
+ });
969
+ }
970
+ const normalizedCause = normalizeErrorInternal(
971
+ cause,
972
+ "Caused by an unknown error.",
973
+ depthRemaining - 1,
974
+ seen
975
+ );
976
+ return compactErrorInfo({
977
+ message,
978
+ ...code ? { code } : {},
979
+ ...name ? { name } : {},
980
+ ...details ? { details } : {},
981
+ cause: normalizedCause
982
+ });
983
+ }
984
+ function compactErrorInfo(info) {
985
+ const safeDetails = toJsonSafeRecord(info.details);
986
+ return {
987
+ message: info.message,
988
+ ...info.code ? { code: info.code } : {},
989
+ ...info.name ? { name: info.name } : {},
990
+ ...safeDetails ? { details: safeDetails } : {},
991
+ ...info.cause ? { cause: info.cause } : {}
992
+ };
993
+ }
994
+ function extractCode(error) {
995
+ const record = asRecord(error);
996
+ const raw = record?.code;
997
+ if (typeof raw === "string" && raw.trim()) {
998
+ return raw.trim();
999
+ }
1000
+ if (typeof raw === "number" && Number.isFinite(raw)) {
1001
+ return String(raw);
1002
+ }
1003
+ return void 0;
1004
+ }
1005
+ function extractName(error) {
1006
+ if (error instanceof Error && error.name.trim()) {
1007
+ return error.name.trim();
1008
+ }
1009
+ const record = asRecord(error);
1010
+ return toNonEmptyString(record?.name);
1011
+ }
1012
+ function extractDetails(error) {
1013
+ const record = asRecord(error);
1014
+ if (!record) return void 0;
1015
+ const details = {};
1016
+ const rawDetails = asRecord(record.details);
1017
+ if (rawDetails) {
1018
+ Object.assign(details, rawDetails);
1019
+ }
1020
+ const action = toNonEmptyString(record.action);
1021
+ if (action) {
1022
+ details.action = action;
1023
+ }
1024
+ const selectorUsed = toNonEmptyString(record.selectorUsed);
1025
+ if (selectorUsed) {
1026
+ details.selectorUsed = selectorUsed;
1027
+ }
1028
+ if (typeof record.status === "number" && Number.isFinite(record.status)) {
1029
+ details.status = record.status;
1030
+ }
1031
+ const failure = asRecord(record.failure);
1032
+ if (failure) {
1033
+ const failureCode = toNonEmptyString(failure.code);
1034
+ const classificationSource = toNonEmptyString(
1035
+ failure.classificationSource
1036
+ );
1037
+ const failureDetails = asRecord(failure.details);
1038
+ if (failureCode || classificationSource || failureDetails) {
1039
+ details.actionFailure = {
1040
+ ...failureCode ? { code: failureCode } : {},
1041
+ ...classificationSource ? { classificationSource } : {},
1042
+ ...failureDetails ? { details: failureDetails } : {}
1043
+ };
1044
+ }
1045
+ }
1046
+ return Object.keys(details).length ? details : void 0;
1047
+ }
1048
+ function extractCause(error) {
1049
+ if (error instanceof Error) {
1050
+ return error.cause;
1051
+ }
1052
+ const record = asRecord(error);
1053
+ return record?.cause;
1054
+ }
1055
+ function asRecord(value) {
1056
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1057
+ return null;
1058
+ }
1059
+ return value;
1060
+ }
1061
+ function toNonEmptyString(value) {
1062
+ if (typeof value !== "string") return void 0;
1063
+ const normalized = value.trim();
1064
+ return normalized.length ? normalized : void 0;
1065
+ }
1066
+ function toJsonSafeRecord(value) {
1067
+ if (!value) return void 0;
1068
+ const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
1069
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
1070
+ return void 0;
1071
+ }
1072
+ const record = sanitized;
1073
+ return Object.keys(record).length > 0 ? record : void 0;
1074
+ }
1075
+ function toJsonSafeValue(value, seen) {
1076
+ if (value === null) return null;
1077
+ if (typeof value === "string" || typeof value === "boolean") {
1078
+ return value;
1079
+ }
1080
+ if (typeof value === "number") {
1081
+ return Number.isFinite(value) ? value : null;
1082
+ }
1083
+ if (typeof value === "bigint") {
1084
+ return value.toString();
1085
+ }
1086
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
1087
+ return void 0;
1088
+ }
1089
+ if (value instanceof Date) {
1090
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
1091
+ }
1092
+ if (Array.isArray(value)) {
1093
+ if (seen.has(value)) return "[Circular]";
1094
+ seen.add(value);
1095
+ const output = value.map((item) => {
1096
+ const next = toJsonSafeValue(item, seen);
1097
+ return next === void 0 ? null : next;
1098
+ });
1099
+ seen.delete(value);
1100
+ return output;
1101
+ }
1102
+ if (value instanceof Set) {
1103
+ if (seen.has(value)) return "[Circular]";
1104
+ seen.add(value);
1105
+ const output = Array.from(value, (item) => {
1106
+ const next = toJsonSafeValue(item, seen);
1107
+ return next === void 0 ? null : next;
1108
+ });
1109
+ seen.delete(value);
1110
+ return output;
1111
+ }
1112
+ if (value instanceof Map) {
1113
+ if (seen.has(value)) return "[Circular]";
1114
+ seen.add(value);
1115
+ const output = {};
1116
+ for (const [key, item] of value.entries()) {
1117
+ const normalizedKey = String(key);
1118
+ const next = toJsonSafeValue(item, seen);
1119
+ if (next !== void 0) {
1120
+ output[normalizedKey] = next;
1121
+ }
1122
+ }
1123
+ seen.delete(value);
1124
+ return output;
1125
+ }
1126
+ if (typeof value === "object") {
1127
+ const objectValue = value;
1128
+ if (seen.has(objectValue)) return "[Circular]";
1129
+ seen.add(objectValue);
1130
+ const output = {};
1131
+ for (const [key, item] of Object.entries(objectValue)) {
1132
+ const next = toJsonSafeValue(item, seen);
1133
+ if (next !== void 0) {
1134
+ output[key] = next;
1135
+ }
1136
+ }
1137
+ seen.delete(objectValue);
1138
+ return output;
1139
+ }
1140
+ return void 0;
1141
+ }
1142
+
909
1143
  // src/storage/namespace.ts
910
1144
  var import_path2 = __toESM(require("path"), 1);
911
1145
  var DEFAULT_NAMESPACE = "default";
@@ -969,11 +1203,12 @@ function dotenvFileOrder(nodeEnv) {
969
1203
  files.push(".env");
970
1204
  return files;
971
1205
  }
972
- function loadDotenvValues(rootDir, baseEnv) {
1206
+ function loadDotenvValues(rootDir, baseEnv, options = {}) {
973
1207
  const values = {};
974
1208
  if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
975
1209
  return values;
976
1210
  }
1211
+ const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
977
1212
  const baseDir = import_path3.default.resolve(rootDir);
978
1213
  const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
979
1214
  for (const filename of dotenvFileOrder(nodeEnv)) {
@@ -987,15 +1222,24 @@ function loadDotenvValues(rootDir, baseEnv) {
987
1222
  values[key] = value;
988
1223
  }
989
1224
  }
990
- } catch {
1225
+ } catch (error) {
1226
+ const message = extractErrorMessage(
1227
+ error,
1228
+ "Unable to read or parse dotenv file."
1229
+ );
1230
+ if (debug) {
1231
+ console.warn(
1232
+ `[opensteer] failed to load dotenv file "${filePath}": ${message}`
1233
+ );
1234
+ }
991
1235
  continue;
992
1236
  }
993
1237
  }
994
1238
  return values;
995
1239
  }
996
- function resolveEnv(rootDir) {
1240
+ function resolveEnv(rootDir, options = {}) {
997
1241
  const baseEnv = process.env;
998
- const dotenvValues = loadDotenvValues(rootDir, baseEnv);
1242
+ const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
999
1243
  return {
1000
1244
  ...dotenvValues,
1001
1245
  ...baseEnv
@@ -1039,13 +1283,22 @@ function assertNoLegacyRuntimeConfig(source, config) {
1039
1283
  );
1040
1284
  }
1041
1285
  }
1042
- function loadConfigFile(rootDir) {
1286
+ function loadConfigFile(rootDir, options = {}) {
1043
1287
  const configPath = import_path3.default.join(rootDir, ".opensteer", "config.json");
1044
1288
  if (!import_fs.default.existsSync(configPath)) return {};
1045
1289
  try {
1046
1290
  const raw = import_fs.default.readFileSync(configPath, "utf8");
1047
1291
  return JSON.parse(raw);
1048
- } catch {
1292
+ } catch (error) {
1293
+ const message = extractErrorMessage(
1294
+ error,
1295
+ "Unable to read or parse config file."
1296
+ );
1297
+ if (options.debug) {
1298
+ console.warn(
1299
+ `[opensteer] failed to load config file "${configPath}": ${message}`
1300
+ );
1301
+ }
1049
1302
  return {};
1050
1303
  }
1051
1304
  }
@@ -1177,6 +1430,8 @@ function resolveCloudSelection(config, env = process.env) {
1177
1430
  };
1178
1431
  }
1179
1432
  function resolveConfig(input = {}) {
1433
+ const processEnv = process.env;
1434
+ const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
1180
1435
  const initialRootDir = input.storage?.rootDir ?? process.cwd();
1181
1436
  const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
1182
1437
  storage: {
@@ -1185,12 +1440,16 @@ function resolveConfig(input = {}) {
1185
1440
  });
1186
1441
  assertNoLegacyAiConfig("Opensteer constructor config", input);
1187
1442
  assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
1188
- const fileConfig = loadConfigFile(initialRootDir);
1443
+ const fileConfig = loadConfigFile(initialRootDir, {
1444
+ debug: debugHint
1445
+ });
1189
1446
  assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
1190
1447
  assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
1191
1448
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1192
1449
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1193
- const env = resolveEnv(envRootDir);
1450
+ const env = resolveEnv(envRootDir, {
1451
+ debug: debugHint
1452
+ });
1194
1453
  if (env.OPENSTEER_AI_MODEL) {
1195
1454
  throw new Error(
1196
1455
  "OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
@@ -1874,9 +2133,11 @@ function createEmptyRegistry(name) {
1874
2133
  var LocalSelectorStorage = class {
1875
2134
  rootDir;
1876
2135
  namespace;
1877
- constructor(rootDir, namespace) {
2136
+ debug;
2137
+ constructor(rootDir, namespace, options = {}) {
1878
2138
  this.rootDir = rootDir;
1879
2139
  this.namespace = normalizeNamespace(namespace);
2140
+ this.debug = options.debug === true;
1880
2141
  }
1881
2142
  getRootDir() {
1882
2143
  return this.rootDir;
@@ -1910,7 +2171,16 @@ var LocalSelectorStorage = class {
1910
2171
  try {
1911
2172
  const raw = import_fs2.default.readFileSync(file, "utf8");
1912
2173
  return JSON.parse(raw);
1913
- } catch {
2174
+ } catch (error) {
2175
+ const message = extractErrorMessage(
2176
+ error,
2177
+ "Unable to parse selector registry JSON."
2178
+ );
2179
+ if (this.debug) {
2180
+ console.warn(
2181
+ `[opensteer] failed to read selector registry "${file}": ${message}`
2182
+ );
2183
+ }
1914
2184
  return createEmptyRegistry(this.namespace);
1915
2185
  }
1916
2186
  }
@@ -1927,7 +2197,16 @@ var LocalSelectorStorage = class {
1927
2197
  try {
1928
2198
  const raw = import_fs2.default.readFileSync(file, "utf8");
1929
2199
  return JSON.parse(raw);
1930
- } catch {
2200
+ } catch (error) {
2201
+ const message = extractErrorMessage(
2202
+ error,
2203
+ "Unable to parse selector file JSON."
2204
+ );
2205
+ if (this.debug) {
2206
+ console.warn(
2207
+ `[opensteer] failed to read selector file "${file}": ${message}`
2208
+ );
2209
+ }
1931
2210
  return null;
1932
2211
  }
1933
2212
  }
@@ -1947,7 +2226,6 @@ var LocalSelectorStorage = class {
1947
2226
 
1948
2227
  // src/html/pipeline.ts
1949
2228
  var cheerio3 = __toESM(require("cheerio"), 1);
1950
- var import_crypto = require("crypto");
1951
2229
 
1952
2230
  // src/html/serializer.ts
1953
2231
  var cheerio = __toESM(require("cheerio"), 1);
@@ -2220,9 +2498,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
2220
2498
  `;
2221
2499
  var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
2222
2500
  var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
2223
- var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
2224
- var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
2225
- var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
2226
2501
 
2227
2502
  // src/element-path/build.ts
2228
2503
  var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
@@ -4012,567 +4287,178 @@ function cleanForAction(html) {
4012
4287
  return compactHtml(htmlOut);
4013
4288
  }
4014
4289
 
4015
- // src/extract-value-normalization.ts
4016
- var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
4017
- function normalizeExtractedValue(raw, attribute) {
4018
- if (raw == null) return null;
4019
- const rawText = String(raw);
4020
- if (!rawText.trim()) return null;
4021
- const normalizedAttribute = String(attribute || "").trim().toLowerCase();
4022
- if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
4023
- const singleValue = pickSingleListAttributeValue(
4024
- normalizedAttribute,
4025
- rawText
4026
- ).trim();
4027
- return singleValue || null;
4290
+ // src/html/pipeline.ts
4291
+ function applyCleaner(mode, html) {
4292
+ switch (mode) {
4293
+ case "clickable":
4294
+ return cleanForClickable(html);
4295
+ case "scrollable":
4296
+ return cleanForScrollable(html);
4297
+ case "extraction":
4298
+ return cleanForExtraction(html);
4299
+ case "full":
4300
+ return cleanForFull(html);
4301
+ case "action":
4302
+ default:
4303
+ return cleanForAction(html);
4028
4304
  }
4029
- const text = rawText.replace(/\s+/g, " ").trim();
4030
- return text || null;
4031
4305
  }
4032
- function pickSingleListAttributeValue(attribute, raw) {
4033
- if (attribute === "ping") {
4034
- const firstUrl = raw.trim().split(/\s+/)[0] || "";
4035
- return firstUrl.trim();
4036
- }
4037
- if (attribute === "srcset" || attribute === "imagesrcset") {
4038
- const picked = pickBestSrcsetCandidate(raw);
4039
- if (picked) return picked;
4040
- return pickFirstSrcsetToken(raw) || "";
4306
+ async function assignCounters(page, html, nodePaths, nodeMeta) {
4307
+ const $ = cheerio3.load(html, { xmlMode: false });
4308
+ const counterIndex = /* @__PURE__ */ new Map();
4309
+ let nextCounter = 1;
4310
+ const assignedByNodeId = /* @__PURE__ */ new Map();
4311
+ $("*").each(function() {
4312
+ const el = $(this);
4313
+ const nodeId = el.attr(OS_NODE_ID_ATTR);
4314
+ if (!nodeId) return;
4315
+ const counter = nextCounter++;
4316
+ assignedByNodeId.set(nodeId, counter);
4317
+ const path5 = nodePaths.get(nodeId);
4318
+ el.attr("c", String(counter));
4319
+ el.removeAttr(OS_NODE_ID_ATTR);
4320
+ if (path5) {
4321
+ counterIndex.set(counter, cloneElementPath(path5));
4322
+ }
4323
+ });
4324
+ try {
4325
+ await syncLiveCounters(page, nodeMeta, assignedByNodeId);
4326
+ } catch (error) {
4327
+ await clearLiveCounters(page);
4328
+ throw error;
4041
4329
  }
4042
- return raw.trim();
4330
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4331
+ return {
4332
+ html: $.html(),
4333
+ counterIndex
4334
+ };
4043
4335
  }
4044
- function pickBestSrcsetCandidate(raw) {
4045
- const candidates = parseSrcsetCandidates(raw);
4046
- if (!candidates.length) return null;
4047
- const widthCandidates = candidates.filter(
4048
- (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
4049
- );
4050
- if (widthCandidates.length) {
4051
- return widthCandidates.reduce(
4052
- (best, candidate) => candidate.width > best.width ? candidate : best
4053
- ).url;
4054
- }
4055
- const densityCandidates = candidates.filter(
4056
- (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
4057
- );
4058
- if (densityCandidates.length) {
4059
- return densityCandidates.reduce(
4060
- (best, candidate) => candidate.density > best.density ? candidate : best
4061
- ).url;
4336
+ async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
4337
+ await clearLiveCounters(page);
4338
+ if (!assignedByNodeId.size) return;
4339
+ const groupedByFrame = /* @__PURE__ */ new Map();
4340
+ for (const [nodeId, counter] of assignedByNodeId.entries()) {
4341
+ const meta = nodeMeta.get(nodeId);
4342
+ if (!meta?.frameToken) continue;
4343
+ const list = groupedByFrame.get(meta.frameToken) || [];
4344
+ list.push({
4345
+ nodeId,
4346
+ counter
4347
+ });
4348
+ groupedByFrame.set(meta.frameToken, list);
4062
4349
  }
4063
- return candidates[0]?.url || null;
4064
- }
4065
- function parseSrcsetCandidates(raw) {
4066
- const text = String(raw || "").trim();
4067
- if (!text) return [];
4068
- const out = [];
4069
- let index = 0;
4070
- while (index < text.length) {
4071
- index = skipSeparators(text, index);
4072
- if (index >= text.length) break;
4073
- const urlToken = readUrlToken(text, index);
4074
- index = urlToken.nextIndex;
4075
- const url = urlToken.value.trim();
4076
- if (!url) continue;
4077
- index = skipWhitespace(text, index);
4078
- const descriptors = [];
4079
- while (index < text.length && text[index] !== ",") {
4080
- const descriptorToken = readDescriptorToken(text, index);
4081
- if (!descriptorToken.value) {
4082
- index = descriptorToken.nextIndex;
4083
- continue;
4350
+ if (!groupedByFrame.size) return;
4351
+ const failures = [];
4352
+ const framesByToken = await mapFramesByToken(page);
4353
+ for (const [frameToken, entries] of groupedByFrame.entries()) {
4354
+ const frame = framesByToken.get(frameToken);
4355
+ if (!frame) {
4356
+ for (const entry of entries) {
4357
+ failures.push({
4358
+ nodeId: entry.nodeId,
4359
+ counter: entry.counter,
4360
+ frameToken,
4361
+ reason: "frame_missing"
4362
+ });
4084
4363
  }
4085
- descriptors.push(descriptorToken.value);
4086
- index = descriptorToken.nextIndex;
4087
- index = skipWhitespace(text, index);
4088
- }
4089
- if (index < text.length && text[index] === ",") {
4090
- index += 1;
4364
+ continue;
4091
4365
  }
4092
- let width = null;
4093
- let density = null;
4094
- for (const descriptor of descriptors) {
4095
- const token = descriptor.trim().toLowerCase();
4096
- if (!token) continue;
4097
- const widthMatch = token.match(/^(\d+)w$/);
4098
- if (widthMatch) {
4099
- const parsed = Number.parseInt(widthMatch[1], 10);
4100
- if (Number.isFinite(parsed)) {
4101
- width = parsed;
4366
+ try {
4367
+ const unresolved = await frame.evaluate(
4368
+ ({ entries: entries2, nodeAttr }) => {
4369
+ const index = /* @__PURE__ */ new Map();
4370
+ const unresolved2 = [];
4371
+ const walk = (root) => {
4372
+ const children = Array.from(root.children);
4373
+ for (const child of children) {
4374
+ const nodeId = child.getAttribute(nodeAttr);
4375
+ if (nodeId) {
4376
+ const list = index.get(nodeId) || [];
4377
+ list.push(child);
4378
+ index.set(nodeId, list);
4379
+ }
4380
+ walk(child);
4381
+ if (child.shadowRoot) {
4382
+ walk(child.shadowRoot);
4383
+ }
4384
+ }
4385
+ };
4386
+ walk(document);
4387
+ for (const entry of entries2) {
4388
+ const matches = index.get(entry.nodeId) || [];
4389
+ if (matches.length !== 1) {
4390
+ unresolved2.push({
4391
+ nodeId: entry.nodeId,
4392
+ counter: entry.counter,
4393
+ matches: matches.length
4394
+ });
4395
+ continue;
4396
+ }
4397
+ matches[0].setAttribute("c", String(entry.counter));
4398
+ }
4399
+ return unresolved2;
4400
+ },
4401
+ {
4402
+ entries,
4403
+ nodeAttr: OS_NODE_ID_ATTR
4102
4404
  }
4103
- continue;
4405
+ );
4406
+ for (const entry of unresolved) {
4407
+ failures.push({
4408
+ nodeId: entry.nodeId,
4409
+ counter: entry.counter,
4410
+ frameToken,
4411
+ reason: "match_count",
4412
+ matches: entry.matches
4413
+ });
4104
4414
  }
4105
- const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4106
- if (densityMatch) {
4107
- const parsed = Number.parseFloat(densityMatch[1]);
4108
- if (Number.isFinite(parsed)) {
4109
- density = parsed;
4110
- }
4415
+ } catch {
4416
+ for (const entry of entries) {
4417
+ failures.push({
4418
+ nodeId: entry.nodeId,
4419
+ counter: entry.counter,
4420
+ frameToken,
4421
+ reason: "frame_unavailable"
4422
+ });
4111
4423
  }
4112
4424
  }
4113
- out.push({
4114
- url,
4115
- width,
4116
- density
4425
+ }
4426
+ if (failures.length) {
4427
+ const preview = failures.slice(0, 3).map((failure) => {
4428
+ const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
4429
+ if (failure.reason === "frame_missing") {
4430
+ return `${base} could not be synchronized because the frame is missing.`;
4431
+ }
4432
+ if (failure.reason === "frame_unavailable") {
4433
+ return `${base} could not be synchronized because frame evaluation failed.`;
4434
+ }
4435
+ return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
4117
4436
  });
4437
+ const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
4438
+ throw new Error(
4439
+ `Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
4440
+ );
4118
4441
  }
4119
- return out;
4120
4442
  }
4121
- function pickFirstSrcsetToken(raw) {
4122
- const candidate = parseSrcsetCandidates(raw)[0];
4123
- if (candidate?.url) {
4124
- return candidate.url;
4125
- }
4126
- const text = String(raw || "");
4127
- const start = skipSeparators(text, 0);
4128
- if (start >= text.length) return null;
4129
- const firstToken = readUrlToken(text, start).value.trim();
4130
- return firstToken || null;
4131
- }
4132
- function skipWhitespace(value, index) {
4133
- let cursor = index;
4134
- while (cursor < value.length && /\s/.test(value[cursor])) {
4135
- cursor += 1;
4136
- }
4137
- return cursor;
4138
- }
4139
- function skipSeparators(value, index) {
4140
- let cursor = skipWhitespace(value, index);
4141
- while (cursor < value.length && value[cursor] === ",") {
4142
- cursor += 1;
4143
- cursor = skipWhitespace(value, cursor);
4144
- }
4145
- return cursor;
4146
- }
4147
- function readUrlToken(value, index) {
4148
- let cursor = index;
4149
- let out = "";
4150
- const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4151
- while (cursor < value.length) {
4152
- const char = value[cursor];
4153
- if (/\s/.test(char)) {
4154
- break;
4155
- }
4156
- if (char === "," && !isDataUrl) {
4157
- break;
4158
- }
4159
- out += char;
4160
- cursor += 1;
4161
- }
4162
- if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4163
- out = out.slice(0, -1);
4164
- }
4165
- return {
4166
- value: out,
4167
- nextIndex: cursor
4168
- };
4169
- }
4170
- function readDescriptorToken(value, index) {
4171
- let cursor = skipWhitespace(value, index);
4172
- let out = "";
4173
- while (cursor < value.length) {
4174
- const char = value[cursor];
4175
- if (char === "," || /\s/.test(char)) {
4176
- break;
4177
- }
4178
- out += char;
4179
- cursor += 1;
4180
- }
4181
- return {
4182
- value: out.trim(),
4183
- nextIndex: cursor
4184
- };
4185
- }
4186
-
4187
- // src/html/counter-runtime.ts
4188
- var CounterResolutionError = class extends Error {
4189
- code;
4190
- constructor(code, message) {
4191
- super(message);
4192
- this.name = "CounterResolutionError";
4193
- this.code = code;
4194
- }
4195
- };
4196
- async function ensureLiveCounters(page, nodeMeta, nodeIds) {
4197
- const out = /* @__PURE__ */ new Map();
4198
- if (!nodeIds.length) return out;
4199
- const grouped = /* @__PURE__ */ new Map();
4200
- for (const nodeId of nodeIds) {
4201
- const meta = nodeMeta.get(nodeId);
4202
- if (!meta) {
4203
- throw new CounterResolutionError(
4204
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4205
- `Missing metadata for node ${nodeId}. Run snapshot() again.`
4206
- );
4207
- }
4208
- const list = grouped.get(meta.frameToken) || [];
4209
- list.push({
4210
- nodeId,
4211
- instanceToken: meta.instanceToken
4212
- });
4213
- grouped.set(meta.frameToken, list);
4214
- }
4215
- const framesByToken = await mapFramesByToken(page);
4216
- let nextCounter = await readGlobalNextCounter(page);
4217
- const usedCounters = /* @__PURE__ */ new Map();
4218
- for (const [frameToken, entries] of grouped.entries()) {
4219
- const frame = framesByToken.get(frameToken);
4220
- if (!frame) {
4221
- throw new CounterResolutionError(
4222
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4223
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4224
- );
4225
- }
4226
- const result = await frame.evaluate(
4227
- ({
4228
- entries: entries2,
4229
- nodeAttr,
4230
- instanceTokenKey,
4231
- counterOwnerKey,
4232
- counterValueKey,
4233
- startCounter
4234
- }) => {
4235
- const helpers = {
4236
- pushNode(map, node) {
4237
- const nodeId = node.getAttribute(nodeAttr);
4238
- if (!nodeId) return;
4239
- const list = map.get(nodeId) || [];
4240
- list.push(node);
4241
- map.set(nodeId, list);
4242
- },
4243
- walk(map, root) {
4244
- const children = Array.from(root.children);
4245
- for (const child of children) {
4246
- helpers.pushNode(map, child);
4247
- helpers.walk(map, child);
4248
- if (child.shadowRoot) {
4249
- helpers.walk(map, child.shadowRoot);
4250
- }
4251
- }
4252
- },
4253
- buildNodeIndex() {
4254
- const map = /* @__PURE__ */ new Map();
4255
- helpers.walk(map, document);
4256
- return map;
4257
- }
4258
- };
4259
- const index = helpers.buildNodeIndex();
4260
- const assigned = [];
4261
- const failures = [];
4262
- let next = Math.max(1, Number(startCounter || 1));
4263
- for (const entry of entries2) {
4264
- const matches = index.get(entry.nodeId) || [];
4265
- if (!matches.length) {
4266
- failures.push({
4267
- nodeId: entry.nodeId,
4268
- reason: "missing"
4269
- });
4270
- continue;
4271
- }
4272
- if (matches.length !== 1) {
4273
- failures.push({
4274
- nodeId: entry.nodeId,
4275
- reason: "ambiguous"
4276
- });
4277
- continue;
4278
- }
4279
- const target = matches[0];
4280
- if (target[instanceTokenKey] !== entry.instanceToken) {
4281
- failures.push({
4282
- nodeId: entry.nodeId,
4283
- reason: "instance_mismatch"
4284
- });
4285
- continue;
4286
- }
4287
- const owned = target[counterOwnerKey] === true;
4288
- const runtimeCounter = Number(target[counterValueKey] || 0);
4289
- if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
4290
- target.setAttribute("c", String(runtimeCounter));
4291
- assigned.push({
4292
- nodeId: entry.nodeId,
4293
- counter: runtimeCounter
4294
- });
4295
- continue;
4296
- }
4297
- const counter = next++;
4298
- target.setAttribute("c", String(counter));
4299
- Object.defineProperty(target, counterOwnerKey, {
4300
- value: true,
4301
- writable: true,
4302
- configurable: true
4303
- });
4304
- Object.defineProperty(target, counterValueKey, {
4305
- value: counter,
4306
- writable: true,
4307
- configurable: true
4308
- });
4309
- assigned.push({ nodeId: entry.nodeId, counter });
4310
- }
4311
- return {
4312
- assigned,
4313
- failures,
4314
- nextCounter: next
4315
- };
4316
- },
4317
- {
4318
- entries,
4319
- nodeAttr: OS_NODE_ID_ATTR,
4320
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4321
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4322
- counterValueKey: OS_COUNTER_VALUE_KEY,
4323
- startCounter: nextCounter
4324
- }
4325
- );
4326
- if (result.failures.length) {
4327
- const first = result.failures[0];
4328
- throw buildCounterFailureError(first.nodeId, first.reason);
4329
- }
4330
- nextCounter = result.nextCounter;
4331
- for (const item of result.assigned) {
4332
- const existingNode = usedCounters.get(item.counter);
4333
- if (existingNode && existingNode !== item.nodeId) {
4334
- throw new CounterResolutionError(
4335
- "ERR_COUNTER_AMBIGUOUS",
4336
- `Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
4337
- );
4338
- }
4339
- usedCounters.set(item.counter, item.nodeId);
4340
- out.set(item.nodeId, item.counter);
4341
- }
4342
- }
4343
- await writeGlobalNextCounter(page, nextCounter);
4344
- return out;
4345
- }
4346
- async function resolveCounterElement(page, snapshot, counter) {
4347
- const binding = readBinding(snapshot, counter);
4348
- const framesByToken = await mapFramesByToken(page);
4349
- const frame = framesByToken.get(binding.frameToken);
4350
- if (!frame) {
4351
- throw new CounterResolutionError(
4352
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4353
- `Counter ${counter} frame is unavailable. Run snapshot() again.`
4354
- );
4355
- }
4356
- const status = await frame.evaluate(
4357
- ({
4358
- nodeId,
4359
- instanceToken,
4360
- counter: counter2,
4361
- nodeAttr,
4362
- instanceTokenKey,
4363
- counterOwnerKey,
4364
- counterValueKey
4365
- }) => {
4366
- const helpers = {
4367
- walk(map, root) {
4368
- const children = Array.from(root.children);
4369
- for (const child of children) {
4370
- const id = child.getAttribute(nodeAttr);
4371
- if (id) {
4372
- const list = map.get(id) || [];
4373
- list.push(child);
4374
- map.set(id, list);
4375
- }
4376
- helpers.walk(map, child);
4377
- if (child.shadowRoot) {
4378
- helpers.walk(map, child.shadowRoot);
4379
- }
4380
- }
4381
- },
4382
- buildNodeIndex() {
4383
- const map = /* @__PURE__ */ new Map();
4384
- helpers.walk(map, document);
4385
- return map;
4386
- }
4387
- };
4388
- const matches = helpers.buildNodeIndex().get(nodeId) || [];
4389
- if (!matches.length) return "missing";
4390
- if (matches.length !== 1) return "ambiguous";
4391
- const target = matches[0];
4392
- if (target[instanceTokenKey] !== instanceToken) {
4393
- return "instance_mismatch";
4394
- }
4395
- if (target[counterOwnerKey] !== true) {
4396
- return "instance_mismatch";
4397
- }
4398
- if (Number(target[counterValueKey] || 0) !== counter2) {
4399
- return "instance_mismatch";
4400
- }
4401
- if (target.getAttribute("c") !== String(counter2)) {
4402
- return "instance_mismatch";
4403
- }
4404
- return "ok";
4405
- },
4406
- {
4407
- nodeId: binding.nodeId,
4408
- instanceToken: binding.instanceToken,
4409
- counter,
4410
- nodeAttr: OS_NODE_ID_ATTR,
4411
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4412
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4413
- counterValueKey: OS_COUNTER_VALUE_KEY
4414
- }
4415
- );
4416
- if (status !== "ok") {
4417
- throw buildCounterFailureError(binding.nodeId, status);
4418
- }
4419
- const handle = await frame.evaluateHandle(
4420
- ({ nodeId, nodeAttr }) => {
4421
- const helpers = {
4422
- walk(matches, root) {
4443
+ async function clearLiveCounters(page) {
4444
+ for (const frame of page.frames()) {
4445
+ try {
4446
+ await frame.evaluate(() => {
4447
+ const walk = (root) => {
4423
4448
  const children = Array.from(root.children);
4424
4449
  for (const child of children) {
4425
- if (child.getAttribute(nodeAttr) === nodeId) {
4426
- matches.push(child);
4427
- }
4428
- helpers.walk(matches, child);
4450
+ child.removeAttribute("c");
4451
+ walk(child);
4429
4452
  if (child.shadowRoot) {
4430
- helpers.walk(matches, child.shadowRoot);
4431
- }
4432
- }
4433
- },
4434
- findUniqueNode() {
4435
- const matches = [];
4436
- helpers.walk(matches, document);
4437
- if (matches.length !== 1) return null;
4438
- return matches[0];
4439
- }
4440
- };
4441
- return helpers.findUniqueNode();
4442
- },
4443
- {
4444
- nodeId: binding.nodeId,
4445
- nodeAttr: OS_NODE_ID_ATTR
4446
- }
4447
- );
4448
- const element = handle.asElement();
4449
- if (!element) {
4450
- await handle.dispose();
4451
- throw new CounterResolutionError(
4452
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4453
- `Counter ${counter} became stale. Run snapshot() again.`
4454
- );
4455
- }
4456
- return element;
4457
- }
4458
- async function resolveCountersBatch(page, snapshot, requests) {
4459
- const out = {};
4460
- if (!requests.length) return out;
4461
- const grouped = /* @__PURE__ */ new Map();
4462
- for (const request of requests) {
4463
- const binding = readBinding(snapshot, request.counter);
4464
- const list = grouped.get(binding.frameToken) || [];
4465
- list.push({
4466
- ...request,
4467
- ...binding
4468
- });
4469
- grouped.set(binding.frameToken, list);
4470
- }
4471
- const framesByToken = await mapFramesByToken(page);
4472
- for (const [frameToken, entries] of grouped.entries()) {
4473
- const frame = framesByToken.get(frameToken);
4474
- if (!frame) {
4475
- throw new CounterResolutionError(
4476
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4477
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4478
- );
4479
- }
4480
- const result = await frame.evaluate(
4481
- ({
4482
- entries: entries2,
4483
- nodeAttr,
4484
- instanceTokenKey,
4485
- counterOwnerKey,
4486
- counterValueKey
4487
- }) => {
4488
- const values = [];
4489
- const failures = [];
4490
- const helpers = {
4491
- walk(map, root) {
4492
- const children = Array.from(root.children);
4493
- for (const child of children) {
4494
- const id = child.getAttribute(nodeAttr);
4495
- if (id) {
4496
- const list = map.get(id) || [];
4497
- list.push(child);
4498
- map.set(id, list);
4499
- }
4500
- helpers.walk(map, child);
4501
- if (child.shadowRoot) {
4502
- helpers.walk(map, child.shadowRoot);
4503
- }
4453
+ walk(child.shadowRoot);
4504
4454
  }
4505
- },
4506
- buildNodeIndex() {
4507
- const map = /* @__PURE__ */ new Map();
4508
- helpers.walk(map, document);
4509
- return map;
4510
- },
4511
- readRawValue(element, attribute) {
4512
- if (attribute) {
4513
- return element.getAttribute(attribute);
4514
- }
4515
- return element.textContent;
4516
- }
4517
- };
4518
- const index = helpers.buildNodeIndex();
4519
- for (const entry of entries2) {
4520
- const matches = index.get(entry.nodeId) || [];
4521
- if (!matches.length) {
4522
- failures.push({
4523
- nodeId: entry.nodeId,
4524
- reason: "missing"
4525
- });
4526
- continue;
4527
- }
4528
- if (matches.length !== 1) {
4529
- failures.push({
4530
- nodeId: entry.nodeId,
4531
- reason: "ambiguous"
4532
- });
4533
- continue;
4534
4455
  }
4535
- const target = matches[0];
4536
- if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
4537
- failures.push({
4538
- nodeId: entry.nodeId,
4539
- reason: "instance_mismatch"
4540
- });
4541
- continue;
4542
- }
4543
- values.push({
4544
- key: entry.key,
4545
- value: helpers.readRawValue(target, entry.attribute)
4546
- });
4547
- }
4548
- return {
4549
- values,
4550
- failures
4551
4456
  };
4552
- },
4553
- {
4554
- entries,
4555
- nodeAttr: OS_NODE_ID_ATTR,
4556
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4557
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4558
- counterValueKey: OS_COUNTER_VALUE_KEY
4559
- }
4560
- );
4561
- if (result.failures.length) {
4562
- const first = result.failures[0];
4563
- throw buildCounterFailureError(first.nodeId, first.reason);
4564
- }
4565
- const attributeByKey = new Map(
4566
- entries.map((entry) => [entry.key, entry.attribute])
4567
- );
4568
- for (const item of result.values) {
4569
- out[item.key] = normalizeExtractedValue(
4570
- item.value,
4571
- attributeByKey.get(item.key)
4572
- );
4457
+ walk(document);
4458
+ });
4459
+ } catch {
4573
4460
  }
4574
4461
  }
4575
- return out;
4576
4462
  }
4577
4463
  async function mapFramesByToken(page) {
4578
4464
  const out = /* @__PURE__ */ new Map();
@@ -4594,539 +4480,760 @@ async function readFrameToken(frame) {
4594
4480
  return null;
4595
4481
  }
4596
4482
  }
4597
- async function readGlobalNextCounter(page) {
4598
- const current = await page.mainFrame().evaluate((counterNextKey) => {
4599
- const win = window;
4600
- return Number(win[counterNextKey] || 0);
4601
- }, OS_COUNTER_NEXT_KEY).catch(() => 0);
4602
- if (Number.isFinite(current) && current > 0) {
4603
- return current;
4604
- }
4605
- let max = 0;
4606
- for (const frame of page.frames()) {
4607
- try {
4608
- const frameMax = await frame.evaluate(
4609
- ({ nodeAttr, counterOwnerKey, counterValueKey }) => {
4610
- let localMax = 0;
4611
- const helpers = {
4612
- walk(root) {
4613
- const children = Array.from(
4614
- root.children
4615
- );
4616
- for (const child of children) {
4617
- const candidate = child;
4618
- const hasNodeId = child.hasAttribute(nodeAttr);
4619
- const owned = candidate[counterOwnerKey] === true;
4620
- if (hasNodeId && owned) {
4621
- const value = Number(
4622
- candidate[counterValueKey] || 0
4623
- );
4624
- if (Number.isFinite(value) && value > localMax) {
4625
- localMax = value;
4626
- }
4627
- }
4628
- helpers.walk(child);
4629
- if (child.shadowRoot) {
4630
- helpers.walk(child.shadowRoot);
4631
- }
4632
- }
4633
- }
4634
- };
4635
- helpers.walk(document);
4636
- return localMax;
4637
- },
4638
- {
4639
- nodeAttr: OS_NODE_ID_ATTR,
4640
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4641
- counterValueKey: OS_COUNTER_VALUE_KEY
4642
- }
4643
- );
4644
- if (frameMax > max) {
4645
- max = frameMax;
4646
- }
4647
- } catch {
4648
- }
4649
- }
4650
- const next = max + 1;
4651
- await writeGlobalNextCounter(page, next);
4652
- return next;
4653
- }
4654
- async function writeGlobalNextCounter(page, nextCounter) {
4655
- await page.mainFrame().evaluate(
4656
- ({ counterNextKey, nextCounter: nextCounter2 }) => {
4657
- const win = window;
4658
- win[counterNextKey] = nextCounter2;
4659
- },
4660
- {
4661
- counterNextKey: OS_COUNTER_NEXT_KEY,
4662
- nextCounter
4663
- }
4664
- ).catch(() => void 0);
4483
+ function stripNodeIds(html) {
4484
+ if (!html.includes(OS_NODE_ID_ATTR)) return html;
4485
+ const $ = cheerio3.load(html, { xmlMode: false });
4486
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4487
+ return $.html();
4665
4488
  }
4666
- function readBinding(snapshot, counter) {
4667
- if (!snapshot.counterBindings) {
4668
- throw new CounterResolutionError(
4669
- "ERR_COUNTER_NOT_FOUND",
4670
- `Counter ${counter} is unavailable because this snapshot has no counter bindings. Run snapshot() with counters first.`
4671
- );
4489
+ async function prepareSnapshot(page, options = {}) {
4490
+ const mode = options.mode ?? "action";
4491
+ const withCounters = options.withCounters ?? true;
4492
+ const shouldMarkInteractive = options.markInteractive ?? true;
4493
+ if (shouldMarkInteractive) {
4494
+ await markInteractiveElements(page);
4672
4495
  }
4673
- const binding = snapshot.counterBindings.get(counter);
4674
- if (!binding) {
4675
- throw new CounterResolutionError(
4676
- "ERR_COUNTER_NOT_FOUND",
4677
- `Counter ${counter} was not found in the current snapshot. Run snapshot() again.`
4496
+ const serialized = await serializePageHTML(page);
4497
+ const rawHtml = serialized.html;
4498
+ const processedHtml = rawHtml;
4499
+ const reducedHtml = applyCleaner(mode, processedHtml);
4500
+ let cleanedHtml = reducedHtml;
4501
+ let counterIndex = null;
4502
+ if (withCounters) {
4503
+ const counted = await assignCounters(
4504
+ page,
4505
+ reducedHtml,
4506
+ serialized.nodePaths,
4507
+ serialized.nodeMeta
4678
4508
  );
4509
+ cleanedHtml = counted.html;
4510
+ counterIndex = counted.counterIndex;
4511
+ } else {
4512
+ cleanedHtml = stripNodeIds(cleanedHtml);
4679
4513
  }
4680
- if (binding.sessionId !== snapshot.snapshotSessionId) {
4681
- throw new CounterResolutionError(
4682
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4683
- `Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
4684
- );
4514
+ if (mode === "extraction") {
4515
+ const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
4516
+ cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
4685
4517
  }
4686
- return binding;
4518
+ return {
4519
+ mode,
4520
+ url: page.url(),
4521
+ rawHtml,
4522
+ processedHtml,
4523
+ reducedHtml,
4524
+ cleanedHtml,
4525
+ counterIndex
4526
+ };
4687
4527
  }
4688
- function buildCounterFailureError(nodeId, reason) {
4689
- if (reason === "ambiguous") {
4690
- return new CounterResolutionError(
4691
- "ERR_COUNTER_AMBIGUOUS",
4692
- `Counter target is ambiguous for node ${nodeId}. Run snapshot() again.`
4528
+
4529
+ // src/element-path/errors.ts
4530
+ var ElementPathError = class extends Error {
4531
+ code;
4532
+ constructor(code, message) {
4533
+ super(message);
4534
+ this.name = "ElementPathError";
4535
+ this.code = code;
4536
+ }
4537
+ };
4538
+
4539
+ // src/element-path/resolver.ts
4540
+ async function resolveElementPath(page, rawPath) {
4541
+ const path5 = sanitizeElementPath(rawPath);
4542
+ let frame = page.mainFrame();
4543
+ let rootHandle = null;
4544
+ for (const hop of path5.context) {
4545
+ const host = await resolveDomPath(frame, hop.host, rootHandle);
4546
+ if (!host) {
4547
+ await disposeHandle(rootHandle);
4548
+ throw new ElementPathError(
4549
+ "ERR_PATH_CONTEXT_HOST_NOT_FOUND",
4550
+ "Unable to resolve context host from stored match selectors."
4551
+ );
4552
+ }
4553
+ if (hop.kind === "iframe") {
4554
+ const nextFrame = await host.element.contentFrame();
4555
+ await host.element.dispose();
4556
+ await disposeHandle(rootHandle);
4557
+ rootHandle = null;
4558
+ if (!nextFrame) {
4559
+ throw new ElementPathError(
4560
+ "ERR_PATH_IFRAME_UNAVAILABLE",
4561
+ "Iframe is unavailable or inaccessible for this path."
4562
+ );
4563
+ }
4564
+ frame = nextFrame;
4565
+ continue;
4566
+ }
4567
+ const shadowRoot = await host.element.evaluateHandle(
4568
+ (element) => element.shadowRoot
4569
+ );
4570
+ await host.element.dispose();
4571
+ const isMissing = await shadowRoot.evaluate((value) => value == null);
4572
+ if (isMissing) {
4573
+ await shadowRoot.dispose();
4574
+ await disposeHandle(rootHandle);
4575
+ throw new ElementPathError(
4576
+ "ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
4577
+ "Shadow root is unavailable for this path."
4578
+ );
4579
+ }
4580
+ await disposeHandle(rootHandle);
4581
+ rootHandle = shadowRoot;
4582
+ }
4583
+ const target = await resolveDomPath(frame, path5.nodes, rootHandle);
4584
+ if (!target) {
4585
+ const diagnostics = await collectCandidateDiagnostics(
4586
+ frame,
4587
+ path5.nodes,
4588
+ rootHandle
4589
+ );
4590
+ await disposeHandle(rootHandle);
4591
+ throw new ElementPathError(
4592
+ "ERR_PATH_TARGET_NOT_FOUND",
4593
+ buildTargetNotFoundMessage(path5.nodes, diagnostics)
4693
4594
  );
4694
4595
  }
4695
- return new CounterResolutionError(
4696
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4697
- `Counter target is stale or missing for node ${nodeId}. Run snapshot() again.`
4596
+ await disposeHandle(rootHandle);
4597
+ if (isPathDebugEnabled()) {
4598
+ debugPath("resolved", {
4599
+ selector: target.selector,
4600
+ mode: target.mode,
4601
+ count: target.count,
4602
+ targetDepth: path5.nodes.length
4603
+ });
4604
+ }
4605
+ return {
4606
+ element: target.element,
4607
+ usedSelector: target.selector || buildPathSelectorHint(path5)
4608
+ };
4609
+ }
4610
+ async function resolveDomPath(frame, domPath, rootHandle) {
4611
+ const candidates = buildPathCandidates(domPath);
4612
+ if (!candidates.length) return null;
4613
+ if (isPathDebugEnabled()) {
4614
+ debugPath("trying selectors", { candidates });
4615
+ }
4616
+ const selected = rootHandle ? await rootHandle.evaluate(selectInRoot, candidates) : await frame.evaluate(selectInDocument, candidates);
4617
+ if (!selected || !selected.selector) return null;
4618
+ const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
4619
+ if (!(root instanceof ShadowRoot)) return null;
4620
+ return root.querySelector(selector);
4621
+ }, selected.selector) : await frame.evaluateHandle(
4622
+ (selector) => document.querySelector(selector),
4623
+ selected.selector
4698
4624
  );
4625
+ const element = handle.asElement();
4626
+ if (!element) {
4627
+ await handle.dispose();
4628
+ return null;
4629
+ }
4630
+ return {
4631
+ element,
4632
+ selector: selected.selector,
4633
+ mode: selected.mode,
4634
+ count: selected.count
4635
+ };
4699
4636
  }
4700
-
4701
- // src/html/pipeline.ts
4702
- function applyCleaner(mode, html) {
4703
- switch (mode) {
4704
- case "clickable":
4705
- return cleanForClickable(html);
4706
- case "scrollable":
4707
- return cleanForScrollable(html);
4708
- case "extraction":
4709
- return cleanForExtraction(html);
4710
- case "full":
4711
- return cleanForFull(html);
4712
- case "action":
4713
- default:
4714
- return cleanForAction(html);
4637
+ async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
4638
+ const candidates = buildPathCandidates(domPath);
4639
+ if (!candidates.length) return [];
4640
+ const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
4641
+ return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
4642
+ selector: String(item?.selector || ""),
4643
+ count: Number(item?.count || 0)
4644
+ })).filter((item) => item.selector) : [];
4645
+ }
4646
+ function buildTargetNotFoundMessage(domPath, diagnostics) {
4647
+ const depth = Array.isArray(domPath) ? domPath.length : 0;
4648
+ const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
4649
+ const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
4650
+ if (!sample)
4651
+ return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
4652
+ return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
4653
+ }
4654
+ function selectInDocument(selectors) {
4655
+ let fallback = null;
4656
+ for (const selector of selectors) {
4657
+ if (!selector) continue;
4658
+ let count = 0;
4659
+ try {
4660
+ count = document.querySelectorAll(selector).length;
4661
+ } catch {
4662
+ count = 0;
4663
+ }
4664
+ if (count === 1) {
4665
+ return {
4666
+ selector,
4667
+ count,
4668
+ mode: "unique"
4669
+ };
4670
+ }
4671
+ if (count > 1 && !fallback) {
4672
+ fallback = {
4673
+ selector,
4674
+ count,
4675
+ mode: "fallback"
4676
+ };
4677
+ }
4715
4678
  }
4679
+ return fallback;
4716
4680
  }
4717
- async function assignCounters(page, html, nodePaths, nodeMeta, snapshotSessionId) {
4718
- const $ = cheerio3.load(html, { xmlMode: false });
4719
- const counterIndex = /* @__PURE__ */ new Map();
4720
- const counterBindings = /* @__PURE__ */ new Map();
4721
- const orderedNodeIds = [];
4722
- $("*").each(function() {
4723
- const el = $(this);
4724
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4725
- if (!nodeId) return;
4726
- orderedNodeIds.push(nodeId);
4727
- });
4728
- const countersByNodeId = await ensureLiveCounters(
4729
- page,
4730
- nodeMeta,
4731
- orderedNodeIds
4732
- );
4733
- $("*").each(function() {
4734
- const el = $(this);
4735
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4736
- if (!nodeId) return;
4737
- const path5 = nodePaths.get(nodeId);
4738
- const meta = nodeMeta.get(nodeId);
4739
- const counter = countersByNodeId.get(nodeId);
4740
- if (counter == null || !Number.isFinite(counter)) {
4741
- throw new Error(
4742
- `Counter assignment failed for node ${nodeId}. Run snapshot() again.`
4743
- );
4681
+ function selectInRoot(root, selectors) {
4682
+ if (!(root instanceof ShadowRoot)) return null;
4683
+ let fallback = null;
4684
+ for (const selector of selectors) {
4685
+ if (!selector) continue;
4686
+ let count = 0;
4687
+ try {
4688
+ count = root.querySelectorAll(selector).length;
4689
+ } catch {
4690
+ count = 0;
4744
4691
  }
4745
- if (counterBindings.has(counter) && counterBindings.get(counter)?.nodeId !== nodeId) {
4746
- throw new Error(
4747
- `Counter ${counter} was assigned to multiple nodes. Run snapshot() again.`
4748
- );
4692
+ if (count === 1) {
4693
+ return {
4694
+ selector,
4695
+ count,
4696
+ mode: "unique"
4697
+ };
4749
4698
  }
4750
- el.attr("c", String(counter));
4751
- el.removeAttr(OS_NODE_ID_ATTR);
4752
- if (path5) {
4753
- counterIndex.set(counter, cloneElementPath(path5));
4699
+ if (count > 1 && !fallback) {
4700
+ fallback = {
4701
+ selector,
4702
+ count,
4703
+ mode: "fallback"
4704
+ };
4754
4705
  }
4755
- if (meta) {
4756
- counterBindings.set(counter, {
4757
- sessionId: snapshotSessionId,
4758
- frameToken: meta.frameToken,
4759
- nodeId,
4760
- instanceToken: meta.instanceToken
4761
- });
4706
+ }
4707
+ return fallback;
4708
+ }
4709
+ function countInDocument(selectors) {
4710
+ const out = [];
4711
+ for (const selector of selectors) {
4712
+ if (!selector) continue;
4713
+ let count = 0;
4714
+ try {
4715
+ count = document.querySelectorAll(selector).length;
4716
+ } catch {
4717
+ count = 0;
4762
4718
  }
4763
- });
4764
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4765
- return {
4766
- html: $.html(),
4767
- counterIndex,
4768
- counterBindings
4769
- };
4719
+ out.push({ selector, count });
4720
+ }
4721
+ return out;
4770
4722
  }
4771
- function stripNodeIds(html) {
4772
- if (!html.includes(OS_NODE_ID_ATTR)) return html;
4773
- const $ = cheerio3.load(html, { xmlMode: false });
4774
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4775
- return $.html();
4723
+ function countInRoot(root, selectors) {
4724
+ if (!(root instanceof ShadowRoot)) return [];
4725
+ const out = [];
4726
+ for (const selector of selectors) {
4727
+ if (!selector) continue;
4728
+ let count = 0;
4729
+ try {
4730
+ count = root.querySelectorAll(selector).length;
4731
+ } catch {
4732
+ count = 0;
4733
+ }
4734
+ out.push({ selector, count });
4735
+ }
4736
+ return out;
4776
4737
  }
4777
- async function prepareSnapshot(page, options = {}) {
4778
- const snapshotSessionId = (0, import_crypto.randomUUID)();
4779
- const mode = options.mode ?? "action";
4780
- const withCounters = options.withCounters ?? true;
4781
- const shouldMarkInteractive = options.markInteractive ?? true;
4782
- if (shouldMarkInteractive) {
4783
- await markInteractiveElements(page);
4738
+ function isPathDebugEnabled() {
4739
+ const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
4740
+ if (!value) return false;
4741
+ const normalized = value.trim().toLowerCase();
4742
+ return normalized === "1" || normalized === "true";
4743
+ }
4744
+ function debugPath(message, data) {
4745
+ if (!isPathDebugEnabled()) return;
4746
+ if (data !== void 0) {
4747
+ console.log(`[opensteer:path] ${message}`, data);
4748
+ } else {
4749
+ console.log(`[opensteer:path] ${message}`);
4750
+ }
4751
+ }
4752
+ async function disposeHandle(handle) {
4753
+ if (!handle) return;
4754
+ try {
4755
+ await handle.dispose();
4756
+ } catch {
4757
+ }
4758
+ }
4759
+
4760
+ // src/actions/actionability-probe.ts
4761
+ async function probeActionabilityState(element) {
4762
+ try {
4763
+ return await element.evaluate((target) => {
4764
+ if (!(target instanceof Element)) {
4765
+ return {
4766
+ connected: false,
4767
+ visible: null,
4768
+ enabled: null,
4769
+ editable: null,
4770
+ blocker: null
4771
+ };
4772
+ }
4773
+ const connected = target.isConnected;
4774
+ if (!connected) {
4775
+ return {
4776
+ connected: false,
4777
+ visible: null,
4778
+ enabled: null,
4779
+ editable: null,
4780
+ blocker: null
4781
+ };
4782
+ }
4783
+ const style = window.getComputedStyle(target);
4784
+ const rect = target.getBoundingClientRect();
4785
+ const hasBox = rect.width > 0 && rect.height > 0;
4786
+ const opacity = Number.parseFloat(style.opacity || "1");
4787
+ const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
4788
+ let enabled = null;
4789
+ if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
4790
+ enabled = !target.disabled;
4791
+ }
4792
+ let editable = null;
4793
+ if (target instanceof HTMLInputElement) {
4794
+ editable = !target.readOnly && !target.disabled;
4795
+ } else if (target instanceof HTMLTextAreaElement) {
4796
+ editable = !target.readOnly && !target.disabled;
4797
+ } else if (target instanceof HTMLSelectElement) {
4798
+ editable = !target.disabled;
4799
+ } else if (target instanceof HTMLElement && target.isContentEditable) {
4800
+ editable = true;
4801
+ }
4802
+ let blocker = null;
4803
+ if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
4804
+ const x = Math.min(
4805
+ Math.max(rect.left + rect.width / 2, 0),
4806
+ window.innerWidth - 1
4807
+ );
4808
+ const y = Math.min(
4809
+ Math.max(rect.top + rect.height / 2, 0),
4810
+ window.innerHeight - 1
4811
+ );
4812
+ const top = document.elementFromPoint(x, y);
4813
+ if (top && top !== target && !target.contains(top)) {
4814
+ const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
4815
+ blocker = {
4816
+ tag: top.tagName.toLowerCase(),
4817
+ id: top.id || null,
4818
+ classes,
4819
+ role: top.getAttribute("role"),
4820
+ text: (top.textContent || "").trim().slice(0, 80) || null
4821
+ };
4822
+ }
4823
+ }
4824
+ return {
4825
+ connected,
4826
+ visible: isVisible,
4827
+ enabled,
4828
+ editable,
4829
+ blocker
4830
+ };
4831
+ });
4832
+ } catch {
4833
+ return null;
4834
+ }
4835
+ }
4836
+
4837
+ // src/extract-value-normalization.ts
4838
+ var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
4839
+ function normalizeExtractedValue(raw, attribute) {
4840
+ if (raw == null) return null;
4841
+ const rawText = String(raw);
4842
+ if (!rawText.trim()) return null;
4843
+ const normalizedAttribute = String(attribute || "").trim().toLowerCase();
4844
+ if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
4845
+ const singleValue = pickSingleListAttributeValue(
4846
+ normalizedAttribute,
4847
+ rawText
4848
+ ).trim();
4849
+ return singleValue || null;
4850
+ }
4851
+ const text = rawText.replace(/\s+/g, " ").trim();
4852
+ return text || null;
4853
+ }
4854
+ function pickSingleListAttributeValue(attribute, raw) {
4855
+ if (attribute === "ping") {
4856
+ const firstUrl = raw.trim().split(/\s+/)[0] || "";
4857
+ return firstUrl.trim();
4858
+ }
4859
+ if (attribute === "srcset" || attribute === "imagesrcset") {
4860
+ const picked = pickBestSrcsetCandidate(raw);
4861
+ if (picked) return picked;
4862
+ return pickFirstSrcsetToken(raw) || "";
4863
+ }
4864
+ return raw.trim();
4865
+ }
4866
+ function pickBestSrcsetCandidate(raw) {
4867
+ const candidates = parseSrcsetCandidates(raw);
4868
+ if (!candidates.length) return null;
4869
+ const widthCandidates = candidates.filter(
4870
+ (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
4871
+ );
4872
+ if (widthCandidates.length) {
4873
+ return widthCandidates.reduce(
4874
+ (best, candidate) => candidate.width > best.width ? candidate : best
4875
+ ).url;
4876
+ }
4877
+ const densityCandidates = candidates.filter(
4878
+ (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
4879
+ );
4880
+ if (densityCandidates.length) {
4881
+ return densityCandidates.reduce(
4882
+ (best, candidate) => candidate.density > best.density ? candidate : best
4883
+ ).url;
4884
+ }
4885
+ return candidates[0]?.url || null;
4886
+ }
4887
+ function parseSrcsetCandidates(raw) {
4888
+ const text = String(raw || "").trim();
4889
+ if (!text) return [];
4890
+ const out = [];
4891
+ let index = 0;
4892
+ while (index < text.length) {
4893
+ index = skipSeparators(text, index);
4894
+ if (index >= text.length) break;
4895
+ const urlToken = readUrlToken(text, index);
4896
+ index = urlToken.nextIndex;
4897
+ const url = urlToken.value.trim();
4898
+ if (!url) continue;
4899
+ index = skipWhitespace(text, index);
4900
+ const descriptors = [];
4901
+ while (index < text.length && text[index] !== ",") {
4902
+ const descriptorToken = readDescriptorToken(text, index);
4903
+ if (!descriptorToken.value) {
4904
+ index = descriptorToken.nextIndex;
4905
+ continue;
4906
+ }
4907
+ descriptors.push(descriptorToken.value);
4908
+ index = descriptorToken.nextIndex;
4909
+ index = skipWhitespace(text, index);
4910
+ }
4911
+ if (index < text.length && text[index] === ",") {
4912
+ index += 1;
4913
+ }
4914
+ let width = null;
4915
+ let density = null;
4916
+ for (const descriptor of descriptors) {
4917
+ const token = descriptor.trim().toLowerCase();
4918
+ if (!token) continue;
4919
+ const widthMatch = token.match(/^(\d+)w$/);
4920
+ if (widthMatch) {
4921
+ const parsed = Number.parseInt(widthMatch[1], 10);
4922
+ if (Number.isFinite(parsed)) {
4923
+ width = parsed;
4924
+ }
4925
+ continue;
4926
+ }
4927
+ const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4928
+ if (densityMatch) {
4929
+ const parsed = Number.parseFloat(densityMatch[1]);
4930
+ if (Number.isFinite(parsed)) {
4931
+ density = parsed;
4932
+ }
4933
+ }
4934
+ }
4935
+ out.push({
4936
+ url,
4937
+ width,
4938
+ density
4939
+ });
4940
+ }
4941
+ return out;
4942
+ }
4943
+ function pickFirstSrcsetToken(raw) {
4944
+ const candidate = parseSrcsetCandidates(raw)[0];
4945
+ if (candidate?.url) {
4946
+ return candidate.url;
4947
+ }
4948
+ const text = String(raw || "");
4949
+ const start = skipSeparators(text, 0);
4950
+ if (start >= text.length) return null;
4951
+ const firstToken = readUrlToken(text, start).value.trim();
4952
+ return firstToken || null;
4953
+ }
4954
+ function skipWhitespace(value, index) {
4955
+ let cursor = index;
4956
+ while (cursor < value.length && /\s/.test(value[cursor])) {
4957
+ cursor += 1;
4958
+ }
4959
+ return cursor;
4960
+ }
4961
+ function skipSeparators(value, index) {
4962
+ let cursor = skipWhitespace(value, index);
4963
+ while (cursor < value.length && value[cursor] === ",") {
4964
+ cursor += 1;
4965
+ cursor = skipWhitespace(value, cursor);
4966
+ }
4967
+ return cursor;
4968
+ }
4969
+ function readUrlToken(value, index) {
4970
+ let cursor = index;
4971
+ let out = "";
4972
+ const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4973
+ while (cursor < value.length) {
4974
+ const char = value[cursor];
4975
+ if (/\s/.test(char)) {
4976
+ break;
4977
+ }
4978
+ if (char === "," && !isDataUrl) {
4979
+ break;
4980
+ }
4981
+ out += char;
4982
+ cursor += 1;
4784
4983
  }
4785
- const serialized = await serializePageHTML(page);
4786
- const rawHtml = serialized.html;
4787
- const processedHtml = rawHtml;
4788
- const reducedHtml = applyCleaner(mode, processedHtml);
4789
- let cleanedHtml = reducedHtml;
4790
- let counterIndex = null;
4791
- let counterBindings = null;
4792
- if (withCounters) {
4793
- const counted = await assignCounters(
4794
- page,
4795
- reducedHtml,
4796
- serialized.nodePaths,
4797
- serialized.nodeMeta,
4798
- snapshotSessionId
4799
- );
4800
- cleanedHtml = counted.html;
4801
- counterIndex = counted.counterIndex;
4802
- counterBindings = counted.counterBindings;
4803
- } else {
4804
- cleanedHtml = stripNodeIds(cleanedHtml);
4984
+ if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4985
+ out = out.slice(0, -1);
4805
4986
  }
4806
- if (mode === "extraction") {
4807
- const $unwrap = cheerio3.load(cleanedHtml, { xmlMode: false });
4808
- cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
4987
+ return {
4988
+ value: out,
4989
+ nextIndex: cursor
4990
+ };
4991
+ }
4992
+ function readDescriptorToken(value, index) {
4993
+ let cursor = skipWhitespace(value, index);
4994
+ let out = "";
4995
+ while (cursor < value.length) {
4996
+ const char = value[cursor];
4997
+ if (char === "," || /\s/.test(char)) {
4998
+ break;
4999
+ }
5000
+ out += char;
5001
+ cursor += 1;
4809
5002
  }
4810
5003
  return {
4811
- snapshotSessionId,
4812
- mode,
4813
- url: page.url(),
4814
- rawHtml,
4815
- processedHtml,
4816
- reducedHtml,
4817
- cleanedHtml,
4818
- counterIndex,
4819
- counterBindings
5004
+ value: out.trim(),
5005
+ nextIndex: cursor
4820
5006
  };
4821
5007
  }
4822
5008
 
4823
- // src/element-path/errors.ts
4824
- var ElementPathError = class extends Error {
5009
+ // src/html/counter-runtime.ts
5010
+ var CounterResolutionError = class extends Error {
4825
5011
  code;
4826
5012
  constructor(code, message) {
4827
5013
  super(message);
4828
- this.name = "ElementPathError";
5014
+ this.name = "CounterResolutionError";
4829
5015
  this.code = code;
4830
5016
  }
4831
5017
  };
4832
-
4833
- // src/element-path/resolver.ts
4834
- async function resolveElementPath(page, rawPath) {
4835
- const path5 = sanitizeElementPath(rawPath);
4836
- let frame = page.mainFrame();
4837
- let rootHandle = null;
4838
- for (const hop of path5.context) {
4839
- const host = await resolveDomPath(frame, hop.host, rootHandle);
4840
- if (!host) {
4841
- await disposeHandle(rootHandle);
4842
- throw new ElementPathError(
4843
- "ERR_PATH_CONTEXT_HOST_NOT_FOUND",
4844
- "Unable to resolve context host from stored match selectors."
4845
- );
4846
- }
4847
- if (hop.kind === "iframe") {
4848
- const nextFrame = await host.element.contentFrame();
4849
- await host.element.dispose();
4850
- await disposeHandle(rootHandle);
4851
- rootHandle = null;
4852
- if (!nextFrame) {
4853
- throw new ElementPathError(
4854
- "ERR_PATH_IFRAME_UNAVAILABLE",
4855
- "Iframe is unavailable or inaccessible for this path."
4856
- );
4857
- }
4858
- frame = nextFrame;
4859
- continue;
4860
- }
4861
- const shadowRoot = await host.element.evaluateHandle(
4862
- (element) => element.shadowRoot
4863
- );
4864
- await host.element.dispose();
4865
- const isMissing = await shadowRoot.evaluate((value) => value == null);
4866
- if (isMissing) {
4867
- await shadowRoot.dispose();
4868
- await disposeHandle(rootHandle);
4869
- throw new ElementPathError(
4870
- "ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
4871
- "Shadow root is unavailable for this path."
4872
- );
4873
- }
4874
- await disposeHandle(rootHandle);
4875
- rootHandle = shadowRoot;
5018
+ async function resolveCounterElement(page, counter) {
5019
+ const normalized = normalizeCounter(counter);
5020
+ if (normalized == null) {
5021
+ throw buildCounterNotFoundError(counter);
4876
5022
  }
4877
- const target = await resolveDomPath(frame, path5.nodes, rootHandle);
4878
- if (!target) {
4879
- const diagnostics = await collectCandidateDiagnostics(
4880
- frame,
4881
- path5.nodes,
4882
- rootHandle
4883
- );
4884
- await disposeHandle(rootHandle);
4885
- throw new ElementPathError(
4886
- "ERR_PATH_TARGET_NOT_FOUND",
4887
- buildTargetNotFoundMessage(path5.nodes, diagnostics)
4888
- );
4889
- }
4890
- await disposeHandle(rootHandle);
4891
- if (isPathDebugEnabled()) {
4892
- debugPath("resolved", {
4893
- selector: target.selector,
4894
- mode: target.mode,
4895
- count: target.count,
4896
- targetDepth: path5.nodes.length
4897
- });
5023
+ const scan = await scanCounterOccurrences(page, [normalized]);
5024
+ const entry = scan.get(normalized);
5025
+ if (!entry || entry.count <= 0 || !entry.frame) {
5026
+ throw buildCounterNotFoundError(counter);
4898
5027
  }
4899
- return {
4900
- element: target.element,
4901
- usedSelector: target.selector || buildPathSelectorHint(path5)
4902
- };
4903
- }
4904
- async function resolveDomPath(frame, domPath, rootHandle) {
4905
- const candidates = buildPathCandidates(domPath);
4906
- if (!candidates.length) return null;
4907
- if (isPathDebugEnabled()) {
4908
- debugPath("trying selectors", { candidates });
5028
+ if (entry.count > 1) {
5029
+ throw buildCounterAmbiguousError(counter);
4909
5030
  }
4910
- const selected = rootHandle ? await rootHandle.evaluate(selectInRoot, candidates) : await frame.evaluate(selectInDocument, candidates);
4911
- if (!selected || !selected.selector) return null;
4912
- const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
4913
- if (!(root instanceof ShadowRoot)) return null;
4914
- return root.querySelector(selector);
4915
- }, selected.selector) : await frame.evaluateHandle(
4916
- (selector) => document.querySelector(selector),
4917
- selected.selector
4918
- );
5031
+ const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
4919
5032
  const element = handle.asElement();
4920
5033
  if (!element) {
4921
5034
  await handle.dispose();
4922
- return null;
5035
+ throw buildCounterNotFoundError(counter);
4923
5036
  }
4924
- return {
4925
- element,
4926
- selector: selected.selector,
4927
- mode: selected.mode,
4928
- count: selected.count
4929
- };
4930
- }
4931
- async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
4932
- const candidates = buildPathCandidates(domPath);
4933
- if (!candidates.length) return [];
4934
- const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
4935
- return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
4936
- selector: String(item?.selector || ""),
4937
- count: Number(item?.count || 0)
4938
- })).filter((item) => item.selector) : [];
4939
- }
4940
- function buildTargetNotFoundMessage(domPath, diagnostics) {
4941
- const depth = Array.isArray(domPath) ? domPath.length : 0;
4942
- const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
4943
- const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
4944
- if (!sample)
4945
- return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
4946
- return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
5037
+ return element;
4947
5038
  }
4948
- function selectInDocument(selectors) {
4949
- let fallback = null;
4950
- for (const selector of selectors) {
4951
- if (!selector) continue;
4952
- let count = 0;
4953
- try {
4954
- count = document.querySelectorAll(selector).length;
4955
- } catch {
4956
- count = 0;
5039
+ async function resolveCountersBatch(page, requests) {
5040
+ const out = {};
5041
+ if (!requests.length) return out;
5042
+ const counters = dedupeCounters(requests);
5043
+ const scan = await scanCounterOccurrences(page, counters);
5044
+ for (const counter of counters) {
5045
+ const entry = scan.get(counter);
5046
+ if (entry.count > 1) {
5047
+ throw buildCounterAmbiguousError(counter);
4957
5048
  }
4958
- if (count === 1) {
4959
- return {
4960
- selector,
4961
- count,
4962
- mode: "unique"
4963
- };
5049
+ }
5050
+ const valueCache = /* @__PURE__ */ new Map();
5051
+ for (const request of requests) {
5052
+ const normalized = normalizeCounter(request.counter);
5053
+ if (normalized == null) {
5054
+ out[request.key] = null;
5055
+ continue;
4964
5056
  }
4965
- if (count > 1 && !fallback) {
4966
- fallback = {
4967
- selector,
4968
- count,
4969
- mode: "fallback"
4970
- };
5057
+ const entry = scan.get(normalized);
5058
+ if (!entry || entry.count <= 0 || !entry.frame) {
5059
+ out[request.key] = null;
5060
+ continue;
4971
5061
  }
4972
- }
4973
- return fallback;
4974
- }
4975
- function selectInRoot(root, selectors) {
4976
- if (!(root instanceof ShadowRoot)) return null;
4977
- let fallback = null;
4978
- for (const selector of selectors) {
4979
- if (!selector) continue;
4980
- let count = 0;
4981
- try {
4982
- count = root.querySelectorAll(selector).length;
4983
- } catch {
4984
- count = 0;
5062
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
5063
+ if (valueCache.has(cacheKey)) {
5064
+ out[request.key] = valueCache.get(cacheKey);
5065
+ continue;
4985
5066
  }
4986
- if (count === 1) {
4987
- return {
4988
- selector,
4989
- count,
4990
- mode: "unique"
4991
- };
5067
+ const read = await readCounterValueInFrame(
5068
+ entry.frame,
5069
+ normalized,
5070
+ request.attribute
5071
+ );
5072
+ if (read.status === "ambiguous") {
5073
+ throw buildCounterAmbiguousError(normalized);
4992
5074
  }
4993
- if (count > 1 && !fallback) {
4994
- fallback = {
4995
- selector,
4996
- count,
4997
- mode: "fallback"
4998
- };
5075
+ if (read.status === "missing") {
5076
+ valueCache.set(cacheKey, null);
5077
+ out[request.key] = null;
5078
+ continue;
4999
5079
  }
5080
+ const normalizedValue = normalizeExtractedValue(
5081
+ read.value ?? null,
5082
+ request.attribute
5083
+ );
5084
+ valueCache.set(cacheKey, normalizedValue);
5085
+ out[request.key] = normalizedValue;
5000
5086
  }
5001
- return fallback;
5087
+ return out;
5002
5088
  }
5003
- function countInDocument(selectors) {
5089
+ function dedupeCounters(requests) {
5090
+ const seen = /* @__PURE__ */ new Set();
5004
5091
  const out = [];
5005
- for (const selector of selectors) {
5006
- if (!selector) continue;
5007
- let count = 0;
5008
- try {
5009
- count = document.querySelectorAll(selector).length;
5010
- } catch {
5011
- count = 0;
5012
- }
5013
- out.push({ selector, count });
5092
+ for (const request of requests) {
5093
+ const normalized = normalizeCounter(request.counter);
5094
+ if (normalized == null || seen.has(normalized)) continue;
5095
+ seen.add(normalized);
5096
+ out.push(normalized);
5014
5097
  }
5015
5098
  return out;
5016
5099
  }
5017
- function countInRoot(root, selectors) {
5018
- if (!(root instanceof ShadowRoot)) return [];
5019
- const out = [];
5020
- for (const selector of selectors) {
5021
- if (!selector) continue;
5022
- let count = 0;
5100
+ function normalizeCounter(counter) {
5101
+ if (!Number.isFinite(counter)) return null;
5102
+ if (!Number.isInteger(counter)) return null;
5103
+ if (counter <= 0) return null;
5104
+ return counter;
5105
+ }
5106
+ async function scanCounterOccurrences(page, counters) {
5107
+ const out = /* @__PURE__ */ new Map();
5108
+ for (const counter of counters) {
5109
+ out.set(counter, {
5110
+ count: 0,
5111
+ frame: null
5112
+ });
5113
+ }
5114
+ if (!counters.length) return out;
5115
+ for (const frame of page.frames()) {
5116
+ let frameCounts;
5023
5117
  try {
5024
- count = root.querySelectorAll(selector).length;
5118
+ frameCounts = await frame.evaluate((candidates) => {
5119
+ const keys = new Set(candidates.map((value) => String(value)));
5120
+ const counts = {};
5121
+ for (const key of keys) {
5122
+ counts[key] = 0;
5123
+ }
5124
+ const walk = (root) => {
5125
+ const children = Array.from(root.children);
5126
+ for (const child of children) {
5127
+ const value = child.getAttribute("c");
5128
+ if (value && keys.has(value)) {
5129
+ counts[value] = (counts[value] || 0) + 1;
5130
+ }
5131
+ walk(child);
5132
+ if (child.shadowRoot) {
5133
+ walk(child.shadowRoot);
5134
+ }
5135
+ }
5136
+ };
5137
+ walk(document);
5138
+ return counts;
5139
+ }, counters);
5025
5140
  } catch {
5026
- count = 0;
5141
+ continue;
5142
+ }
5143
+ for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
5144
+ const counter = Number.parseInt(rawCounter, 10);
5145
+ if (!Number.isFinite(counter)) continue;
5146
+ const count = Number(rawCount || 0);
5147
+ if (!Number.isFinite(count) || count <= 0) continue;
5148
+ const entry = out.get(counter);
5149
+ entry.count += count;
5150
+ if (!entry.frame) {
5151
+ entry.frame = frame;
5152
+ }
5027
5153
  }
5028
- out.push({ selector, count });
5029
5154
  }
5030
5155
  return out;
5031
5156
  }
5032
- function isPathDebugEnabled() {
5033
- const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
5034
- if (!value) return false;
5035
- const normalized = value.trim().toLowerCase();
5036
- return normalized === "1" || normalized === "true";
5037
- }
5038
- function debugPath(message, data) {
5039
- if (!isPathDebugEnabled()) return;
5040
- if (data !== void 0) {
5041
- console.log(`[opensteer:path] ${message}`, data);
5042
- } else {
5043
- console.log(`[opensteer:path] ${message}`);
5044
- }
5045
- }
5046
- async function disposeHandle(handle) {
5047
- if (!handle) return;
5048
- try {
5049
- await handle.dispose();
5050
- } catch {
5051
- }
5157
+ async function resolveUniqueHandleInFrame(frame, counter) {
5158
+ return frame.evaluateHandle((targetCounter) => {
5159
+ const matches = [];
5160
+ const walk = (root) => {
5161
+ const children = Array.from(root.children);
5162
+ for (const child of children) {
5163
+ if (child.getAttribute("c") === targetCounter) {
5164
+ matches.push(child);
5165
+ }
5166
+ walk(child);
5167
+ if (child.shadowRoot) {
5168
+ walk(child.shadowRoot);
5169
+ }
5170
+ }
5171
+ };
5172
+ walk(document);
5173
+ if (matches.length !== 1) {
5174
+ return null;
5175
+ }
5176
+ return matches[0];
5177
+ }, String(counter));
5052
5178
  }
5053
-
5054
- // src/actions/actionability-probe.ts
5055
- async function probeActionabilityState(element) {
5179
+ async function readCounterValueInFrame(frame, counter, attribute) {
5056
5180
  try {
5057
- return await element.evaluate((target) => {
5058
- if (!(target instanceof Element)) {
5059
- return {
5060
- connected: false,
5061
- visible: null,
5062
- enabled: null,
5063
- editable: null,
5064
- blocker: null
5065
- };
5066
- }
5067
- const connected = target.isConnected;
5068
- if (!connected) {
5069
- return {
5070
- connected: false,
5071
- visible: null,
5072
- enabled: null,
5073
- editable: null,
5074
- blocker: null
5181
+ return await frame.evaluate(
5182
+ ({ targetCounter, attribute: attribute2 }) => {
5183
+ const matches = [];
5184
+ const walk = (root) => {
5185
+ const children = Array.from(root.children);
5186
+ for (const child of children) {
5187
+ if (child.getAttribute("c") === targetCounter) {
5188
+ matches.push(child);
5189
+ }
5190
+ walk(child);
5191
+ if (child.shadowRoot) {
5192
+ walk(child.shadowRoot);
5193
+ }
5194
+ }
5075
5195
  };
5076
- }
5077
- const style = window.getComputedStyle(target);
5078
- const rect = target.getBoundingClientRect();
5079
- const hasBox = rect.width > 0 && rect.height > 0;
5080
- const opacity = Number.parseFloat(style.opacity || "1");
5081
- const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
5082
- let enabled = null;
5083
- if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
5084
- enabled = !target.disabled;
5085
- }
5086
- let editable = null;
5087
- if (target instanceof HTMLInputElement) {
5088
- editable = !target.readOnly && !target.disabled;
5089
- } else if (target instanceof HTMLTextAreaElement) {
5090
- editable = !target.readOnly && !target.disabled;
5091
- } else if (target instanceof HTMLSelectElement) {
5092
- editable = !target.disabled;
5093
- } else if (target instanceof HTMLElement && target.isContentEditable) {
5094
- editable = true;
5095
- }
5096
- let blocker = null;
5097
- if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
5098
- const x = Math.min(
5099
- Math.max(rect.left + rect.width / 2, 0),
5100
- window.innerWidth - 1
5101
- );
5102
- const y = Math.min(
5103
- Math.max(rect.top + rect.height / 2, 0),
5104
- window.innerHeight - 1
5105
- );
5106
- const top = document.elementFromPoint(x, y);
5107
- if (top && top !== target && !target.contains(top)) {
5108
- const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
5109
- blocker = {
5110
- tag: top.tagName.toLowerCase(),
5111
- id: top.id || null,
5112
- classes,
5113
- role: top.getAttribute("role"),
5114
- text: (top.textContent || "").trim().slice(0, 80) || null
5196
+ walk(document);
5197
+ if (!matches.length) {
5198
+ return {
5199
+ status: "missing"
5115
5200
  };
5116
5201
  }
5202
+ if (matches.length > 1) {
5203
+ return {
5204
+ status: "ambiguous"
5205
+ };
5206
+ }
5207
+ const target = matches[0];
5208
+ const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
5209
+ return {
5210
+ status: "ok",
5211
+ value
5212
+ };
5213
+ },
5214
+ {
5215
+ targetCounter: String(counter),
5216
+ attribute
5117
5217
  }
5118
- return {
5119
- connected,
5120
- visible: isVisible,
5121
- enabled,
5122
- editable,
5123
- blocker
5124
- };
5125
- });
5218
+ );
5126
5219
  } catch {
5127
- return null;
5220
+ return {
5221
+ status: "missing"
5222
+ };
5128
5223
  }
5129
5224
  }
5225
+ function buildCounterNotFoundError(counter) {
5226
+ return new CounterResolutionError(
5227
+ "ERR_COUNTER_NOT_FOUND",
5228
+ `Counter ${counter} was not found in the live DOM.`
5229
+ );
5230
+ }
5231
+ function buildCounterAmbiguousError(counter) {
5232
+ return new CounterResolutionError(
5233
+ "ERR_COUNTER_AMBIGUOUS",
5234
+ `Counter ${counter} matches multiple live elements.`
5235
+ );
5236
+ }
5130
5237
 
5131
5238
  // src/actions/failure-classifier.ts
5132
5239
  var ACTION_FAILURE_CODES = [
@@ -5177,7 +5284,7 @@ function defaultActionFailureMessage(action) {
5177
5284
  function classifyActionFailure(input) {
5178
5285
  const typed = classifyTypedError(input.error);
5179
5286
  if (typed) return typed;
5180
- const message = extractErrorMessage(input.error, input.fallbackMessage);
5287
+ const message = extractErrorMessage2(input.error, input.fallbackMessage);
5181
5288
  const fromCallLog = classifyFromPlaywrightMessage(message, input.probe);
5182
5289
  if (fromCallLog) return fromCallLog;
5183
5290
  const fromProbe = classifyFromProbe(input.probe);
@@ -5186,7 +5293,7 @@ function classifyActionFailure(input) {
5186
5293
  if (fromHeuristic) return fromHeuristic;
5187
5294
  return buildFailure({
5188
5295
  code: "UNKNOWN",
5189
- message: ensureMessage(input.fallbackMessage, "Action failed."),
5296
+ message: ensureMessage(message, input.fallbackMessage),
5190
5297
  classificationSource: "unknown"
5191
5298
  });
5192
5299
  }
@@ -5242,13 +5349,6 @@ function classifyTypedError(error) {
5242
5349
  classificationSource: "typed_error"
5243
5350
  });
5244
5351
  }
5245
- if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
5246
- return buildFailure({
5247
- code: "TARGET_UNAVAILABLE",
5248
- message: error.message,
5249
- classificationSource: "typed_error"
5250
- });
5251
- }
5252
5352
  if (error.code === "ERR_COUNTER_AMBIGUOUS") {
5253
5353
  return buildFailure({
5254
5354
  code: "TARGET_AMBIGUOUS",
@@ -5256,13 +5356,6 @@ function classifyTypedError(error) {
5256
5356
  classificationSource: "typed_error"
5257
5357
  });
5258
5358
  }
5259
- if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
5260
- return buildFailure({
5261
- code: "TARGET_STALE",
5262
- message: error.message,
5263
- classificationSource: "typed_error"
5264
- });
5265
- }
5266
5359
  }
5267
5360
  return null;
5268
5361
  }
@@ -5446,13 +5539,22 @@ function defaultRetryableForCode(code) {
5446
5539
  return true;
5447
5540
  }
5448
5541
  }
5449
- function extractErrorMessage(error, fallbackMessage) {
5542
+ function extractErrorMessage2(error, fallbackMessage) {
5450
5543
  if (error instanceof Error && error.message.trim()) {
5451
5544
  return error.message;
5452
5545
  }
5453
5546
  if (typeof error === "string" && error.trim()) {
5454
5547
  return error.trim();
5455
5548
  }
5549
+ if (error && typeof error === "object" && !Array.isArray(error)) {
5550
+ const record = error;
5551
+ if (typeof record.message === "string" && record.message.trim()) {
5552
+ return record.message.trim();
5553
+ }
5554
+ if (typeof record.error === "string" && record.error.trim()) {
5555
+ return record.error.trim();
5556
+ }
5557
+ }
5456
5558
  return ensureMessage(fallbackMessage, "Action failed.");
5457
5559
  }
5458
5560
  function ensureMessage(value, fallback) {
@@ -7772,7 +7874,8 @@ function withTokenQuery(wsUrl, token) {
7772
7874
  // src/cloud/local-cache-sync.ts
7773
7875
  var import_fs3 = __toESM(require("fs"), 1);
7774
7876
  var import_path5 = __toESM(require("path"), 1);
7775
- function collectLocalSelectorCacheEntries(storage) {
7877
+ function collectLocalSelectorCacheEntries(storage, options = {}) {
7878
+ const debug = options.debug === true;
7776
7879
  const namespace = storage.getNamespace();
7777
7880
  const namespaceDir = storage.getNamespaceDir();
7778
7881
  if (!import_fs3.default.existsSync(namespaceDir)) return [];
@@ -7781,7 +7884,7 @@ function collectLocalSelectorCacheEntries(storage) {
7781
7884
  for (const fileName of fileNames) {
7782
7885
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
7783
7886
  const filePath = import_path5.default.join(namespaceDir, fileName);
7784
- const selector = readSelectorFile(filePath);
7887
+ const selector = readSelectorFile(filePath, debug);
7785
7888
  if (!selector) continue;
7786
7889
  const descriptionHash = normalizeDescriptionHash(selector.id);
7787
7890
  const method = normalizeMethod(selector.method);
@@ -7806,11 +7909,20 @@ function collectLocalSelectorCacheEntries(storage) {
7806
7909
  }
7807
7910
  return dedupeNewest(entries);
7808
7911
  }
7809
- function readSelectorFile(filePath) {
7912
+ function readSelectorFile(filePath, debug) {
7810
7913
  try {
7811
7914
  const raw = import_fs3.default.readFileSync(filePath, "utf8");
7812
7915
  return JSON.parse(raw);
7813
- } catch {
7916
+ } catch (error) {
7917
+ const message = extractErrorMessage(
7918
+ error,
7919
+ "Unable to parse selector cache file JSON."
7920
+ );
7921
+ if (debug) {
7922
+ console.warn(
7923
+ `[opensteer] failed to read local selector cache file "${filePath}": ${message}`
7924
+ );
7925
+ }
7814
7926
  return null;
7815
7927
  }
7816
7928
  }
@@ -10018,7 +10130,9 @@ var Opensteer = class _Opensteer {
10018
10130
  this.aiExtract = this.createLazyExtractCallback(model);
10019
10131
  const rootDir = resolved.storage?.rootDir || process.cwd();
10020
10132
  this.namespace = resolveNamespace(resolved, rootDir);
10021
- this.storage = new LocalSelectorStorage(rootDir, this.namespace);
10133
+ this.storage = new LocalSelectorStorage(rootDir, this.namespace, {
10134
+ debug: Boolean(resolved.debug)
10135
+ });
10022
10136
  this.pool = new BrowserPool(resolved.browser || {});
10023
10137
  if (cloudSelection.cloud) {
10024
10138
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
@@ -10037,6 +10151,14 @@ var Opensteer = class _Opensteer {
10037
10151
  this.cloud = null;
10038
10152
  }
10039
10153
  }
10154
+ logDebugError(context, error) {
10155
+ if (!this.config.debug) return;
10156
+ const normalized = normalizeError(error, "Unknown error.");
10157
+ const codeSuffix = normalized.code && normalized.code.trim() ? ` [${normalized.code.trim()}]` : "";
10158
+ console.warn(
10159
+ `[opensteer] ${context}: ${normalized.message}${codeSuffix}`
10160
+ );
10161
+ }
10040
10162
  createLazyResolveCallback(model) {
10041
10163
  let resolverPromise = null;
10042
10164
  return async (...args) => {
@@ -10127,7 +10249,8 @@ var Opensteer = class _Opensteer {
10127
10249
  let tabs;
10128
10250
  try {
10129
10251
  tabs = await this.invokeCloudAction("tabs", {});
10130
- } catch {
10252
+ } catch (error) {
10253
+ this.logDebugError("cloud page reference sync (tabs lookup) failed", error);
10131
10254
  return;
10132
10255
  }
10133
10256
  if (!tabs.length) {
@@ -10265,12 +10388,7 @@ var Opensteer = class _Opensteer {
10265
10388
  try {
10266
10389
  await this.syncLocalSelectorCacheToCloud();
10267
10390
  } catch (error) {
10268
- if (this.config.debug) {
10269
- const message = error instanceof Error ? error.message : String(error);
10270
- console.warn(
10271
- `[opensteer] cloud selector cache sync failed: ${message}`
10272
- );
10273
- }
10391
+ this.logDebugError("cloud selector cache sync failed", error);
10274
10392
  }
10275
10393
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
10276
10394
  this.cloud.localRunId = localRunId;
@@ -10302,7 +10420,12 @@ var Opensteer = class _Opensteer {
10302
10420
  this.cloud.actionClient = actionClient;
10303
10421
  this.cloud.sessionId = sessionId;
10304
10422
  this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
10305
- await this.syncCloudPageRef().catch(() => void 0);
10423
+ await this.syncCloudPageRef().catch((error) => {
10424
+ this.logDebugError(
10425
+ "cloud page reference sync after launch failed",
10426
+ error
10427
+ );
10428
+ });
10306
10429
  this.announceCloudSession({
10307
10430
  sessionId: session2.sessionId,
10308
10431
  workspaceId: session2.cloudSession.workspaceId,
@@ -10389,7 +10512,9 @@ var Opensteer = class _Opensteer {
10389
10512
  }
10390
10513
  async syncLocalSelectorCacheToCloud() {
10391
10514
  if (!this.cloud) return;
10392
- const entries = collectLocalSelectorCacheEntries(this.storage);
10515
+ const entries = collectLocalSelectorCacheEntries(this.storage, {
10516
+ debug: Boolean(this.config.debug)
10517
+ });
10393
10518
  if (!entries.length) return;
10394
10519
  await this.cloud.sessionClient.importSelectorCache({
10395
10520
  entries
@@ -10398,9 +10523,12 @@ var Opensteer = class _Opensteer {
10398
10523
  async goto(url, options) {
10399
10524
  if (this.cloud) {
10400
10525
  await this.invokeCloudActionAndResetCache("goto", { url, options });
10401
- await this.syncCloudPageRef({ expectedUrl: url }).catch(
10402
- () => void 0
10403
- );
10526
+ await this.syncCloudPageRef({ expectedUrl: url }).catch((error) => {
10527
+ this.logDebugError(
10528
+ "cloud page reference sync after goto failed",
10529
+ error
10530
+ );
10531
+ });
10404
10532
  return;
10405
10533
  }
10406
10534
  const { waitUntil = "domcontentloaded", ...rest } = options ?? {};
@@ -10501,7 +10629,7 @@ var Opensteer = class _Opensteer {
10501
10629
  let persistPath = null;
10502
10630
  try {
10503
10631
  if (storageKey && resolution.shouldPersist) {
10504
- persistPath = await this.buildPathFromResolvedHandle(
10632
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10505
10633
  handle,
10506
10634
  "hover",
10507
10635
  resolution.counter
@@ -10600,7 +10728,7 @@ var Opensteer = class _Opensteer {
10600
10728
  let persistPath = null;
10601
10729
  try {
10602
10730
  if (storageKey && resolution.shouldPersist) {
10603
- persistPath = await this.buildPathFromResolvedHandle(
10731
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10604
10732
  handle,
10605
10733
  "input",
10606
10734
  resolution.counter
@@ -10703,7 +10831,7 @@ var Opensteer = class _Opensteer {
10703
10831
  let persistPath = null;
10704
10832
  try {
10705
10833
  if (storageKey && resolution.shouldPersist) {
10706
- persistPath = await this.buildPathFromResolvedHandle(
10834
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10707
10835
  handle,
10708
10836
  "select",
10709
10837
  resolution.counter
@@ -10813,7 +10941,7 @@ var Opensteer = class _Opensteer {
10813
10941
  let persistPath = null;
10814
10942
  try {
10815
10943
  if (storageKey && resolution.shouldPersist) {
10816
- persistPath = await this.buildPathFromResolvedHandle(
10944
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10817
10945
  handle,
10818
10946
  "scroll",
10819
10947
  resolution.counter
@@ -10912,7 +11040,12 @@ var Opensteer = class _Opensteer {
10912
11040
  }
10913
11041
  );
10914
11042
  await this.syncCloudPageRef({ expectedUrl: result.url }).catch(
10915
- () => void 0
11043
+ (error) => {
11044
+ this.logDebugError(
11045
+ "cloud page reference sync after newTab failed",
11046
+ error
11047
+ );
11048
+ }
10916
11049
  );
10917
11050
  return result;
10918
11051
  }
@@ -10924,7 +11057,12 @@ var Opensteer = class _Opensteer {
10924
11057
  async switchTab(index) {
10925
11058
  if (this.cloud) {
10926
11059
  await this.invokeCloudActionAndResetCache("switchTab", { index });
10927
- await this.syncCloudPageRef().catch(() => void 0);
11060
+ await this.syncCloudPageRef().catch((error) => {
11061
+ this.logDebugError(
11062
+ "cloud page reference sync after switchTab failed",
11063
+ error
11064
+ );
11065
+ });
10928
11066
  return;
10929
11067
  }
10930
11068
  const page = await switchTab(this.context, index);
@@ -10934,7 +11072,12 @@ var Opensteer = class _Opensteer {
10934
11072
  async closeTab(index) {
10935
11073
  if (this.cloud) {
10936
11074
  await this.invokeCloudActionAndResetCache("closeTab", { index });
10937
- await this.syncCloudPageRef().catch(() => void 0);
11075
+ await this.syncCloudPageRef().catch((error) => {
11076
+ this.logDebugError(
11077
+ "cloud page reference sync after closeTab failed",
11078
+ error
11079
+ );
11080
+ });
10938
11081
  return;
10939
11082
  }
10940
11083
  const newPage = await closeTab(this.context, this.page, index);
@@ -11094,22 +11237,28 @@ var Opensteer = class _Opensteer {
11094
11237
  const handle = await this.resolveCounterHandle(resolution.counter);
11095
11238
  try {
11096
11239
  if (storageKey && resolution.shouldPersist) {
11097
- const persistPath = await this.buildPathFromResolvedHandle(
11240
+ const persistPath = await this.tryBuildPathFromResolvedHandle(
11098
11241
  handle,
11099
11242
  method,
11100
11243
  resolution.counter
11101
11244
  );
11102
- this.persistPath(
11103
- storageKey,
11104
- method,
11105
- options.description,
11106
- persistPath
11107
- );
11245
+ if (persistPath) {
11246
+ this.persistPath(
11247
+ storageKey,
11248
+ method,
11249
+ options.description,
11250
+ persistPath
11251
+ );
11252
+ }
11108
11253
  }
11109
11254
  return await counterFn(handle);
11110
11255
  } catch (err) {
11111
- const message = err instanceof Error ? err.message : `${method} failed.`;
11112
- throw new Error(message);
11256
+ if (err instanceof Error) {
11257
+ throw err;
11258
+ }
11259
+ throw new Error(
11260
+ `${method} failed. ${extractErrorMessage(err, "Unknown error.")}`
11261
+ );
11113
11262
  } finally {
11114
11263
  await handle.dispose();
11115
11264
  }
@@ -11138,7 +11287,7 @@ var Opensteer = class _Opensteer {
11138
11287
  let persistPath = null;
11139
11288
  try {
11140
11289
  if (storageKey && resolution.shouldPersist) {
11141
- persistPath = await this.buildPathFromResolvedHandle(
11290
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11142
11291
  handle,
11143
11292
  "uploadFile",
11144
11293
  resolution.counter
@@ -11236,6 +11385,9 @@ var Opensteer = class _Opensteer {
11236
11385
  if (this.cloud) {
11237
11386
  return await this.invokeCloudAction("extract", options);
11238
11387
  }
11388
+ if (options.schema !== void 0) {
11389
+ assertValidExtractSchemaRoot(options.schema);
11390
+ }
11239
11391
  const storageKey = this.resolveStorageKey(options.description);
11240
11392
  const schemaHash = options.schema ? computeSchemaHash(options.schema) : null;
11241
11393
  const stored = storageKey ? this.storage.readSelector(storageKey) : null;
@@ -11244,7 +11396,7 @@ var Opensteer = class _Opensteer {
11244
11396
  try {
11245
11397
  payload = normalizePersistedExtractPayload(stored.path);
11246
11398
  } catch (err) {
11247
- const message = err instanceof Error ? err.message : "Unknown error";
11399
+ const message = extractErrorMessage(err, "Unknown error.");
11248
11400
  const selectorFile = storageKey ? this.storage.getSelectorPath(storageKey) : "unknown selector file";
11249
11401
  throw new Error(
11250
11402
  `Cached extraction selector is invalid for the current schema at "${selectorFile}". Delete the cached selector and rerun extraction. ${message}`
@@ -11261,7 +11413,16 @@ var Opensteer = class _Opensteer {
11261
11413
  fields.push(...schemaFields);
11262
11414
  }
11263
11415
  if (!fields.length) {
11264
- const planResult = await this.parseAiExtractPlan(options);
11416
+ let planResult;
11417
+ try {
11418
+ planResult = await this.parseAiExtractPlan(options);
11419
+ } catch (error) {
11420
+ const message = extractErrorMessage(error, "Unknown error.");
11421
+ const contextMessage = options.schema ? "Schema extraction did not resolve deterministic field targets, so Opensteer attempted AI extraction planning." : "Opensteer attempted AI extraction planning.";
11422
+ throw new Error(`${contextMessage} ${message}`, {
11423
+ cause: error
11424
+ });
11425
+ }
11265
11426
  if (planResult.fields.length) {
11266
11427
  fields.push(...planResult.fields);
11267
11428
  } else if (planResult.data !== void 0) {
@@ -11402,7 +11563,7 @@ var Opensteer = class _Opensteer {
11402
11563
  let persistPath = null;
11403
11564
  try {
11404
11565
  if (storageKey && resolution.shouldPersist) {
11405
- persistPath = await this.buildPathFromResolvedHandle(
11566
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11406
11567
  handle,
11407
11568
  "click",
11408
11569
  resolution.counter
@@ -11498,17 +11659,6 @@ var Opensteer = class _Opensteer {
11498
11659
  }
11499
11660
  }
11500
11661
  if (options.element != null) {
11501
- const pathFromElement = await this.tryBuildPathFromCounter(
11502
- options.element
11503
- );
11504
- if (pathFromElement) {
11505
- return {
11506
- path: pathFromElement,
11507
- counter: null,
11508
- shouldPersist: Boolean(storageKey),
11509
- source: "element"
11510
- };
11511
- }
11512
11662
  return {
11513
11663
  path: null,
11514
11664
  counter: options.element,
@@ -11536,17 +11686,6 @@ var Opensteer = class _Opensteer {
11536
11686
  options.description
11537
11687
  );
11538
11688
  if (resolved?.counter != null) {
11539
- const pathFromAiCounter = await this.tryBuildPathFromCounter(
11540
- resolved.counter
11541
- );
11542
- if (pathFromAiCounter) {
11543
- return {
11544
- path: pathFromAiCounter,
11545
- counter: null,
11546
- shouldPersist: Boolean(storageKey),
11547
- source: "ai"
11548
- };
11549
- }
11550
11689
  return {
11551
11690
  path: null,
11552
11691
  counter: resolved.counter,
@@ -11628,23 +11767,22 @@ var Opensteer = class _Opensteer {
11628
11767
  try {
11629
11768
  const builtPath = await buildElementPathFromHandle(handle);
11630
11769
  if (builtPath) {
11631
- return this.withIndexedIframeContext(builtPath, indexedPath);
11770
+ const withFrameContext = await this.withHandleIframeContext(
11771
+ handle,
11772
+ builtPath
11773
+ );
11774
+ return this.withIndexedIframeContext(
11775
+ withFrameContext,
11776
+ indexedPath
11777
+ );
11632
11778
  }
11633
11779
  return indexedPath;
11634
11780
  } finally {
11635
11781
  await handle.dispose();
11636
11782
  }
11637
11783
  }
11638
- async tryBuildPathFromCounter(counter) {
11639
- try {
11640
- return await this.buildPathFromElement(counter);
11641
- } catch {
11642
- return null;
11643
- }
11644
- }
11645
11784
  async resolveCounterHandle(element) {
11646
- const snapshot = await this.ensureSnapshotWithCounters();
11647
- return resolveCounterElement(this.page, snapshot, element);
11785
+ return resolveCounterElement(this.page, element);
11648
11786
  }
11649
11787
  async resolveCounterHandleForAction(action, description, element) {
11650
11788
  try {
@@ -11668,8 +11806,12 @@ var Opensteer = class _Opensteer {
11668
11806
  const indexedPath = await this.readPathFromCounterIndex(counter);
11669
11807
  const builtPath = await buildElementPathFromHandle(handle);
11670
11808
  if (builtPath) {
11809
+ const withFrameContext = await this.withHandleIframeContext(
11810
+ handle,
11811
+ builtPath
11812
+ );
11671
11813
  const normalized = this.withIndexedIframeContext(
11672
- builtPath,
11814
+ withFrameContext,
11673
11815
  indexedPath
11674
11816
  );
11675
11817
  if (normalized.nodes.length) return normalized;
@@ -11679,15 +11821,34 @@ var Opensteer = class _Opensteer {
11679
11821
  `Unable to build element path from counter ${counter} during ${action}.`
11680
11822
  );
11681
11823
  }
11824
+ async tryBuildPathFromResolvedHandle(handle, action, counter) {
11825
+ try {
11826
+ return await this.buildPathFromResolvedHandle(handle, action, counter);
11827
+ } catch (error) {
11828
+ this.logDebugError(
11829
+ `path persistence skipped for ${action} counter ${counter}`,
11830
+ error
11831
+ );
11832
+ return null;
11833
+ }
11834
+ }
11682
11835
  withIndexedIframeContext(builtPath, indexedPath) {
11683
11836
  const normalizedBuilt = this.normalizePath(builtPath);
11684
11837
  if (!indexedPath) return normalizedBuilt;
11685
11838
  const iframePrefix = collectIframeContextPrefix(indexedPath);
11686
11839
  if (!iframePrefix.length) return normalizedBuilt;
11840
+ const builtContext = cloneContextHops(normalizedBuilt.context);
11841
+ const overlap = measureContextOverlap(iframePrefix, builtContext);
11842
+ const missingPrefix = cloneContextHops(
11843
+ iframePrefix.slice(0, iframePrefix.length - overlap)
11844
+ );
11845
+ if (!missingPrefix.length) {
11846
+ return normalizedBuilt;
11847
+ }
11687
11848
  const merged = {
11688
11849
  context: [
11689
- ...cloneContextHops(iframePrefix),
11690
- ...cloneContextHops(normalizedBuilt.context)
11850
+ ...missingPrefix,
11851
+ ...builtContext
11691
11852
  ],
11692
11853
  nodes: cloneElementPath(normalizedBuilt).nodes
11693
11854
  };
@@ -11697,9 +11858,48 @@ var Opensteer = class _Opensteer {
11697
11858
  if (fallback.nodes.length) return fallback;
11698
11859
  return normalizedBuilt;
11699
11860
  }
11861
+ async withHandleIframeContext(handle, path5) {
11862
+ const ownFrame = await handle.ownerFrame();
11863
+ if (!ownFrame) {
11864
+ return this.normalizePath(path5);
11865
+ }
11866
+ let frame = ownFrame;
11867
+ let prefix = [];
11868
+ while (frame && frame !== this.page.mainFrame()) {
11869
+ const parent = frame.parentFrame();
11870
+ if (!parent) break;
11871
+ const frameElement = await frame.frameElement().catch(() => null);
11872
+ if (!frameElement) break;
11873
+ try {
11874
+ const frameElementPath = await buildElementPathFromHandle(frameElement);
11875
+ if (frameElementPath?.nodes.length) {
11876
+ const segment = [
11877
+ ...cloneContextHops(frameElementPath.context),
11878
+ {
11879
+ kind: "iframe",
11880
+ host: cloneElementPath(frameElementPath).nodes
11881
+ }
11882
+ ];
11883
+ prefix = [...segment, ...prefix];
11884
+ }
11885
+ } finally {
11886
+ await frameElement.dispose().catch(() => void 0);
11887
+ }
11888
+ frame = parent;
11889
+ }
11890
+ if (!prefix.length) {
11891
+ return this.normalizePath(path5);
11892
+ }
11893
+ return this.normalizePath({
11894
+ context: [...prefix, ...cloneContextHops(path5.context)],
11895
+ nodes: cloneElementPath(path5).nodes
11896
+ });
11897
+ }
11700
11898
  async readPathFromCounterIndex(counter) {
11701
- const snapshot = await this.ensureSnapshotWithCounters();
11702
- const indexed = snapshot.counterIndex?.get(counter);
11899
+ if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
11900
+ return null;
11901
+ }
11902
+ const indexed = this.snapshotCache.counterIndex.get(counter);
11703
11903
  if (!indexed) return null;
11704
11904
  const normalized = this.normalizePath(indexed);
11705
11905
  if (!normalized.nodes.length) return null;
@@ -11710,15 +11910,6 @@ var Opensteer = class _Opensteer {
11710
11910
  if (!path5) return null;
11711
11911
  return this.normalizePath(path5);
11712
11912
  }
11713
- async ensureSnapshotWithCounters() {
11714
- if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
11715
- await this.snapshot({
11716
- mode: "full",
11717
- withCounters: true
11718
- });
11719
- }
11720
- return this.snapshotCache;
11721
- }
11722
11913
  persistPath(id, method, description, path5) {
11723
11914
  const now = Date.now();
11724
11915
  const safeFile = this.storage.getSelectorFileName(id);
@@ -11923,12 +12114,6 @@ var Opensteer = class _Opensteer {
11923
12114
  };
11924
12115
  }
11925
12116
  async buildFieldTargetsFromSchema(schema) {
11926
- if (!schema || typeof schema !== "object") {
11927
- return [];
11928
- }
11929
- if (Array.isArray(schema)) {
11930
- return [];
11931
- }
11932
12117
  const fields = [];
11933
12118
  await this.collectFieldTargetsFromSchemaObject(
11934
12119
  schema,
@@ -11974,17 +12159,6 @@ var Opensteer = class _Opensteer {
11974
12159
  return;
11975
12160
  }
11976
12161
  if (normalized.element != null) {
11977
- const path5 = await this.tryBuildPathFromCounter(
11978
- normalized.element
11979
- );
11980
- if (path5) {
11981
- fields.push({
11982
- key: fieldKey,
11983
- path: path5,
11984
- attribute: normalized.attribute
11985
- });
11986
- return;
11987
- }
11988
12162
  fields.push({
11989
12163
  key: fieldKey,
11990
12164
  counter: normalized.element,
@@ -12002,6 +12176,10 @@ var Opensteer = class _Opensteer {
12002
12176
  path: path5,
12003
12177
  attribute: normalized.attribute
12004
12178
  });
12179
+ } else {
12180
+ throw new Error(
12181
+ `Extraction schema field "${fieldKey}" uses selector "${normalized.selector}", but no matching element path could be built from the current page snapshot.`
12182
+ );
12005
12183
  }
12006
12184
  return;
12007
12185
  }
@@ -12025,15 +12203,6 @@ var Opensteer = class _Opensteer {
12025
12203
  continue;
12026
12204
  }
12027
12205
  if (fieldPlan.element != null) {
12028
- const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
12029
- if (path6) {
12030
- fields.push({
12031
- key,
12032
- path: path6,
12033
- attribute: fieldPlan.attribute
12034
- });
12035
- continue;
12036
- }
12037
12206
  fields.push({
12038
12207
  key,
12039
12208
  counter: fieldPlan.element,
@@ -12088,12 +12257,7 @@ var Opensteer = class _Opensteer {
12088
12257
  }
12089
12258
  }
12090
12259
  if (counterRequests.length) {
12091
- const snapshot = await this.ensureSnapshotWithCounters();
12092
- const counterValues = await resolveCountersBatch(
12093
- this.page,
12094
- snapshot,
12095
- counterRequests
12096
- );
12260
+ const counterValues = await resolveCountersBatch(this.page, counterRequests);
12097
12261
  Object.assign(result, counterValues);
12098
12262
  }
12099
12263
  if (pathFields.length) {
@@ -12123,7 +12287,7 @@ var Opensteer = class _Opensteer {
12123
12287
  const path5 = await this.buildPathFromElement(field.counter);
12124
12288
  if (!path5) {
12125
12289
  throw new Error(
12126
- `Unable to build element path from counter ${field.counter} for extraction field "${field.key}".`
12290
+ `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
12127
12291
  );
12128
12292
  }
12129
12293
  resolved.push({
@@ -12145,7 +12309,7 @@ var Opensteer = class _Opensteer {
12145
12309
  }
12146
12310
  resolveStorageKey(description) {
12147
12311
  if (!description) return null;
12148
- return (0, import_crypto2.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12312
+ return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12149
12313
  }
12150
12314
  normalizePath(path5) {
12151
12315
  return sanitizeElementPath(path5);
@@ -12169,6 +12333,33 @@ function collectIframeContextPrefix(path5) {
12169
12333
  if (lastIframeIndex < 0) return [];
12170
12334
  return cloneContextHops(context.slice(0, lastIframeIndex + 1));
12171
12335
  }
12336
+ function measureContextOverlap(indexedPrefix, builtContext) {
12337
+ const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
12338
+ for (let size = maxOverlap; size > 0; size -= 1) {
12339
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
12340
+ return size;
12341
+ }
12342
+ }
12343
+ for (let size = maxOverlap; size > 0; size -= 1) {
12344
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
12345
+ return size;
12346
+ }
12347
+ }
12348
+ return 0;
12349
+ }
12350
+ function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
12351
+ for (let idx = 0; idx < size; idx += 1) {
12352
+ const left = indexedPrefix[indexedPrefix.length - size + idx];
12353
+ const right = builtContext[idx];
12354
+ if (left.kind !== right.kind) {
12355
+ return false;
12356
+ }
12357
+ if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
12358
+ return false;
12359
+ }
12360
+ }
12361
+ return true;
12362
+ }
12172
12363
  function normalizeSchemaValue(value) {
12173
12364
  if (!value) return null;
12174
12365
  if (typeof value !== "object" || Array.isArray(value)) {
@@ -12190,7 +12381,7 @@ function normalizeExtractSource(source) {
12190
12381
  }
12191
12382
  function computeSchemaHash(schema) {
12192
12383
  const stable = stableStringify(schema);
12193
- return (0, import_crypto2.createHash)("sha256").update(stable).digest("hex");
12384
+ return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
12194
12385
  }
12195
12386
  function buildPathMap(fields) {
12196
12387
  const out = {};
@@ -12352,13 +12543,28 @@ function countNonNullLeaves(value) {
12352
12543
  function isPrimitiveLike(value) {
12353
12544
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
12354
12545
  }
12546
+ function assertValidExtractSchemaRoot(schema) {
12547
+ if (!schema || typeof schema !== "object") {
12548
+ throw new Error(
12549
+ "Invalid extraction schema: expected a JSON object at the top level."
12550
+ );
12551
+ }
12552
+ if (Array.isArray(schema)) {
12553
+ throw new Error(
12554
+ 'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
12555
+ );
12556
+ }
12557
+ }
12355
12558
  function parseAiExtractResponse(response) {
12356
12559
  if (typeof response === "string") {
12357
12560
  const trimmed = stripCodeFence2(response);
12358
12561
  try {
12359
12562
  return JSON.parse(trimmed);
12360
12563
  } catch {
12361
- throw new Error("LLM extraction returned a non-JSON string.");
12564
+ const preview = summarizeForError(trimmed);
12565
+ throw new Error(
12566
+ `LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
12567
+ );
12362
12568
  }
12363
12569
  }
12364
12570
  if (response && typeof response === "object") {
@@ -12383,6 +12589,12 @@ function stripCodeFence2(input) {
12383
12589
  if (lastFence === -1) return withoutHeader.trim();
12384
12590
  return withoutHeader.slice(0, lastFence).trim();
12385
12591
  }
12592
+ function summarizeForError(input, maxLength = 180) {
12593
+ const compact = input.replace(/\s+/g, " ").trim();
12594
+ if (!compact) return "";
12595
+ if (compact.length <= maxLength) return compact;
12596
+ return `${compact.slice(0, maxLength)}...`;
12597
+ }
12386
12598
  function getScrollDelta2(options) {
12387
12599
  const amount = typeof options.amount === "number" ? options.amount : 600;
12388
12600
  const absoluteAmount = Math.abs(amount);
@@ -12405,7 +12617,7 @@ function isInternalOrBlankPageUrl(url) {
12405
12617
  }
12406
12618
  function buildLocalRunId(namespace) {
12407
12619
  const normalized = namespace.trim() || "default";
12408
- return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto2.randomUUID)().slice(0, 8)}`;
12620
+ return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
12409
12621
  }
12410
12622
 
12411
12623
  // src/ai/index.ts
@@ -12460,7 +12672,6 @@ init_model();
12460
12672
  createExtractCallback,
12461
12673
  createResolveCallback,
12462
12674
  createTab,
12463
- ensureLiveCounters,
12464
12675
  exportCookies,
12465
12676
  extractArrayRowsWithPaths,
12466
12677
  extractArrayWithPaths,