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.
@@ -108,6 +108,15 @@ function resolveProviderInfo(modelStr) {
108
108
  return info;
109
109
  }
110
110
  }
111
+ const slash = modelStr.indexOf("/");
112
+ if (slash > 0) {
113
+ const provider = modelStr.slice(0, slash).trim().toLowerCase();
114
+ if (provider) {
115
+ throw new Error(
116
+ `Unsupported model provider prefix "${provider}" in model "${modelStr}". Use one of: openai, anthropic, google, xai, groq.`
117
+ );
118
+ }
119
+ }
111
120
  return { pkg: "@ai-sdk/openai", providerFn: "openai" };
112
121
  }
113
122
  function stripProviderPrefix(modelStr) {
@@ -351,7 +360,7 @@ var import_net = require("net");
351
360
  var import_fs4 = require("fs");
352
361
 
353
362
  // src/opensteer.ts
354
- var import_crypto2 = require("crypto");
363
+ var import_crypto = require("crypto");
355
364
 
356
365
  // src/browser/pool.ts
357
366
  var import_playwright = require("playwright");
@@ -821,6 +830,232 @@ var import_path3 = __toESM(require("path"), 1);
821
830
  var import_url = require("url");
822
831
  var import_dotenv = require("dotenv");
823
832
 
833
+ // src/error-normalization.ts
834
+ function extractErrorMessage(error, fallback = "Unknown error.") {
835
+ if (error instanceof Error) {
836
+ const message = error.message.trim();
837
+ if (message) return message;
838
+ const name = error.name.trim();
839
+ if (name) return name;
840
+ }
841
+ if (typeof error === "string" && error.trim()) {
842
+ return error.trim();
843
+ }
844
+ const record = asRecord(error);
845
+ const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
846
+ if (recordMessage) {
847
+ return recordMessage;
848
+ }
849
+ return fallback;
850
+ }
851
+ function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
852
+ const seen = /* @__PURE__ */ new WeakSet();
853
+ return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
854
+ }
855
+ function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
856
+ const record = asRecord(error);
857
+ if (record) {
858
+ if (seen.has(record)) {
859
+ return {
860
+ message: extractErrorMessage(error, fallback)
861
+ };
862
+ }
863
+ seen.add(record);
864
+ }
865
+ const message = extractErrorMessage(error, fallback);
866
+ const code = extractCode(error);
867
+ const name = extractName(error);
868
+ const details = extractDetails(error);
869
+ if (depthRemaining <= 0) {
870
+ return compactErrorInfo({
871
+ message,
872
+ ...code ? { code } : {},
873
+ ...name ? { name } : {},
874
+ ...details ? { details } : {}
875
+ });
876
+ }
877
+ const cause = extractCause(error);
878
+ if (!cause) {
879
+ return compactErrorInfo({
880
+ message,
881
+ ...code ? { code } : {},
882
+ ...name ? { name } : {},
883
+ ...details ? { details } : {}
884
+ });
885
+ }
886
+ const normalizedCause = normalizeErrorInternal(
887
+ cause,
888
+ "Caused by an unknown error.",
889
+ depthRemaining - 1,
890
+ seen
891
+ );
892
+ return compactErrorInfo({
893
+ message,
894
+ ...code ? { code } : {},
895
+ ...name ? { name } : {},
896
+ ...details ? { details } : {},
897
+ cause: normalizedCause
898
+ });
899
+ }
900
+ function compactErrorInfo(info) {
901
+ const safeDetails = toJsonSafeRecord(info.details);
902
+ return {
903
+ message: info.message,
904
+ ...info.code ? { code: info.code } : {},
905
+ ...info.name ? { name: info.name } : {},
906
+ ...safeDetails ? { details: safeDetails } : {},
907
+ ...info.cause ? { cause: info.cause } : {}
908
+ };
909
+ }
910
+ function extractCode(error) {
911
+ const record = asRecord(error);
912
+ const raw = record?.code;
913
+ if (typeof raw === "string" && raw.trim()) {
914
+ return raw.trim();
915
+ }
916
+ if (typeof raw === "number" && Number.isFinite(raw)) {
917
+ return String(raw);
918
+ }
919
+ return void 0;
920
+ }
921
+ function extractName(error) {
922
+ if (error instanceof Error && error.name.trim()) {
923
+ return error.name.trim();
924
+ }
925
+ const record = asRecord(error);
926
+ return toNonEmptyString(record?.name);
927
+ }
928
+ function extractDetails(error) {
929
+ const record = asRecord(error);
930
+ if (!record) return void 0;
931
+ const details = {};
932
+ const rawDetails = asRecord(record.details);
933
+ if (rawDetails) {
934
+ Object.assign(details, rawDetails);
935
+ }
936
+ const action = toNonEmptyString(record.action);
937
+ if (action) {
938
+ details.action = action;
939
+ }
940
+ const selectorUsed = toNonEmptyString(record.selectorUsed);
941
+ if (selectorUsed) {
942
+ details.selectorUsed = selectorUsed;
943
+ }
944
+ if (typeof record.status === "number" && Number.isFinite(record.status)) {
945
+ details.status = record.status;
946
+ }
947
+ const failure = asRecord(record.failure);
948
+ if (failure) {
949
+ const failureCode = toNonEmptyString(failure.code);
950
+ const classificationSource = toNonEmptyString(
951
+ failure.classificationSource
952
+ );
953
+ const failureDetails = asRecord(failure.details);
954
+ if (failureCode || classificationSource || failureDetails) {
955
+ details.actionFailure = {
956
+ ...failureCode ? { code: failureCode } : {},
957
+ ...classificationSource ? { classificationSource } : {},
958
+ ...failureDetails ? { details: failureDetails } : {}
959
+ };
960
+ }
961
+ }
962
+ return Object.keys(details).length ? details : void 0;
963
+ }
964
+ function extractCause(error) {
965
+ if (error instanceof Error) {
966
+ return error.cause;
967
+ }
968
+ const record = asRecord(error);
969
+ return record?.cause;
970
+ }
971
+ function asRecord(value) {
972
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
973
+ return null;
974
+ }
975
+ return value;
976
+ }
977
+ function toNonEmptyString(value) {
978
+ if (typeof value !== "string") return void 0;
979
+ const normalized = value.trim();
980
+ return normalized.length ? normalized : void 0;
981
+ }
982
+ function toJsonSafeRecord(value) {
983
+ if (!value) return void 0;
984
+ const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
985
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
986
+ return void 0;
987
+ }
988
+ const record = sanitized;
989
+ return Object.keys(record).length > 0 ? record : void 0;
990
+ }
991
+ function toJsonSafeValue(value, seen) {
992
+ if (value === null) return null;
993
+ if (typeof value === "string" || typeof value === "boolean") {
994
+ return value;
995
+ }
996
+ if (typeof value === "number") {
997
+ return Number.isFinite(value) ? value : null;
998
+ }
999
+ if (typeof value === "bigint") {
1000
+ return value.toString();
1001
+ }
1002
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
1003
+ return void 0;
1004
+ }
1005
+ if (value instanceof Date) {
1006
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
1007
+ }
1008
+ if (Array.isArray(value)) {
1009
+ if (seen.has(value)) return "[Circular]";
1010
+ seen.add(value);
1011
+ const output = value.map((item) => {
1012
+ const next = toJsonSafeValue(item, seen);
1013
+ return next === void 0 ? null : next;
1014
+ });
1015
+ seen.delete(value);
1016
+ return output;
1017
+ }
1018
+ if (value instanceof Set) {
1019
+ if (seen.has(value)) return "[Circular]";
1020
+ seen.add(value);
1021
+ const output = Array.from(value, (item) => {
1022
+ const next = toJsonSafeValue(item, seen);
1023
+ return next === void 0 ? null : next;
1024
+ });
1025
+ seen.delete(value);
1026
+ return output;
1027
+ }
1028
+ if (value instanceof Map) {
1029
+ if (seen.has(value)) return "[Circular]";
1030
+ seen.add(value);
1031
+ const output = {};
1032
+ for (const [key, item] of value.entries()) {
1033
+ const normalizedKey = String(key);
1034
+ const next = toJsonSafeValue(item, seen);
1035
+ if (next !== void 0) {
1036
+ output[normalizedKey] = next;
1037
+ }
1038
+ }
1039
+ seen.delete(value);
1040
+ return output;
1041
+ }
1042
+ if (typeof value === "object") {
1043
+ const objectValue = value;
1044
+ if (seen.has(objectValue)) return "[Circular]";
1045
+ seen.add(objectValue);
1046
+ const output = {};
1047
+ for (const [key, item] of Object.entries(objectValue)) {
1048
+ const next = toJsonSafeValue(item, seen);
1049
+ if (next !== void 0) {
1050
+ output[key] = next;
1051
+ }
1052
+ }
1053
+ seen.delete(objectValue);
1054
+ return output;
1055
+ }
1056
+ return void 0;
1057
+ }
1058
+
824
1059
  // src/storage/namespace.ts
825
1060
  var import_path2 = __toESM(require("path"), 1);
826
1061
  var DEFAULT_NAMESPACE = "default";
@@ -884,11 +1119,12 @@ function dotenvFileOrder(nodeEnv) {
884
1119
  files.push(".env");
885
1120
  return files;
886
1121
  }
887
- function loadDotenvValues(rootDir, baseEnv) {
1122
+ function loadDotenvValues(rootDir, baseEnv, options = {}) {
888
1123
  const values = {};
889
1124
  if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
890
1125
  return values;
891
1126
  }
1127
+ const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
892
1128
  const baseDir = import_path3.default.resolve(rootDir);
893
1129
  const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
894
1130
  for (const filename of dotenvFileOrder(nodeEnv)) {
@@ -902,15 +1138,24 @@ function loadDotenvValues(rootDir, baseEnv) {
902
1138
  values[key] = value;
903
1139
  }
904
1140
  }
905
- } catch {
1141
+ } catch (error) {
1142
+ const message = extractErrorMessage(
1143
+ error,
1144
+ "Unable to read or parse dotenv file."
1145
+ );
1146
+ if (debug) {
1147
+ console.warn(
1148
+ `[opensteer] failed to load dotenv file "${filePath}": ${message}`
1149
+ );
1150
+ }
906
1151
  continue;
907
1152
  }
908
1153
  }
909
1154
  return values;
910
1155
  }
911
- function resolveEnv(rootDir) {
1156
+ function resolveEnv(rootDir, options = {}) {
912
1157
  const baseEnv = process.env;
913
- const dotenvValues = loadDotenvValues(rootDir, baseEnv);
1158
+ const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
914
1159
  return {
915
1160
  ...dotenvValues,
916
1161
  ...baseEnv
@@ -954,13 +1199,22 @@ function assertNoLegacyRuntimeConfig(source, config) {
954
1199
  );
955
1200
  }
956
1201
  }
957
- function loadConfigFile(rootDir) {
1202
+ function loadConfigFile(rootDir, options = {}) {
958
1203
  const configPath = import_path3.default.join(rootDir, ".opensteer", "config.json");
959
1204
  if (!import_fs.default.existsSync(configPath)) return {};
960
1205
  try {
961
1206
  const raw = import_fs.default.readFileSync(configPath, "utf8");
962
1207
  return JSON.parse(raw);
963
- } catch {
1208
+ } catch (error) {
1209
+ const message = extractErrorMessage(
1210
+ error,
1211
+ "Unable to read or parse config file."
1212
+ );
1213
+ if (options.debug) {
1214
+ console.warn(
1215
+ `[opensteer] failed to load config file "${configPath}": ${message}`
1216
+ );
1217
+ }
964
1218
  return {};
965
1219
  }
966
1220
  }
@@ -1092,6 +1346,8 @@ function resolveCloudSelection(config, env = process.env) {
1092
1346
  };
1093
1347
  }
1094
1348
  function resolveConfig(input = {}) {
1349
+ const processEnv = process.env;
1350
+ const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
1095
1351
  const initialRootDir = input.storage?.rootDir ?? process.cwd();
1096
1352
  const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
1097
1353
  storage: {
@@ -1100,12 +1356,16 @@ function resolveConfig(input = {}) {
1100
1356
  });
1101
1357
  assertNoLegacyAiConfig("Opensteer constructor config", input);
1102
1358
  assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
1103
- const fileConfig = loadConfigFile(initialRootDir);
1359
+ const fileConfig = loadConfigFile(initialRootDir, {
1360
+ debug: debugHint
1361
+ });
1104
1362
  assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
1105
1363
  assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
1106
1364
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1107
1365
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1108
- const env = resolveEnv(envRootDir);
1366
+ const env = resolveEnv(envRootDir, {
1367
+ debug: debugHint
1368
+ });
1109
1369
  if (env.OPENSTEER_AI_MODEL) {
1110
1370
  throw new Error(
1111
1371
  "OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
@@ -1789,9 +2049,11 @@ function createEmptyRegistry(name) {
1789
2049
  var LocalSelectorStorage = class {
1790
2050
  rootDir;
1791
2051
  namespace;
1792
- constructor(rootDir, namespace) {
2052
+ debug;
2053
+ constructor(rootDir, namespace, options = {}) {
1793
2054
  this.rootDir = rootDir;
1794
2055
  this.namespace = normalizeNamespace(namespace);
2056
+ this.debug = options.debug === true;
1795
2057
  }
1796
2058
  getRootDir() {
1797
2059
  return this.rootDir;
@@ -1825,7 +2087,16 @@ var LocalSelectorStorage = class {
1825
2087
  try {
1826
2088
  const raw = import_fs2.default.readFileSync(file, "utf8");
1827
2089
  return JSON.parse(raw);
1828
- } catch {
2090
+ } catch (error) {
2091
+ const message = extractErrorMessage(
2092
+ error,
2093
+ "Unable to parse selector registry JSON."
2094
+ );
2095
+ if (this.debug) {
2096
+ console.warn(
2097
+ `[opensteer] failed to read selector registry "${file}": ${message}`
2098
+ );
2099
+ }
1829
2100
  return createEmptyRegistry(this.namespace);
1830
2101
  }
1831
2102
  }
@@ -1842,7 +2113,16 @@ var LocalSelectorStorage = class {
1842
2113
  try {
1843
2114
  const raw = import_fs2.default.readFileSync(file, "utf8");
1844
2115
  return JSON.parse(raw);
1845
- } catch {
2116
+ } catch (error) {
2117
+ const message = extractErrorMessage(
2118
+ error,
2119
+ "Unable to parse selector file JSON."
2120
+ );
2121
+ if (this.debug) {
2122
+ console.warn(
2123
+ `[opensteer] failed to read selector file "${file}": ${message}`
2124
+ );
2125
+ }
1846
2126
  return null;
1847
2127
  }
1848
2128
  }
@@ -1862,7 +2142,6 @@ var LocalSelectorStorage = class {
1862
2142
 
1863
2143
  // src/html/pipeline.ts
1864
2144
  var cheerio3 = __toESM(require("cheerio"), 1);
1865
- var import_crypto = require("crypto");
1866
2145
 
1867
2146
  // src/html/serializer.ts
1868
2147
  var cheerio = __toESM(require("cheerio"), 1);
@@ -2135,9 +2414,6 @@ var ENSURE_NAME_SHIM_SCRIPT = `
2135
2414
  `;
2136
2415
  var OS_FRAME_TOKEN_KEY = "__opensteerFrameToken";
2137
2416
  var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
2138
- var OS_COUNTER_OWNER_KEY = "__opensteerCounterOwner";
2139
- var OS_COUNTER_VALUE_KEY = "__opensteerCounterValue";
2140
- var OS_COUNTER_NEXT_KEY = "__opensteerCounterNext";
2141
2417
 
2142
2418
  // src/element-path/build.ts
2143
2419
  var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
@@ -3927,567 +4203,178 @@ function cleanForAction(html) {
3927
4203
  return compactHtml(htmlOut);
3928
4204
  }
3929
4205
 
3930
- // src/extract-value-normalization.ts
3931
- var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
3932
- function normalizeExtractedValue(raw, attribute) {
3933
- if (raw == null) return null;
3934
- const rawText = String(raw);
3935
- if (!rawText.trim()) return null;
3936
- const normalizedAttribute = String(attribute || "").trim().toLowerCase();
3937
- if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
3938
- const singleValue = pickSingleListAttributeValue(
3939
- normalizedAttribute,
3940
- rawText
3941
- ).trim();
3942
- return singleValue || null;
4206
+ // src/html/pipeline.ts
4207
+ function applyCleaner(mode, html) {
4208
+ switch (mode) {
4209
+ case "clickable":
4210
+ return cleanForClickable(html);
4211
+ case "scrollable":
4212
+ return cleanForScrollable(html);
4213
+ case "extraction":
4214
+ return cleanForExtraction(html);
4215
+ case "full":
4216
+ return cleanForFull(html);
4217
+ case "action":
4218
+ default:
4219
+ return cleanForAction(html);
3943
4220
  }
3944
- const text = rawText.replace(/\s+/g, " ").trim();
3945
- return text || null;
3946
4221
  }
3947
- function pickSingleListAttributeValue(attribute, raw) {
3948
- if (attribute === "ping") {
3949
- const firstUrl = raw.trim().split(/\s+/)[0] || "";
3950
- return firstUrl.trim();
3951
- }
3952
- if (attribute === "srcset" || attribute === "imagesrcset") {
3953
- const picked = pickBestSrcsetCandidate(raw);
3954
- if (picked) return picked;
3955
- return pickFirstSrcsetToken(raw) || "";
4222
+ async function assignCounters(page, html, nodePaths, nodeMeta) {
4223
+ const $ = cheerio3.load(html, { xmlMode: false });
4224
+ const counterIndex = /* @__PURE__ */ new Map();
4225
+ let nextCounter = 1;
4226
+ const assignedByNodeId = /* @__PURE__ */ new Map();
4227
+ $("*").each(function() {
4228
+ const el = $(this);
4229
+ const nodeId = el.attr(OS_NODE_ID_ATTR);
4230
+ if (!nodeId) return;
4231
+ const counter = nextCounter++;
4232
+ assignedByNodeId.set(nodeId, counter);
4233
+ const path5 = nodePaths.get(nodeId);
4234
+ el.attr("c", String(counter));
4235
+ el.removeAttr(OS_NODE_ID_ATTR);
4236
+ if (path5) {
4237
+ counterIndex.set(counter, cloneElementPath(path5));
4238
+ }
4239
+ });
4240
+ try {
4241
+ await syncLiveCounters(page, nodeMeta, assignedByNodeId);
4242
+ } catch (error) {
4243
+ await clearLiveCounters(page);
4244
+ throw error;
3956
4245
  }
3957
- return raw.trim();
4246
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4247
+ return {
4248
+ html: $.html(),
4249
+ counterIndex
4250
+ };
3958
4251
  }
3959
- function pickBestSrcsetCandidate(raw) {
3960
- const candidates = parseSrcsetCandidates(raw);
3961
- if (!candidates.length) return null;
3962
- const widthCandidates = candidates.filter(
3963
- (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
3964
- );
3965
- if (widthCandidates.length) {
3966
- return widthCandidates.reduce(
3967
- (best, candidate) => candidate.width > best.width ? candidate : best
3968
- ).url;
3969
- }
3970
- const densityCandidates = candidates.filter(
3971
- (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
3972
- );
3973
- if (densityCandidates.length) {
3974
- return densityCandidates.reduce(
3975
- (best, candidate) => candidate.density > best.density ? candidate : best
3976
- ).url;
4252
+ async function syncLiveCounters(page, nodeMeta, assignedByNodeId) {
4253
+ await clearLiveCounters(page);
4254
+ if (!assignedByNodeId.size) return;
4255
+ const groupedByFrame = /* @__PURE__ */ new Map();
4256
+ for (const [nodeId, counter] of assignedByNodeId.entries()) {
4257
+ const meta = nodeMeta.get(nodeId);
4258
+ if (!meta?.frameToken) continue;
4259
+ const list = groupedByFrame.get(meta.frameToken) || [];
4260
+ list.push({
4261
+ nodeId,
4262
+ counter
4263
+ });
4264
+ groupedByFrame.set(meta.frameToken, list);
3977
4265
  }
3978
- return candidates[0]?.url || null;
3979
- }
3980
- function parseSrcsetCandidates(raw) {
3981
- const text = String(raw || "").trim();
3982
- if (!text) return [];
3983
- const out = [];
3984
- let index = 0;
3985
- while (index < text.length) {
3986
- index = skipSeparators(text, index);
3987
- if (index >= text.length) break;
3988
- const urlToken = readUrlToken(text, index);
3989
- index = urlToken.nextIndex;
3990
- const url = urlToken.value.trim();
3991
- if (!url) continue;
3992
- index = skipWhitespace(text, index);
3993
- const descriptors = [];
3994
- while (index < text.length && text[index] !== ",") {
3995
- const descriptorToken = readDescriptorToken(text, index);
3996
- if (!descriptorToken.value) {
3997
- index = descriptorToken.nextIndex;
3998
- continue;
4266
+ if (!groupedByFrame.size) return;
4267
+ const failures = [];
4268
+ const framesByToken = await mapFramesByToken(page);
4269
+ for (const [frameToken, entries] of groupedByFrame.entries()) {
4270
+ const frame = framesByToken.get(frameToken);
4271
+ if (!frame) {
4272
+ for (const entry of entries) {
4273
+ failures.push({
4274
+ nodeId: entry.nodeId,
4275
+ counter: entry.counter,
4276
+ frameToken,
4277
+ reason: "frame_missing"
4278
+ });
3999
4279
  }
4000
- descriptors.push(descriptorToken.value);
4001
- index = descriptorToken.nextIndex;
4002
- index = skipWhitespace(text, index);
4003
- }
4004
- if (index < text.length && text[index] === ",") {
4005
- index += 1;
4280
+ continue;
4006
4281
  }
4007
- let width = null;
4008
- let density = null;
4009
- for (const descriptor of descriptors) {
4010
- const token = descriptor.trim().toLowerCase();
4011
- if (!token) continue;
4012
- const widthMatch = token.match(/^(\d+)w$/);
4013
- if (widthMatch) {
4014
- const parsed = Number.parseInt(widthMatch[1], 10);
4015
- if (Number.isFinite(parsed)) {
4016
- width = parsed;
4282
+ try {
4283
+ const unresolved = await frame.evaluate(
4284
+ ({ entries: entries2, nodeAttr }) => {
4285
+ const index = /* @__PURE__ */ new Map();
4286
+ const unresolved2 = [];
4287
+ const walk = (root) => {
4288
+ const children = Array.from(root.children);
4289
+ for (const child of children) {
4290
+ const nodeId = child.getAttribute(nodeAttr);
4291
+ if (nodeId) {
4292
+ const list = index.get(nodeId) || [];
4293
+ list.push(child);
4294
+ index.set(nodeId, list);
4295
+ }
4296
+ walk(child);
4297
+ if (child.shadowRoot) {
4298
+ walk(child.shadowRoot);
4299
+ }
4300
+ }
4301
+ };
4302
+ walk(document);
4303
+ for (const entry of entries2) {
4304
+ const matches = index.get(entry.nodeId) || [];
4305
+ if (matches.length !== 1) {
4306
+ unresolved2.push({
4307
+ nodeId: entry.nodeId,
4308
+ counter: entry.counter,
4309
+ matches: matches.length
4310
+ });
4311
+ continue;
4312
+ }
4313
+ matches[0].setAttribute("c", String(entry.counter));
4314
+ }
4315
+ return unresolved2;
4316
+ },
4317
+ {
4318
+ entries,
4319
+ nodeAttr: OS_NODE_ID_ATTR
4017
4320
  }
4018
- continue;
4321
+ );
4322
+ for (const entry of unresolved) {
4323
+ failures.push({
4324
+ nodeId: entry.nodeId,
4325
+ counter: entry.counter,
4326
+ frameToken,
4327
+ reason: "match_count",
4328
+ matches: entry.matches
4329
+ });
4019
4330
  }
4020
- const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4021
- if (densityMatch) {
4022
- const parsed = Number.parseFloat(densityMatch[1]);
4023
- if (Number.isFinite(parsed)) {
4024
- density = parsed;
4025
- }
4331
+ } catch {
4332
+ for (const entry of entries) {
4333
+ failures.push({
4334
+ nodeId: entry.nodeId,
4335
+ counter: entry.counter,
4336
+ frameToken,
4337
+ reason: "frame_unavailable"
4338
+ });
4026
4339
  }
4027
4340
  }
4028
- out.push({
4029
- url,
4030
- width,
4031
- density
4341
+ }
4342
+ if (failures.length) {
4343
+ const preview = failures.slice(0, 3).map((failure) => {
4344
+ const base = `counter ${failure.counter} (nodeId "${failure.nodeId}") in frame "${failure.frameToken}"`;
4345
+ if (failure.reason === "frame_missing") {
4346
+ return `${base} could not be synchronized because the frame is missing.`;
4347
+ }
4348
+ if (failure.reason === "frame_unavailable") {
4349
+ return `${base} could not be synchronized because frame evaluation failed.`;
4350
+ }
4351
+ return `${base} expected exactly one live node but found ${failure.matches ?? 0}.`;
4032
4352
  });
4353
+ const remaining = failures.length > 3 ? ` (+${failures.length - 3} more)` : "";
4354
+ throw new Error(
4355
+ `Failed to synchronize snapshot counters with the live DOM: ${preview.join(" ")}${remaining}`
4356
+ );
4033
4357
  }
4034
- return out;
4035
4358
  }
4036
- function pickFirstSrcsetToken(raw) {
4037
- const candidate = parseSrcsetCandidates(raw)[0];
4038
- if (candidate?.url) {
4039
- return candidate.url;
4040
- }
4041
- const text = String(raw || "");
4042
- const start = skipSeparators(text, 0);
4043
- if (start >= text.length) return null;
4044
- const firstToken = readUrlToken(text, start).value.trim();
4045
- return firstToken || null;
4046
- }
4047
- function skipWhitespace(value, index) {
4048
- let cursor = index;
4049
- while (cursor < value.length && /\s/.test(value[cursor])) {
4050
- cursor += 1;
4051
- }
4052
- return cursor;
4053
- }
4054
- function skipSeparators(value, index) {
4055
- let cursor = skipWhitespace(value, index);
4056
- while (cursor < value.length && value[cursor] === ",") {
4057
- cursor += 1;
4058
- cursor = skipWhitespace(value, cursor);
4059
- }
4060
- return cursor;
4061
- }
4062
- function readUrlToken(value, index) {
4063
- let cursor = index;
4064
- let out = "";
4065
- const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4066
- while (cursor < value.length) {
4067
- const char = value[cursor];
4068
- if (/\s/.test(char)) {
4069
- break;
4070
- }
4071
- if (char === "," && !isDataUrl) {
4072
- break;
4073
- }
4074
- out += char;
4075
- cursor += 1;
4076
- }
4077
- if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4078
- out = out.slice(0, -1);
4079
- }
4080
- return {
4081
- value: out,
4082
- nextIndex: cursor
4083
- };
4084
- }
4085
- function readDescriptorToken(value, index) {
4086
- let cursor = skipWhitespace(value, index);
4087
- let out = "";
4088
- while (cursor < value.length) {
4089
- const char = value[cursor];
4090
- if (char === "," || /\s/.test(char)) {
4091
- break;
4092
- }
4093
- out += char;
4094
- cursor += 1;
4095
- }
4096
- return {
4097
- value: out.trim(),
4098
- nextIndex: cursor
4099
- };
4100
- }
4101
-
4102
- // src/html/counter-runtime.ts
4103
- var CounterResolutionError = class extends Error {
4104
- code;
4105
- constructor(code, message) {
4106
- super(message);
4107
- this.name = "CounterResolutionError";
4108
- this.code = code;
4109
- }
4110
- };
4111
- async function ensureLiveCounters(page, nodeMeta, nodeIds) {
4112
- const out = /* @__PURE__ */ new Map();
4113
- if (!nodeIds.length) return out;
4114
- const grouped = /* @__PURE__ */ new Map();
4115
- for (const nodeId of nodeIds) {
4116
- const meta = nodeMeta.get(nodeId);
4117
- if (!meta) {
4118
- throw new CounterResolutionError(
4119
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4120
- `Missing metadata for node ${nodeId}. Run snapshot() again.`
4121
- );
4122
- }
4123
- const list = grouped.get(meta.frameToken) || [];
4124
- list.push({
4125
- nodeId,
4126
- instanceToken: meta.instanceToken
4127
- });
4128
- grouped.set(meta.frameToken, list);
4129
- }
4130
- const framesByToken = await mapFramesByToken(page);
4131
- let nextCounter = await readGlobalNextCounter(page);
4132
- const usedCounters = /* @__PURE__ */ new Map();
4133
- for (const [frameToken, entries] of grouped.entries()) {
4134
- const frame = framesByToken.get(frameToken);
4135
- if (!frame) {
4136
- throw new CounterResolutionError(
4137
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4138
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4139
- );
4140
- }
4141
- const result = await frame.evaluate(
4142
- ({
4143
- entries: entries2,
4144
- nodeAttr,
4145
- instanceTokenKey,
4146
- counterOwnerKey,
4147
- counterValueKey,
4148
- startCounter
4149
- }) => {
4150
- const helpers = {
4151
- pushNode(map, node) {
4152
- const nodeId = node.getAttribute(nodeAttr);
4153
- if (!nodeId) return;
4154
- const list = map.get(nodeId) || [];
4155
- list.push(node);
4156
- map.set(nodeId, list);
4157
- },
4158
- walk(map, root) {
4159
- const children = Array.from(root.children);
4160
- for (const child of children) {
4161
- helpers.pushNode(map, child);
4162
- helpers.walk(map, child);
4163
- if (child.shadowRoot) {
4164
- helpers.walk(map, child.shadowRoot);
4165
- }
4166
- }
4167
- },
4168
- buildNodeIndex() {
4169
- const map = /* @__PURE__ */ new Map();
4170
- helpers.walk(map, document);
4171
- return map;
4172
- }
4173
- };
4174
- const index = helpers.buildNodeIndex();
4175
- const assigned = [];
4176
- const failures = [];
4177
- let next = Math.max(1, Number(startCounter || 1));
4178
- for (const entry of entries2) {
4179
- const matches = index.get(entry.nodeId) || [];
4180
- if (!matches.length) {
4181
- failures.push({
4182
- nodeId: entry.nodeId,
4183
- reason: "missing"
4184
- });
4185
- continue;
4186
- }
4187
- if (matches.length !== 1) {
4188
- failures.push({
4189
- nodeId: entry.nodeId,
4190
- reason: "ambiguous"
4191
- });
4192
- continue;
4193
- }
4194
- const target = matches[0];
4195
- if (target[instanceTokenKey] !== entry.instanceToken) {
4196
- failures.push({
4197
- nodeId: entry.nodeId,
4198
- reason: "instance_mismatch"
4199
- });
4200
- continue;
4201
- }
4202
- const owned = target[counterOwnerKey] === true;
4203
- const runtimeCounter = Number(target[counterValueKey] || 0);
4204
- if (owned && Number.isFinite(runtimeCounter) && runtimeCounter > 0) {
4205
- target.setAttribute("c", String(runtimeCounter));
4206
- assigned.push({
4207
- nodeId: entry.nodeId,
4208
- counter: runtimeCounter
4209
- });
4210
- continue;
4211
- }
4212
- const counter = next++;
4213
- target.setAttribute("c", String(counter));
4214
- Object.defineProperty(target, counterOwnerKey, {
4215
- value: true,
4216
- writable: true,
4217
- configurable: true
4218
- });
4219
- Object.defineProperty(target, counterValueKey, {
4220
- value: counter,
4221
- writable: true,
4222
- configurable: true
4223
- });
4224
- assigned.push({ nodeId: entry.nodeId, counter });
4225
- }
4226
- return {
4227
- assigned,
4228
- failures,
4229
- nextCounter: next
4230
- };
4231
- },
4232
- {
4233
- entries,
4234
- nodeAttr: OS_NODE_ID_ATTR,
4235
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4236
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4237
- counterValueKey: OS_COUNTER_VALUE_KEY,
4238
- startCounter: nextCounter
4239
- }
4240
- );
4241
- if (result.failures.length) {
4242
- const first = result.failures[0];
4243
- throw buildCounterFailureError(first.nodeId, first.reason);
4244
- }
4245
- nextCounter = result.nextCounter;
4246
- for (const item of result.assigned) {
4247
- const existingNode = usedCounters.get(item.counter);
4248
- if (existingNode && existingNode !== item.nodeId) {
4249
- throw new CounterResolutionError(
4250
- "ERR_COUNTER_AMBIGUOUS",
4251
- `Counter ${item.counter} is assigned to multiple nodes (${existingNode}, ${item.nodeId}). Run snapshot() again.`
4252
- );
4253
- }
4254
- usedCounters.set(item.counter, item.nodeId);
4255
- out.set(item.nodeId, item.counter);
4256
- }
4257
- }
4258
- await writeGlobalNextCounter(page, nextCounter);
4259
- return out;
4260
- }
4261
- async function resolveCounterElement(page, snapshot, counter) {
4262
- const binding = readBinding(snapshot, counter);
4263
- const framesByToken = await mapFramesByToken(page);
4264
- const frame = framesByToken.get(binding.frameToken);
4265
- if (!frame) {
4266
- throw new CounterResolutionError(
4267
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4268
- `Counter ${counter} frame is unavailable. Run snapshot() again.`
4269
- );
4270
- }
4271
- const status = await frame.evaluate(
4272
- ({
4273
- nodeId,
4274
- instanceToken,
4275
- counter: counter2,
4276
- nodeAttr,
4277
- instanceTokenKey,
4278
- counterOwnerKey,
4279
- counterValueKey
4280
- }) => {
4281
- const helpers = {
4282
- walk(map, root) {
4283
- const children = Array.from(root.children);
4284
- for (const child of children) {
4285
- const id = child.getAttribute(nodeAttr);
4286
- if (id) {
4287
- const list = map.get(id) || [];
4288
- list.push(child);
4289
- map.set(id, list);
4290
- }
4291
- helpers.walk(map, child);
4292
- if (child.shadowRoot) {
4293
- helpers.walk(map, child.shadowRoot);
4294
- }
4295
- }
4296
- },
4297
- buildNodeIndex() {
4298
- const map = /* @__PURE__ */ new Map();
4299
- helpers.walk(map, document);
4300
- return map;
4301
- }
4302
- };
4303
- const matches = helpers.buildNodeIndex().get(nodeId) || [];
4304
- if (!matches.length) return "missing";
4305
- if (matches.length !== 1) return "ambiguous";
4306
- const target = matches[0];
4307
- if (target[instanceTokenKey] !== instanceToken) {
4308
- return "instance_mismatch";
4309
- }
4310
- if (target[counterOwnerKey] !== true) {
4311
- return "instance_mismatch";
4312
- }
4313
- if (Number(target[counterValueKey] || 0) !== counter2) {
4314
- return "instance_mismatch";
4315
- }
4316
- if (target.getAttribute("c") !== String(counter2)) {
4317
- return "instance_mismatch";
4318
- }
4319
- return "ok";
4320
- },
4321
- {
4322
- nodeId: binding.nodeId,
4323
- instanceToken: binding.instanceToken,
4324
- counter,
4325
- nodeAttr: OS_NODE_ID_ATTR,
4326
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4327
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4328
- counterValueKey: OS_COUNTER_VALUE_KEY
4329
- }
4330
- );
4331
- if (status !== "ok") {
4332
- throw buildCounterFailureError(binding.nodeId, status);
4333
- }
4334
- const handle = await frame.evaluateHandle(
4335
- ({ nodeId, nodeAttr }) => {
4336
- const helpers = {
4337
- walk(matches, root) {
4359
+ async function clearLiveCounters(page) {
4360
+ for (const frame of page.frames()) {
4361
+ try {
4362
+ await frame.evaluate(() => {
4363
+ const walk = (root) => {
4338
4364
  const children = Array.from(root.children);
4339
4365
  for (const child of children) {
4340
- if (child.getAttribute(nodeAttr) === nodeId) {
4341
- matches.push(child);
4342
- }
4343
- helpers.walk(matches, child);
4366
+ child.removeAttribute("c");
4367
+ walk(child);
4344
4368
  if (child.shadowRoot) {
4345
- helpers.walk(matches, child.shadowRoot);
4346
- }
4347
- }
4348
- },
4349
- findUniqueNode() {
4350
- const matches = [];
4351
- helpers.walk(matches, document);
4352
- if (matches.length !== 1) return null;
4353
- return matches[0];
4354
- }
4355
- };
4356
- return helpers.findUniqueNode();
4357
- },
4358
- {
4359
- nodeId: binding.nodeId,
4360
- nodeAttr: OS_NODE_ID_ATTR
4361
- }
4362
- );
4363
- const element = handle.asElement();
4364
- if (!element) {
4365
- await handle.dispose();
4366
- throw new CounterResolutionError(
4367
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4368
- `Counter ${counter} became stale. Run snapshot() again.`
4369
- );
4370
- }
4371
- return element;
4372
- }
4373
- async function resolveCountersBatch(page, snapshot, requests) {
4374
- const out = {};
4375
- if (!requests.length) return out;
4376
- const grouped = /* @__PURE__ */ new Map();
4377
- for (const request of requests) {
4378
- const binding = readBinding(snapshot, request.counter);
4379
- const list = grouped.get(binding.frameToken) || [];
4380
- list.push({
4381
- ...request,
4382
- ...binding
4383
- });
4384
- grouped.set(binding.frameToken, list);
4385
- }
4386
- const framesByToken = await mapFramesByToken(page);
4387
- for (const [frameToken, entries] of grouped.entries()) {
4388
- const frame = framesByToken.get(frameToken);
4389
- if (!frame) {
4390
- throw new CounterResolutionError(
4391
- "ERR_COUNTER_FRAME_UNAVAILABLE",
4392
- `Counter frame ${frameToken} is unavailable. Run snapshot() again.`
4393
- );
4394
- }
4395
- const result = await frame.evaluate(
4396
- ({
4397
- entries: entries2,
4398
- nodeAttr,
4399
- instanceTokenKey,
4400
- counterOwnerKey,
4401
- counterValueKey
4402
- }) => {
4403
- const values = [];
4404
- const failures = [];
4405
- const helpers = {
4406
- walk(map, root) {
4407
- const children = Array.from(root.children);
4408
- for (const child of children) {
4409
- const id = child.getAttribute(nodeAttr);
4410
- if (id) {
4411
- const list = map.get(id) || [];
4412
- list.push(child);
4413
- map.set(id, list);
4414
- }
4415
- helpers.walk(map, child);
4416
- if (child.shadowRoot) {
4417
- helpers.walk(map, child.shadowRoot);
4418
- }
4369
+ walk(child.shadowRoot);
4419
4370
  }
4420
- },
4421
- buildNodeIndex() {
4422
- const map = /* @__PURE__ */ new Map();
4423
- helpers.walk(map, document);
4424
- return map;
4425
- },
4426
- readRawValue(element, attribute) {
4427
- if (attribute) {
4428
- return element.getAttribute(attribute);
4429
- }
4430
- return element.textContent;
4431
- }
4432
- };
4433
- const index = helpers.buildNodeIndex();
4434
- for (const entry of entries2) {
4435
- const matches = index.get(entry.nodeId) || [];
4436
- if (!matches.length) {
4437
- failures.push({
4438
- nodeId: entry.nodeId,
4439
- reason: "missing"
4440
- });
4441
- continue;
4442
- }
4443
- if (matches.length !== 1) {
4444
- failures.push({
4445
- nodeId: entry.nodeId,
4446
- reason: "ambiguous"
4447
- });
4448
- continue;
4449
4371
  }
4450
- const target = matches[0];
4451
- if (target[instanceTokenKey] !== entry.instanceToken || target[counterOwnerKey] !== true || Number(target[counterValueKey] || 0) !== entry.counter || target.getAttribute("c") !== String(entry.counter)) {
4452
- failures.push({
4453
- nodeId: entry.nodeId,
4454
- reason: "instance_mismatch"
4455
- });
4456
- continue;
4457
- }
4458
- values.push({
4459
- key: entry.key,
4460
- value: helpers.readRawValue(target, entry.attribute)
4461
- });
4462
- }
4463
- return {
4464
- values,
4465
- failures
4466
4372
  };
4467
- },
4468
- {
4469
- entries,
4470
- nodeAttr: OS_NODE_ID_ATTR,
4471
- instanceTokenKey: OS_INSTANCE_TOKEN_KEY,
4472
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4473
- counterValueKey: OS_COUNTER_VALUE_KEY
4474
- }
4475
- );
4476
- if (result.failures.length) {
4477
- const first = result.failures[0];
4478
- throw buildCounterFailureError(first.nodeId, first.reason);
4479
- }
4480
- const attributeByKey = new Map(
4481
- entries.map((entry) => [entry.key, entry.attribute])
4482
- );
4483
- for (const item of result.values) {
4484
- out[item.key] = normalizeExtractedValue(
4485
- item.value,
4486
- attributeByKey.get(item.key)
4487
- );
4373
+ walk(document);
4374
+ });
4375
+ } catch {
4488
4376
  }
4489
4377
  }
4490
- return out;
4491
4378
  }
4492
4379
  async function mapFramesByToken(page) {
4493
4380
  const out = /* @__PURE__ */ new Map();
@@ -4509,193 +4396,18 @@ async function readFrameToken(frame) {
4509
4396
  return null;
4510
4397
  }
4511
4398
  }
4512
- async function readGlobalNextCounter(page) {
4513
- const current = await page.mainFrame().evaluate((counterNextKey) => {
4514
- const win = window;
4515
- return Number(win[counterNextKey] || 0);
4516
- }, OS_COUNTER_NEXT_KEY).catch(() => 0);
4517
- if (Number.isFinite(current) && current > 0) {
4518
- return current;
4519
- }
4520
- let max = 0;
4521
- for (const frame of page.frames()) {
4522
- try {
4523
- const frameMax = await frame.evaluate(
4524
- ({ nodeAttr, counterOwnerKey, counterValueKey }) => {
4525
- let localMax = 0;
4526
- const helpers = {
4527
- walk(root) {
4528
- const children = Array.from(
4529
- root.children
4530
- );
4531
- for (const child of children) {
4532
- const candidate = child;
4533
- const hasNodeId = child.hasAttribute(nodeAttr);
4534
- const owned = candidate[counterOwnerKey] === true;
4535
- if (hasNodeId && owned) {
4536
- const value = Number(
4537
- candidate[counterValueKey] || 0
4538
- );
4539
- if (Number.isFinite(value) && value > localMax) {
4540
- localMax = value;
4541
- }
4542
- }
4543
- helpers.walk(child);
4544
- if (child.shadowRoot) {
4545
- helpers.walk(child.shadowRoot);
4546
- }
4547
- }
4548
- }
4549
- };
4550
- helpers.walk(document);
4551
- return localMax;
4552
- },
4553
- {
4554
- nodeAttr: OS_NODE_ID_ATTR,
4555
- counterOwnerKey: OS_COUNTER_OWNER_KEY,
4556
- counterValueKey: OS_COUNTER_VALUE_KEY
4557
- }
4558
- );
4559
- if (frameMax > max) {
4560
- max = frameMax;
4561
- }
4562
- } catch {
4563
- }
4564
- }
4565
- const next = max + 1;
4566
- await writeGlobalNextCounter(page, next);
4567
- return next;
4568
- }
4569
- async function writeGlobalNextCounter(page, nextCounter) {
4570
- await page.mainFrame().evaluate(
4571
- ({ counterNextKey, nextCounter: nextCounter2 }) => {
4572
- const win = window;
4573
- win[counterNextKey] = nextCounter2;
4574
- },
4575
- {
4576
- counterNextKey: OS_COUNTER_NEXT_KEY,
4577
- nextCounter
4578
- }
4579
- ).catch(() => void 0);
4399
+ function stripNodeIds(html) {
4400
+ if (!html.includes(OS_NODE_ID_ATTR)) return html;
4401
+ const $ = cheerio3.load(html, { xmlMode: false });
4402
+ $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4403
+ return $.html();
4580
4404
  }
4581
- function readBinding(snapshot, counter) {
4582
- if (!snapshot.counterBindings) {
4583
- throw new CounterResolutionError(
4584
- "ERR_COUNTER_NOT_FOUND",
4585
- `Counter ${counter} is unavailable because this snapshot has no counter bindings. Run snapshot() with counters first.`
4586
- );
4587
- }
4588
- const binding = snapshot.counterBindings.get(counter);
4589
- if (!binding) {
4590
- throw new CounterResolutionError(
4591
- "ERR_COUNTER_NOT_FOUND",
4592
- `Counter ${counter} was not found in the current snapshot. Run snapshot() again.`
4593
- );
4594
- }
4595
- if (binding.sessionId !== snapshot.snapshotSessionId) {
4596
- throw new CounterResolutionError(
4597
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4598
- `Counter ${counter} is stale for this snapshot session. Run snapshot() again.`
4599
- );
4600
- }
4601
- return binding;
4602
- }
4603
- function buildCounterFailureError(nodeId, reason) {
4604
- if (reason === "ambiguous") {
4605
- return new CounterResolutionError(
4606
- "ERR_COUNTER_AMBIGUOUS",
4607
- `Counter target is ambiguous for node ${nodeId}. Run snapshot() again.`
4608
- );
4609
- }
4610
- return new CounterResolutionError(
4611
- "ERR_COUNTER_STALE_OR_NOT_FOUND",
4612
- `Counter target is stale or missing for node ${nodeId}. Run snapshot() again.`
4613
- );
4614
- }
4615
-
4616
- // src/html/pipeline.ts
4617
- function applyCleaner(mode, html) {
4618
- switch (mode) {
4619
- case "clickable":
4620
- return cleanForClickable(html);
4621
- case "scrollable":
4622
- return cleanForScrollable(html);
4623
- case "extraction":
4624
- return cleanForExtraction(html);
4625
- case "full":
4626
- return cleanForFull(html);
4627
- case "action":
4628
- default:
4629
- return cleanForAction(html);
4630
- }
4631
- }
4632
- async function assignCounters(page, html, nodePaths, nodeMeta, snapshotSessionId) {
4633
- const $ = cheerio3.load(html, { xmlMode: false });
4634
- const counterIndex = /* @__PURE__ */ new Map();
4635
- const counterBindings = /* @__PURE__ */ new Map();
4636
- const orderedNodeIds = [];
4637
- $("*").each(function() {
4638
- const el = $(this);
4639
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4640
- if (!nodeId) return;
4641
- orderedNodeIds.push(nodeId);
4642
- });
4643
- const countersByNodeId = await ensureLiveCounters(
4644
- page,
4645
- nodeMeta,
4646
- orderedNodeIds
4647
- );
4648
- $("*").each(function() {
4649
- const el = $(this);
4650
- const nodeId = el.attr(OS_NODE_ID_ATTR);
4651
- if (!nodeId) return;
4652
- const path5 = nodePaths.get(nodeId);
4653
- const meta = nodeMeta.get(nodeId);
4654
- const counter = countersByNodeId.get(nodeId);
4655
- if (counter == null || !Number.isFinite(counter)) {
4656
- throw new Error(
4657
- `Counter assignment failed for node ${nodeId}. Run snapshot() again.`
4658
- );
4659
- }
4660
- if (counterBindings.has(counter) && counterBindings.get(counter)?.nodeId !== nodeId) {
4661
- throw new Error(
4662
- `Counter ${counter} was assigned to multiple nodes. Run snapshot() again.`
4663
- );
4664
- }
4665
- el.attr("c", String(counter));
4666
- el.removeAttr(OS_NODE_ID_ATTR);
4667
- if (path5) {
4668
- counterIndex.set(counter, cloneElementPath(path5));
4669
- }
4670
- if (meta) {
4671
- counterBindings.set(counter, {
4672
- sessionId: snapshotSessionId,
4673
- frameToken: meta.frameToken,
4674
- nodeId,
4675
- instanceToken: meta.instanceToken
4676
- });
4677
- }
4678
- });
4679
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4680
- return {
4681
- html: $.html(),
4682
- counterIndex,
4683
- counterBindings
4684
- };
4685
- }
4686
- function stripNodeIds(html) {
4687
- if (!html.includes(OS_NODE_ID_ATTR)) return html;
4688
- const $ = cheerio3.load(html, { xmlMode: false });
4689
- $(`[${OS_NODE_ID_ATTR}]`).removeAttr(OS_NODE_ID_ATTR);
4690
- return $.html();
4691
- }
4692
- async function prepareSnapshot(page, options = {}) {
4693
- const snapshotSessionId = (0, import_crypto.randomUUID)();
4694
- const mode = options.mode ?? "action";
4695
- const withCounters = options.withCounters ?? true;
4696
- const shouldMarkInteractive = options.markInteractive ?? true;
4697
- if (shouldMarkInteractive) {
4698
- await markInteractiveElements(page);
4405
+ async function prepareSnapshot(page, options = {}) {
4406
+ const mode = options.mode ?? "action";
4407
+ const withCounters = options.withCounters ?? true;
4408
+ const shouldMarkInteractive = options.markInteractive ?? true;
4409
+ if (shouldMarkInteractive) {
4410
+ await markInteractiveElements(page);
4699
4411
  }
4700
4412
  const serialized = await serializePageHTML(page);
4701
4413
  const rawHtml = serialized.html;
@@ -4703,18 +4415,15 @@ async function prepareSnapshot(page, options = {}) {
4703
4415
  const reducedHtml = applyCleaner(mode, processedHtml);
4704
4416
  let cleanedHtml = reducedHtml;
4705
4417
  let counterIndex = null;
4706
- let counterBindings = null;
4707
4418
  if (withCounters) {
4708
4419
  const counted = await assignCounters(
4709
4420
  page,
4710
4421
  reducedHtml,
4711
4422
  serialized.nodePaths,
4712
- serialized.nodeMeta,
4713
- snapshotSessionId
4423
+ serialized.nodeMeta
4714
4424
  );
4715
4425
  cleanedHtml = counted.html;
4716
4426
  counterIndex = counted.counterIndex;
4717
- counterBindings = counted.counterBindings;
4718
4427
  } else {
4719
4428
  cleanedHtml = stripNodeIds(cleanedHtml);
4720
4429
  }
@@ -4723,325 +4432,724 @@ async function prepareSnapshot(page, options = {}) {
4723
4432
  cleanedHtml = $unwrap("body").html()?.trim() || cleanedHtml;
4724
4433
  }
4725
4434
  return {
4726
- snapshotSessionId,
4727
4435
  mode,
4728
4436
  url: page.url(),
4729
4437
  rawHtml,
4730
4438
  processedHtml,
4731
4439
  reducedHtml,
4732
4440
  cleanedHtml,
4733
- counterIndex,
4734
- counterBindings
4441
+ counterIndex
4442
+ };
4443
+ }
4444
+
4445
+ // src/element-path/errors.ts
4446
+ var ElementPathError = class extends Error {
4447
+ code;
4448
+ constructor(code, message) {
4449
+ super(message);
4450
+ this.name = "ElementPathError";
4451
+ this.code = code;
4452
+ }
4453
+ };
4454
+
4455
+ // src/element-path/resolver.ts
4456
+ async function resolveElementPath(page, rawPath) {
4457
+ const path5 = sanitizeElementPath(rawPath);
4458
+ let frame = page.mainFrame();
4459
+ let rootHandle = null;
4460
+ for (const hop of path5.context) {
4461
+ const host = await resolveDomPath(frame, hop.host, rootHandle);
4462
+ if (!host) {
4463
+ await disposeHandle(rootHandle);
4464
+ throw new ElementPathError(
4465
+ "ERR_PATH_CONTEXT_HOST_NOT_FOUND",
4466
+ "Unable to resolve context host from stored match selectors."
4467
+ );
4468
+ }
4469
+ if (hop.kind === "iframe") {
4470
+ const nextFrame = await host.element.contentFrame();
4471
+ await host.element.dispose();
4472
+ await disposeHandle(rootHandle);
4473
+ rootHandle = null;
4474
+ if (!nextFrame) {
4475
+ throw new ElementPathError(
4476
+ "ERR_PATH_IFRAME_UNAVAILABLE",
4477
+ "Iframe is unavailable or inaccessible for this path."
4478
+ );
4479
+ }
4480
+ frame = nextFrame;
4481
+ continue;
4482
+ }
4483
+ const shadowRoot = await host.element.evaluateHandle(
4484
+ (element) => element.shadowRoot
4485
+ );
4486
+ await host.element.dispose();
4487
+ const isMissing = await shadowRoot.evaluate((value) => value == null);
4488
+ if (isMissing) {
4489
+ await shadowRoot.dispose();
4490
+ await disposeHandle(rootHandle);
4491
+ throw new ElementPathError(
4492
+ "ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
4493
+ "Shadow root is unavailable for this path."
4494
+ );
4495
+ }
4496
+ await disposeHandle(rootHandle);
4497
+ rootHandle = shadowRoot;
4498
+ }
4499
+ const target = await resolveDomPath(frame, path5.nodes, rootHandle);
4500
+ if (!target) {
4501
+ const diagnostics = await collectCandidateDiagnostics(
4502
+ frame,
4503
+ path5.nodes,
4504
+ rootHandle
4505
+ );
4506
+ await disposeHandle(rootHandle);
4507
+ throw new ElementPathError(
4508
+ "ERR_PATH_TARGET_NOT_FOUND",
4509
+ buildTargetNotFoundMessage(path5.nodes, diagnostics)
4510
+ );
4511
+ }
4512
+ await disposeHandle(rootHandle);
4513
+ if (isPathDebugEnabled()) {
4514
+ debugPath("resolved", {
4515
+ selector: target.selector,
4516
+ mode: target.mode,
4517
+ count: target.count,
4518
+ targetDepth: path5.nodes.length
4519
+ });
4520
+ }
4521
+ return {
4522
+ element: target.element,
4523
+ usedSelector: target.selector || buildPathSelectorHint(path5)
4524
+ };
4525
+ }
4526
+ async function resolveDomPath(frame, domPath, rootHandle) {
4527
+ const candidates = buildPathCandidates(domPath);
4528
+ if (!candidates.length) return null;
4529
+ if (isPathDebugEnabled()) {
4530
+ debugPath("trying selectors", { candidates });
4531
+ }
4532
+ const selected = rootHandle ? await rootHandle.evaluate(selectInRoot, candidates) : await frame.evaluate(selectInDocument, candidates);
4533
+ if (!selected || !selected.selector) return null;
4534
+ const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
4535
+ if (!(root instanceof ShadowRoot)) return null;
4536
+ return root.querySelector(selector);
4537
+ }, selected.selector) : await frame.evaluateHandle(
4538
+ (selector) => document.querySelector(selector),
4539
+ selected.selector
4540
+ );
4541
+ const element = handle.asElement();
4542
+ if (!element) {
4543
+ await handle.dispose();
4544
+ return null;
4545
+ }
4546
+ return {
4547
+ element,
4548
+ selector: selected.selector,
4549
+ mode: selected.mode,
4550
+ count: selected.count
4551
+ };
4552
+ }
4553
+ async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
4554
+ const candidates = buildPathCandidates(domPath);
4555
+ if (!candidates.length) return [];
4556
+ const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
4557
+ return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
4558
+ selector: String(item?.selector || ""),
4559
+ count: Number(item?.count || 0)
4560
+ })).filter((item) => item.selector) : [];
4561
+ }
4562
+ function buildTargetNotFoundMessage(domPath, diagnostics) {
4563
+ const depth = Array.isArray(domPath) ? domPath.length : 0;
4564
+ const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
4565
+ const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
4566
+ if (!sample)
4567
+ return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
4568
+ return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
4569
+ }
4570
+ function selectInDocument(selectors) {
4571
+ let fallback = null;
4572
+ for (const selector of selectors) {
4573
+ if (!selector) continue;
4574
+ let count = 0;
4575
+ try {
4576
+ count = document.querySelectorAll(selector).length;
4577
+ } catch {
4578
+ count = 0;
4579
+ }
4580
+ if (count === 1) {
4581
+ return {
4582
+ selector,
4583
+ count,
4584
+ mode: "unique"
4585
+ };
4586
+ }
4587
+ if (count > 1 && !fallback) {
4588
+ fallback = {
4589
+ selector,
4590
+ count,
4591
+ mode: "fallback"
4592
+ };
4593
+ }
4594
+ }
4595
+ return fallback;
4596
+ }
4597
+ function selectInRoot(root, selectors) {
4598
+ if (!(root instanceof ShadowRoot)) return null;
4599
+ let fallback = null;
4600
+ for (const selector of selectors) {
4601
+ if (!selector) continue;
4602
+ let count = 0;
4603
+ try {
4604
+ count = root.querySelectorAll(selector).length;
4605
+ } catch {
4606
+ count = 0;
4607
+ }
4608
+ if (count === 1) {
4609
+ return {
4610
+ selector,
4611
+ count,
4612
+ mode: "unique"
4613
+ };
4614
+ }
4615
+ if (count > 1 && !fallback) {
4616
+ fallback = {
4617
+ selector,
4618
+ count,
4619
+ mode: "fallback"
4620
+ };
4621
+ }
4622
+ }
4623
+ return fallback;
4624
+ }
4625
+ function countInDocument(selectors) {
4626
+ const out = [];
4627
+ for (const selector of selectors) {
4628
+ if (!selector) continue;
4629
+ let count = 0;
4630
+ try {
4631
+ count = document.querySelectorAll(selector).length;
4632
+ } catch {
4633
+ count = 0;
4634
+ }
4635
+ out.push({ selector, count });
4636
+ }
4637
+ return out;
4638
+ }
4639
+ function countInRoot(root, selectors) {
4640
+ if (!(root instanceof ShadowRoot)) return [];
4641
+ const out = [];
4642
+ for (const selector of selectors) {
4643
+ if (!selector) continue;
4644
+ let count = 0;
4645
+ try {
4646
+ count = root.querySelectorAll(selector).length;
4647
+ } catch {
4648
+ count = 0;
4649
+ }
4650
+ out.push({ selector, count });
4651
+ }
4652
+ return out;
4653
+ }
4654
+ function isPathDebugEnabled() {
4655
+ const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
4656
+ if (!value) return false;
4657
+ const normalized = value.trim().toLowerCase();
4658
+ return normalized === "1" || normalized === "true";
4659
+ }
4660
+ function debugPath(message, data) {
4661
+ if (!isPathDebugEnabled()) return;
4662
+ if (data !== void 0) {
4663
+ console.log(`[opensteer:path] ${message}`, data);
4664
+ } else {
4665
+ console.log(`[opensteer:path] ${message}`);
4666
+ }
4667
+ }
4668
+ async function disposeHandle(handle) {
4669
+ if (!handle) return;
4670
+ try {
4671
+ await handle.dispose();
4672
+ } catch {
4673
+ }
4674
+ }
4675
+
4676
+ // src/actions/actionability-probe.ts
4677
+ async function probeActionabilityState(element) {
4678
+ try {
4679
+ return await element.evaluate((target) => {
4680
+ if (!(target instanceof Element)) {
4681
+ return {
4682
+ connected: false,
4683
+ visible: null,
4684
+ enabled: null,
4685
+ editable: null,
4686
+ blocker: null
4687
+ };
4688
+ }
4689
+ const connected = target.isConnected;
4690
+ if (!connected) {
4691
+ return {
4692
+ connected: false,
4693
+ visible: null,
4694
+ enabled: null,
4695
+ editable: null,
4696
+ blocker: null
4697
+ };
4698
+ }
4699
+ const style = window.getComputedStyle(target);
4700
+ const rect = target.getBoundingClientRect();
4701
+ const hasBox = rect.width > 0 && rect.height > 0;
4702
+ const opacity = Number.parseFloat(style.opacity || "1");
4703
+ const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
4704
+ let enabled = null;
4705
+ if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
4706
+ enabled = !target.disabled;
4707
+ }
4708
+ let editable = null;
4709
+ if (target instanceof HTMLInputElement) {
4710
+ editable = !target.readOnly && !target.disabled;
4711
+ } else if (target instanceof HTMLTextAreaElement) {
4712
+ editable = !target.readOnly && !target.disabled;
4713
+ } else if (target instanceof HTMLSelectElement) {
4714
+ editable = !target.disabled;
4715
+ } else if (target instanceof HTMLElement && target.isContentEditable) {
4716
+ editable = true;
4717
+ }
4718
+ let blocker = null;
4719
+ if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
4720
+ const x = Math.min(
4721
+ Math.max(rect.left + rect.width / 2, 0),
4722
+ window.innerWidth - 1
4723
+ );
4724
+ const y = Math.min(
4725
+ Math.max(rect.top + rect.height / 2, 0),
4726
+ window.innerHeight - 1
4727
+ );
4728
+ const top = document.elementFromPoint(x, y);
4729
+ if (top && top !== target && !target.contains(top)) {
4730
+ const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
4731
+ blocker = {
4732
+ tag: top.tagName.toLowerCase(),
4733
+ id: top.id || null,
4734
+ classes,
4735
+ role: top.getAttribute("role"),
4736
+ text: (top.textContent || "").trim().slice(0, 80) || null
4737
+ };
4738
+ }
4739
+ }
4740
+ return {
4741
+ connected,
4742
+ visible: isVisible,
4743
+ enabled,
4744
+ editable,
4745
+ blocker
4746
+ };
4747
+ });
4748
+ } catch {
4749
+ return null;
4750
+ }
4751
+ }
4752
+
4753
+ // src/extract-value-normalization.ts
4754
+ var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
4755
+ function normalizeExtractedValue(raw, attribute) {
4756
+ if (raw == null) return null;
4757
+ const rawText = String(raw);
4758
+ if (!rawText.trim()) return null;
4759
+ const normalizedAttribute = String(attribute || "").trim().toLowerCase();
4760
+ if (URL_LIST_ATTRIBUTES.has(normalizedAttribute)) {
4761
+ const singleValue = pickSingleListAttributeValue(
4762
+ normalizedAttribute,
4763
+ rawText
4764
+ ).trim();
4765
+ return singleValue || null;
4766
+ }
4767
+ const text = rawText.replace(/\s+/g, " ").trim();
4768
+ return text || null;
4769
+ }
4770
+ function pickSingleListAttributeValue(attribute, raw) {
4771
+ if (attribute === "ping") {
4772
+ const firstUrl = raw.trim().split(/\s+/)[0] || "";
4773
+ return firstUrl.trim();
4774
+ }
4775
+ if (attribute === "srcset" || attribute === "imagesrcset") {
4776
+ const picked = pickBestSrcsetCandidate(raw);
4777
+ if (picked) return picked;
4778
+ return pickFirstSrcsetToken(raw) || "";
4779
+ }
4780
+ return raw.trim();
4781
+ }
4782
+ function pickBestSrcsetCandidate(raw) {
4783
+ const candidates = parseSrcsetCandidates(raw);
4784
+ if (!candidates.length) return null;
4785
+ const widthCandidates = candidates.filter(
4786
+ (candidate) => typeof candidate.width === "number" && Number.isFinite(candidate.width) && candidate.width > 0
4787
+ );
4788
+ if (widthCandidates.length) {
4789
+ return widthCandidates.reduce(
4790
+ (best, candidate) => candidate.width > best.width ? candidate : best
4791
+ ).url;
4792
+ }
4793
+ const densityCandidates = candidates.filter(
4794
+ (candidate) => typeof candidate.density === "number" && Number.isFinite(candidate.density) && candidate.density > 0
4795
+ );
4796
+ if (densityCandidates.length) {
4797
+ return densityCandidates.reduce(
4798
+ (best, candidate) => candidate.density > best.density ? candidate : best
4799
+ ).url;
4800
+ }
4801
+ return candidates[0]?.url || null;
4802
+ }
4803
+ function parseSrcsetCandidates(raw) {
4804
+ const text = String(raw || "").trim();
4805
+ if (!text) return [];
4806
+ const out = [];
4807
+ let index = 0;
4808
+ while (index < text.length) {
4809
+ index = skipSeparators(text, index);
4810
+ if (index >= text.length) break;
4811
+ const urlToken = readUrlToken(text, index);
4812
+ index = urlToken.nextIndex;
4813
+ const url = urlToken.value.trim();
4814
+ if (!url) continue;
4815
+ index = skipWhitespace(text, index);
4816
+ const descriptors = [];
4817
+ while (index < text.length && text[index] !== ",") {
4818
+ const descriptorToken = readDescriptorToken(text, index);
4819
+ if (!descriptorToken.value) {
4820
+ index = descriptorToken.nextIndex;
4821
+ continue;
4822
+ }
4823
+ descriptors.push(descriptorToken.value);
4824
+ index = descriptorToken.nextIndex;
4825
+ index = skipWhitespace(text, index);
4826
+ }
4827
+ if (index < text.length && text[index] === ",") {
4828
+ index += 1;
4829
+ }
4830
+ let width = null;
4831
+ let density = null;
4832
+ for (const descriptor of descriptors) {
4833
+ const token = descriptor.trim().toLowerCase();
4834
+ if (!token) continue;
4835
+ const widthMatch = token.match(/^(\d+)w$/);
4836
+ if (widthMatch) {
4837
+ const parsed = Number.parseInt(widthMatch[1], 10);
4838
+ if (Number.isFinite(parsed)) {
4839
+ width = parsed;
4840
+ }
4841
+ continue;
4842
+ }
4843
+ const densityMatch = token.match(/^(\d*\.?\d+)x$/);
4844
+ if (densityMatch) {
4845
+ const parsed = Number.parseFloat(densityMatch[1]);
4846
+ if (Number.isFinite(parsed)) {
4847
+ density = parsed;
4848
+ }
4849
+ }
4850
+ }
4851
+ out.push({
4852
+ url,
4853
+ width,
4854
+ density
4855
+ });
4856
+ }
4857
+ return out;
4858
+ }
4859
+ function pickFirstSrcsetToken(raw) {
4860
+ const candidate = parseSrcsetCandidates(raw)[0];
4861
+ if (candidate?.url) {
4862
+ return candidate.url;
4863
+ }
4864
+ const text = String(raw || "");
4865
+ const start = skipSeparators(text, 0);
4866
+ if (start >= text.length) return null;
4867
+ const firstToken = readUrlToken(text, start).value.trim();
4868
+ return firstToken || null;
4869
+ }
4870
+ function skipWhitespace(value, index) {
4871
+ let cursor = index;
4872
+ while (cursor < value.length && /\s/.test(value[cursor])) {
4873
+ cursor += 1;
4874
+ }
4875
+ return cursor;
4876
+ }
4877
+ function skipSeparators(value, index) {
4878
+ let cursor = skipWhitespace(value, index);
4879
+ while (cursor < value.length && value[cursor] === ",") {
4880
+ cursor += 1;
4881
+ cursor = skipWhitespace(value, cursor);
4882
+ }
4883
+ return cursor;
4884
+ }
4885
+ function readUrlToken(value, index) {
4886
+ let cursor = index;
4887
+ let out = "";
4888
+ const isDataUrl = value.slice(index, index + 5).toLowerCase().startsWith("data:");
4889
+ while (cursor < value.length) {
4890
+ const char = value[cursor];
4891
+ if (/\s/.test(char)) {
4892
+ break;
4893
+ }
4894
+ if (char === "," && !isDataUrl) {
4895
+ break;
4896
+ }
4897
+ out += char;
4898
+ cursor += 1;
4899
+ }
4900
+ if (isDataUrl && out.endsWith(",") && cursor < value.length) {
4901
+ out = out.slice(0, -1);
4902
+ }
4903
+ return {
4904
+ value: out,
4905
+ nextIndex: cursor
4906
+ };
4907
+ }
4908
+ function readDescriptorToken(value, index) {
4909
+ let cursor = skipWhitespace(value, index);
4910
+ let out = "";
4911
+ while (cursor < value.length) {
4912
+ const char = value[cursor];
4913
+ if (char === "," || /\s/.test(char)) {
4914
+ break;
4915
+ }
4916
+ out += char;
4917
+ cursor += 1;
4918
+ }
4919
+ return {
4920
+ value: out.trim(),
4921
+ nextIndex: cursor
4735
4922
  };
4736
4923
  }
4737
4924
 
4738
- // src/element-path/errors.ts
4739
- var ElementPathError = class extends Error {
4925
+ // src/html/counter-runtime.ts
4926
+ var CounterResolutionError = class extends Error {
4740
4927
  code;
4741
4928
  constructor(code, message) {
4742
4929
  super(message);
4743
- this.name = "ElementPathError";
4930
+ this.name = "CounterResolutionError";
4744
4931
  this.code = code;
4745
4932
  }
4746
4933
  };
4747
-
4748
- // src/element-path/resolver.ts
4749
- async function resolveElementPath(page, rawPath) {
4750
- const path5 = sanitizeElementPath(rawPath);
4751
- let frame = page.mainFrame();
4752
- let rootHandle = null;
4753
- for (const hop of path5.context) {
4754
- const host = await resolveDomPath(frame, hop.host, rootHandle);
4755
- if (!host) {
4756
- await disposeHandle(rootHandle);
4757
- throw new ElementPathError(
4758
- "ERR_PATH_CONTEXT_HOST_NOT_FOUND",
4759
- "Unable to resolve context host from stored match selectors."
4760
- );
4761
- }
4762
- if (hop.kind === "iframe") {
4763
- const nextFrame = await host.element.contentFrame();
4764
- await host.element.dispose();
4765
- await disposeHandle(rootHandle);
4766
- rootHandle = null;
4767
- if (!nextFrame) {
4768
- throw new ElementPathError(
4769
- "ERR_PATH_IFRAME_UNAVAILABLE",
4770
- "Iframe is unavailable or inaccessible for this path."
4771
- );
4772
- }
4773
- frame = nextFrame;
4774
- continue;
4775
- }
4776
- const shadowRoot = await host.element.evaluateHandle(
4777
- (element) => element.shadowRoot
4778
- );
4779
- await host.element.dispose();
4780
- const isMissing = await shadowRoot.evaluate((value) => value == null);
4781
- if (isMissing) {
4782
- await shadowRoot.dispose();
4783
- await disposeHandle(rootHandle);
4784
- throw new ElementPathError(
4785
- "ERR_PATH_SHADOW_ROOT_UNAVAILABLE",
4786
- "Shadow root is unavailable for this path."
4787
- );
4788
- }
4789
- await disposeHandle(rootHandle);
4790
- rootHandle = shadowRoot;
4791
- }
4792
- const target = await resolveDomPath(frame, path5.nodes, rootHandle);
4793
- if (!target) {
4794
- const diagnostics = await collectCandidateDiagnostics(
4795
- frame,
4796
- path5.nodes,
4797
- rootHandle
4798
- );
4799
- await disposeHandle(rootHandle);
4800
- throw new ElementPathError(
4801
- "ERR_PATH_TARGET_NOT_FOUND",
4802
- buildTargetNotFoundMessage(path5.nodes, diagnostics)
4803
- );
4934
+ async function resolveCounterElement(page, counter) {
4935
+ const normalized = normalizeCounter(counter);
4936
+ if (normalized == null) {
4937
+ throw buildCounterNotFoundError(counter);
4804
4938
  }
4805
- await disposeHandle(rootHandle);
4806
- if (isPathDebugEnabled()) {
4807
- debugPath("resolved", {
4808
- selector: target.selector,
4809
- mode: target.mode,
4810
- count: target.count,
4811
- targetDepth: path5.nodes.length
4812
- });
4939
+ const scan = await scanCounterOccurrences(page, [normalized]);
4940
+ const entry = scan.get(normalized);
4941
+ if (!entry || entry.count <= 0 || !entry.frame) {
4942
+ throw buildCounterNotFoundError(counter);
4813
4943
  }
4814
- return {
4815
- element: target.element,
4816
- usedSelector: target.selector || buildPathSelectorHint(path5)
4817
- };
4818
- }
4819
- async function resolveDomPath(frame, domPath, rootHandle) {
4820
- const candidates = buildPathCandidates(domPath);
4821
- if (!candidates.length) return null;
4822
- if (isPathDebugEnabled()) {
4823
- debugPath("trying selectors", { candidates });
4944
+ if (entry.count > 1) {
4945
+ throw buildCounterAmbiguousError(counter);
4824
4946
  }
4825
- const selected = rootHandle ? await rootHandle.evaluate(selectInRoot, candidates) : await frame.evaluate(selectInDocument, candidates);
4826
- if (!selected || !selected.selector) return null;
4827
- const handle = rootHandle ? await rootHandle.evaluateHandle((root, selector) => {
4828
- if (!(root instanceof ShadowRoot)) return null;
4829
- return root.querySelector(selector);
4830
- }, selected.selector) : await frame.evaluateHandle(
4831
- (selector) => document.querySelector(selector),
4832
- selected.selector
4833
- );
4947
+ const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
4834
4948
  const element = handle.asElement();
4835
4949
  if (!element) {
4836
4950
  await handle.dispose();
4837
- return null;
4951
+ throw buildCounterNotFoundError(counter);
4838
4952
  }
4839
- return {
4840
- element,
4841
- selector: selected.selector,
4842
- mode: selected.mode,
4843
- count: selected.count
4844
- };
4845
- }
4846
- async function collectCandidateDiagnostics(frame, domPath, rootHandle) {
4847
- const candidates = buildPathCandidates(domPath);
4848
- if (!candidates.length) return [];
4849
- const diagnostics = rootHandle ? await rootHandle.evaluate(countInRoot, candidates) : await frame.evaluate(countInDocument, candidates);
4850
- return Array.isArray(diagnostics) ? diagnostics.map((item) => ({
4851
- selector: String(item?.selector || ""),
4852
- count: Number(item?.count || 0)
4853
- })).filter((item) => item.selector) : [];
4854
- }
4855
- function buildTargetNotFoundMessage(domPath, diagnostics) {
4856
- const depth = Array.isArray(domPath) ? domPath.length : 0;
4857
- const sample = diagnostics.slice(0, 4).map((item) => `"${item.selector}" => ${item.count}`).join(", ");
4858
- const base = "Element path resolution failed (ERR_PATH_TARGET_NOT_FOUND): no selector candidate matched the current DOM.";
4859
- if (!sample)
4860
- return `${base} Tried ${Math.max(diagnostics.length, 0)} candidates.`;
4861
- return `${base} Target depth ${depth}. Candidate counts: ${sample}.`;
4953
+ return element;
4862
4954
  }
4863
- function selectInDocument(selectors) {
4864
- let fallback = null;
4865
- for (const selector of selectors) {
4866
- if (!selector) continue;
4867
- let count = 0;
4868
- try {
4869
- count = document.querySelectorAll(selector).length;
4870
- } catch {
4871
- count = 0;
4872
- }
4873
- if (count === 1) {
4874
- return {
4875
- selector,
4876
- count,
4877
- mode: "unique"
4878
- };
4879
- }
4880
- if (count > 1 && !fallback) {
4881
- fallback = {
4882
- selector,
4883
- count,
4884
- mode: "fallback"
4885
- };
4955
+ async function resolveCountersBatch(page, requests) {
4956
+ const out = {};
4957
+ if (!requests.length) return out;
4958
+ const counters = dedupeCounters(requests);
4959
+ const scan = await scanCounterOccurrences(page, counters);
4960
+ for (const counter of counters) {
4961
+ const entry = scan.get(counter);
4962
+ if (entry.count > 1) {
4963
+ throw buildCounterAmbiguousError(counter);
4886
4964
  }
4887
4965
  }
4888
- return fallback;
4889
- }
4890
- function selectInRoot(root, selectors) {
4891
- if (!(root instanceof ShadowRoot)) return null;
4892
- let fallback = null;
4893
- for (const selector of selectors) {
4894
- if (!selector) continue;
4895
- let count = 0;
4896
- try {
4897
- count = root.querySelectorAll(selector).length;
4898
- } catch {
4899
- count = 0;
4966
+ const valueCache = /* @__PURE__ */ new Map();
4967
+ for (const request of requests) {
4968
+ const normalized = normalizeCounter(request.counter);
4969
+ if (normalized == null) {
4970
+ out[request.key] = null;
4971
+ continue;
4900
4972
  }
4901
- if (count === 1) {
4902
- return {
4903
- selector,
4904
- count,
4905
- mode: "unique"
4906
- };
4973
+ const entry = scan.get(normalized);
4974
+ if (!entry || entry.count <= 0 || !entry.frame) {
4975
+ out[request.key] = null;
4976
+ continue;
4907
4977
  }
4908
- if (count > 1 && !fallback) {
4909
- fallback = {
4910
- selector,
4911
- count,
4912
- mode: "fallback"
4913
- };
4978
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
4979
+ if (valueCache.has(cacheKey)) {
4980
+ out[request.key] = valueCache.get(cacheKey);
4981
+ continue;
4914
4982
  }
4915
- }
4916
- return fallback;
4917
- }
4918
- function countInDocument(selectors) {
4919
- const out = [];
4920
- for (const selector of selectors) {
4921
- if (!selector) continue;
4922
- let count = 0;
4923
- try {
4924
- count = document.querySelectorAll(selector).length;
4925
- } catch {
4926
- count = 0;
4983
+ const read = await readCounterValueInFrame(
4984
+ entry.frame,
4985
+ normalized,
4986
+ request.attribute
4987
+ );
4988
+ if (read.status === "ambiguous") {
4989
+ throw buildCounterAmbiguousError(normalized);
4927
4990
  }
4928
- out.push({ selector, count });
4991
+ if (read.status === "missing") {
4992
+ valueCache.set(cacheKey, null);
4993
+ out[request.key] = null;
4994
+ continue;
4995
+ }
4996
+ const normalizedValue = normalizeExtractedValue(
4997
+ read.value ?? null,
4998
+ request.attribute
4999
+ );
5000
+ valueCache.set(cacheKey, normalizedValue);
5001
+ out[request.key] = normalizedValue;
4929
5002
  }
4930
5003
  return out;
4931
5004
  }
4932
- function countInRoot(root, selectors) {
4933
- if (!(root instanceof ShadowRoot)) return [];
5005
+ function dedupeCounters(requests) {
5006
+ const seen = /* @__PURE__ */ new Set();
4934
5007
  const out = [];
4935
- for (const selector of selectors) {
4936
- if (!selector) continue;
4937
- let count = 0;
4938
- try {
4939
- count = root.querySelectorAll(selector).length;
4940
- } catch {
4941
- count = 0;
4942
- }
4943
- out.push({ selector, count });
5008
+ for (const request of requests) {
5009
+ const normalized = normalizeCounter(request.counter);
5010
+ if (normalized == null || seen.has(normalized)) continue;
5011
+ seen.add(normalized);
5012
+ out.push(normalized);
4944
5013
  }
4945
5014
  return out;
4946
5015
  }
4947
- function isPathDebugEnabled() {
4948
- const value = process.env.OPENSTEER_DEBUG_PATH || process.env.OPENSTEER_DEBUG || process.env.DEBUG_SELECTORS;
4949
- if (!value) return false;
4950
- const normalized = value.trim().toLowerCase();
4951
- return normalized === "1" || normalized === "true";
4952
- }
4953
- function debugPath(message, data) {
4954
- if (!isPathDebugEnabled()) return;
4955
- if (data !== void 0) {
4956
- console.log(`[opensteer:path] ${message}`, data);
4957
- } else {
4958
- console.log(`[opensteer:path] ${message}`);
4959
- }
5016
+ function normalizeCounter(counter) {
5017
+ if (!Number.isFinite(counter)) return null;
5018
+ if (!Number.isInteger(counter)) return null;
5019
+ if (counter <= 0) return null;
5020
+ return counter;
4960
5021
  }
4961
- async function disposeHandle(handle) {
4962
- if (!handle) return;
4963
- try {
4964
- await handle.dispose();
4965
- } catch {
5022
+ async function scanCounterOccurrences(page, counters) {
5023
+ const out = /* @__PURE__ */ new Map();
5024
+ for (const counter of counters) {
5025
+ out.set(counter, {
5026
+ count: 0,
5027
+ frame: null
5028
+ });
4966
5029
  }
4967
- }
4968
-
4969
- // src/actions/actionability-probe.ts
4970
- async function probeActionabilityState(element) {
4971
- try {
4972
- return await element.evaluate((target) => {
4973
- if (!(target instanceof Element)) {
4974
- return {
4975
- connected: false,
4976
- visible: null,
4977
- enabled: null,
4978
- editable: null,
4979
- blocker: null
4980
- };
4981
- }
4982
- const connected = target.isConnected;
4983
- if (!connected) {
4984
- return {
4985
- connected: false,
4986
- visible: null,
4987
- enabled: null,
4988
- editable: null,
4989
- blocker: null
5030
+ if (!counters.length) return out;
5031
+ for (const frame of page.frames()) {
5032
+ let frameCounts;
5033
+ try {
5034
+ frameCounts = await frame.evaluate((candidates) => {
5035
+ const keys = new Set(candidates.map((value) => String(value)));
5036
+ const counts = {};
5037
+ for (const key of keys) {
5038
+ counts[key] = 0;
5039
+ }
5040
+ const walk = (root) => {
5041
+ const children = Array.from(root.children);
5042
+ for (const child of children) {
5043
+ const value = child.getAttribute("c");
5044
+ if (value && keys.has(value)) {
5045
+ counts[value] = (counts[value] || 0) + 1;
5046
+ }
5047
+ walk(child);
5048
+ if (child.shadowRoot) {
5049
+ walk(child.shadowRoot);
5050
+ }
5051
+ }
4990
5052
  };
5053
+ walk(document);
5054
+ return counts;
5055
+ }, counters);
5056
+ } catch {
5057
+ continue;
5058
+ }
5059
+ for (const [rawCounter, rawCount] of Object.entries(frameCounts)) {
5060
+ const counter = Number.parseInt(rawCounter, 10);
5061
+ if (!Number.isFinite(counter)) continue;
5062
+ const count = Number(rawCount || 0);
5063
+ if (!Number.isFinite(count) || count <= 0) continue;
5064
+ const entry = out.get(counter);
5065
+ entry.count += count;
5066
+ if (!entry.frame) {
5067
+ entry.frame = frame;
4991
5068
  }
4992
- const style = window.getComputedStyle(target);
4993
- const rect = target.getBoundingClientRect();
4994
- const hasBox = rect.width > 0 && rect.height > 0;
4995
- const opacity = Number.parseFloat(style.opacity || "1");
4996
- const isVisible = hasBox && style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && (!Number.isFinite(opacity) || opacity > 0);
4997
- let enabled = null;
4998
- if (target instanceof HTMLButtonElement || target instanceof HTMLInputElement || target instanceof HTMLSelectElement || target instanceof HTMLTextAreaElement || target instanceof HTMLOptionElement || target instanceof HTMLOptGroupElement || target instanceof HTMLFieldSetElement) {
4999
- enabled = !target.disabled;
5000
- }
5001
- let editable = null;
5002
- if (target instanceof HTMLInputElement) {
5003
- editable = !target.readOnly && !target.disabled;
5004
- } else if (target instanceof HTMLTextAreaElement) {
5005
- editable = !target.readOnly && !target.disabled;
5006
- } else if (target instanceof HTMLSelectElement) {
5007
- editable = !target.disabled;
5008
- } else if (target instanceof HTMLElement && target.isContentEditable) {
5009
- editable = true;
5069
+ }
5070
+ }
5071
+ return out;
5072
+ }
5073
+ async function resolveUniqueHandleInFrame(frame, counter) {
5074
+ return frame.evaluateHandle((targetCounter) => {
5075
+ const matches = [];
5076
+ const walk = (root) => {
5077
+ const children = Array.from(root.children);
5078
+ for (const child of children) {
5079
+ if (child.getAttribute("c") === targetCounter) {
5080
+ matches.push(child);
5081
+ }
5082
+ walk(child);
5083
+ if (child.shadowRoot) {
5084
+ walk(child.shadowRoot);
5085
+ }
5010
5086
  }
5011
- let blocker = null;
5012
- if (hasBox && window.innerWidth > 0 && window.innerHeight > 0) {
5013
- const x = Math.min(
5014
- Math.max(rect.left + rect.width / 2, 0),
5015
- window.innerWidth - 1
5016
- );
5017
- const y = Math.min(
5018
- Math.max(rect.top + rect.height / 2, 0),
5019
- window.innerHeight - 1
5020
- );
5021
- const top = document.elementFromPoint(x, y);
5022
- if (top && top !== target && !target.contains(top)) {
5023
- const classes = String(top.className || "").split(/\s+/).map((value) => value.trim()).filter(Boolean).slice(0, 5);
5024
- blocker = {
5025
- tag: top.tagName.toLowerCase(),
5026
- id: top.id || null,
5027
- classes,
5028
- role: top.getAttribute("role"),
5029
- text: (top.textContent || "").trim().slice(0, 80) || null
5087
+ };
5088
+ walk(document);
5089
+ if (matches.length !== 1) {
5090
+ return null;
5091
+ }
5092
+ return matches[0];
5093
+ }, String(counter));
5094
+ }
5095
+ async function readCounterValueInFrame(frame, counter, attribute) {
5096
+ try {
5097
+ return await frame.evaluate(
5098
+ ({ targetCounter, attribute: attribute2 }) => {
5099
+ const matches = [];
5100
+ const walk = (root) => {
5101
+ const children = Array.from(root.children);
5102
+ for (const child of children) {
5103
+ if (child.getAttribute("c") === targetCounter) {
5104
+ matches.push(child);
5105
+ }
5106
+ walk(child);
5107
+ if (child.shadowRoot) {
5108
+ walk(child.shadowRoot);
5109
+ }
5110
+ }
5111
+ };
5112
+ walk(document);
5113
+ if (!matches.length) {
5114
+ return {
5115
+ status: "missing"
5116
+ };
5117
+ }
5118
+ if (matches.length > 1) {
5119
+ return {
5120
+ status: "ambiguous"
5030
5121
  };
5031
5122
  }
5123
+ const target = matches[0];
5124
+ const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
5125
+ return {
5126
+ status: "ok",
5127
+ value
5128
+ };
5129
+ },
5130
+ {
5131
+ targetCounter: String(counter),
5132
+ attribute
5032
5133
  }
5033
- return {
5034
- connected,
5035
- visible: isVisible,
5036
- enabled,
5037
- editable,
5038
- blocker
5039
- };
5040
- });
5134
+ );
5041
5135
  } catch {
5042
- return null;
5136
+ return {
5137
+ status: "missing"
5138
+ };
5043
5139
  }
5044
5140
  }
5141
+ function buildCounterNotFoundError(counter) {
5142
+ return new CounterResolutionError(
5143
+ "ERR_COUNTER_NOT_FOUND",
5144
+ `Counter ${counter} was not found in the live DOM.`
5145
+ );
5146
+ }
5147
+ function buildCounterAmbiguousError(counter) {
5148
+ return new CounterResolutionError(
5149
+ "ERR_COUNTER_AMBIGUOUS",
5150
+ `Counter ${counter} matches multiple live elements.`
5151
+ );
5152
+ }
5045
5153
 
5046
5154
  // src/actions/failure-classifier.ts
5047
5155
  var ACTION_FAILURE_CODES = [
@@ -5092,7 +5200,7 @@ function defaultActionFailureMessage(action) {
5092
5200
  function classifyActionFailure(input) {
5093
5201
  const typed = classifyTypedError(input.error);
5094
5202
  if (typed) return typed;
5095
- const message = extractErrorMessage(input.error, input.fallbackMessage);
5203
+ const message = extractErrorMessage2(input.error, input.fallbackMessage);
5096
5204
  const fromCallLog = classifyFromPlaywrightMessage(message, input.probe);
5097
5205
  if (fromCallLog) return fromCallLog;
5098
5206
  const fromProbe = classifyFromProbe(input.probe);
@@ -5101,7 +5209,7 @@ function classifyActionFailure(input) {
5101
5209
  if (fromHeuristic) return fromHeuristic;
5102
5210
  return buildFailure({
5103
5211
  code: "UNKNOWN",
5104
- message: ensureMessage(input.fallbackMessage, "Action failed."),
5212
+ message: ensureMessage(message, input.fallbackMessage),
5105
5213
  classificationSource: "unknown"
5106
5214
  });
5107
5215
  }
@@ -5157,13 +5265,6 @@ function classifyTypedError(error) {
5157
5265
  classificationSource: "typed_error"
5158
5266
  });
5159
5267
  }
5160
- if (error.code === "ERR_COUNTER_FRAME_UNAVAILABLE") {
5161
- return buildFailure({
5162
- code: "TARGET_UNAVAILABLE",
5163
- message: error.message,
5164
- classificationSource: "typed_error"
5165
- });
5166
- }
5167
5268
  if (error.code === "ERR_COUNTER_AMBIGUOUS") {
5168
5269
  return buildFailure({
5169
5270
  code: "TARGET_AMBIGUOUS",
@@ -5171,13 +5272,6 @@ function classifyTypedError(error) {
5171
5272
  classificationSource: "typed_error"
5172
5273
  });
5173
5274
  }
5174
- if (error.code === "ERR_COUNTER_STALE_OR_NOT_FOUND") {
5175
- return buildFailure({
5176
- code: "TARGET_STALE",
5177
- message: error.message,
5178
- classificationSource: "typed_error"
5179
- });
5180
- }
5181
5275
  }
5182
5276
  return null;
5183
5277
  }
@@ -5361,13 +5455,22 @@ function defaultRetryableForCode(code) {
5361
5455
  return true;
5362
5456
  }
5363
5457
  }
5364
- function extractErrorMessage(error, fallbackMessage) {
5458
+ function extractErrorMessage2(error, fallbackMessage) {
5365
5459
  if (error instanceof Error && error.message.trim()) {
5366
5460
  return error.message;
5367
5461
  }
5368
5462
  if (typeof error === "string" && error.trim()) {
5369
5463
  return error.trim();
5370
5464
  }
5465
+ if (error && typeof error === "object" && !Array.isArray(error)) {
5466
+ const record = error;
5467
+ if (typeof record.message === "string" && record.message.trim()) {
5468
+ return record.message.trim();
5469
+ }
5470
+ if (typeof record.error === "string" && record.error.trim()) {
5471
+ return record.error.trim();
5472
+ }
5473
+ }
5371
5474
  return ensureMessage(fallbackMessage, "Action failed.");
5372
5475
  }
5373
5476
  function ensureMessage(value, fallback) {
@@ -7677,7 +7780,8 @@ function withTokenQuery(wsUrl, token) {
7677
7780
  // src/cloud/local-cache-sync.ts
7678
7781
  var import_fs3 = __toESM(require("fs"), 1);
7679
7782
  var import_path5 = __toESM(require("path"), 1);
7680
- function collectLocalSelectorCacheEntries(storage) {
7783
+ function collectLocalSelectorCacheEntries(storage, options = {}) {
7784
+ const debug = options.debug === true;
7681
7785
  const namespace = storage.getNamespace();
7682
7786
  const namespaceDir = storage.getNamespaceDir();
7683
7787
  if (!import_fs3.default.existsSync(namespaceDir)) return [];
@@ -7686,7 +7790,7 @@ function collectLocalSelectorCacheEntries(storage) {
7686
7790
  for (const fileName of fileNames) {
7687
7791
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
7688
7792
  const filePath = import_path5.default.join(namespaceDir, fileName);
7689
- const selector = readSelectorFile(filePath);
7793
+ const selector = readSelectorFile(filePath, debug);
7690
7794
  if (!selector) continue;
7691
7795
  const descriptionHash = normalizeDescriptionHash(selector.id);
7692
7796
  const method = normalizeMethod(selector.method);
@@ -7711,11 +7815,20 @@ function collectLocalSelectorCacheEntries(storage) {
7711
7815
  }
7712
7816
  return dedupeNewest(entries);
7713
7817
  }
7714
- function readSelectorFile(filePath) {
7818
+ function readSelectorFile(filePath, debug) {
7715
7819
  try {
7716
7820
  const raw = import_fs3.default.readFileSync(filePath, "utf8");
7717
7821
  return JSON.parse(raw);
7718
- } catch {
7822
+ } catch (error) {
7823
+ const message = extractErrorMessage(
7824
+ error,
7825
+ "Unable to parse selector cache file JSON."
7826
+ );
7827
+ if (debug) {
7828
+ console.warn(
7829
+ `[opensteer] failed to read local selector cache file "${filePath}": ${message}`
7830
+ );
7831
+ }
7719
7832
  return null;
7720
7833
  }
7721
7834
  }
@@ -9923,7 +10036,9 @@ var Opensteer = class _Opensteer {
9923
10036
  this.aiExtract = this.createLazyExtractCallback(model);
9924
10037
  const rootDir = resolved.storage?.rootDir || process.cwd();
9925
10038
  this.namespace = resolveNamespace(resolved, rootDir);
9926
- this.storage = new LocalSelectorStorage(rootDir, this.namespace);
10039
+ this.storage = new LocalSelectorStorage(rootDir, this.namespace, {
10040
+ debug: Boolean(resolved.debug)
10041
+ });
9927
10042
  this.pool = new BrowserPool(resolved.browser || {});
9928
10043
  if (cloudSelection.cloud) {
9929
10044
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
@@ -9942,6 +10057,14 @@ var Opensteer = class _Opensteer {
9942
10057
  this.cloud = null;
9943
10058
  }
9944
10059
  }
10060
+ logDebugError(context, error) {
10061
+ if (!this.config.debug) return;
10062
+ const normalized = normalizeError(error, "Unknown error.");
10063
+ const codeSuffix = normalized.code && normalized.code.trim() ? ` [${normalized.code.trim()}]` : "";
10064
+ console.warn(
10065
+ `[opensteer] ${context}: ${normalized.message}${codeSuffix}`
10066
+ );
10067
+ }
9945
10068
  createLazyResolveCallback(model) {
9946
10069
  let resolverPromise = null;
9947
10070
  return async (...args) => {
@@ -10032,7 +10155,8 @@ var Opensteer = class _Opensteer {
10032
10155
  let tabs;
10033
10156
  try {
10034
10157
  tabs = await this.invokeCloudAction("tabs", {});
10035
- } catch {
10158
+ } catch (error) {
10159
+ this.logDebugError("cloud page reference sync (tabs lookup) failed", error);
10036
10160
  return;
10037
10161
  }
10038
10162
  if (!tabs.length) {
@@ -10170,12 +10294,7 @@ var Opensteer = class _Opensteer {
10170
10294
  try {
10171
10295
  await this.syncLocalSelectorCacheToCloud();
10172
10296
  } catch (error) {
10173
- if (this.config.debug) {
10174
- const message = error instanceof Error ? error.message : String(error);
10175
- console.warn(
10176
- `[opensteer] cloud selector cache sync failed: ${message}`
10177
- );
10178
- }
10297
+ this.logDebugError("cloud selector cache sync failed", error);
10179
10298
  }
10180
10299
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
10181
10300
  this.cloud.localRunId = localRunId;
@@ -10207,7 +10326,12 @@ var Opensteer = class _Opensteer {
10207
10326
  this.cloud.actionClient = actionClient;
10208
10327
  this.cloud.sessionId = sessionId;
10209
10328
  this.cloud.cloudSessionUrl = session3.cloudSessionUrl;
10210
- await this.syncCloudPageRef().catch(() => void 0);
10329
+ await this.syncCloudPageRef().catch((error) => {
10330
+ this.logDebugError(
10331
+ "cloud page reference sync after launch failed",
10332
+ error
10333
+ );
10334
+ });
10211
10335
  this.announceCloudSession({
10212
10336
  sessionId: session3.sessionId,
10213
10337
  workspaceId: session3.cloudSession.workspaceId,
@@ -10294,7 +10418,9 @@ var Opensteer = class _Opensteer {
10294
10418
  }
10295
10419
  async syncLocalSelectorCacheToCloud() {
10296
10420
  if (!this.cloud) return;
10297
- const entries = collectLocalSelectorCacheEntries(this.storage);
10421
+ const entries = collectLocalSelectorCacheEntries(this.storage, {
10422
+ debug: Boolean(this.config.debug)
10423
+ });
10298
10424
  if (!entries.length) return;
10299
10425
  await this.cloud.sessionClient.importSelectorCache({
10300
10426
  entries
@@ -10303,9 +10429,12 @@ var Opensteer = class _Opensteer {
10303
10429
  async goto(url, options) {
10304
10430
  if (this.cloud) {
10305
10431
  await this.invokeCloudActionAndResetCache("goto", { url, options });
10306
- await this.syncCloudPageRef({ expectedUrl: url }).catch(
10307
- () => void 0
10308
- );
10432
+ await this.syncCloudPageRef({ expectedUrl: url }).catch((error) => {
10433
+ this.logDebugError(
10434
+ "cloud page reference sync after goto failed",
10435
+ error
10436
+ );
10437
+ });
10309
10438
  return;
10310
10439
  }
10311
10440
  const { waitUntil = "domcontentloaded", ...rest } = options ?? {};
@@ -10406,7 +10535,7 @@ var Opensteer = class _Opensteer {
10406
10535
  let persistPath = null;
10407
10536
  try {
10408
10537
  if (storageKey && resolution.shouldPersist) {
10409
- persistPath = await this.buildPathFromResolvedHandle(
10538
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10410
10539
  handle,
10411
10540
  "hover",
10412
10541
  resolution.counter
@@ -10505,7 +10634,7 @@ var Opensteer = class _Opensteer {
10505
10634
  let persistPath = null;
10506
10635
  try {
10507
10636
  if (storageKey && resolution.shouldPersist) {
10508
- persistPath = await this.buildPathFromResolvedHandle(
10637
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10509
10638
  handle,
10510
10639
  "input",
10511
10640
  resolution.counter
@@ -10608,7 +10737,7 @@ var Opensteer = class _Opensteer {
10608
10737
  let persistPath = null;
10609
10738
  try {
10610
10739
  if (storageKey && resolution.shouldPersist) {
10611
- persistPath = await this.buildPathFromResolvedHandle(
10740
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10612
10741
  handle,
10613
10742
  "select",
10614
10743
  resolution.counter
@@ -10718,7 +10847,7 @@ var Opensteer = class _Opensteer {
10718
10847
  let persistPath = null;
10719
10848
  try {
10720
10849
  if (storageKey && resolution.shouldPersist) {
10721
- persistPath = await this.buildPathFromResolvedHandle(
10850
+ persistPath = await this.tryBuildPathFromResolvedHandle(
10722
10851
  handle,
10723
10852
  "scroll",
10724
10853
  resolution.counter
@@ -10817,7 +10946,12 @@ var Opensteer = class _Opensteer {
10817
10946
  }
10818
10947
  );
10819
10948
  await this.syncCloudPageRef({ expectedUrl: result.url }).catch(
10820
- () => void 0
10949
+ (error) => {
10950
+ this.logDebugError(
10951
+ "cloud page reference sync after newTab failed",
10952
+ error
10953
+ );
10954
+ }
10821
10955
  );
10822
10956
  return result;
10823
10957
  }
@@ -10829,7 +10963,12 @@ var Opensteer = class _Opensteer {
10829
10963
  async switchTab(index) {
10830
10964
  if (this.cloud) {
10831
10965
  await this.invokeCloudActionAndResetCache("switchTab", { index });
10832
- await this.syncCloudPageRef().catch(() => void 0);
10966
+ await this.syncCloudPageRef().catch((error) => {
10967
+ this.logDebugError(
10968
+ "cloud page reference sync after switchTab failed",
10969
+ error
10970
+ );
10971
+ });
10833
10972
  return;
10834
10973
  }
10835
10974
  const page = await switchTab(this.context, index);
@@ -10839,7 +10978,12 @@ var Opensteer = class _Opensteer {
10839
10978
  async closeTab(index) {
10840
10979
  if (this.cloud) {
10841
10980
  await this.invokeCloudActionAndResetCache("closeTab", { index });
10842
- await this.syncCloudPageRef().catch(() => void 0);
10981
+ await this.syncCloudPageRef().catch((error) => {
10982
+ this.logDebugError(
10983
+ "cloud page reference sync after closeTab failed",
10984
+ error
10985
+ );
10986
+ });
10843
10987
  return;
10844
10988
  }
10845
10989
  const newPage = await closeTab(this.context, this.page, index);
@@ -10999,22 +11143,28 @@ var Opensteer = class _Opensteer {
10999
11143
  const handle = await this.resolveCounterHandle(resolution.counter);
11000
11144
  try {
11001
11145
  if (storageKey && resolution.shouldPersist) {
11002
- const persistPath = await this.buildPathFromResolvedHandle(
11146
+ const persistPath = await this.tryBuildPathFromResolvedHandle(
11003
11147
  handle,
11004
11148
  method,
11005
11149
  resolution.counter
11006
11150
  );
11007
- this.persistPath(
11008
- storageKey,
11009
- method,
11010
- options.description,
11011
- persistPath
11012
- );
11151
+ if (persistPath) {
11152
+ this.persistPath(
11153
+ storageKey,
11154
+ method,
11155
+ options.description,
11156
+ persistPath
11157
+ );
11158
+ }
11013
11159
  }
11014
11160
  return await counterFn(handle);
11015
11161
  } catch (err) {
11016
- const message = err instanceof Error ? err.message : `${method} failed.`;
11017
- throw new Error(message);
11162
+ if (err instanceof Error) {
11163
+ throw err;
11164
+ }
11165
+ throw new Error(
11166
+ `${method} failed. ${extractErrorMessage(err, "Unknown error.")}`
11167
+ );
11018
11168
  } finally {
11019
11169
  await handle.dispose();
11020
11170
  }
@@ -11043,7 +11193,7 @@ var Opensteer = class _Opensteer {
11043
11193
  let persistPath = null;
11044
11194
  try {
11045
11195
  if (storageKey && resolution.shouldPersist) {
11046
- persistPath = await this.buildPathFromResolvedHandle(
11196
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11047
11197
  handle,
11048
11198
  "uploadFile",
11049
11199
  resolution.counter
@@ -11141,6 +11291,9 @@ var Opensteer = class _Opensteer {
11141
11291
  if (this.cloud) {
11142
11292
  return await this.invokeCloudAction("extract", options);
11143
11293
  }
11294
+ if (options.schema !== void 0) {
11295
+ assertValidExtractSchemaRoot(options.schema);
11296
+ }
11144
11297
  const storageKey = this.resolveStorageKey(options.description);
11145
11298
  const schemaHash = options.schema ? computeSchemaHash(options.schema) : null;
11146
11299
  const stored = storageKey ? this.storage.readSelector(storageKey) : null;
@@ -11149,7 +11302,7 @@ var Opensteer = class _Opensteer {
11149
11302
  try {
11150
11303
  payload = normalizePersistedExtractPayload(stored.path);
11151
11304
  } catch (err) {
11152
- const message = err instanceof Error ? err.message : "Unknown error";
11305
+ const message = extractErrorMessage(err, "Unknown error.");
11153
11306
  const selectorFile = storageKey ? this.storage.getSelectorPath(storageKey) : "unknown selector file";
11154
11307
  throw new Error(
11155
11308
  `Cached extraction selector is invalid for the current schema at "${selectorFile}". Delete the cached selector and rerun extraction. ${message}`
@@ -11166,7 +11319,16 @@ var Opensteer = class _Opensteer {
11166
11319
  fields.push(...schemaFields);
11167
11320
  }
11168
11321
  if (!fields.length) {
11169
- const planResult = await this.parseAiExtractPlan(options);
11322
+ let planResult;
11323
+ try {
11324
+ planResult = await this.parseAiExtractPlan(options);
11325
+ } catch (error) {
11326
+ const message = extractErrorMessage(error, "Unknown error.");
11327
+ const contextMessage = options.schema ? "Schema extraction did not resolve deterministic field targets, so Opensteer attempted AI extraction planning." : "Opensteer attempted AI extraction planning.";
11328
+ throw new Error(`${contextMessage} ${message}`, {
11329
+ cause: error
11330
+ });
11331
+ }
11170
11332
  if (planResult.fields.length) {
11171
11333
  fields.push(...planResult.fields);
11172
11334
  } else if (planResult.data !== void 0) {
@@ -11307,7 +11469,7 @@ var Opensteer = class _Opensteer {
11307
11469
  let persistPath = null;
11308
11470
  try {
11309
11471
  if (storageKey && resolution.shouldPersist) {
11310
- persistPath = await this.buildPathFromResolvedHandle(
11472
+ persistPath = await this.tryBuildPathFromResolvedHandle(
11311
11473
  handle,
11312
11474
  "click",
11313
11475
  resolution.counter
@@ -11403,17 +11565,6 @@ var Opensteer = class _Opensteer {
11403
11565
  }
11404
11566
  }
11405
11567
  if (options.element != null) {
11406
- const pathFromElement = await this.tryBuildPathFromCounter(
11407
- options.element
11408
- );
11409
- if (pathFromElement) {
11410
- return {
11411
- path: pathFromElement,
11412
- counter: null,
11413
- shouldPersist: Boolean(storageKey),
11414
- source: "element"
11415
- };
11416
- }
11417
11568
  return {
11418
11569
  path: null,
11419
11570
  counter: options.element,
@@ -11441,17 +11592,6 @@ var Opensteer = class _Opensteer {
11441
11592
  options.description
11442
11593
  );
11443
11594
  if (resolved?.counter != null) {
11444
- const pathFromAiCounter = await this.tryBuildPathFromCounter(
11445
- resolved.counter
11446
- );
11447
- if (pathFromAiCounter) {
11448
- return {
11449
- path: pathFromAiCounter,
11450
- counter: null,
11451
- shouldPersist: Boolean(storageKey),
11452
- source: "ai"
11453
- };
11454
- }
11455
11595
  return {
11456
11596
  path: null,
11457
11597
  counter: resolved.counter,
@@ -11533,23 +11673,22 @@ var Opensteer = class _Opensteer {
11533
11673
  try {
11534
11674
  const builtPath = await buildElementPathFromHandle(handle);
11535
11675
  if (builtPath) {
11536
- return this.withIndexedIframeContext(builtPath, indexedPath);
11676
+ const withFrameContext = await this.withHandleIframeContext(
11677
+ handle,
11678
+ builtPath
11679
+ );
11680
+ return this.withIndexedIframeContext(
11681
+ withFrameContext,
11682
+ indexedPath
11683
+ );
11537
11684
  }
11538
11685
  return indexedPath;
11539
11686
  } finally {
11540
11687
  await handle.dispose();
11541
11688
  }
11542
11689
  }
11543
- async tryBuildPathFromCounter(counter) {
11544
- try {
11545
- return await this.buildPathFromElement(counter);
11546
- } catch {
11547
- return null;
11548
- }
11549
- }
11550
11690
  async resolveCounterHandle(element) {
11551
- const snapshot = await this.ensureSnapshotWithCounters();
11552
- return resolveCounterElement(this.page, snapshot, element);
11691
+ return resolveCounterElement(this.page, element);
11553
11692
  }
11554
11693
  async resolveCounterHandleForAction(action, description, element) {
11555
11694
  try {
@@ -11573,8 +11712,12 @@ var Opensteer = class _Opensteer {
11573
11712
  const indexedPath = await this.readPathFromCounterIndex(counter);
11574
11713
  const builtPath = await buildElementPathFromHandle(handle);
11575
11714
  if (builtPath) {
11715
+ const withFrameContext = await this.withHandleIframeContext(
11716
+ handle,
11717
+ builtPath
11718
+ );
11576
11719
  const normalized = this.withIndexedIframeContext(
11577
- builtPath,
11720
+ withFrameContext,
11578
11721
  indexedPath
11579
11722
  );
11580
11723
  if (normalized.nodes.length) return normalized;
@@ -11584,15 +11727,34 @@ var Opensteer = class _Opensteer {
11584
11727
  `Unable to build element path from counter ${counter} during ${action}.`
11585
11728
  );
11586
11729
  }
11730
+ async tryBuildPathFromResolvedHandle(handle, action, counter) {
11731
+ try {
11732
+ return await this.buildPathFromResolvedHandle(handle, action, counter);
11733
+ } catch (error) {
11734
+ this.logDebugError(
11735
+ `path persistence skipped for ${action} counter ${counter}`,
11736
+ error
11737
+ );
11738
+ return null;
11739
+ }
11740
+ }
11587
11741
  withIndexedIframeContext(builtPath, indexedPath) {
11588
11742
  const normalizedBuilt = this.normalizePath(builtPath);
11589
11743
  if (!indexedPath) return normalizedBuilt;
11590
11744
  const iframePrefix = collectIframeContextPrefix(indexedPath);
11591
11745
  if (!iframePrefix.length) return normalizedBuilt;
11746
+ const builtContext = cloneContextHops(normalizedBuilt.context);
11747
+ const overlap = measureContextOverlap(iframePrefix, builtContext);
11748
+ const missingPrefix = cloneContextHops(
11749
+ iframePrefix.slice(0, iframePrefix.length - overlap)
11750
+ );
11751
+ if (!missingPrefix.length) {
11752
+ return normalizedBuilt;
11753
+ }
11592
11754
  const merged = {
11593
11755
  context: [
11594
- ...cloneContextHops(iframePrefix),
11595
- ...cloneContextHops(normalizedBuilt.context)
11756
+ ...missingPrefix,
11757
+ ...builtContext
11596
11758
  ],
11597
11759
  nodes: cloneElementPath(normalizedBuilt).nodes
11598
11760
  };
@@ -11602,9 +11764,48 @@ var Opensteer = class _Opensteer {
11602
11764
  if (fallback.nodes.length) return fallback;
11603
11765
  return normalizedBuilt;
11604
11766
  }
11767
+ async withHandleIframeContext(handle, path5) {
11768
+ const ownFrame = await handle.ownerFrame();
11769
+ if (!ownFrame) {
11770
+ return this.normalizePath(path5);
11771
+ }
11772
+ let frame = ownFrame;
11773
+ let prefix2 = [];
11774
+ while (frame && frame !== this.page.mainFrame()) {
11775
+ const parent = frame.parentFrame();
11776
+ if (!parent) break;
11777
+ const frameElement = await frame.frameElement().catch(() => null);
11778
+ if (!frameElement) break;
11779
+ try {
11780
+ const frameElementPath = await buildElementPathFromHandle(frameElement);
11781
+ if (frameElementPath?.nodes.length) {
11782
+ const segment = [
11783
+ ...cloneContextHops(frameElementPath.context),
11784
+ {
11785
+ kind: "iframe",
11786
+ host: cloneElementPath(frameElementPath).nodes
11787
+ }
11788
+ ];
11789
+ prefix2 = [...segment, ...prefix2];
11790
+ }
11791
+ } finally {
11792
+ await frameElement.dispose().catch(() => void 0);
11793
+ }
11794
+ frame = parent;
11795
+ }
11796
+ if (!prefix2.length) {
11797
+ return this.normalizePath(path5);
11798
+ }
11799
+ return this.normalizePath({
11800
+ context: [...prefix2, ...cloneContextHops(path5.context)],
11801
+ nodes: cloneElementPath(path5).nodes
11802
+ });
11803
+ }
11605
11804
  async readPathFromCounterIndex(counter) {
11606
- const snapshot = await this.ensureSnapshotWithCounters();
11607
- const indexed = snapshot.counterIndex?.get(counter);
11805
+ if (!this.snapshotCache || this.snapshotCache.url !== this.page.url() || !this.snapshotCache.counterIndex) {
11806
+ return null;
11807
+ }
11808
+ const indexed = this.snapshotCache.counterIndex.get(counter);
11608
11809
  if (!indexed) return null;
11609
11810
  const normalized = this.normalizePath(indexed);
11610
11811
  if (!normalized.nodes.length) return null;
@@ -11615,15 +11816,6 @@ var Opensteer = class _Opensteer {
11615
11816
  if (!path5) return null;
11616
11817
  return this.normalizePath(path5);
11617
11818
  }
11618
- async ensureSnapshotWithCounters() {
11619
- if (!this.snapshotCache || !this.snapshotCache.counterBindings || this.snapshotCache.url !== this.page.url()) {
11620
- await this.snapshot({
11621
- mode: "full",
11622
- withCounters: true
11623
- });
11624
- }
11625
- return this.snapshotCache;
11626
- }
11627
11819
  persistPath(id, method, description, path5) {
11628
11820
  const now = Date.now();
11629
11821
  const safeFile = this.storage.getSelectorFileName(id);
@@ -11828,12 +12020,6 @@ var Opensteer = class _Opensteer {
11828
12020
  };
11829
12021
  }
11830
12022
  async buildFieldTargetsFromSchema(schema) {
11831
- if (!schema || typeof schema !== "object") {
11832
- return [];
11833
- }
11834
- if (Array.isArray(schema)) {
11835
- return [];
11836
- }
11837
12023
  const fields = [];
11838
12024
  await this.collectFieldTargetsFromSchemaObject(
11839
12025
  schema,
@@ -11879,17 +12065,6 @@ var Opensteer = class _Opensteer {
11879
12065
  return;
11880
12066
  }
11881
12067
  if (normalized.element != null) {
11882
- const path5 = await this.tryBuildPathFromCounter(
11883
- normalized.element
11884
- );
11885
- if (path5) {
11886
- fields.push({
11887
- key: fieldKey,
11888
- path: path5,
11889
- attribute: normalized.attribute
11890
- });
11891
- return;
11892
- }
11893
12068
  fields.push({
11894
12069
  key: fieldKey,
11895
12070
  counter: normalized.element,
@@ -11907,6 +12082,10 @@ var Opensteer = class _Opensteer {
11907
12082
  path: path5,
11908
12083
  attribute: normalized.attribute
11909
12084
  });
12085
+ } else {
12086
+ throw new Error(
12087
+ `Extraction schema field "${fieldKey}" uses selector "${normalized.selector}", but no matching element path could be built from the current page snapshot.`
12088
+ );
11910
12089
  }
11911
12090
  return;
11912
12091
  }
@@ -11930,15 +12109,6 @@ var Opensteer = class _Opensteer {
11930
12109
  continue;
11931
12110
  }
11932
12111
  if (fieldPlan.element != null) {
11933
- const path6 = await this.tryBuildPathFromCounter(fieldPlan.element);
11934
- if (path6) {
11935
- fields.push({
11936
- key,
11937
- path: path6,
11938
- attribute: fieldPlan.attribute
11939
- });
11940
- continue;
11941
- }
11942
12112
  fields.push({
11943
12113
  key,
11944
12114
  counter: fieldPlan.element,
@@ -11993,12 +12163,7 @@ var Opensteer = class _Opensteer {
11993
12163
  }
11994
12164
  }
11995
12165
  if (counterRequests.length) {
11996
- const snapshot = await this.ensureSnapshotWithCounters();
11997
- const counterValues = await resolveCountersBatch(
11998
- this.page,
11999
- snapshot,
12000
- counterRequests
12001
- );
12166
+ const counterValues = await resolveCountersBatch(this.page, counterRequests);
12002
12167
  Object.assign(result, counterValues);
12003
12168
  }
12004
12169
  if (pathFields.length) {
@@ -12028,7 +12193,7 @@ var Opensteer = class _Opensteer {
12028
12193
  const path5 = await this.buildPathFromElement(field.counter);
12029
12194
  if (!path5) {
12030
12195
  throw new Error(
12031
- `Unable to build element path from counter ${field.counter} for extraction field "${field.key}".`
12196
+ `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
12032
12197
  );
12033
12198
  }
12034
12199
  resolved.push({
@@ -12050,7 +12215,7 @@ var Opensteer = class _Opensteer {
12050
12215
  }
12051
12216
  resolveStorageKey(description) {
12052
12217
  if (!description) return null;
12053
- return (0, import_crypto2.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12218
+ return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
12054
12219
  }
12055
12220
  normalizePath(path5) {
12056
12221
  return sanitizeElementPath(path5);
@@ -12074,6 +12239,33 @@ function collectIframeContextPrefix(path5) {
12074
12239
  if (lastIframeIndex < 0) return [];
12075
12240
  return cloneContextHops(context.slice(0, lastIframeIndex + 1));
12076
12241
  }
12242
+ function measureContextOverlap(indexedPrefix, builtContext) {
12243
+ const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
12244
+ for (let size = maxOverlap; size > 0; size -= 1) {
12245
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
12246
+ return size;
12247
+ }
12248
+ }
12249
+ for (let size = maxOverlap; size > 0; size -= 1) {
12250
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
12251
+ return size;
12252
+ }
12253
+ }
12254
+ return 0;
12255
+ }
12256
+ function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
12257
+ for (let idx = 0; idx < size; idx += 1) {
12258
+ const left = indexedPrefix[indexedPrefix.length - size + idx];
12259
+ const right = builtContext[idx];
12260
+ if (left.kind !== right.kind) {
12261
+ return false;
12262
+ }
12263
+ if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
12264
+ return false;
12265
+ }
12266
+ }
12267
+ return true;
12268
+ }
12077
12269
  function normalizeSchemaValue(value) {
12078
12270
  if (!value) return null;
12079
12271
  if (typeof value !== "object" || Array.isArray(value)) {
@@ -12095,7 +12287,7 @@ function normalizeExtractSource(source) {
12095
12287
  }
12096
12288
  function computeSchemaHash(schema) {
12097
12289
  const stable = stableStringify(schema);
12098
- return (0, import_crypto2.createHash)("sha256").update(stable).digest("hex");
12290
+ return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
12099
12291
  }
12100
12292
  function buildPathMap(fields) {
12101
12293
  const out = {};
@@ -12257,13 +12449,28 @@ function countNonNullLeaves(value) {
12257
12449
  function isPrimitiveLike(value) {
12258
12450
  return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
12259
12451
  }
12452
+ function assertValidExtractSchemaRoot(schema) {
12453
+ if (!schema || typeof schema !== "object") {
12454
+ throw new Error(
12455
+ "Invalid extraction schema: expected a JSON object at the top level."
12456
+ );
12457
+ }
12458
+ if (Array.isArray(schema)) {
12459
+ throw new Error(
12460
+ 'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
12461
+ );
12462
+ }
12463
+ }
12260
12464
  function parseAiExtractResponse(response) {
12261
12465
  if (typeof response === "string") {
12262
12466
  const trimmed = stripCodeFence2(response);
12263
12467
  try {
12264
12468
  return JSON.parse(trimmed);
12265
12469
  } catch {
12266
- throw new Error("LLM extraction returned a non-JSON string.");
12470
+ const preview = summarizeForError(trimmed);
12471
+ throw new Error(
12472
+ `LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
12473
+ );
12267
12474
  }
12268
12475
  }
12269
12476
  if (response && typeof response === "object") {
@@ -12288,6 +12495,12 @@ function stripCodeFence2(input) {
12288
12495
  if (lastFence === -1) return withoutHeader.trim();
12289
12496
  return withoutHeader.slice(0, lastFence).trim();
12290
12497
  }
12498
+ function summarizeForError(input, maxLength = 180) {
12499
+ const compact = input.replace(/\s+/g, " ").trim();
12500
+ if (!compact) return "";
12501
+ if (compact.length <= maxLength) return compact;
12502
+ return `${compact.slice(0, maxLength)}...`;
12503
+ }
12291
12504
  function getScrollDelta2(options) {
12292
12505
  const amount = typeof options.amount === "number" ? options.amount : 600;
12293
12506
  const absoluteAmount = Math.abs(amount);
@@ -12310,7 +12523,7 @@ function isInternalOrBlankPageUrl(url) {
12310
12523
  }
12311
12524
  function buildLocalRunId(namespace) {
12312
12525
  const normalized = namespace.trim() || "default";
12313
- return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto2.randomUUID)().slice(0, 8)}`;
12526
+ return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
12314
12527
  }
12315
12528
 
12316
12529
  // src/cli/paths.ts
@@ -12627,7 +12840,16 @@ function enqueueRequest(request, socket) {
12627
12840
  void handleRequest(request, socket);
12628
12841
  return;
12629
12842
  }
12630
- requestQueue = requestQueue.then(() => handleRequest(request, socket)).catch(() => {
12843
+ requestQueue = requestQueue.then(() => handleRequest(request, socket)).catch((error) => {
12844
+ sendResponse(
12845
+ socket,
12846
+ buildErrorResponse(
12847
+ request.id,
12848
+ error,
12849
+ "Unexpected server error while handling request.",
12850
+ "CLI_INTERNAL_ERROR"
12851
+ )
12852
+ );
12631
12853
  });
12632
12854
  }
12633
12855
  async function handleRequest(request, socket) {
@@ -12636,7 +12858,11 @@ async function handleRequest(request, socket) {
12636
12858
  sendResponse(socket, {
12637
12859
  id,
12638
12860
  ok: false,
12639
- error: `Session '${session}' is shutting down.`
12861
+ error: `Session '${session}' is shutting down.`,
12862
+ errorInfo: {
12863
+ message: `Session '${session}' is shutting down.`,
12864
+ code: "SESSION_SHUTTING_DOWN"
12865
+ }
12640
12866
  });
12641
12867
  return;
12642
12868
  }
@@ -12644,7 +12870,11 @@ async function handleRequest(request, socket) {
12644
12870
  sendResponse(socket, {
12645
12871
  id,
12646
12872
  ok: false,
12647
- error: `Session '${session}' is shutting down. Retry your command.`
12873
+ error: `Session '${session}' is shutting down. Retry your command.`,
12874
+ errorInfo: {
12875
+ message: `Session '${session}' is shutting down. Retry your command.`,
12876
+ code: "SESSION_SHUTTING_DOWN"
12877
+ }
12648
12878
  });
12649
12879
  return;
12650
12880
  }
@@ -12660,7 +12890,16 @@ async function handleRequest(request, socket) {
12660
12890
  sendResponse(socket, {
12661
12891
  id,
12662
12892
  ok: false,
12663
- error: `Session '${session}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`
12893
+ error: `Session '${session}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
12894
+ errorInfo: {
12895
+ message: `Session '${session}' is already bound to selector namespace '${selectorNamespace}'. Requested '${requestedName}' does not match. Use the same --name for this session or start a different --session.`,
12896
+ code: "SESSION_NAMESPACE_MISMATCH",
12897
+ details: {
12898
+ session,
12899
+ activeNamespace: selectorNamespace,
12900
+ requestedNamespace: requestedName
12901
+ }
12902
+ }
12664
12903
  });
12665
12904
  return;
12666
12905
  }
@@ -12718,11 +12957,10 @@ async function handleRequest(request, socket) {
12718
12957
  }
12719
12958
  });
12720
12959
  } catch (err) {
12721
- sendResponse(socket, {
12722
- id,
12723
- ok: false,
12724
- error: err instanceof Error ? err.message : String(err)
12725
- });
12960
+ sendResponse(
12961
+ socket,
12962
+ buildErrorResponse(id, err, "Failed to open browser session.")
12963
+ );
12726
12964
  }
12727
12965
  return;
12728
12966
  }
@@ -12738,11 +12976,10 @@ async function handleRequest(request, socket) {
12738
12976
  result: { sessionClosed: true }
12739
12977
  });
12740
12978
  } catch (err) {
12741
- sendResponse(socket, {
12742
- id,
12743
- ok: false,
12744
- error: err instanceof Error ? err.message : String(err)
12745
- });
12979
+ sendResponse(
12980
+ socket,
12981
+ buildErrorResponse(id, err, "Failed to close browser session.")
12982
+ );
12746
12983
  }
12747
12984
  beginShutdown();
12748
12985
  return;
@@ -12755,7 +12992,14 @@ async function handleRequest(request, socket) {
12755
12992
  sendResponse(socket, {
12756
12993
  id,
12757
12994
  ok: false,
12758
- error: `No browser session in session '${session}'. Call 'opensteer open --session ${session}' first, or use 'opensteer sessions' to list active sessions.`
12995
+ error: `No browser session in session '${session}'. Call 'opensteer open --session ${session}' first, or use 'opensteer sessions' to list active sessions.`,
12996
+ errorInfo: {
12997
+ message: `No browser session in session '${session}'. Call 'opensteer open --session ${session}' first, or use 'opensteer sessions' to list active sessions.`,
12998
+ code: "SESSION_NOT_OPEN",
12999
+ details: {
13000
+ session
13001
+ }
13002
+ }
12759
13003
  });
12760
13004
  return;
12761
13005
  }
@@ -12764,7 +13008,14 @@ async function handleRequest(request, socket) {
12764
13008
  sendResponse(socket, {
12765
13009
  id,
12766
13010
  ok: false,
12767
- error: `Unknown command: ${command}`
13011
+ error: `Unknown command: ${command}`,
13012
+ errorInfo: {
13013
+ message: `Unknown command: ${command}`,
13014
+ code: "UNKNOWN_COMMAND",
13015
+ details: {
13016
+ command
13017
+ }
13018
+ }
12768
13019
  });
12769
13020
  return;
12770
13021
  }
@@ -12772,11 +13023,12 @@ async function handleRequest(request, socket) {
12772
13023
  const result = await handler(instance, args);
12773
13024
  sendResponse(socket, { id, ok: true, result });
12774
13025
  } catch (err) {
12775
- sendResponse(socket, {
12776
- id,
12777
- ok: false,
12778
- error: err instanceof Error ? err.message : String(err)
12779
- });
13026
+ sendResponse(
13027
+ socket,
13028
+ buildErrorResponse(id, err, `Command "${command}" failed.`, void 0, {
13029
+ command
13030
+ })
13031
+ );
12780
13032
  }
12781
13033
  }
12782
13034
  if ((0, import_fs4.existsSync)(socketPath)) {
@@ -12797,7 +13049,11 @@ var server = (0, import_net.createServer)((socket) => {
12797
13049
  sendResponse(socket, {
12798
13050
  id: 0,
12799
13051
  ok: false,
12800
- error: "Invalid JSON request"
13052
+ error: "Invalid JSON request",
13053
+ errorInfo: {
13054
+ message: "Invalid JSON request",
13055
+ code: "INVALID_JSON_REQUEST"
13056
+ }
12801
13057
  });
12802
13058
  }
12803
13059
  }
@@ -12836,3 +13092,25 @@ async function shutdown() {
12836
13092
  }
12837
13093
  process.on("SIGTERM", shutdown);
12838
13094
  process.on("SIGINT", shutdown);
13095
+ function buildErrorResponse(id, error, fallbackMessage, fallbackCode, details) {
13096
+ const normalized = normalizeError(error, fallbackMessage);
13097
+ let mergedDetails;
13098
+ if (normalized.details || details) {
13099
+ mergedDetails = {
13100
+ ...normalized.details || {},
13101
+ ...details || {}
13102
+ };
13103
+ }
13104
+ return {
13105
+ id,
13106
+ ok: false,
13107
+ error: normalized.message,
13108
+ errorInfo: {
13109
+ message: normalized.message,
13110
+ ...normalized.code || fallbackCode ? { code: normalized.code || fallbackCode } : {},
13111
+ ...normalized.name ? { name: normalized.name } : {},
13112
+ ...mergedDetails ? { details: mergedDetails } : {},
13113
+ ...normalized.cause ? { cause: normalized.cause } : {}
13114
+ }
13115
+ };
13116
+ }