opensteer 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/bin/opensteer.mjs +94 -8
  2. package/dist/{browser-profile-client-DK9qa_Dj.d.cts → browser-profile-client-D6PuRefA.d.cts} +1 -1
  3. package/dist/{browser-profile-client-CaL-mwqs.d.ts → browser-profile-client-OUHaODro.d.ts} +1 -1
  4. package/dist/{chunk-7RMY26CM.js → chunk-54KNQTOL.js} +172 -55
  5. package/dist/{chunk-WJI7TGBQ.js → chunk-6B6LOYU3.js} +1 -1
  6. package/dist/{chunk-F2VDVOJO.js → chunk-G6V2DJRN.js} +451 -609
  7. package/dist/chunk-K5CL76MG.js +81 -0
  8. package/dist/{chunk-WDRMHPWL.js → chunk-KPPOTU3D.js} +159 -180
  9. package/dist/cli/auth.cjs +186 -95
  10. package/dist/cli/auth.d.cts +1 -1
  11. package/dist/cli/auth.d.ts +1 -1
  12. package/dist/cli/auth.js +2 -2
  13. package/dist/cli/local-profile.cjs +197 -0
  14. package/dist/cli/local-profile.d.cts +18 -0
  15. package/dist/cli/local-profile.d.ts +18 -0
  16. package/dist/cli/local-profile.js +97 -0
  17. package/dist/cli/profile.cjs +1747 -1279
  18. package/dist/cli/profile.d.cts +2 -2
  19. package/dist/cli/profile.d.ts +2 -2
  20. package/dist/cli/profile.js +469 -7
  21. package/dist/cli/server.cjs +759 -257
  22. package/dist/cli/server.js +69 -16
  23. package/dist/index.cjs +688 -238
  24. package/dist/index.d.cts +7 -5
  25. package/dist/index.d.ts +7 -5
  26. package/dist/index.js +4 -3
  27. package/dist/{types-BxiRblC7.d.cts → types-BWItZPl_.d.cts} +31 -13
  28. package/dist/{types-BxiRblC7.d.ts → types-BWItZPl_.d.ts} +31 -13
  29. package/package.json +2 -2
  30. package/skills/opensteer/SKILL.md +34 -14
  31. package/skills/opensteer/references/cli-reference.md +1 -1
  32. package/skills/opensteer/references/examples.md +5 -3
  33. package/skills/opensteer/references/sdk-reference.md +16 -14
@@ -424,8 +424,8 @@ __export(profile_exports, {
424
424
  runOpensteerProfileCli: () => runOpensteerProfileCli
425
425
  });
426
426
  module.exports = __toCommonJS(profile_exports);
427
- var import_node_path3 = __toESM(require("path"), 1);
428
- var import_promises3 = require("readline/promises");
427
+ var import_node_path4 = __toESM(require("path"), 1);
428
+ var import_promises4 = require("readline/promises");
429
429
 
430
430
  // src/config.ts
431
431
  var import_fs = __toESM(require("fs"), 1);
@@ -659,6 +659,65 @@ function toJsonSafeValue(value, seen) {
659
659
  return void 0;
660
660
  }
661
661
 
662
+ // src/cloud/credential-selection.ts
663
+ function selectCloudCredential(options) {
664
+ const apiKey = normalizeNonEmptyString(options.apiKey);
665
+ const accessToken = normalizeNonEmptyString(options.accessToken);
666
+ if (apiKey) {
667
+ if (options.authScheme === "bearer") {
668
+ return {
669
+ apiKey,
670
+ authScheme: "bearer",
671
+ kind: "access-token",
672
+ token: apiKey,
673
+ compatibilityBearerApiKey: true
674
+ };
675
+ }
676
+ return {
677
+ apiKey,
678
+ authScheme: "api-key",
679
+ kind: "api-key",
680
+ token: apiKey
681
+ };
682
+ }
683
+ if (accessToken) {
684
+ return {
685
+ accessToken,
686
+ authScheme: "bearer",
687
+ kind: "access-token",
688
+ token: accessToken
689
+ };
690
+ }
691
+ return null;
692
+ }
693
+ function selectCloudCredentialByPrecedence(layers, authScheme) {
694
+ for (const layer of layers) {
695
+ const hasApiKey = layer.hasApiKey ?? Object.prototype.hasOwnProperty.call(layer, "apiKey");
696
+ const hasAccessToken = layer.hasAccessToken ?? Object.prototype.hasOwnProperty.call(layer, "accessToken");
697
+ if (!hasApiKey && !hasAccessToken) {
698
+ continue;
699
+ }
700
+ return {
701
+ source: layer.source,
702
+ apiKey: layer.apiKey,
703
+ accessToken: layer.accessToken,
704
+ hasApiKey,
705
+ hasAccessToken,
706
+ credential: selectCloudCredential({
707
+ apiKey: layer.apiKey,
708
+ accessToken: layer.accessToken,
709
+ authScheme: layer.authScheme ?? authScheme
710
+ })
711
+ };
712
+ }
713
+ return null;
714
+ }
715
+ function normalizeNonEmptyString(value) {
716
+ if (typeof value !== "string") return void 0;
717
+ const normalized = value.trim();
718
+ return normalized.length ? normalized : void 0;
719
+ }
720
+
662
721
  // src/storage/namespace.ts
663
722
  var import_path = __toESM(require("path"), 1);
664
723
  var DEFAULT_NAMESPACE = "default";
@@ -697,9 +756,10 @@ var DEFAULT_CONFIG = {
697
756
  headless: false,
698
757
  executablePath: void 0,
699
758
  slowMo: 0,
700
- connectUrl: void 0,
701
- channel: void 0,
702
- profileDir: void 0
759
+ mode: void 0,
760
+ cdpUrl: void 0,
761
+ userDataDir: void 0,
762
+ profileDirectory: void 0
703
763
  },
704
764
  storage: {
705
765
  rootDir: process.cwd()
@@ -983,11 +1043,6 @@ function normalizeCloudOptions(value) {
983
1043
  }
984
1044
  return value;
985
1045
  }
986
- function normalizeNonEmptyString(value) {
987
- if (typeof value !== "string") return void 0;
988
- const normalized = value.trim();
989
- return normalized.length ? normalized : void 0;
990
- }
991
1046
  function parseCloudEnabled(value, source) {
992
1047
  if (value == null) return void 0;
993
1048
  if (typeof value === "boolean") return value;
@@ -996,6 +1051,18 @@ function parseCloudEnabled(value, source) {
996
1051
  `Invalid ${source} value "${String(value)}". Use true, false, or a cloud options object.`
997
1052
  );
998
1053
  }
1054
+ function resolveCloudCredentialFields(selectedLayer) {
1055
+ const credential = selectedLayer?.credential;
1056
+ if (!credential) return {};
1057
+ if (credential.kind === "api-key" || credential.compatibilityBearerApiKey === true && selectedLayer?.source !== "env") {
1058
+ return {
1059
+ apiKey: credential.token
1060
+ };
1061
+ }
1062
+ return {
1063
+ accessToken: credential.token
1064
+ };
1065
+ }
999
1066
  function resolveCloudSelection(config, env = process.env) {
1000
1067
  const configCloud = parseCloudEnabled(config.cloud, "cloud");
1001
1068
  if (configCloud !== void 0) {
@@ -1032,7 +1099,12 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1032
1099
  });
1033
1100
  assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
1034
1101
  assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
1102
+ const fileCloudOptions = normalizeCloudOptions(fileConfig.cloud);
1103
+ const fileHasCloudApiKey = hasOwn(fileCloudOptions, "apiKey");
1104
+ const fileHasCloudAccessToken = hasOwn(fileCloudOptions, "accessToken");
1035
1105
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1106
+ assertNoRemovedBrowserConfig(input.browser, "Opensteer constructor config");
1107
+ assertNoRemovedBrowserConfig(fileConfig.browser, ".opensteer/config.json");
1036
1108
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1037
1109
  const env = resolveEnv(envRootDir, {
1038
1110
  debug: debugHint,
@@ -1048,14 +1120,30 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1048
1120
  "OPENSTEER_RUNTIME is no longer supported. Use OPENSTEER_MODE instead."
1049
1121
  );
1050
1122
  }
1123
+ if (env.OPENSTEER_CONNECT_URL != null) {
1124
+ throw new Error(
1125
+ "OPENSTEER_CONNECT_URL is no longer supported. Use OPENSTEER_CDP_URL instead."
1126
+ );
1127
+ }
1128
+ if (env.OPENSTEER_CHANNEL != null) {
1129
+ throw new Error(
1130
+ "OPENSTEER_CHANNEL is no longer supported. Use OPENSTEER_BROWSER plus OPENSTEER_BROWSER_PATH when needed."
1131
+ );
1132
+ }
1133
+ if (env.OPENSTEER_PROFILE_DIR != null) {
1134
+ throw new Error(
1135
+ "OPENSTEER_PROFILE_DIR is no longer supported. Use OPENSTEER_USER_DATA_DIR and OPENSTEER_PROFILE_DIRECTORY instead."
1136
+ );
1137
+ }
1051
1138
  const envConfig = {
1052
1139
  browser: {
1053
1140
  headless: parseBool(env.OPENSTEER_HEADLESS),
1054
1141
  executablePath: env.OPENSTEER_BROWSER_PATH || void 0,
1055
1142
  slowMo: parseNumber(env.OPENSTEER_SLOW_MO),
1056
- connectUrl: env.OPENSTEER_CONNECT_URL || void 0,
1057
- channel: env.OPENSTEER_CHANNEL || void 0,
1058
- profileDir: env.OPENSTEER_PROFILE_DIR || void 0
1143
+ mode: env.OPENSTEER_BROWSER === "real" || env.OPENSTEER_BROWSER === "chromium" ? env.OPENSTEER_BROWSER : void 0,
1144
+ cdpUrl: env.OPENSTEER_CDP_URL || void 0,
1145
+ userDataDir: env.OPENSTEER_USER_DATA_DIR || void 0,
1146
+ profileDirectory: env.OPENSTEER_PROFILE_DIRECTORY || void 0
1059
1147
  },
1060
1148
  cursor: {
1061
1149
  enabled: parseBool(env.OPENSTEER_CURSOR)
@@ -1066,17 +1154,38 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1066
1154
  const mergedWithFile = mergeDeep(runtimeDefaults, fileConfig);
1067
1155
  const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
1068
1156
  const resolved = mergeDeep(mergedWithEnv, input);
1157
+ const browserHeadlessExplicit = input.browser?.headless !== void 0 || fileConfig.browser?.headless !== void 0 || envConfig.browser?.headless !== void 0;
1158
+ if (!browserHeadlessExplicit && resolved.browser?.mode === "real") {
1159
+ resolved.browser = {
1160
+ ...resolved.browser,
1161
+ headless: true
1162
+ };
1163
+ }
1164
+ function assertNoRemovedBrowserConfig(value, source) {
1165
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1166
+ return;
1167
+ }
1168
+ const record = value;
1169
+ if (record.connectUrl !== void 0) {
1170
+ throw new Error(
1171
+ `${source}.browser.connectUrl is no longer supported. Use browser.cdpUrl instead.`
1172
+ );
1173
+ }
1174
+ if (record.channel !== void 0) {
1175
+ throw new Error(
1176
+ `${source}.browser.channel is no longer supported. Use browser.mode plus browser.executablePath instead.`
1177
+ );
1178
+ }
1179
+ if (record.profileDir !== void 0) {
1180
+ throw new Error(
1181
+ `${source}.browser.profileDir is no longer supported. Use browser.userDataDir and browser.profileDirectory instead.`
1182
+ );
1183
+ }
1184
+ }
1069
1185
  const envApiKey = resolveOpensteerApiKey(env);
1070
1186
  const envAccessTokenRaw = resolveOpensteerAccessToken(env);
1071
1187
  const envBaseUrl = resolveOpensteerBaseUrl(env);
1072
1188
  const envAuthScheme = resolveOpensteerAuthScheme(env);
1073
- if (envApiKey && envAccessTokenRaw) {
1074
- throw new Error(
1075
- "OPENSTEER_API_KEY and OPENSTEER_ACCESS_TOKEN are mutually exclusive. Set only one."
1076
- );
1077
- }
1078
- const envAccessToken = envAccessTokenRaw || (envAuthScheme === "bearer" ? envApiKey : void 0);
1079
- const envApiCredential = envAuthScheme === "bearer" && !envAccessTokenRaw ? void 0 : envApiKey;
1080
1189
  const envCloudProfileId = resolveOpensteerCloudProfileId(env);
1081
1190
  const envCloudProfileReuseIfActive = resolveOpensteerCloudProfileReuseIfActive(env);
1082
1191
  const envCloudAnnounce = parseCloudAnnounce(
@@ -1105,11 +1214,6 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1105
1214
  const inputHasCloudBaseUrl = Boolean(
1106
1215
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
1107
1216
  );
1108
- if (normalizeNonEmptyString(inputCloudOptions?.apiKey) && normalizeNonEmptyString(inputCloudOptions?.accessToken)) {
1109
- throw new Error(
1110
- "cloud.apiKey and cloud.accessToken are mutually exclusive. Set only one."
1111
- );
1112
- }
1113
1217
  const cloudSelection = resolveCloudSelection({
1114
1218
  cloud: resolved.cloud
1115
1219
  }, env);
@@ -1120,11 +1224,6 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1120
1224
  accessToken: resolvedCloudAccessTokenRaw,
1121
1225
  ...resolvedCloudRest
1122
1226
  } = resolvedCloud;
1123
- if (normalizeNonEmptyString(resolvedCloudApiKeyRaw) && normalizeNonEmptyString(resolvedCloudAccessTokenRaw)) {
1124
- throw new Error(
1125
- "Cloud config cannot include both apiKey and accessToken at the same time."
1126
- );
1127
- }
1128
1227
  const resolvedCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1129
1228
  resolvedCloud.browserProfile,
1130
1229
  "resolved.cloud.browserProfile"
@@ -1136,25 +1235,40 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1136
1235
  const browserProfile = inputCloudBrowserProfile ?? envCloudBrowserProfile ?? resolvedCloudBrowserProfile;
1137
1236
  let authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1138
1237
  const announce = inputCloudAnnounce ?? envCloudAnnounce ?? parseCloudAnnounce(resolvedCloud.announce, "cloud.announce") ?? "always";
1139
- const credentialOverriddenByInput = inputHasCloudApiKey || inputHasCloudAccessToken;
1140
- let apiKey = normalizeNonEmptyString(resolvedCloudApiKeyRaw);
1141
- let accessToken = normalizeNonEmptyString(resolvedCloudAccessTokenRaw);
1142
- if (!credentialOverriddenByInput) {
1143
- if (envAccessToken) {
1144
- accessToken = envAccessToken;
1145
- apiKey = void 0;
1146
- } else if (envApiCredential) {
1147
- apiKey = envApiCredential;
1148
- accessToken = void 0;
1149
- }
1150
- }
1238
+ const selectedCredentialLayer = selectCloudCredentialByPrecedence(
1239
+ [
1240
+ {
1241
+ source: "input",
1242
+ apiKey: inputCloudOptions?.apiKey,
1243
+ accessToken: inputCloudOptions?.accessToken,
1244
+ hasApiKey: inputHasCloudApiKey,
1245
+ hasAccessToken: inputHasCloudAccessToken
1246
+ },
1247
+ {
1248
+ source: "env",
1249
+ apiKey: envApiKey,
1250
+ accessToken: envAccessTokenRaw,
1251
+ hasApiKey: envApiKey !== void 0,
1252
+ hasAccessToken: envAccessTokenRaw !== void 0
1253
+ },
1254
+ {
1255
+ source: "file",
1256
+ apiKey: fileCloudOptions?.apiKey,
1257
+ accessToken: fileCloudOptions?.accessToken,
1258
+ hasApiKey: fileHasCloudApiKey,
1259
+ hasAccessToken: fileHasCloudAccessToken
1260
+ }
1261
+ ],
1262
+ authScheme
1263
+ );
1264
+ const { apiKey, accessToken } = resolveCloudCredentialFields(selectedCredentialLayer);
1151
1265
  if (accessToken) {
1152
1266
  authScheme = "bearer";
1153
1267
  }
1154
1268
  resolved.cloud = {
1155
1269
  ...resolvedCloudRest,
1156
- ...inputHasCloudApiKey ? { apiKey: resolvedCloudApiKeyRaw } : apiKey ? { apiKey } : {},
1157
- ...inputHasCloudAccessToken ? { accessToken: resolvedCloudAccessTokenRaw } : accessToken ? { accessToken } : {},
1270
+ ...apiKey ? { apiKey } : selectedCredentialLayer?.hasApiKey && !accessToken ? { apiKey: selectedCredentialLayer.apiKey } : {},
1271
+ ...accessToken ? { accessToken } : selectedCredentialLayer?.hasAccessToken && !apiKey ? { accessToken: selectedCredentialLayer.accessToken } : {},
1158
1272
  authScheme,
1159
1273
  announce,
1160
1274
  ...browserProfile ? { browserProfile } : {}
@@ -1209,7 +1323,13 @@ function getCallerFilePath() {
1209
1323
  var import_crypto = require("crypto");
1210
1324
 
1211
1325
  // src/browser/pool.ts
1212
- var import_playwright2 = require("playwright");
1326
+ var import_node_child_process = require("child_process");
1327
+ var import_node_fs = require("fs");
1328
+ var import_promises = require("fs/promises");
1329
+ var import_node_net = require("net");
1330
+ var import_node_os = require("os");
1331
+ var import_node_path = require("path");
1332
+ var import_playwright = require("playwright");
1213
1333
 
1214
1334
  // src/browser/cdp-proxy.ts
1215
1335
  var import_ws = __toESM(require("ws"), 1);
@@ -1548,839 +1668,598 @@ function errorMessage(error) {
1548
1668
  return error instanceof Error ? error.message : String(error);
1549
1669
  }
1550
1670
 
1551
- // src/browser/chromium-profile.ts
1552
- var import_node_util = require("util");
1553
- var import_node_child_process2 = require("child_process");
1554
- var import_node_crypto = require("crypto");
1555
- var import_promises = require("fs/promises");
1556
- var import_node_fs = require("fs");
1557
- var import_node_path = require("path");
1558
- var import_node_os = require("os");
1559
- var import_playwright = require("playwright");
1560
-
1561
- // src/auth/keychain-store.ts
1562
- var import_node_child_process = require("child_process");
1563
- function commandExists(command) {
1564
- const result = (0, import_node_child_process.spawnSync)(command, ["--help"], {
1565
- encoding: "utf8",
1566
- stdio: "ignore"
1567
- });
1568
- return result.error == null;
1671
+ // src/browser/chrome.ts
1672
+ var import_os = require("os");
1673
+ var import_path3 = require("path");
1674
+ var import_fs2 = require("fs");
1675
+ function detectChromePaths() {
1676
+ const os2 = (0, import_os.platform)();
1677
+ if (os2 === "darwin") {
1678
+ const executable2 = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
1679
+ return {
1680
+ executable: (0, import_fs2.existsSync)(executable2) ? executable2 : null,
1681
+ defaultUserDataDir: (0, import_path3.join)(
1682
+ (0, import_os.homedir)(),
1683
+ "Library",
1684
+ "Application Support",
1685
+ "Google",
1686
+ "Chrome"
1687
+ )
1688
+ };
1689
+ }
1690
+ if (os2 === "win32") {
1691
+ const executable2 = (0, import_path3.join)(
1692
+ process.env.PROGRAMFILES || "C:\\Program Files",
1693
+ "Google",
1694
+ "Chrome",
1695
+ "Application",
1696
+ "chrome.exe"
1697
+ );
1698
+ return {
1699
+ executable: (0, import_fs2.existsSync)(executable2) ? executable2 : null,
1700
+ defaultUserDataDir: (0, import_path3.join)(
1701
+ process.env.LOCALAPPDATA || (0, import_path3.join)((0, import_os.homedir)(), "AppData", "Local"),
1702
+ "Google",
1703
+ "Chrome",
1704
+ "User Data"
1705
+ )
1706
+ };
1707
+ }
1708
+ const executable = "/usr/bin/google-chrome";
1709
+ return {
1710
+ executable: (0, import_fs2.existsSync)(executable) ? executable : null,
1711
+ defaultUserDataDir: (0, import_path3.join)((0, import_os.homedir)(), ".config", "google-chrome")
1712
+ };
1569
1713
  }
1570
- function commandFailed(result) {
1571
- return typeof result.status === "number" && result.status !== 0;
1714
+ function expandHome(p) {
1715
+ if (p.startsWith("~/") || p === "~") {
1716
+ return (0, import_path3.join)((0, import_os.homedir)(), p.slice(1));
1717
+ }
1718
+ return p;
1572
1719
  }
1573
- function sanitizeCommandArgs(command, args) {
1574
- if (command !== "security") {
1575
- return args;
1720
+ function listLocalChromeProfiles(userDataDir = detectChromePaths().defaultUserDataDir) {
1721
+ const resolvedUserDataDir = expandHome(userDataDir);
1722
+ const localStatePath = (0, import_path3.join)(resolvedUserDataDir, "Local State");
1723
+ if (!(0, import_fs2.existsSync)(localStatePath)) {
1724
+ return [];
1576
1725
  }
1577
- const sanitized = [];
1578
- for (let index = 0; index < args.length; index += 1) {
1579
- const value = args[index];
1580
- sanitized.push(value);
1581
- if (value === "-w" && index + 1 < args.length) {
1582
- sanitized.push("[REDACTED]");
1583
- index += 1;
1726
+ try {
1727
+ const raw = JSON.parse((0, import_fs2.readFileSync)(localStatePath, "utf-8"));
1728
+ const infoCache = raw && typeof raw === "object" && !Array.isArray(raw) && raw.profile && typeof raw.profile === "object" && !Array.isArray(raw.profile) ? raw.profile.info_cache : void 0;
1729
+ if (!infoCache || typeof infoCache !== "object") {
1730
+ return [];
1584
1731
  }
1732
+ return Object.entries(infoCache).map(([directory, info]) => {
1733
+ const record = info && typeof info === "object" && !Array.isArray(info) ? info : {};
1734
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : directory;
1735
+ return {
1736
+ directory,
1737
+ name
1738
+ };
1739
+ }).filter((profile) => profile.directory.trim().length > 0).sort(
1740
+ (left, right) => left.directory.localeCompare(right.directory)
1741
+ );
1742
+ } catch {
1743
+ return [];
1585
1744
  }
1586
- return sanitized;
1587
- }
1588
- function buildCommandError(command, args, result) {
1589
- const stderr = typeof result.stderr === "string" && result.stderr.trim() ? result.stderr.trim() : `Command "${command}" failed with status ${String(result.status)}.`;
1590
- const sanitizedArgs = sanitizeCommandArgs(command, args);
1591
- return new Error(
1592
- [
1593
- `Unable to persist credential via ${command}.`,
1594
- `${command} ${sanitizedArgs.join(" ")}`,
1595
- stderr
1596
- ].join(" ")
1597
- );
1598
1745
  }
1599
- function createMacosSecurityStore() {
1600
- return {
1601
- backend: "macos-security",
1602
- get(service, account) {
1603
- const result = (0, import_node_child_process.spawnSync)(
1604
- "security",
1605
- ["find-generic-password", "-s", service, "-a", account, "-w"],
1606
- { encoding: "utf8" }
1607
- );
1608
- if (commandFailed(result)) {
1609
- return null;
1746
+
1747
+ // src/browser/pool.ts
1748
+ var BrowserPool = class {
1749
+ browser = null;
1750
+ cdpProxy = null;
1751
+ launchedProcess = null;
1752
+ tempUserDataDir = null;
1753
+ defaults;
1754
+ constructor(defaults = {}) {
1755
+ this.defaults = defaults;
1756
+ }
1757
+ async launch(options = {}) {
1758
+ if (this.browser || this.cdpProxy || this.launchedProcess || this.tempUserDataDir) {
1759
+ await this.close();
1760
+ }
1761
+ const mode = options.mode ?? this.defaults.mode ?? "chromium";
1762
+ const cdpUrl = options.cdpUrl ?? this.defaults.cdpUrl;
1763
+ const userDataDir = options.userDataDir ?? this.defaults.userDataDir;
1764
+ const profileDirectory = options.profileDirectory ?? this.defaults.profileDirectory;
1765
+ const executablePath = options.executablePath ?? this.defaults.executablePath;
1766
+ if (cdpUrl) {
1767
+ if (mode === "real") {
1768
+ throw new Error(
1769
+ 'cdpUrl cannot be combined with mode "real". Use one browser launch path at a time.'
1770
+ );
1610
1771
  }
1611
- const secret = result.stdout.trim();
1612
- return secret.length ? secret : null;
1613
- },
1614
- set(service, account, secret) {
1615
- const args = [
1616
- "add-generic-password",
1617
- "-U",
1618
- "-s",
1619
- service,
1620
- "-a",
1621
- account,
1622
- "-w",
1623
- secret
1624
- ];
1625
- const result = (0, import_node_child_process.spawnSync)("security", args, { encoding: "utf8" });
1626
- if (commandFailed(result)) {
1627
- throw buildCommandError("security", args, result);
1772
+ if (userDataDir || profileDirectory) {
1773
+ throw new Error(
1774
+ "userDataDir/profileDirectory cannot be combined with cdpUrl."
1775
+ );
1628
1776
  }
1629
- },
1630
- delete(service, account) {
1631
- const args = ["delete-generic-password", "-s", service, "-a", account];
1632
- const result = (0, import_node_child_process.spawnSync)("security", args, { encoding: "utf8" });
1633
- if (commandFailed(result)) {
1634
- return;
1777
+ if (options.context && Object.keys(options.context).length > 0) {
1778
+ throw new Error(
1779
+ "context launch options are not supported when attaching over CDP."
1780
+ );
1635
1781
  }
1782
+ return this.connectToRunning(cdpUrl, options.timeout);
1636
1783
  }
1637
- };
1638
- }
1639
- function createLinuxSecretToolStore() {
1640
- return {
1641
- backend: "linux-secret-tool",
1642
- get(service, account) {
1643
- const result = (0, import_node_child_process.spawnSync)(
1644
- "secret-tool",
1645
- ["lookup", "service", service, "account", account],
1646
- {
1647
- encoding: "utf8"
1648
- }
1784
+ if (mode !== "real" && (userDataDir || profileDirectory)) {
1785
+ throw new Error(
1786
+ 'userDataDir/profileDirectory require mode "real".'
1649
1787
  );
1650
- if (commandFailed(result)) {
1651
- return null;
1652
- }
1653
- const secret = result.stdout.trim();
1654
- return secret.length ? secret : null;
1655
- },
1656
- set(service, account, secret) {
1657
- const args = [
1658
- "store",
1659
- "--label",
1660
- "Opensteer CLI",
1661
- "service",
1662
- service,
1663
- "account",
1664
- account
1665
- ];
1666
- const result = (0, import_node_child_process.spawnSync)("secret-tool", args, {
1667
- encoding: "utf8",
1668
- input: secret
1669
- });
1670
- if (commandFailed(result)) {
1671
- throw buildCommandError("secret-tool", args, result);
1788
+ }
1789
+ if (mode === "real") {
1790
+ if (options.context && Object.keys(options.context).length > 0) {
1791
+ throw new Error(
1792
+ "context launch options are not supported for real-browser mode."
1793
+ );
1672
1794
  }
1673
- },
1674
- delete(service, account) {
1675
- const args = ["clear", "service", service, "account", account];
1676
- (0, import_node_child_process.spawnSync)("secret-tool", args, {
1677
- encoding: "utf8"
1795
+ return this.launchOwnedRealBrowser({
1796
+ ...options,
1797
+ executablePath,
1798
+ userDataDir,
1799
+ profileDirectory
1678
1800
  });
1679
1801
  }
1680
- };
1681
- }
1682
- function createKeychainStore() {
1683
- if (process.platform === "darwin") {
1684
- if (!commandExists("security")) {
1685
- return null;
1686
- }
1687
- return createMacosSecurityStore();
1802
+ return this.launchSandbox(options);
1688
1803
  }
1689
- if (process.platform === "linux") {
1690
- if (!commandExists("secret-tool")) {
1691
- return null;
1804
+ async close() {
1805
+ const browser = this.browser;
1806
+ const cdpProxy = this.cdpProxy;
1807
+ const launchedProcess = this.launchedProcess;
1808
+ const tempUserDataDir = this.tempUserDataDir;
1809
+ this.browser = null;
1810
+ this.cdpProxy = null;
1811
+ this.launchedProcess = null;
1812
+ this.tempUserDataDir = null;
1813
+ try {
1814
+ if (browser) {
1815
+ await browser.close().catch(() => void 0);
1816
+ }
1817
+ } finally {
1818
+ cdpProxy?.close();
1819
+ await killProcessTree(launchedProcess);
1820
+ if (tempUserDataDir) {
1821
+ await (0, import_promises.rm)(tempUserDataDir, {
1822
+ recursive: true,
1823
+ force: true
1824
+ }).catch(() => void 0);
1825
+ }
1692
1826
  }
1693
- return createLinuxSecretToolStore();
1694
- }
1695
- return null;
1696
- }
1697
-
1698
- // src/browser/chrome.ts
1699
- var import_os = require("os");
1700
- var import_path3 = require("path");
1701
- function expandHome(p) {
1702
- if (p.startsWith("~/") || p === "~") {
1703
- return (0, import_path3.join)((0, import_os.homedir)(), p.slice(1));
1704
1827
  }
1705
- return p;
1706
- }
1707
-
1708
- // src/browser/chromium-profile.ts
1709
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
1710
- var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
1711
- var AES_BLOCK_BYTES = 16;
1712
- var MAC_KEY_ITERATIONS = 1003;
1713
- var LINUX_KEY_ITERATIONS = 1;
1714
- var KEY_LENGTH = 16;
1715
- var KEY_SALT = "saltysalt";
1716
- var DEFAULT_CHROMIUM_BRAND = {
1717
- macService: "Chrome Safe Storage",
1718
- macAccount: "Chrome",
1719
- linuxApplications: ["chrome", "google-chrome"]
1720
- };
1721
- var CHROMIUM_BRANDS = [
1722
- {
1723
- match: ["bravesoftware", "brave-browser"],
1724
- brand: {
1725
- macService: "Brave Safe Storage",
1726
- macAccount: "Brave",
1727
- linuxApplications: ["brave-browser", "brave"]
1728
- }
1729
- },
1730
- {
1731
- match: ["microsoft", "edge"],
1732
- brand: {
1733
- macService: "Microsoft Edge Safe Storage",
1734
- macAccount: "Microsoft Edge",
1735
- linuxApplications: ["microsoft-edge"],
1736
- playwrightChannel: "msedge"
1737
- }
1738
- },
1739
- {
1740
- match: ["google", "chrome beta"],
1741
- brand: {
1742
- macService: "Chrome Beta Safe Storage",
1743
- macAccount: "Chrome Beta",
1744
- linuxApplications: ["chrome-beta"],
1745
- playwrightChannel: "chrome-beta"
1828
+ async connectToRunning(cdpUrl, timeout) {
1829
+ let browser = null;
1830
+ let cdpProxy = null;
1831
+ try {
1832
+ const { browserWsUrl, targets } = await discoverTargets(cdpUrl);
1833
+ if (targets.length === 0) {
1834
+ throw new Error(
1835
+ "No page targets found. Is the browser running with an open window?"
1836
+ );
1837
+ }
1838
+ cdpProxy = new CDPProxy(browserWsUrl, targets[0].id);
1839
+ const proxyWsUrl = await cdpProxy.start();
1840
+ browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
1841
+ timeout: timeout ?? 3e4
1842
+ });
1843
+ this.browser = browser;
1844
+ this.cdpProxy = cdpProxy;
1845
+ const { context, page } = await pickBrowserContextAndPage(browser);
1846
+ return { browser, context, page, isExternal: true };
1847
+ } catch (error) {
1848
+ if (browser) {
1849
+ await browser.close().catch(() => void 0);
1850
+ }
1851
+ cdpProxy?.close();
1852
+ this.browser = null;
1853
+ this.cdpProxy = null;
1854
+ throw error;
1746
1855
  }
1747
- },
1748
- {
1749
- match: ["google", "chrome"],
1750
- brand: {
1751
- macService: "Chrome Safe Storage",
1752
- macAccount: "Chrome",
1753
- linuxApplications: ["chrome", "google-chrome"],
1754
- playwrightChannel: "chrome"
1856
+ }
1857
+ async launchOwnedRealBrowser(options) {
1858
+ const chromePaths = detectChromePaths();
1859
+ const executablePath = options.executablePath ?? chromePaths.executable ?? void 0;
1860
+ if (!executablePath) {
1861
+ throw new Error(
1862
+ "Chrome was not found. Set browser.executablePath or install Chrome in a supported location."
1863
+ );
1755
1864
  }
1756
- },
1757
- {
1758
- match: ["chromium"],
1759
- brand: {
1760
- macService: "Chromium Safe Storage",
1761
- macAccount: "Chromium",
1762
- linuxApplications: ["chromium"]
1865
+ const sourceUserDataDir = expandHome(
1866
+ options.userDataDir ?? chromePaths.defaultUserDataDir
1867
+ );
1868
+ const profileDirectory = options.profileDirectory ?? "Default";
1869
+ const tempUserDataDir = await cloneProfileToTempDir(
1870
+ sourceUserDataDir,
1871
+ profileDirectory
1872
+ );
1873
+ const debugPort = await reserveDebugPort();
1874
+ const headless = resolveLaunchHeadless(
1875
+ "real",
1876
+ options.headless,
1877
+ this.defaults.headless
1878
+ );
1879
+ const launchArgs = buildRealBrowserLaunchArgs({
1880
+ userDataDir: tempUserDataDir,
1881
+ profileDirectory,
1882
+ debugPort,
1883
+ headless
1884
+ });
1885
+ const processHandle = (0, import_node_child_process.spawn)(executablePath, launchArgs, {
1886
+ detached: process.platform !== "win32",
1887
+ stdio: "ignore"
1888
+ });
1889
+ processHandle.unref();
1890
+ let browser = null;
1891
+ try {
1892
+ const wsUrl = await resolveCdpWebSocketUrl(
1893
+ `http://127.0.0.1:${debugPort}`,
1894
+ options.timeout ?? 3e4
1895
+ );
1896
+ browser = await import_playwright.chromium.connectOverCDP(wsUrl, {
1897
+ timeout: options.timeout ?? 3e4
1898
+ });
1899
+ const { context, page } = await createOwnedBrowserContextAndPage(
1900
+ browser,
1901
+ {
1902
+ headless,
1903
+ initialUrl: options.initialUrl,
1904
+ timeoutMs: options.timeout ?? 3e4
1905
+ }
1906
+ );
1907
+ this.browser = browser;
1908
+ this.launchedProcess = processHandle;
1909
+ this.tempUserDataDir = tempUserDataDir;
1910
+ return { browser, context, page, isExternal: false };
1911
+ } catch (error) {
1912
+ await browser?.close().catch(() => void 0);
1913
+ await killProcessTree(processHandle);
1914
+ await (0, import_promises.rm)(tempUserDataDir, {
1915
+ recursive: true,
1916
+ force: true
1917
+ }).catch(() => void 0);
1918
+ throw error;
1763
1919
  }
1764
1920
  }
1765
- ];
1766
- function directoryExists(filePath) {
1767
- try {
1768
- return (0, import_node_fs.statSync)(filePath).isDirectory();
1769
- } catch {
1770
- return false;
1921
+ async launchSandbox(options) {
1922
+ const browser = await import_playwright.chromium.launch({
1923
+ headless: resolveLaunchHeadless(
1924
+ "chromium",
1925
+ options.headless,
1926
+ this.defaults.headless
1927
+ ),
1928
+ executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
1929
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
1930
+ timeout: options.timeout
1931
+ });
1932
+ const context = await browser.newContext(options.context || {});
1933
+ const page = await context.newPage();
1934
+ this.browser = browser;
1935
+ return { browser, context, page, isExternal: false };
1771
1936
  }
1937
+ };
1938
+ async function pickBrowserContextAndPage(browser) {
1939
+ const context = getPrimaryBrowserContext(browser);
1940
+ const pages = context.pages();
1941
+ const page = pages.find((candidate) => isInspectablePageUrl2(candidate.url())) || pages[0] || await context.newPage();
1942
+ return { context, page };
1772
1943
  }
1773
- function fileExists(filePath) {
1774
- try {
1775
- return (0, import_node_fs.statSync)(filePath).isFile();
1776
- } catch {
1777
- return false;
1944
+ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
1945
+ if (requestedHeadless !== void 0) {
1946
+ return requestedHeadless;
1778
1947
  }
1779
- }
1780
- function resolveCookieDbPath(profileDir) {
1781
- const candidates = [(0, import_node_path.join)(profileDir, "Network", "Cookies"), (0, import_node_path.join)(profileDir, "Cookies")];
1782
- for (const candidate of candidates) {
1783
- if (fileExists(candidate)) {
1784
- return candidate;
1785
- }
1948
+ if (defaultHeadless !== void 0) {
1949
+ return defaultHeadless;
1786
1950
  }
1787
- return null;
1951
+ return mode === "real";
1788
1952
  }
1789
- async function selectProfileDirFromUserDataDir(userDataDir) {
1790
- const entries = await (0, import_promises.readdir)(userDataDir, {
1791
- withFileTypes: true
1792
- }).catch(() => []);
1793
- const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path.join)(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
1794
- return candidates;
1953
+ async function createOwnedBrowserContextAndPage(browser, options) {
1954
+ const context = getPrimaryBrowserContext(browser);
1955
+ const page = await createOwnedBrowserPage(browser, context, options);
1956
+ return { context, page };
1795
1957
  }
1796
- async function resolveChromiumProfileLocation(inputPath) {
1797
- const expandedPath = expandHome(inputPath.trim());
1798
- if (!expandedPath) {
1799
- throw new Error("Profile path cannot be empty.");
1958
+ async function createOwnedBrowserPage(browser, context, options) {
1959
+ const targetUrl = options.initialUrl ?? "about:blank";
1960
+ const existingPages = new Set(context.pages());
1961
+ const browserSession = await browser.newBrowserCDPSession();
1962
+ try {
1963
+ const { targetId } = await browserSession.send("Target.createTarget", {
1964
+ url: targetUrl,
1965
+ newWindow: !options.headless
1966
+ });
1967
+ await browserSession.send("Target.activateTarget", { targetId }).catch(() => void 0);
1968
+ const page = await waitForOwnedBrowserPage(context, {
1969
+ existingPages,
1970
+ targetUrl,
1971
+ timeoutMs: options.timeoutMs
1972
+ });
1973
+ if (targetUrl !== "about:blank") {
1974
+ await page.waitForLoadState("domcontentloaded", {
1975
+ timeout: options.timeoutMs
1976
+ });
1977
+ }
1978
+ await closeDisposableStartupTargets(browserSession, targetId);
1979
+ return page;
1980
+ } finally {
1981
+ await browserSession.detach().catch(() => void 0);
1800
1982
  }
1801
- if (fileExists(expandedPath) && (0, import_node_path.basename)(expandedPath) === "Cookies") {
1802
- const directParent = (0, import_node_path.dirname)(expandedPath);
1803
- const profileDir = (0, import_node_path.basename)(directParent) === "Network" ? (0, import_node_path.dirname)(directParent) : directParent;
1804
- const userDataDir = (0, import_node_path.dirname)(profileDir);
1805
- return {
1806
- userDataDir,
1807
- profileDir,
1808
- profileDirectory: (0, import_node_path.basename)(profileDir),
1809
- cookieDbPath: expandedPath,
1810
- localStatePath: fileExists((0, import_node_path.join)(userDataDir, "Local State")) ? (0, import_node_path.join)(userDataDir, "Local State") : null
1811
- };
1983
+ }
1984
+ async function closeDisposableStartupTargets(browserSession, preservedTargetId) {
1985
+ const response = await browserSession.send("Target.getTargets").catch(() => null);
1986
+ if (!response) {
1987
+ return;
1812
1988
  }
1813
- if (fileExists(expandedPath)) {
1814
- throw new Error(
1815
- `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
1816
- );
1989
+ for (const targetInfo of response.targetInfos) {
1990
+ if (targetInfo.targetId === preservedTargetId || targetInfo.type !== "page" || !isDisposableStartupPageUrl(targetInfo.url)) {
1991
+ continue;
1992
+ }
1993
+ await browserSession.send("Target.closeTarget", { targetId: targetInfo.targetId }).catch(() => void 0);
1817
1994
  }
1818
- if (!directoryExists(expandedPath)) {
1819
- throw new Error(
1820
- `Could not find a Chromium profile at "${inputPath}".`
1821
- );
1995
+ }
1996
+ async function waitForOwnedBrowserPage(context, options) {
1997
+ const deadline = Date.now() + options.timeoutMs;
1998
+ let fallbackPage = null;
1999
+ while (Date.now() < deadline) {
2000
+ for (const candidate of context.pages()) {
2001
+ if (options.existingPages.has(candidate)) {
2002
+ continue;
2003
+ }
2004
+ const url = candidate.url();
2005
+ if (!isInspectablePageUrl2(url)) {
2006
+ continue;
2007
+ }
2008
+ fallbackPage ??= candidate;
2009
+ if (options.targetUrl === "about:blank") {
2010
+ return candidate;
2011
+ }
2012
+ if (pageLooselyMatchesUrl(url, options.targetUrl)) {
2013
+ return candidate;
2014
+ }
2015
+ }
2016
+ await sleep(100);
1822
2017
  }
1823
- const directCookieDb = resolveCookieDbPath(expandedPath);
1824
- if (directCookieDb) {
1825
- const userDataDir = (0, import_node_path.dirname)(expandedPath);
1826
- return {
1827
- userDataDir,
1828
- profileDir: expandedPath,
1829
- profileDirectory: (0, import_node_path.basename)(expandedPath),
1830
- cookieDbPath: directCookieDb,
1831
- localStatePath: fileExists((0, import_node_path.join)(userDataDir, "Local State")) ? (0, import_node_path.join)(userDataDir, "Local State") : null
1832
- };
2018
+ if (fallbackPage) {
2019
+ return fallbackPage;
1833
2020
  }
1834
- const localStatePath = (0, import_node_path.join)(expandedPath, "Local State");
1835
- if (!fileExists(localStatePath)) {
2021
+ throw new Error(
2022
+ `Chrome created a target for ${options.targetUrl}, but Playwright did not expose the page in time.`
2023
+ );
2024
+ }
2025
+ function getPrimaryBrowserContext(browser) {
2026
+ const contexts = browser.contexts();
2027
+ if (contexts.length === 0) {
1836
2028
  throw new Error(
1837
- `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
2029
+ "Connection succeeded but no browser contexts were exposed."
1838
2030
  );
1839
2031
  }
1840
- const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
1841
- if (profileDirs.length === 0) {
1842
- throw new Error(
1843
- `No Chromium profile with a Cookies database was found under "${inputPath}".`
1844
- );
2032
+ return contexts[0];
2033
+ }
2034
+ function isInspectablePageUrl2(url) {
2035
+ return url === "about:blank" || url.startsWith("http://") || url.startsWith("https://");
2036
+ }
2037
+ function isDisposableStartupPageUrl(url) {
2038
+ return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/";
2039
+ }
2040
+ function pageLooselyMatchesUrl(currentUrl, initialUrl) {
2041
+ try {
2042
+ const current = new URL(currentUrl);
2043
+ const requested = new URL(initialUrl);
2044
+ if (current.href === requested.href) {
2045
+ return true;
2046
+ }
2047
+ return current.hostname === requested.hostname && current.pathname === requested.pathname;
2048
+ } catch {
2049
+ return currentUrl === initialUrl;
1845
2050
  }
1846
- if (profileDirs.length > 1) {
1847
- const candidates = profileDirs.map((entry) => (0, import_node_path.basename)(entry)).join(", ");
2051
+ }
2052
+ function normalizeDiscoveryUrl(cdpUrl) {
2053
+ let parsed;
2054
+ try {
2055
+ parsed = new URL(cdpUrl);
2056
+ } catch {
1848
2057
  throw new Error(
1849
- `"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
2058
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1850
2059
  );
1851
2060
  }
1852
- const selectedProfileDir = profileDirs[0];
1853
- const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
1854
- if (!cookieDbPath) {
2061
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
2062
+ return parsed;
2063
+ }
2064
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1855
2065
  throw new Error(
1856
- `No Chromium Cookies database was found for "${inputPath}".`
2066
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
1857
2067
  );
1858
2068
  }
1859
- return {
1860
- userDataDir: expandedPath,
1861
- profileDir: selectedProfileDir,
1862
- profileDirectory: (0, import_node_path.basename)(selectedProfileDir),
1863
- cookieDbPath,
1864
- localStatePath
1865
- };
2069
+ const normalized = new URL(parsed.toString());
2070
+ normalized.pathname = "/json/version";
2071
+ normalized.search = "";
2072
+ normalized.hash = "";
2073
+ return normalized;
1866
2074
  }
1867
- function resolvePersistentChromiumLaunchProfile(inputPath) {
1868
- const expandedPath = expandHome(inputPath.trim());
1869
- if (!expandedPath) {
1870
- return {
1871
- userDataDir: inputPath
1872
- };
1873
- }
1874
- if (fileExists(expandedPath) && (0, import_node_path.basename)(expandedPath) === "Cookies") {
1875
- const directParent = (0, import_node_path.dirname)(expandedPath);
1876
- const profileDir = (0, import_node_path.basename)(directParent) === "Network" ? (0, import_node_path.dirname)(directParent) : directParent;
1877
- return {
1878
- userDataDir: (0, import_node_path.dirname)(profileDir),
1879
- profileDirectory: (0, import_node_path.basename)(profileDir)
1880
- };
2075
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
2076
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
2077
+ return cdpUrl;
1881
2078
  }
1882
- if (directoryExists(expandedPath) && resolveCookieDbPath(expandedPath) && fileExists((0, import_node_path.join)((0, import_node_path.dirname)(expandedPath), "Local State"))) {
1883
- return {
1884
- userDataDir: (0, import_node_path.dirname)(expandedPath),
1885
- profileDirectory: (0, import_node_path.basename)(expandedPath)
1886
- };
2079
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
2080
+ const deadline = Date.now() + timeoutMs;
2081
+ let lastError = "CDP discovery did not respond.";
2082
+ while (Date.now() < deadline) {
2083
+ const remaining = Math.max(deadline - Date.now(), 1e3);
2084
+ try {
2085
+ const response = await fetch(versionUrl, {
2086
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
2087
+ });
2088
+ if (!response.ok) {
2089
+ lastError = `${response.status} ${response.statusText}`;
2090
+ } else {
2091
+ const payload = await response.json();
2092
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
2093
+ if (wsUrl && wsUrl.trim()) {
2094
+ return wsUrl;
2095
+ }
2096
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
2097
+ }
2098
+ } catch (error) {
2099
+ lastError = error instanceof Error ? error.message : "Unknown error";
2100
+ }
2101
+ await sleep(100);
1887
2102
  }
1888
- return {
1889
- userDataDir: expandedPath
1890
- };
2103
+ throw new Error(
2104
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
2105
+ );
1891
2106
  }
1892
- function detectChromiumBrand(location) {
1893
- const normalizedPath = location.userDataDir.toLowerCase();
1894
- for (const candidate of CHROMIUM_BRANDS) {
1895
- if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
1896
- return candidate.brand;
1897
- }
1898
- }
1899
- return DEFAULT_CHROMIUM_BRAND;
1900
- }
1901
- async function createSqliteSnapshot(dbPath) {
1902
- const snapshotDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-cookie-db-"));
1903
- const snapshotPath = (0, import_node_path.join)(snapshotDir, "Cookies");
1904
- await (0, import_promises.copyFile)(dbPath, snapshotPath);
1905
- for (const suffix of ["-wal", "-shm", "-journal"]) {
1906
- const source = `${dbPath}${suffix}`;
1907
- if (!(0, import_node_fs.existsSync)(source)) {
1908
- continue;
1909
- }
1910
- await (0, import_promises.copyFile)(source, `${snapshotPath}${suffix}`);
1911
- }
1912
- return {
1913
- snapshotPath,
1914
- cleanup: async () => {
1915
- await (0, import_promises.rm)(snapshotDir, { recursive: true, force: true });
1916
- }
1917
- };
1918
- }
1919
- async function querySqliteJson(dbPath, query) {
1920
- const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
1921
- encoding: "utf8",
1922
- maxBuffer: 64 * 1024 * 1024
2107
+ async function reserveDebugPort() {
2108
+ return await new Promise((resolve, reject) => {
2109
+ const server = (0, import_node_net.createServer)();
2110
+ server.unref();
2111
+ server.on("error", reject);
2112
+ server.listen(0, "127.0.0.1", () => {
2113
+ const address = server.address();
2114
+ if (!address || typeof address === "string") {
2115
+ server.close();
2116
+ reject(new Error("Failed to reserve a local debug port."));
2117
+ return;
2118
+ }
2119
+ server.close((error) => {
2120
+ if (error) {
2121
+ reject(error);
2122
+ return;
2123
+ }
2124
+ resolve(address.port);
2125
+ });
2126
+ });
1923
2127
  });
1924
- const stdout = result.stdout;
1925
- const trimmed = stdout.trim();
1926
- if (!trimmed) {
1927
- return [];
1928
- }
1929
- return JSON.parse(trimmed);
1930
- }
1931
- function convertChromiumTimestampToUnixSeconds(value) {
1932
- if (!value || value === "0") {
1933
- return -1;
1934
- }
1935
- const micros = BigInt(value);
1936
- if (micros <= CHROMIUM_EPOCH_MICROS) {
1937
- return -1;
1938
- }
1939
- return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
1940
2128
  }
1941
- function mapChromiumSameSite(value) {
1942
- if (value === 2) {
1943
- return "Strict";
2129
+ async function cloneProfileToTempDir(userDataDir, profileDirectory) {
2130
+ const resolvedUserDataDir = expandHome(userDataDir);
2131
+ const tempUserDataDir = await (0, import_promises.mkdtemp)(
2132
+ (0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-real-browser-")
2133
+ );
2134
+ const sourceProfileDir = (0, import_node_path.join)(resolvedUserDataDir, profileDirectory);
2135
+ const targetProfileDir = (0, import_node_path.join)(tempUserDataDir, profileDirectory);
2136
+ if ((0, import_node_fs.existsSync)(sourceProfileDir)) {
2137
+ await (0, import_promises.cp)(sourceProfileDir, targetProfileDir, {
2138
+ recursive: true
2139
+ });
2140
+ } else {
2141
+ await (0, import_promises.mkdir)(targetProfileDir, {
2142
+ recursive: true
2143
+ });
1944
2144
  }
1945
- if (value === 0) {
1946
- return "None";
2145
+ const localStatePath = (0, import_node_path.join)(resolvedUserDataDir, "Local State");
2146
+ if ((0, import_node_fs.existsSync)(localStatePath)) {
2147
+ await (0, import_promises.copyFile)(localStatePath, (0, import_node_path.join)(tempUserDataDir, "Local State"));
2148
+ }
2149
+ return tempUserDataDir;
2150
+ }
2151
+ function buildRealBrowserLaunchArgs(options) {
2152
+ const args = [
2153
+ `--user-data-dir=${options.userDataDir}`,
2154
+ `--profile-directory=${options.profileDirectory}`,
2155
+ `--remote-debugging-port=${options.debugPort}`,
2156
+ "--no-first-run",
2157
+ "--no-default-browser-check",
2158
+ "--disable-background-networking",
2159
+ "--disable-sync",
2160
+ "--disable-popup-blocking"
2161
+ ];
2162
+ if (options.headless) {
2163
+ args.push("--headless=new");
1947
2164
  }
1948
- return "Lax";
2165
+ return args;
1949
2166
  }
1950
- function stripChromiumPadding(buffer) {
1951
- const paddingLength = buffer[buffer.length - 1];
1952
- if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
1953
- return buffer;
2167
+ async function killProcessTree(processHandle) {
2168
+ if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
2169
+ return;
1954
2170
  }
1955
- return buffer.subarray(0, buffer.length - paddingLength);
1956
- }
1957
- function stripDomainHashPrefix(buffer, hostKey) {
1958
- if (buffer.length < 32) {
1959
- return buffer;
2171
+ if (process.platform === "win32") {
2172
+ await new Promise((resolve) => {
2173
+ const killer = (0, import_node_child_process.spawn)(
2174
+ "taskkill",
2175
+ ["/pid", String(processHandle.pid), "/t", "/f"],
2176
+ {
2177
+ stdio: "ignore"
2178
+ }
2179
+ );
2180
+ killer.on("error", () => resolve());
2181
+ killer.on("exit", () => resolve());
2182
+ });
2183
+ return;
1960
2184
  }
1961
- const domainHash = (0, import_node_crypto.createHash)("sha256").update(hostKey, "utf8").digest();
1962
- if (buffer.subarray(0, 32).equals(domainHash)) {
1963
- return buffer.subarray(32);
2185
+ try {
2186
+ process.kill(-processHandle.pid, "SIGKILL");
2187
+ } catch {
2188
+ try {
2189
+ processHandle.kill("SIGKILL");
2190
+ } catch {
2191
+ }
1964
2192
  }
1965
- return buffer;
1966
2193
  }
1967
- function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
1968
- const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
1969
- const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
1970
- const decipher = (0, import_node_crypto.createDecipheriv)("aes-128-cbc", key, iv);
1971
- const plaintext = Buffer.concat([
1972
- decipher.update(ciphertext),
1973
- decipher.final()
1974
- ]);
1975
- return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
1976
- "utf8"
1977
- );
2194
+ async function sleep(ms) {
2195
+ await new Promise((resolve) => setTimeout(resolve, ms));
1978
2196
  }
1979
- function decryptChromiumAes256GcmValue(encryptedValue, key) {
1980
- const nonce = encryptedValue.subarray(3, 15);
1981
- const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
1982
- const authTag = encryptedValue.subarray(encryptedValue.length - 16);
1983
- const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", key, nonce);
1984
- decipher.setAuthTag(authTag);
1985
- return Buffer.concat([
1986
- decipher.update(ciphertext),
1987
- decipher.final()
1988
- ]).toString("utf8");
2197
+
2198
+ // src/navigation.ts
2199
+ var DEFAULT_TIMEOUT = 3e4;
2200
+ var DEFAULT_SETTLE_MS = 750;
2201
+ var FRAME_EVALUATE_GRACE_MS = 200;
2202
+ var TRANSIENT_CONTEXT_RETRY_DELAY_MS = 25;
2203
+ var STEALTH_WORLD_NAME = "__opensteer_wait__";
2204
+ var StealthWaitUnavailableError = class extends Error {
2205
+ constructor(cause) {
2206
+ super("Stealth visual wait requires Chromium CDP support.", { cause });
2207
+ this.name = "StealthWaitUnavailableError";
2208
+ }
2209
+ };
2210
+ function isStealthWaitUnavailableError(error) {
2211
+ return error instanceof StealthWaitUnavailableError;
1989
2212
  }
1990
- async function dpapiUnprotect(buffer) {
1991
- const script = [
1992
- `$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
1993
- "$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
1994
- " $inputBytes,",
1995
- " $null,",
1996
- " [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
1997
- ")",
1998
- "[Convert]::ToBase64String($plainBytes)"
1999
- ].join("\n");
2000
- const { stdout } = await execFileAsync(
2001
- "powershell.exe",
2002
- ["-NoProfile", "-NonInteractive", "-Command", script],
2003
- {
2004
- encoding: "utf8",
2005
- maxBuffer: 8 * 1024 * 1024
2213
+ var FRAME_OWNER_VISIBILITY_FUNCTION = `function() {
2214
+ if (!(this instanceof HTMLElement)) return false;
2215
+
2216
+ var rect = this.getBoundingClientRect();
2217
+ if (rect.width <= 0 || rect.height <= 0) return false;
2218
+ if (
2219
+ rect.bottom <= 0 ||
2220
+ rect.right <= 0 ||
2221
+ rect.top >= window.innerHeight ||
2222
+ rect.left >= window.innerWidth
2223
+ ) {
2224
+ return false;
2006
2225
  }
2007
- );
2008
- return Buffer.from(stdout.trim(), "base64");
2009
- }
2010
- async function buildChromiumDecryptor(location) {
2011
- if (process.platform === "darwin") {
2012
- const brand = detectChromiumBrand(location);
2013
- const keychainStore = createKeychainStore();
2014
- const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
2015
- if (!password) {
2016
- throw new Error(
2017
- `Unable to read ${brand.macService} from macOS Keychain.`
2018
- );
2226
+
2227
+ var style = window.getComputedStyle(this);
2228
+ if (
2229
+ style.display === 'none' ||
2230
+ style.visibility === 'hidden' ||
2231
+ Number(style.opacity) === 0
2232
+ ) {
2233
+ return false;
2019
2234
  }
2020
- const key = (0, import_node_crypto.pbkdf2Sync)(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
2021
- return async (row) => decryptChromiumAes128CbcValue(
2022
- Buffer.from(row.encrypted_value || "", "hex"),
2023
- key,
2024
- row.host_key
2025
- );
2026
- }
2027
- if (process.platform === "linux") {
2028
- const brand = detectChromiumBrand(location);
2029
- const keychainStore = createKeychainStore();
2030
- const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
2031
- const key = (0, import_node_crypto.pbkdf2Sync)(
2032
- password || "peanuts",
2033
- KEY_SALT,
2034
- LINUX_KEY_ITERATIONS,
2035
- KEY_LENGTH,
2036
- "sha1"
2037
- );
2038
- return async (row) => decryptChromiumAes128CbcValue(
2039
- Buffer.from(row.encrypted_value || "", "hex"),
2040
- key,
2041
- row.host_key
2042
- );
2043
- }
2044
- if (process.platform === "win32") {
2045
- if (!location.localStatePath) {
2046
- throw new Error(
2047
- `Unable to locate Chromium Local State for profile: ${location.profileDir}`
2048
- );
2235
+
2236
+ return true;
2237
+ }`;
2238
+ function buildStabilityScript(timeout, settleMs) {
2239
+ return `new Promise(function(resolve) {
2240
+ var deadline = Date.now() + ${timeout};
2241
+ var resolved = false;
2242
+ var timer = null;
2243
+ var observers = [];
2244
+ var observedShadowRoots = [];
2245
+ var fonts = document.fonts;
2246
+ var fontsReady = !fonts || fonts.status === 'loaded';
2247
+ var lastRelevantMutationAt = Date.now();
2248
+
2249
+ function clearObservers() {
2250
+ for (var i = 0; i < observers.length; i++) {
2251
+ observers[i].disconnect();
2252
+ }
2253
+ observers = [];
2049
2254
  }
2050
- const localState = JSON.parse(
2051
- await (0, import_promises.readFile)(location.localStatePath, "utf8")
2052
- );
2053
- const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
2054
- if (!encryptedKeyBase64) {
2055
- throw new Error(
2056
- `Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
2057
- );
2058
- }
2059
- const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
2060
- const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
2061
- return async (row) => {
2062
- const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
2063
- if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
2064
- const decrypted = await dpapiUnprotect(encryptedValue);
2065
- return decrypted.toString("utf8");
2066
- }
2067
- return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
2068
- };
2069
- }
2070
- throw new Error(
2071
- `Local Chromium cookie sync is not supported on ${process.platform}.`
2072
- );
2073
- }
2074
- function buildPlaywrightCookie(row, value) {
2075
- if (!row.name.trim()) {
2076
- return null;
2077
- }
2078
- if (!row.host_key.trim()) {
2079
- return null;
2080
- }
2081
- const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
2082
- if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
2083
- return null;
2084
- }
2085
- return {
2086
- name: row.name,
2087
- value,
2088
- domain: row.host_key,
2089
- path: row.path || "/",
2090
- expires,
2091
- httpOnly: row.is_httponly === 1,
2092
- secure: row.is_secure === 1,
2093
- sameSite: mapChromiumSameSite(row.samesite)
2094
- };
2095
- }
2096
- async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
2097
- const location = await resolveChromiumProfileLocation(inputPath);
2098
- try {
2099
- return await loadCookiesFromSqlite(location);
2100
- } catch (error) {
2101
- if (!isMissingSqliteBinary(error)) {
2102
- throw error;
2103
- }
2104
- }
2105
- return await loadCookiesFromBrowserSnapshot(location, options);
2106
- }
2107
- async function loadCookiesFromSqlite(location) {
2108
- const snapshot = await createSqliteSnapshot(location.cookieDbPath);
2109
- try {
2110
- const rows = await querySqliteJson(
2111
- snapshot.snapshotPath,
2112
- [
2113
- "SELECT",
2114
- " host_key,",
2115
- " name,",
2116
- " value,",
2117
- " hex(encrypted_value) AS encrypted_value,",
2118
- " path,",
2119
- " CAST(expires_utc AS TEXT) AS expires_utc,",
2120
- " is_secure,",
2121
- " is_httponly,",
2122
- " has_expires,",
2123
- " samesite",
2124
- "FROM cookies"
2125
- ].join(" ")
2126
- );
2127
- const decryptValue = await buildChromiumDecryptor(location);
2128
- const cookies = [];
2129
- for (const row of rows) {
2130
- let value = row.value || "";
2131
- if (!value && row.encrypted_value) {
2132
- value = await decryptValue(row);
2133
- }
2134
- const cookie = buildPlaywrightCookie(row, value);
2135
- if (cookie) {
2136
- cookies.push(cookie);
2137
- }
2138
- }
2139
- return cookies;
2140
- } finally {
2141
- await snapshot.cleanup();
2142
- }
2143
- }
2144
- async function loadCookiesFromBrowserSnapshot(location, options) {
2145
- const snapshotRootDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-profile-"));
2146
- const snapshotProfileDir = (0, import_node_path.join)(
2147
- snapshotRootDir,
2148
- (0, import_node_path.basename)(location.profileDir)
2149
- );
2150
- let context = null;
2151
- try {
2152
- await (0, import_promises.cp)(location.profileDir, snapshotProfileDir, {
2153
- recursive: true
2154
- });
2155
- if (location.localStatePath) {
2156
- await (0, import_promises.copyFile)(location.localStatePath, (0, import_node_path.join)(snapshotRootDir, "Local State"));
2157
- }
2158
- const brand = detectChromiumBrand(location);
2159
- const args = [`--profile-directory=${(0, import_node_path.basename)(snapshotProfileDir)}`];
2160
- context = await import_playwright.chromium.launchPersistentContext(snapshotRootDir, {
2161
- channel: brand.playwrightChannel,
2162
- headless: options.headless ?? true,
2163
- timeout: options.timeout ?? 12e4,
2164
- args
2165
- });
2166
- return await context.cookies();
2167
- } finally {
2168
- await context?.close().catch(() => void 0);
2169
- await (0, import_promises.rm)(snapshotRootDir, { recursive: true, force: true });
2170
- }
2171
- }
2172
- function isMissingSqliteBinary(error) {
2173
- return Boolean(
2174
- error && typeof error === "object" && "code" in error && error.code === "ENOENT"
2175
- );
2176
- }
2177
-
2178
- // src/browser/pool.ts
2179
- var BrowserPool = class {
2180
- browser = null;
2181
- persistentContext = null;
2182
- cdpProxy = null;
2183
- defaults;
2184
- constructor(defaults = {}) {
2185
- this.defaults = defaults;
2186
- }
2187
- async launch(options = {}) {
2188
- if (this.browser || this.cdpProxy) {
2189
- await this.close();
2190
- }
2191
- const connectUrl = options.connectUrl ?? this.defaults.connectUrl;
2192
- const channel = options.channel ?? this.defaults.channel;
2193
- const profileDir = options.profileDir ?? this.defaults.profileDir;
2194
- if (connectUrl) {
2195
- return this.connectToRunning(connectUrl, options.timeout);
2196
- }
2197
- if (profileDir) {
2198
- return this.launchPersistentProfile(options, channel, profileDir);
2199
- }
2200
- if (channel) {
2201
- return this.launchWithChannel(options, channel);
2202
- }
2203
- return this.launchSandbox(options);
2204
- }
2205
- async close() {
2206
- const browser = this.browser;
2207
- const persistentContext = this.persistentContext;
2208
- this.browser = null;
2209
- this.persistentContext = null;
2210
- try {
2211
- if (persistentContext) {
2212
- await persistentContext.close();
2213
- } else if (browser) {
2214
- await browser.close();
2215
- }
2216
- } finally {
2217
- this.cdpProxy?.close();
2218
- this.cdpProxy = null;
2219
- }
2220
- }
2221
- async connectToRunning(connectUrl, timeout) {
2222
- this.cdpProxy?.close();
2223
- this.cdpProxy = null;
2224
- let browser = null;
2225
- try {
2226
- const { browserWsUrl, targets } = await discoverTargets(connectUrl);
2227
- if (targets.length === 0) {
2228
- throw new Error(
2229
- "No page targets found. Is the browser running with an open window?"
2230
- );
2231
- }
2232
- const target = targets[0];
2233
- this.cdpProxy = new CDPProxy(browserWsUrl, target.id);
2234
- const proxyWsUrl = await this.cdpProxy.start();
2235
- browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
2236
- timeout: timeout ?? 3e4
2237
- });
2238
- this.browser = browser;
2239
- this.persistentContext = null;
2240
- const contexts = browser.contexts();
2241
- if (contexts.length === 0) {
2242
- throw new Error(
2243
- "Connection succeeded but no browser contexts found. Is the browser running with an open window?"
2244
- );
2245
- }
2246
- const context = contexts[0];
2247
- const pages = context.pages();
2248
- const page = pages.length > 0 ? pages[0] : await context.newPage();
2249
- return { browser, context, page, isExternal: true };
2250
- } catch (error) {
2251
- if (browser) {
2252
- await browser.close().catch(() => void 0);
2253
- }
2254
- this.browser = null;
2255
- this.persistentContext = null;
2256
- this.cdpProxy?.close();
2257
- this.cdpProxy = null;
2258
- throw error;
2259
- }
2260
- }
2261
- async launchPersistentProfile(options, channel, profileDir) {
2262
- const args = [];
2263
- const launchProfile = resolvePersistentChromiumLaunchProfile(profileDir);
2264
- if (launchProfile.profileDirectory) {
2265
- args.push(`--profile-directory=${launchProfile.profileDirectory}`);
2266
- }
2267
- const context = await import_playwright2.chromium.launchPersistentContext(
2268
- launchProfile.userDataDir,
2269
- {
2270
- channel,
2271
- headless: options.headless ?? this.defaults.headless,
2272
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
2273
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
2274
- timeout: options.timeout,
2275
- ...options.context || {},
2276
- args
2277
- }
2278
- );
2279
- const browser = context.browser();
2280
- if (!browser) {
2281
- await context.close().catch(() => void 0);
2282
- throw new Error("Persistent browser launch did not expose a browser instance.");
2283
- }
2284
- this.browser = browser;
2285
- this.persistentContext = context;
2286
- const pages = context.pages();
2287
- const page = pages.length > 0 ? pages[0] : await context.newPage();
2288
- return { browser, context, page, isExternal: false };
2289
- }
2290
- async launchWithChannel(options, channel) {
2291
- const browser = await import_playwright2.chromium.launch({
2292
- channel,
2293
- headless: options.headless ?? this.defaults.headless,
2294
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
2295
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
2296
- timeout: options.timeout
2297
- });
2298
- this.browser = browser;
2299
- this.persistentContext = null;
2300
- const context = await browser.newContext(options.context || {});
2301
- const page = await context.newPage();
2302
- return { browser, context, page, isExternal: false };
2303
- }
2304
- async launchSandbox(options) {
2305
- const browser = await import_playwright2.chromium.launch({
2306
- headless: options.headless ?? this.defaults.headless,
2307
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
2308
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
2309
- timeout: options.timeout
2310
- });
2311
- const context = await browser.newContext(options.context || {});
2312
- const page = await context.newPage();
2313
- this.browser = browser;
2314
- this.persistentContext = null;
2315
- return { browser, context, page, isExternal: false };
2316
- }
2317
- };
2318
-
2319
- // src/navigation.ts
2320
- var DEFAULT_TIMEOUT = 3e4;
2321
- var DEFAULT_SETTLE_MS = 750;
2322
- var FRAME_EVALUATE_GRACE_MS = 200;
2323
- var TRANSIENT_CONTEXT_RETRY_DELAY_MS = 25;
2324
- var STEALTH_WORLD_NAME = "__opensteer_wait__";
2325
- var StealthWaitUnavailableError = class extends Error {
2326
- constructor(cause) {
2327
- super("Stealth visual wait requires Chromium CDP support.", { cause });
2328
- this.name = "StealthWaitUnavailableError";
2329
- }
2330
- };
2331
- function isStealthWaitUnavailableError(error) {
2332
- return error instanceof StealthWaitUnavailableError;
2333
- }
2334
- var FRAME_OWNER_VISIBILITY_FUNCTION = `function() {
2335
- if (!(this instanceof HTMLElement)) return false;
2336
-
2337
- var rect = this.getBoundingClientRect();
2338
- if (rect.width <= 0 || rect.height <= 0) return false;
2339
- if (
2340
- rect.bottom <= 0 ||
2341
- rect.right <= 0 ||
2342
- rect.top >= window.innerHeight ||
2343
- rect.left >= window.innerWidth
2344
- ) {
2345
- return false;
2346
- }
2347
-
2348
- var style = window.getComputedStyle(this);
2349
- if (
2350
- style.display === 'none' ||
2351
- style.visibility === 'hidden' ||
2352
- Number(style.opacity) === 0
2353
- ) {
2354
- return false;
2355
- }
2356
-
2357
- return true;
2358
- }`;
2359
- function buildStabilityScript(timeout, settleMs) {
2360
- return `new Promise(function(resolve) {
2361
- var deadline = Date.now() + ${timeout};
2362
- var resolved = false;
2363
- var timer = null;
2364
- var observers = [];
2365
- var observedShadowRoots = [];
2366
- var fonts = document.fonts;
2367
- var fontsReady = !fonts || fonts.status === 'loaded';
2368
- var lastRelevantMutationAt = Date.now();
2369
-
2370
- function clearObservers() {
2371
- for (var i = 0; i < observers.length; i++) {
2372
- observers[i].disconnect();
2373
- }
2374
- observers = [];
2375
- }
2376
-
2377
- function done() {
2378
- if (resolved) return;
2379
- resolved = true;
2380
- if (timer) clearTimeout(timer);
2381
- if (safetyTimer) clearTimeout(safetyTimer);
2382
- clearObservers();
2383
- resolve();
2255
+
2256
+ function done() {
2257
+ if (resolved) return;
2258
+ resolved = true;
2259
+ if (timer) clearTimeout(timer);
2260
+ if (safetyTimer) clearTimeout(safetyTimer);
2261
+ clearObservers();
2262
+ resolve();
2384
2263
  }
2385
2264
 
2386
2265
  function isElementVisiblyIntersectingViewport(element) {
@@ -2694,7 +2573,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2694
2573
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
2695
2574
  Math.max(0, deadline - Date.now())
2696
2575
  );
2697
- await sleep(retryDelay);
2576
+ await sleep2(retryDelay);
2698
2577
  }
2699
2578
  }
2700
2579
  }
@@ -2727,7 +2606,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2727
2606
  () => ({ kind: "resolved" }),
2728
2607
  (error) => ({ kind: "rejected", error })
2729
2608
  );
2730
- const timeoutPromise = sleep(
2609
+ const timeoutPromise = sleep2(
2731
2610
  timeout + FRAME_EVALUATE_GRACE_MS
2732
2611
  ).then(() => ({ kind: "timeout" }));
2733
2612
  const result = await Promise.race([
@@ -2869,14 +2748,14 @@ function isIgnorableFrameError(error) {
2869
2748
  const message = error.message;
2870
2749
  return message.includes("Frame was detached") || message.includes("Target page, context or browser has been closed") || isTransientExecutionContextError(error) || message.includes("No frame for given id found");
2871
2750
  }
2872
- function sleep(ms) {
2751
+ function sleep2(ms) {
2873
2752
  return new Promise((resolve) => {
2874
2753
  setTimeout(resolve, ms);
2875
2754
  });
2876
2755
  }
2877
2756
 
2878
2757
  // src/storage/local.ts
2879
- var import_fs2 = __toESM(require("fs"), 1);
2758
+ var import_fs3 = __toESM(require("fs"), 1);
2880
2759
  var import_path4 = __toESM(require("path"), 1);
2881
2760
 
2882
2761
  // src/storage/registry.ts
@@ -2920,14 +2799,14 @@ var LocalSelectorStorage = class {
2920
2799
  return import_path4.default.join(this.getNamespaceDir(), this.getSelectorFileName(id));
2921
2800
  }
2922
2801
  ensureDirs() {
2923
- import_fs2.default.mkdirSync(this.getNamespaceDir(), { recursive: true });
2802
+ import_fs3.default.mkdirSync(this.getNamespaceDir(), { recursive: true });
2924
2803
  }
2925
2804
  loadRegistry() {
2926
2805
  this.ensureDirs();
2927
2806
  const file = this.getRegistryPath();
2928
- if (!import_fs2.default.existsSync(file)) return createEmptyRegistry(this.namespace);
2807
+ if (!import_fs3.default.existsSync(file)) return createEmptyRegistry(this.namespace);
2929
2808
  try {
2930
- const raw = import_fs2.default.readFileSync(file, "utf8");
2809
+ const raw = import_fs3.default.readFileSync(file, "utf8");
2931
2810
  return JSON.parse(raw);
2932
2811
  } catch (error) {
2933
2812
  const message = extractErrorMessage(
@@ -2944,16 +2823,16 @@ var LocalSelectorStorage = class {
2944
2823
  }
2945
2824
  saveRegistry(registry) {
2946
2825
  this.ensureDirs();
2947
- import_fs2.default.writeFileSync(
2826
+ import_fs3.default.writeFileSync(
2948
2827
  this.getRegistryPath(),
2949
2828
  JSON.stringify(registry, null, 2)
2950
2829
  );
2951
2830
  }
2952
2831
  readSelector(id) {
2953
2832
  const file = this.getSelectorPath(id);
2954
- if (!import_fs2.default.existsSync(file)) return null;
2833
+ if (!import_fs3.default.existsSync(file)) return null;
2955
2834
  try {
2956
- const raw = import_fs2.default.readFileSync(file, "utf8");
2835
+ const raw = import_fs3.default.readFileSync(file, "utf8");
2957
2836
  return JSON.parse(raw);
2958
2837
  } catch (error) {
2959
2838
  const message = extractErrorMessage(
@@ -2970,15 +2849,15 @@ var LocalSelectorStorage = class {
2970
2849
  }
2971
2850
  writeSelector(payload) {
2972
2851
  this.ensureDirs();
2973
- import_fs2.default.writeFileSync(
2852
+ import_fs3.default.writeFileSync(
2974
2853
  this.getSelectorPath(payload.id),
2975
2854
  JSON.stringify(payload, null, 2)
2976
2855
  );
2977
2856
  }
2978
2857
  clearNamespace() {
2979
2858
  const dir = this.getNamespaceDir();
2980
- if (!import_fs2.default.existsSync(dir)) return;
2981
- import_fs2.default.rmSync(dir, { recursive: true, force: true });
2859
+ if (!import_fs3.default.existsSync(dir)) return;
2860
+ import_fs3.default.rmSync(dir, { recursive: true, force: true });
2982
2861
  }
2983
2862
  };
2984
2863
 
@@ -7422,7 +7301,7 @@ var AdaptiveNetworkTracker = class {
7422
7301
  this.idleSince = 0;
7423
7302
  }
7424
7303
  const remaining = Math.max(1, options.deadline - now);
7425
- await sleep2(Math.min(NETWORK_POLL_MS, remaining));
7304
+ await sleep3(Math.min(NETWORK_POLL_MS, remaining));
7426
7305
  }
7427
7306
  }
7428
7307
  handleRequestStarted = (request) => {
@@ -7467,7 +7346,7 @@ var AdaptiveNetworkTracker = class {
7467
7346
  return false;
7468
7347
  }
7469
7348
  };
7470
- async function sleep2(ms) {
7349
+ async function sleep3(ms) {
7471
7350
  await new Promise((resolve) => {
7472
7351
  setTimeout(resolve, ms);
7473
7352
  });
@@ -8944,15 +8823,15 @@ function withTokenQuery(wsUrl, token) {
8944
8823
  }
8945
8824
 
8946
8825
  // src/cloud/local-cache-sync.ts
8947
- var import_fs3 = __toESM(require("fs"), 1);
8826
+ var import_fs4 = __toESM(require("fs"), 1);
8948
8827
  var import_path5 = __toESM(require("path"), 1);
8949
8828
  function collectLocalSelectorCacheEntries(storage, options = {}) {
8950
8829
  const debug = options.debug === true;
8951
8830
  const namespace = storage.getNamespace();
8952
8831
  const namespaceDir = storage.getNamespaceDir();
8953
- if (!import_fs3.default.existsSync(namespaceDir)) return [];
8832
+ if (!import_fs4.default.existsSync(namespaceDir)) return [];
8954
8833
  const entries = [];
8955
- const fileNames = import_fs3.default.readdirSync(namespaceDir);
8834
+ const fileNames = import_fs4.default.readdirSync(namespaceDir);
8956
8835
  for (const fileName of fileNames) {
8957
8836
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
8958
8837
  const filePath = import_path5.default.join(namespaceDir, fileName);
@@ -8983,7 +8862,7 @@ function collectLocalSelectorCacheEntries(storage, options = {}) {
8983
8862
  }
8984
8863
  function readSelectorFile(filePath, debug) {
8985
8864
  try {
8986
- const raw = import_fs3.default.readFileSync(filePath, "utf8");
8865
+ const raw = import_fs4.default.readFileSync(filePath, "utf8");
8987
8866
  return JSON.parse(raw);
8988
8867
  } catch (error) {
8989
8868
  const message = extractErrorMessage(
@@ -9078,13 +8957,13 @@ function dedupeNewest(entries) {
9078
8957
  }
9079
8958
 
9080
8959
  // src/cloud/cdp-client.ts
9081
- var import_playwright3 = require("playwright");
8960
+ var import_playwright2 = require("playwright");
9082
8961
  var CloudCdpClient = class {
9083
8962
  async connect(args) {
9084
8963
  const endpoint = withTokenQuery2(args.wsUrl, args.token);
9085
8964
  let browser;
9086
8965
  try {
9087
- browser = await import_playwright3.chromium.connectOverCDP(endpoint);
8966
+ browser = await import_playwright2.chromium.connectOverCDP(endpoint);
9088
8967
  } catch (error) {
9089
8968
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
9090
8969
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -10877,7 +10756,7 @@ async function executeAgentAction(page, action) {
10877
10756
  }
10878
10757
  case "wait": {
10879
10758
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
10880
- await sleep3(ms);
10759
+ await sleep4(ms);
10881
10760
  return;
10882
10761
  }
10883
10762
  case "goto": {
@@ -11042,7 +10921,7 @@ async function pressKeyCombo(page, combo) {
11042
10921
  }
11043
10922
  }
11044
10923
  }
11045
- function sleep3(ms) {
10924
+ function sleep4(ms) {
11046
10925
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11047
10926
  }
11048
10927
 
@@ -11073,7 +10952,7 @@ var OpensteerCuaAgentHandler = class {
11073
10952
  if (isMutatingAgentAction(action)) {
11074
10953
  this.onMutatingAction?.(action);
11075
10954
  }
11076
- await sleep4(this.config.waitBetweenActionsMs);
10955
+ await sleep5(this.config.waitBetweenActionsMs);
11077
10956
  });
11078
10957
  try {
11079
10958
  const result = await this.client.execute({
@@ -11135,7 +11014,7 @@ var OpensteerCuaAgentHandler = class {
11135
11014
  await this.cursorController.preview({ x, y }, "agent");
11136
11015
  }
11137
11016
  };
11138
- function sleep4(ms) {
11017
+ function sleep5(ms) {
11139
11018
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11140
11019
  }
11141
11020
 
@@ -11571,7 +11450,7 @@ var CursorController = class {
11571
11450
  for (const step of motion.points) {
11572
11451
  await this.renderer.move(step, this.style);
11573
11452
  if (motion.stepDelayMs > 0) {
11574
- await sleep5(motion.stepDelayMs);
11453
+ await sleep6(motion.stepDelayMs);
11575
11454
  }
11576
11455
  }
11577
11456
  if (shouldPulse(intent)) {
@@ -11729,7 +11608,7 @@ function clamp2(value, min, max) {
11729
11608
  function shouldPulse(intent) {
11730
11609
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
11731
11610
  }
11732
- function sleep5(ms) {
11611
+ function sleep6(ms) {
11733
11612
  return new Promise((resolve) => setTimeout(resolve, ms));
11734
11613
  }
11735
11614
 
@@ -11780,30 +11659,20 @@ var Opensteer = class _Opensteer {
11780
11659
  this.pool = new BrowserPool(resolved.browser || {});
11781
11660
  if (cloudSelection.cloud) {
11782
11661
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
11783
- const apiKey = cloudConfig?.apiKey?.trim();
11784
- const accessToken = cloudConfig?.accessToken?.trim();
11785
- if (apiKey && accessToken) {
11786
- throw new Error(
11787
- "Cloud mode cannot use both cloud.apiKey and cloud.accessToken. Set only one credential."
11788
- );
11789
- }
11790
- let credential = "";
11791
- let authScheme = cloudConfig?.authScheme ?? "api-key";
11792
- if (accessToken) {
11793
- credential = accessToken;
11794
- authScheme = "bearer";
11795
- } else if (apiKey) {
11796
- credential = apiKey;
11797
- }
11662
+ const credential = selectCloudCredential({
11663
+ apiKey: cloudConfig?.apiKey,
11664
+ accessToken: cloudConfig?.accessToken,
11665
+ authScheme: cloudConfig?.authScheme
11666
+ });
11798
11667
  if (!credential) {
11799
11668
  throw new Error(
11800
11669
  "Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
11801
11670
  );
11802
11671
  }
11803
11672
  this.cloud = createCloudRuntimeState(
11804
- credential,
11673
+ credential.token,
11805
11674
  cloudConfig?.baseUrl,
11806
- authScheme
11675
+ credential.authScheme
11807
11676
  );
11808
11677
  } else {
11809
11678
  this.cloud = null;
@@ -12124,9 +11993,11 @@ var Opensteer = class _Opensteer {
12124
11993
  }
12125
11994
  const session = await this.pool.launch({
12126
11995
  ...options,
12127
- connectUrl: options.connectUrl ?? this.config.browser?.connectUrl,
12128
- channel: options.channel ?? this.config.browser?.channel,
12129
- profileDir: options.profileDir ?? this.config.browser?.profileDir
11996
+ mode: options.mode ?? this.config.browser?.mode,
11997
+ cdpUrl: options.cdpUrl ?? this.config.browser?.cdpUrl,
11998
+ userDataDir: options.userDataDir ?? this.config.browser?.userDataDir,
11999
+ profileDirectory: options.profileDirectory ?? this.config.browser?.profileDirectory,
12000
+ executablePath: options.executablePath ?? this.config.browser?.executablePath
12130
12001
  });
12131
12002
  this.browser = session.browser;
12132
12003
  this.contextRef = session.context;
@@ -12157,6 +12028,32 @@ var Opensteer = class _Opensteer {
12157
12028
  instance.snapshotCache = null;
12158
12029
  return instance;
12159
12030
  }
12031
+ static listLocalProfiles(userDataDir) {
12032
+ return listLocalChromeProfiles(userDataDir);
12033
+ }
12034
+ static fromSystemChrome(browser = {}, config = {}) {
12035
+ const chromePaths = detectChromePaths();
12036
+ const executablePath = browser.executablePath ?? config.browser?.executablePath ?? chromePaths.executable ?? void 0;
12037
+ if (!executablePath) {
12038
+ throw new Error(
12039
+ "Chrome was not found. Pass executablePath explicitly or install Chrome in a supported location."
12040
+ );
12041
+ }
12042
+ const userDataDir = browser.userDataDir ?? config.browser?.userDataDir ?? chromePaths.defaultUserDataDir;
12043
+ const autoDetectedProfiles = listLocalChromeProfiles(userDataDir);
12044
+ const profileDirectory = browser.profileDirectory ?? config.browser?.profileDirectory ?? autoDetectedProfiles[0]?.directory ?? "Default";
12045
+ return new _Opensteer({
12046
+ ...config,
12047
+ browser: {
12048
+ ...config.browser || {},
12049
+ mode: "real",
12050
+ headless: browser.headless ?? config.browser?.headless ?? true,
12051
+ executablePath,
12052
+ userDataDir,
12053
+ profileDirectory
12054
+ }
12055
+ });
12056
+ }
12160
12057
  async close() {
12161
12058
  this.snapshotCache = null;
12162
12059
  if (this.cloud) {
@@ -14124,400 +14021,992 @@ var Opensteer = class _Opensteer {
14124
14021
  });
14125
14022
  continue;
14126
14023
  }
14127
- pathFields.push({
14128
- key: field.key,
14129
- path: this.normalizePath(field.path),
14130
- attribute: field.attribute
14024
+ pathFields.push({
14025
+ key: field.key,
14026
+ path: this.normalizePath(field.path),
14027
+ attribute: field.attribute
14028
+ });
14029
+ }
14030
+ if (currentUrlKeys.length) {
14031
+ const pageUrl = this.page.url();
14032
+ for (const key of currentUrlKeys) {
14033
+ result[key] = pageUrl;
14034
+ }
14035
+ }
14036
+ if (counterRequests.length) {
14037
+ const counterValues = await resolveCountersBatch(this.page, counterRequests);
14038
+ Object.assign(result, counterValues);
14039
+ }
14040
+ if (pathFields.length) {
14041
+ const pathValues = await extractWithPaths(this.page, pathFields);
14042
+ Object.assign(result, pathValues);
14043
+ }
14044
+ return result;
14045
+ }
14046
+ async resolveFieldTargetsToPersistableFields(fields) {
14047
+ const resolved = [];
14048
+ for (const field of fields) {
14049
+ if ("source" in field) {
14050
+ resolved.push({
14051
+ key: field.key,
14052
+ source: "current_url"
14053
+ });
14054
+ continue;
14055
+ }
14056
+ if ("path" in field) {
14057
+ resolved.push({
14058
+ key: field.key,
14059
+ path: this.normalizePath(field.path),
14060
+ attribute: field.attribute
14061
+ });
14062
+ continue;
14063
+ }
14064
+ const path7 = await this.buildPathFromElement(field.counter);
14065
+ if (!path7) {
14066
+ throw new Error(
14067
+ `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
14068
+ );
14069
+ }
14070
+ resolved.push({
14071
+ key: field.key,
14072
+ path: path7,
14073
+ attribute: field.attribute
14074
+ });
14075
+ }
14076
+ return resolved;
14077
+ }
14078
+ buildActionResult(storageKey, method, persisted, selectorUsed) {
14079
+ return {
14080
+ method,
14081
+ namespace: this.storage.getNamespace(),
14082
+ persisted,
14083
+ pathFile: storageKey && persisted ? this.storage.getSelectorFileName(storageKey) : null,
14084
+ selectorUsed: selectorUsed || null
14085
+ };
14086
+ }
14087
+ resolveStorageKey(description) {
14088
+ if (!description) return null;
14089
+ return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
14090
+ }
14091
+ normalizePath(path7) {
14092
+ return sanitizeElementPath(path7);
14093
+ }
14094
+ };
14095
+ function formatActionFailureMessage(action, description, cause) {
14096
+ const label = description ? `"${description}"` : "unnamed target";
14097
+ return `${action} action failed for ${label}: ${cause}`;
14098
+ }
14099
+ function cloneContextHops(context) {
14100
+ return JSON.parse(JSON.stringify(context || []));
14101
+ }
14102
+ function collectIframeContextPrefix(path7) {
14103
+ const context = path7.context || [];
14104
+ let lastIframeIndex = -1;
14105
+ for (let index = 0; index < context.length; index += 1) {
14106
+ if (context[index]?.kind === "iframe") {
14107
+ lastIframeIndex = index;
14108
+ }
14109
+ }
14110
+ if (lastIframeIndex < 0) return [];
14111
+ return cloneContextHops(context.slice(0, lastIframeIndex + 1));
14112
+ }
14113
+ function measureContextOverlap(indexedPrefix, builtContext) {
14114
+ const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
14115
+ for (let size = maxOverlap; size > 0; size -= 1) {
14116
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
14117
+ return size;
14118
+ }
14119
+ }
14120
+ for (let size = maxOverlap; size > 0; size -= 1) {
14121
+ if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
14122
+ return size;
14123
+ }
14124
+ }
14125
+ return 0;
14126
+ }
14127
+ function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
14128
+ for (let idx = 0; idx < size; idx += 1) {
14129
+ const left = indexedPrefix[indexedPrefix.length - size + idx];
14130
+ const right = builtContext[idx];
14131
+ if (left.kind !== right.kind) {
14132
+ return false;
14133
+ }
14134
+ if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
14135
+ return false;
14136
+ }
14137
+ }
14138
+ return true;
14139
+ }
14140
+ function normalizeSchemaValue(value) {
14141
+ if (!value) return null;
14142
+ if (typeof value !== "object" || Array.isArray(value)) {
14143
+ return null;
14144
+ }
14145
+ const field = value;
14146
+ return {
14147
+ element: field.element,
14148
+ selector: field.selector,
14149
+ attribute: field.attribute,
14150
+ source: normalizeExtractSource(field.source)
14151
+ };
14152
+ }
14153
+ function normalizeExtractSource(source) {
14154
+ if (typeof source !== "string") return void 0;
14155
+ const normalized = source.trim().toLowerCase();
14156
+ if (normalized === "current_url") return "current_url";
14157
+ return void 0;
14158
+ }
14159
+ function computeSchemaHash(schema) {
14160
+ const stable = stableStringify(schema);
14161
+ return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
14162
+ }
14163
+ function buildPathMap(fields) {
14164
+ const out = {};
14165
+ for (const field of fields) {
14166
+ out[field.key] = cloneElementPath(field.path);
14167
+ }
14168
+ return out;
14169
+ }
14170
+ function toPathFields(fields) {
14171
+ return fields.filter(isPersistablePathField).map((field) => ({
14172
+ key: field.key,
14173
+ path: field.path,
14174
+ attribute: field.attribute
14175
+ }));
14176
+ }
14177
+ function normalizePersistedExtractPayload(raw) {
14178
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14179
+ throw new Error(
14180
+ "Invalid persisted extraction payload: expected an object payload."
14181
+ );
14182
+ }
14183
+ const root = {};
14184
+ for (const [key, value] of Object.entries(raw)) {
14185
+ const normalizedKey = String(key || "").trim();
14186
+ if (!normalizedKey) continue;
14187
+ if (normalizedKey.startsWith("$")) {
14188
+ throw new Error(
14189
+ `Invalid persisted extraction payload key "${normalizedKey}": root keys must not start with "$".`
14190
+ );
14191
+ }
14192
+ root[normalizedKey] = normalizePersistedExtractNode(
14193
+ value,
14194
+ normalizedKey
14195
+ );
14196
+ }
14197
+ return root;
14198
+ }
14199
+ function normalizePersistedExtractNode(raw, label) {
14200
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14201
+ throw new Error(
14202
+ `Invalid persisted extraction node at "${label}": expected an object.`
14203
+ );
14204
+ }
14205
+ const record = raw;
14206
+ if (record.$path) {
14207
+ if (typeof record.$path !== "object") {
14208
+ throw new Error(
14209
+ `Invalid persisted extraction value node at "${label}": "$path" must be an element path object.`
14210
+ );
14211
+ }
14212
+ return {
14213
+ $path: sanitizeElementPath(record.$path),
14214
+ attribute: typeof record.attribute === "string" ? record.attribute : void 0
14215
+ };
14216
+ }
14217
+ if (record.$source != null) {
14218
+ const source = normalizeExtractSource(record.$source);
14219
+ if (!source) {
14220
+ throw new Error(
14221
+ `Invalid persisted extraction source node at "${label}": unsupported "$source" value.`
14222
+ );
14223
+ }
14224
+ return {
14225
+ $source: source
14226
+ };
14227
+ }
14228
+ if (record.$array) {
14229
+ if (!record.$array || typeof record.$array !== "object" || Array.isArray(record.$array)) {
14230
+ throw new Error(
14231
+ `Invalid persisted extraction array node at "${label}": "$array" must be an object.`
14232
+ );
14233
+ }
14234
+ const arrayRecord = record.$array;
14235
+ if (arrayRecord.itemParentPath !== void 0 || arrayRecord.item !== void 0) {
14236
+ throw new Error(
14237
+ `Legacy persisted extraction array format detected at "${label}". Clear cached selectors in .opensteer/selectors/<namespace> and rerun extraction.`
14238
+ );
14239
+ }
14240
+ if (!Array.isArray(arrayRecord.variants) || !arrayRecord.variants.length) {
14241
+ throw new Error(
14242
+ `Invalid persisted extraction array node at "${label}": variants must be a non-empty array.`
14243
+ );
14244
+ }
14245
+ const variants = arrayRecord.variants.map((variantRaw, index) => {
14246
+ if (!variantRaw || typeof variantRaw !== "object" || Array.isArray(variantRaw)) {
14247
+ throw new Error(
14248
+ `Invalid persisted extraction array variant at "${label}"[${index}]: expected an object.`
14249
+ );
14250
+ }
14251
+ const variant = variantRaw;
14252
+ if (!variant.itemParentPath || typeof variant.itemParentPath !== "object") {
14253
+ throw new Error(
14254
+ `Invalid persisted extraction array variant at "${label}"[${index}]: itemParentPath is required.`
14255
+ );
14256
+ }
14257
+ if (!variant.item || typeof variant.item !== "object" || Array.isArray(variant.item)) {
14258
+ throw new Error(
14259
+ `Invalid persisted extraction array variant at "${label}"[${index}]: item is required.`
14260
+ );
14261
+ }
14262
+ return {
14263
+ itemParentPath: sanitizeElementPath(
14264
+ variant.itemParentPath
14265
+ ),
14266
+ item: normalizePersistedExtractNode(
14267
+ variant.item,
14268
+ `${label}[${index}]`
14269
+ )
14270
+ };
14271
+ });
14272
+ return {
14273
+ $array: {
14274
+ variants
14275
+ }
14276
+ };
14277
+ }
14278
+ const objectNode = {};
14279
+ for (const [key, value] of Object.entries(record)) {
14280
+ const normalizedKey = String(key || "").trim();
14281
+ if (!normalizedKey) continue;
14282
+ if (normalizedKey.startsWith("$")) {
14283
+ throw new Error(
14284
+ `Invalid persisted extraction node at "${label}": unexpected reserved key "${normalizedKey}".`
14285
+ );
14286
+ }
14287
+ objectNode[normalizedKey] = normalizePersistedExtractNode(
14288
+ value,
14289
+ `${label}.${normalizedKey}`
14290
+ );
14291
+ }
14292
+ return objectNode;
14293
+ }
14294
+ function computeArrayRowCoverage(value, flat) {
14295
+ if (isPrimitiveLike(value)) {
14296
+ return value == null ? 0 : 1;
14297
+ }
14298
+ const flatCoverage = Object.values(flat).reduce((sum, current) => {
14299
+ return current == null ? sum : sum + 1;
14300
+ }, 0);
14301
+ if (flatCoverage > 0) return flatCoverage;
14302
+ return countNonNullLeaves(value);
14303
+ }
14304
+ function countNonNullLeaves(value) {
14305
+ if (value == null) return 0;
14306
+ if (Array.isArray(value)) {
14307
+ return value.reduce(
14308
+ (sum, current) => sum + countNonNullLeaves(current),
14309
+ 0
14310
+ );
14311
+ }
14312
+ if (typeof value === "object") {
14313
+ return Object.values(value).reduce(
14314
+ (sum, current) => sum + countNonNullLeaves(current),
14315
+ 0
14316
+ );
14317
+ }
14318
+ return 1;
14319
+ }
14320
+ function isPrimitiveLike(value) {
14321
+ return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
14322
+ }
14323
+ function assertValidExtractSchemaRoot(schema) {
14324
+ if (!schema || typeof schema !== "object") {
14325
+ throw new Error(
14326
+ "Invalid extraction schema: expected a JSON object at the top level."
14327
+ );
14328
+ }
14329
+ if (Array.isArray(schema)) {
14330
+ throw new Error(
14331
+ 'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
14332
+ );
14333
+ }
14334
+ }
14335
+ function parseAiExtractResponse(response) {
14336
+ if (typeof response === "string") {
14337
+ const trimmed = stripCodeFence2(response);
14338
+ try {
14339
+ return JSON.parse(trimmed);
14340
+ } catch {
14341
+ const preview = summarizeForError(trimmed);
14342
+ throw new Error(
14343
+ `LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
14344
+ );
14345
+ }
14346
+ }
14347
+ if (response && typeof response === "object") {
14348
+ const candidate = response;
14349
+ if (candidate.fields || candidate.paths || candidate.data !== void 0) {
14350
+ return candidate;
14351
+ }
14352
+ }
14353
+ return {
14354
+ data: response
14355
+ };
14356
+ }
14357
+ function stripCodeFence2(input) {
14358
+ const trimmed = input.trim();
14359
+ if (!trimmed.startsWith("```")) return trimmed;
14360
+ const firstBreak = trimmed.indexOf("\n");
14361
+ if (firstBreak === -1) {
14362
+ return trimmed.replace(/```/g, "").trim();
14363
+ }
14364
+ const withoutHeader = trimmed.slice(firstBreak + 1);
14365
+ const lastFence = withoutHeader.lastIndexOf("```");
14366
+ if (lastFence === -1) return withoutHeader.trim();
14367
+ return withoutHeader.slice(0, lastFence).trim();
14368
+ }
14369
+ function summarizeForError(input, maxLength = 180) {
14370
+ const compact = input.replace(/\s+/g, " ").trim();
14371
+ if (!compact) return "";
14372
+ if (compact.length <= maxLength) return compact;
14373
+ return `${compact.slice(0, maxLength)}...`;
14374
+ }
14375
+ function getScrollDelta2(options) {
14376
+ const amount = typeof options.amount === "number" ? options.amount : 600;
14377
+ const absoluteAmount = Math.abs(amount);
14378
+ switch (options.direction) {
14379
+ case "up":
14380
+ return { x: 0, y: -absoluteAmount };
14381
+ case "left":
14382
+ return { x: -absoluteAmount, y: 0 };
14383
+ case "right":
14384
+ return { x: absoluteAmount, y: 0 };
14385
+ case "down":
14386
+ default:
14387
+ return { x: 0, y: absoluteAmount };
14388
+ }
14389
+ }
14390
+ function isInternalOrBlankPageUrl(url) {
14391
+ if (!url) return true;
14392
+ if (url === "about:blank") return true;
14393
+ return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
14394
+ }
14395
+ function normalizeCloudBrowserProfilePreference(value, source) {
14396
+ if (!value) {
14397
+ return void 0;
14398
+ }
14399
+ const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
14400
+ if (!profileId) {
14401
+ throw new Error(
14402
+ `Invalid cloud browser profile in ${source}: profileId must be a non-empty string.`
14403
+ );
14404
+ }
14405
+ if (value.reuseIfActive !== void 0 && typeof value.reuseIfActive !== "boolean") {
14406
+ throw new Error(
14407
+ `Invalid cloud browser profile in ${source}: reuseIfActive must be a boolean.`
14408
+ );
14409
+ }
14410
+ return {
14411
+ profileId,
14412
+ reuseIfActive: value.reuseIfActive
14413
+ };
14414
+ }
14415
+ function buildLocalRunId(namespace) {
14416
+ const normalized = namespace.trim() || "default";
14417
+ return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
14418
+ }
14419
+
14420
+ // src/browser/chromium-profile.ts
14421
+ var import_node_util = require("util");
14422
+ var import_node_child_process3 = require("child_process");
14423
+ var import_node_crypto = require("crypto");
14424
+ var import_promises3 = require("fs/promises");
14425
+ var import_node_fs2 = require("fs");
14426
+ var import_node_path2 = require("path");
14427
+ var import_node_os2 = require("os");
14428
+ var import_playwright3 = require("playwright");
14429
+
14430
+ // src/auth/keychain-store.ts
14431
+ var import_node_child_process2 = require("child_process");
14432
+ function commandExists(command) {
14433
+ const result = (0, import_node_child_process2.spawnSync)(command, ["--help"], {
14434
+ encoding: "utf8",
14435
+ stdio: "ignore"
14436
+ });
14437
+ return result.error == null;
14438
+ }
14439
+ function commandFailed(result) {
14440
+ return typeof result.status === "number" && result.status !== 0;
14441
+ }
14442
+ function sanitizeCommandArgs(command, args) {
14443
+ if (command !== "security") {
14444
+ return args;
14445
+ }
14446
+ const sanitized = [];
14447
+ for (let index = 0; index < args.length; index += 1) {
14448
+ const value = args[index];
14449
+ sanitized.push(value);
14450
+ if (value === "-w" && index + 1 < args.length) {
14451
+ sanitized.push("[REDACTED]");
14452
+ index += 1;
14453
+ }
14454
+ }
14455
+ return sanitized;
14456
+ }
14457
+ function buildCommandError(command, args, result) {
14458
+ const stderr = typeof result.stderr === "string" && result.stderr.trim() ? result.stderr.trim() : `Command "${command}" failed with status ${String(result.status)}.`;
14459
+ const sanitizedArgs = sanitizeCommandArgs(command, args);
14460
+ return new Error(
14461
+ [
14462
+ `Unable to persist credential via ${command}.`,
14463
+ `${command} ${sanitizedArgs.join(" ")}`,
14464
+ stderr
14465
+ ].join(" ")
14466
+ );
14467
+ }
14468
+ function createMacosSecurityStore() {
14469
+ return {
14470
+ backend: "macos-security",
14471
+ get(service, account) {
14472
+ const result = (0, import_node_child_process2.spawnSync)(
14473
+ "security",
14474
+ ["find-generic-password", "-s", service, "-a", account, "-w"],
14475
+ { encoding: "utf8" }
14476
+ );
14477
+ if (commandFailed(result)) {
14478
+ return null;
14479
+ }
14480
+ const secret = result.stdout.trim();
14481
+ return secret.length ? secret : null;
14482
+ },
14483
+ set(service, account, secret) {
14484
+ const args = [
14485
+ "add-generic-password",
14486
+ "-U",
14487
+ "-s",
14488
+ service,
14489
+ "-a",
14490
+ account,
14491
+ "-w",
14492
+ secret
14493
+ ];
14494
+ const result = (0, import_node_child_process2.spawnSync)("security", args, { encoding: "utf8" });
14495
+ if (commandFailed(result)) {
14496
+ throw buildCommandError("security", args, result);
14497
+ }
14498
+ },
14499
+ delete(service, account) {
14500
+ const args = ["delete-generic-password", "-s", service, "-a", account];
14501
+ const result = (0, import_node_child_process2.spawnSync)("security", args, { encoding: "utf8" });
14502
+ if (commandFailed(result)) {
14503
+ return;
14504
+ }
14505
+ }
14506
+ };
14507
+ }
14508
+ function createLinuxSecretToolStore() {
14509
+ return {
14510
+ backend: "linux-secret-tool",
14511
+ get(service, account) {
14512
+ const result = (0, import_node_child_process2.spawnSync)(
14513
+ "secret-tool",
14514
+ ["lookup", "service", service, "account", account],
14515
+ {
14516
+ encoding: "utf8"
14517
+ }
14518
+ );
14519
+ if (commandFailed(result)) {
14520
+ return null;
14521
+ }
14522
+ const secret = result.stdout.trim();
14523
+ return secret.length ? secret : null;
14524
+ },
14525
+ set(service, account, secret) {
14526
+ const args = [
14527
+ "store",
14528
+ "--label",
14529
+ "Opensteer CLI",
14530
+ "service",
14531
+ service,
14532
+ "account",
14533
+ account
14534
+ ];
14535
+ const result = (0, import_node_child_process2.spawnSync)("secret-tool", args, {
14536
+ encoding: "utf8",
14537
+ input: secret
14131
14538
  });
14132
- }
14133
- if (currentUrlKeys.length) {
14134
- const pageUrl = this.page.url();
14135
- for (const key of currentUrlKeys) {
14136
- result[key] = pageUrl;
14539
+ if (commandFailed(result)) {
14540
+ throw buildCommandError("secret-tool", args, result);
14137
14541
  }
14542
+ },
14543
+ delete(service, account) {
14544
+ const args = ["clear", "service", service, "account", account];
14545
+ (0, import_node_child_process2.spawnSync)("secret-tool", args, {
14546
+ encoding: "utf8"
14547
+ });
14138
14548
  }
14139
- if (counterRequests.length) {
14140
- const counterValues = await resolveCountersBatch(this.page, counterRequests);
14141
- Object.assign(result, counterValues);
14142
- }
14143
- if (pathFields.length) {
14144
- const pathValues = await extractWithPaths(this.page, pathFields);
14145
- Object.assign(result, pathValues);
14549
+ };
14550
+ }
14551
+ function createKeychainStore() {
14552
+ if (process.platform === "darwin") {
14553
+ if (!commandExists("security")) {
14554
+ return null;
14146
14555
  }
14147
- return result;
14556
+ return createMacosSecurityStore();
14148
14557
  }
14149
- async resolveFieldTargetsToPersistableFields(fields) {
14150
- const resolved = [];
14151
- for (const field of fields) {
14152
- if ("source" in field) {
14153
- resolved.push({
14154
- key: field.key,
14155
- source: "current_url"
14156
- });
14157
- continue;
14158
- }
14159
- if ("path" in field) {
14160
- resolved.push({
14161
- key: field.key,
14162
- path: this.normalizePath(field.path),
14163
- attribute: field.attribute
14164
- });
14165
- continue;
14166
- }
14167
- const path7 = await this.buildPathFromElement(field.counter);
14168
- if (!path7) {
14169
- throw new Error(
14170
- `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
14171
- );
14172
- }
14173
- resolved.push({
14174
- key: field.key,
14175
- path: path7,
14176
- attribute: field.attribute
14177
- });
14558
+ if (process.platform === "linux") {
14559
+ if (!commandExists("secret-tool")) {
14560
+ return null;
14178
14561
  }
14179
- return resolved;
14180
- }
14181
- buildActionResult(storageKey, method, persisted, selectorUsed) {
14182
- return {
14183
- method,
14184
- namespace: this.storage.getNamespace(),
14185
- persisted,
14186
- pathFile: storageKey && persisted ? this.storage.getSelectorFileName(storageKey) : null,
14187
- selectorUsed: selectorUsed || null
14188
- };
14189
- }
14190
- resolveStorageKey(description) {
14191
- if (!description) return null;
14192
- return (0, import_crypto.createHash)("sha256").update(description).digest("hex").slice(0, 16);
14193
- }
14194
- normalizePath(path7) {
14195
- return sanitizeElementPath(path7);
14562
+ return createLinuxSecretToolStore();
14196
14563
  }
14197
- };
14198
- function formatActionFailureMessage(action, description, cause) {
14199
- const label = description ? `"${description}"` : "unnamed target";
14200
- return `${action} action failed for ${label}: ${cause}`;
14201
- }
14202
- function cloneContextHops(context) {
14203
- return JSON.parse(JSON.stringify(context || []));
14564
+ return null;
14204
14565
  }
14205
- function collectIframeContextPrefix(path7) {
14206
- const context = path7.context || [];
14207
- let lastIframeIndex = -1;
14208
- for (let index = 0; index < context.length; index += 1) {
14209
- if (context[index]?.kind === "iframe") {
14210
- lastIframeIndex = index;
14566
+
14567
+ // src/browser/chromium-profile.ts
14568
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process3.execFile);
14569
+ var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
14570
+ var AES_BLOCK_BYTES = 16;
14571
+ var MAC_KEY_ITERATIONS = 1003;
14572
+ var LINUX_KEY_ITERATIONS = 1;
14573
+ var KEY_LENGTH = 16;
14574
+ var KEY_SALT = "saltysalt";
14575
+ var DEFAULT_CHROMIUM_BRAND = {
14576
+ macService: "Chrome Safe Storage",
14577
+ macAccount: "Chrome",
14578
+ linuxApplications: ["chrome", "google-chrome"]
14579
+ };
14580
+ var CHROMIUM_BRANDS = [
14581
+ {
14582
+ match: ["bravesoftware", "brave-browser"],
14583
+ brand: {
14584
+ macService: "Brave Safe Storage",
14585
+ macAccount: "Brave",
14586
+ linuxApplications: ["brave-browser", "brave"]
14211
14587
  }
14212
- }
14213
- if (lastIframeIndex < 0) return [];
14214
- return cloneContextHops(context.slice(0, lastIframeIndex + 1));
14215
- }
14216
- function measureContextOverlap(indexedPrefix, builtContext) {
14217
- const maxOverlap = Math.min(indexedPrefix.length, builtContext.length);
14218
- for (let size = maxOverlap; size > 0; size -= 1) {
14219
- if (matchesContextPrefix(indexedPrefix, builtContext, size, true)) {
14220
- return size;
14588
+ },
14589
+ {
14590
+ match: ["microsoft", "edge"],
14591
+ brand: {
14592
+ macService: "Microsoft Edge Safe Storage",
14593
+ macAccount: "Microsoft Edge",
14594
+ linuxApplications: ["microsoft-edge"],
14595
+ playwrightChannel: "msedge"
14221
14596
  }
14222
- }
14223
- for (let size = maxOverlap; size > 0; size -= 1) {
14224
- if (matchesContextPrefix(indexedPrefix, builtContext, size, false)) {
14225
- return size;
14597
+ },
14598
+ {
14599
+ match: ["google", "chrome beta"],
14600
+ brand: {
14601
+ macService: "Chrome Beta Safe Storage",
14602
+ macAccount: "Chrome Beta",
14603
+ linuxApplications: ["chrome-beta"],
14604
+ playwrightChannel: "chrome-beta"
14226
14605
  }
14227
- }
14228
- return 0;
14229
- }
14230
- function matchesContextPrefix(indexedPrefix, builtContext, size, strictHost) {
14231
- for (let idx = 0; idx < size; idx += 1) {
14232
- const left = indexedPrefix[indexedPrefix.length - size + idx];
14233
- const right = builtContext[idx];
14234
- if (left.kind !== right.kind) {
14235
- return false;
14606
+ },
14607
+ {
14608
+ match: ["google", "chrome"],
14609
+ brand: {
14610
+ macService: "Chrome Safe Storage",
14611
+ macAccount: "Chrome",
14612
+ linuxApplications: ["chrome", "google-chrome"],
14613
+ playwrightChannel: "chrome"
14236
14614
  }
14237
- if (strictHost && JSON.stringify(left.host) !== JSON.stringify(right.host)) {
14238
- return false;
14615
+ },
14616
+ {
14617
+ match: ["chromium"],
14618
+ brand: {
14619
+ macService: "Chromium Safe Storage",
14620
+ macAccount: "Chromium",
14621
+ linuxApplications: ["chromium"]
14239
14622
  }
14240
14623
  }
14241
- return true;
14624
+ ];
14625
+ function directoryExists(filePath) {
14626
+ try {
14627
+ return (0, import_node_fs2.statSync)(filePath).isDirectory();
14628
+ } catch {
14629
+ return false;
14630
+ }
14242
14631
  }
14243
- function normalizeSchemaValue(value) {
14244
- if (!value) return null;
14245
- if (typeof value !== "object" || Array.isArray(value)) {
14246
- return null;
14632
+ function fileExists(filePath) {
14633
+ try {
14634
+ return (0, import_node_fs2.statSync)(filePath).isFile();
14635
+ } catch {
14636
+ return false;
14247
14637
  }
14248
- const field = value;
14249
- return {
14250
- element: field.element,
14251
- selector: field.selector,
14252
- attribute: field.attribute,
14253
- source: normalizeExtractSource(field.source)
14254
- };
14255
14638
  }
14256
- function normalizeExtractSource(source) {
14257
- if (typeof source !== "string") return void 0;
14258
- const normalized = source.trim().toLowerCase();
14259
- if (normalized === "current_url") return "current_url";
14260
- return void 0;
14639
+ function resolveCookieDbPath(profileDir) {
14640
+ const candidates = [(0, import_node_path2.join)(profileDir, "Network", "Cookies"), (0, import_node_path2.join)(profileDir, "Cookies")];
14641
+ for (const candidate of candidates) {
14642
+ if (fileExists(candidate)) {
14643
+ return candidate;
14644
+ }
14645
+ }
14646
+ return null;
14261
14647
  }
14262
- function computeSchemaHash(schema) {
14263
- const stable = stableStringify(schema);
14264
- return (0, import_crypto.createHash)("sha256").update(stable).digest("hex");
14648
+ async function selectProfileDirFromUserDataDir(userDataDir) {
14649
+ const entries = await (0, import_promises3.readdir)(userDataDir, {
14650
+ withFileTypes: true
14651
+ }).catch(() => []);
14652
+ const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => (0, import_node_path2.join)(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
14653
+ return candidates;
14265
14654
  }
14266
- function buildPathMap(fields) {
14267
- const out = {};
14268
- for (const field of fields) {
14269
- out[field.key] = cloneElementPath(field.path);
14655
+ async function resolveChromiumProfileLocation(inputPath) {
14656
+ const expandedPath = expandHome(inputPath.trim());
14657
+ if (!expandedPath) {
14658
+ throw new Error("Profile path cannot be empty.");
14659
+ }
14660
+ if (fileExists(expandedPath) && (0, import_node_path2.basename)(expandedPath) === "Cookies") {
14661
+ const directParent = (0, import_node_path2.dirname)(expandedPath);
14662
+ const profileDir = (0, import_node_path2.basename)(directParent) === "Network" ? (0, import_node_path2.dirname)(directParent) : directParent;
14663
+ const userDataDir = (0, import_node_path2.dirname)(profileDir);
14664
+ return {
14665
+ userDataDir,
14666
+ profileDir,
14667
+ profileDirectory: (0, import_node_path2.basename)(profileDir),
14668
+ cookieDbPath: expandedPath,
14669
+ localStatePath: fileExists((0, import_node_path2.join)(userDataDir, "Local State")) ? (0, import_node_path2.join)(userDataDir, "Local State") : null
14670
+ };
14671
+ }
14672
+ if (fileExists(expandedPath)) {
14673
+ throw new Error(
14674
+ `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
14675
+ );
14676
+ }
14677
+ if (!directoryExists(expandedPath)) {
14678
+ throw new Error(
14679
+ `Could not find a Chromium profile at "${inputPath}".`
14680
+ );
14681
+ }
14682
+ const directCookieDb = resolveCookieDbPath(expandedPath);
14683
+ if (directCookieDb) {
14684
+ const userDataDir = (0, import_node_path2.dirname)(expandedPath);
14685
+ return {
14686
+ userDataDir,
14687
+ profileDir: expandedPath,
14688
+ profileDirectory: (0, import_node_path2.basename)(expandedPath),
14689
+ cookieDbPath: directCookieDb,
14690
+ localStatePath: fileExists((0, import_node_path2.join)(userDataDir, "Local State")) ? (0, import_node_path2.join)(userDataDir, "Local State") : null
14691
+ };
14270
14692
  }
14271
- return out;
14272
- }
14273
- function toPathFields(fields) {
14274
- return fields.filter(isPersistablePathField).map((field) => ({
14275
- key: field.key,
14276
- path: field.path,
14277
- attribute: field.attribute
14278
- }));
14279
- }
14280
- function normalizePersistedExtractPayload(raw) {
14281
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14693
+ const localStatePath = (0, import_node_path2.join)(expandedPath, "Local State");
14694
+ if (!fileExists(localStatePath)) {
14282
14695
  throw new Error(
14283
- "Invalid persisted extraction payload: expected an object payload."
14696
+ `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
14284
14697
  );
14285
14698
  }
14286
- const root = {};
14287
- for (const [key, value] of Object.entries(raw)) {
14288
- const normalizedKey = String(key || "").trim();
14289
- if (!normalizedKey) continue;
14290
- if (normalizedKey.startsWith("$")) {
14291
- throw new Error(
14292
- `Invalid persisted extraction payload key "${normalizedKey}": root keys must not start with "$".`
14293
- );
14294
- }
14295
- root[normalizedKey] = normalizePersistedExtractNode(
14296
- value,
14297
- normalizedKey
14699
+ const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
14700
+ if (profileDirs.length === 0) {
14701
+ throw new Error(
14702
+ `No Chromium profile with a Cookies database was found under "${inputPath}".`
14298
14703
  );
14299
14704
  }
14300
- return root;
14301
- }
14302
- function normalizePersistedExtractNode(raw, label) {
14303
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14705
+ if (profileDirs.length > 1) {
14706
+ const candidates = profileDirs.map((entry) => (0, import_node_path2.basename)(entry)).join(", ");
14304
14707
  throw new Error(
14305
- `Invalid persisted extraction node at "${label}": expected an object.`
14708
+ `"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
14306
14709
  );
14307
14710
  }
14308
- const record = raw;
14309
- if (record.$path) {
14310
- if (typeof record.$path !== "object") {
14311
- throw new Error(
14312
- `Invalid persisted extraction value node at "${label}": "$path" must be an element path object.`
14313
- );
14314
- }
14315
- return {
14316
- $path: sanitizeElementPath(record.$path),
14317
- attribute: typeof record.attribute === "string" ? record.attribute : void 0
14318
- };
14711
+ const selectedProfileDir = profileDirs[0];
14712
+ const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
14713
+ if (!cookieDbPath) {
14714
+ throw new Error(
14715
+ `No Chromium Cookies database was found for "${inputPath}".`
14716
+ );
14319
14717
  }
14320
- if (record.$source != null) {
14321
- const source = normalizeExtractSource(record.$source);
14322
- if (!source) {
14323
- throw new Error(
14324
- `Invalid persisted extraction source node at "${label}": unsupported "$source" value.`
14325
- );
14718
+ return {
14719
+ userDataDir: expandedPath,
14720
+ profileDir: selectedProfileDir,
14721
+ profileDirectory: (0, import_node_path2.basename)(selectedProfileDir),
14722
+ cookieDbPath,
14723
+ localStatePath
14724
+ };
14725
+ }
14726
+ function detectChromiumBrand(location) {
14727
+ const normalizedPath = location.userDataDir.toLowerCase();
14728
+ for (const candidate of CHROMIUM_BRANDS) {
14729
+ if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
14730
+ return candidate.brand;
14326
14731
  }
14327
- return {
14328
- $source: source
14329
- };
14330
14732
  }
14331
- if (record.$array) {
14332
- if (!record.$array || typeof record.$array !== "object" || Array.isArray(record.$array)) {
14333
- throw new Error(
14334
- `Invalid persisted extraction array node at "${label}": "$array" must be an object.`
14335
- );
14336
- }
14337
- const arrayRecord = record.$array;
14338
- if (arrayRecord.itemParentPath !== void 0 || arrayRecord.item !== void 0) {
14339
- throw new Error(
14340
- `Legacy persisted extraction array format detected at "${label}". Clear cached selectors in .opensteer/selectors/<namespace> and rerun extraction.`
14341
- );
14342
- }
14343
- if (!Array.isArray(arrayRecord.variants) || !arrayRecord.variants.length) {
14344
- throw new Error(
14345
- `Invalid persisted extraction array node at "${label}": variants must be a non-empty array.`
14346
- );
14733
+ return DEFAULT_CHROMIUM_BRAND;
14734
+ }
14735
+ async function createSqliteSnapshot(dbPath) {
14736
+ const snapshotDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "opensteer-cookie-db-"));
14737
+ const snapshotPath = (0, import_node_path2.join)(snapshotDir, "Cookies");
14738
+ await (0, import_promises3.copyFile)(dbPath, snapshotPath);
14739
+ for (const suffix of ["-wal", "-shm", "-journal"]) {
14740
+ const source = `${dbPath}${suffix}`;
14741
+ if (!(0, import_node_fs2.existsSync)(source)) {
14742
+ continue;
14347
14743
  }
14348
- const variants = arrayRecord.variants.map((variantRaw, index) => {
14349
- if (!variantRaw || typeof variantRaw !== "object" || Array.isArray(variantRaw)) {
14350
- throw new Error(
14351
- `Invalid persisted extraction array variant at "${label}"[${index}]: expected an object.`
14352
- );
14353
- }
14354
- const variant = variantRaw;
14355
- if (!variant.itemParentPath || typeof variant.itemParentPath !== "object") {
14356
- throw new Error(
14357
- `Invalid persisted extraction array variant at "${label}"[${index}]: itemParentPath is required.`
14358
- );
14359
- }
14360
- if (!variant.item || typeof variant.item !== "object" || Array.isArray(variant.item)) {
14361
- throw new Error(
14362
- `Invalid persisted extraction array variant at "${label}"[${index}]: item is required.`
14363
- );
14364
- }
14365
- return {
14366
- itemParentPath: sanitizeElementPath(
14367
- variant.itemParentPath
14368
- ),
14369
- item: normalizePersistedExtractNode(
14370
- variant.item,
14371
- `${label}[${index}]`
14372
- )
14373
- };
14374
- });
14375
- return {
14376
- $array: {
14377
- variants
14378
- }
14379
- };
14744
+ await (0, import_promises3.copyFile)(source, `${snapshotPath}${suffix}`);
14380
14745
  }
14381
- const objectNode = {};
14382
- for (const [key, value] of Object.entries(record)) {
14383
- const normalizedKey = String(key || "").trim();
14384
- if (!normalizedKey) continue;
14385
- if (normalizedKey.startsWith("$")) {
14386
- throw new Error(
14387
- `Invalid persisted extraction node at "${label}": unexpected reserved key "${normalizedKey}".`
14388
- );
14746
+ return {
14747
+ snapshotPath,
14748
+ cleanup: async () => {
14749
+ await (0, import_promises3.rm)(snapshotDir, { recursive: true, force: true });
14389
14750
  }
14390
- objectNode[normalizedKey] = normalizePersistedExtractNode(
14391
- value,
14392
- `${label}.${normalizedKey}`
14393
- );
14751
+ };
14752
+ }
14753
+ async function querySqliteJson(dbPath, query) {
14754
+ const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
14755
+ encoding: "utf8",
14756
+ maxBuffer: 64 * 1024 * 1024
14757
+ });
14758
+ const stdout = result.stdout;
14759
+ const trimmed = stdout.trim();
14760
+ if (!trimmed) {
14761
+ return [];
14394
14762
  }
14395
- return objectNode;
14763
+ return JSON.parse(trimmed);
14396
14764
  }
14397
- function computeArrayRowCoverage(value, flat) {
14398
- if (isPrimitiveLike(value)) {
14399
- return value == null ? 0 : 1;
14765
+ function convertChromiumTimestampToUnixSeconds(value) {
14766
+ if (!value || value === "0") {
14767
+ return -1;
14400
14768
  }
14401
- const flatCoverage = Object.values(flat).reduce((sum, current) => {
14402
- return current == null ? sum : sum + 1;
14403
- }, 0);
14404
- if (flatCoverage > 0) return flatCoverage;
14405
- return countNonNullLeaves(value);
14769
+ const micros = BigInt(value);
14770
+ if (micros <= CHROMIUM_EPOCH_MICROS) {
14771
+ return -1;
14772
+ }
14773
+ return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
14406
14774
  }
14407
- function countNonNullLeaves(value) {
14408
- if (value == null) return 0;
14409
- if (Array.isArray(value)) {
14410
- return value.reduce(
14411
- (sum, current) => sum + countNonNullLeaves(current),
14412
- 0
14775
+ function mapChromiumSameSite(value) {
14776
+ if (value === 2) {
14777
+ return "Strict";
14778
+ }
14779
+ if (value === 0) {
14780
+ return "None";
14781
+ }
14782
+ return "Lax";
14783
+ }
14784
+ function stripChromiumPadding(buffer) {
14785
+ const paddingLength = buffer[buffer.length - 1];
14786
+ if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
14787
+ return buffer;
14788
+ }
14789
+ return buffer.subarray(0, buffer.length - paddingLength);
14790
+ }
14791
+ function stripDomainHashPrefix(buffer, hostKey) {
14792
+ if (buffer.length < 32) {
14793
+ return buffer;
14794
+ }
14795
+ const domainHash = (0, import_node_crypto.createHash)("sha256").update(hostKey, "utf8").digest();
14796
+ if (buffer.subarray(0, 32).equals(domainHash)) {
14797
+ return buffer.subarray(32);
14798
+ }
14799
+ return buffer;
14800
+ }
14801
+ function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
14802
+ const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
14803
+ const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
14804
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-128-cbc", key, iv);
14805
+ const plaintext = Buffer.concat([
14806
+ decipher.update(ciphertext),
14807
+ decipher.final()
14808
+ ]);
14809
+ return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
14810
+ "utf8"
14811
+ );
14812
+ }
14813
+ function decryptChromiumAes256GcmValue(encryptedValue, key) {
14814
+ const nonce = encryptedValue.subarray(3, 15);
14815
+ const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
14816
+ const authTag = encryptedValue.subarray(encryptedValue.length - 16);
14817
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", key, nonce);
14818
+ decipher.setAuthTag(authTag);
14819
+ return Buffer.concat([
14820
+ decipher.update(ciphertext),
14821
+ decipher.final()
14822
+ ]).toString("utf8");
14823
+ }
14824
+ async function dpapiUnprotect(buffer) {
14825
+ const script = [
14826
+ `$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
14827
+ "$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
14828
+ " $inputBytes,",
14829
+ " $null,",
14830
+ " [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
14831
+ ")",
14832
+ "[Convert]::ToBase64String($plainBytes)"
14833
+ ].join("\n");
14834
+ const { stdout } = await execFileAsync(
14835
+ "powershell.exe",
14836
+ ["-NoProfile", "-NonInteractive", "-Command", script],
14837
+ {
14838
+ encoding: "utf8",
14839
+ maxBuffer: 8 * 1024 * 1024
14840
+ }
14841
+ );
14842
+ return Buffer.from(stdout.trim(), "base64");
14843
+ }
14844
+ async function buildChromiumDecryptor(location) {
14845
+ if (process.platform === "darwin") {
14846
+ const brand = detectChromiumBrand(location);
14847
+ const keychainStore = createKeychainStore();
14848
+ const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
14849
+ if (!password) {
14850
+ throw new Error(
14851
+ `Unable to read ${brand.macService} from macOS Keychain.`
14852
+ );
14853
+ }
14854
+ const key = (0, import_node_crypto.pbkdf2Sync)(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
14855
+ return async (row) => decryptChromiumAes128CbcValue(
14856
+ Buffer.from(row.encrypted_value || "", "hex"),
14857
+ key,
14858
+ row.host_key
14413
14859
  );
14414
14860
  }
14415
- if (typeof value === "object") {
14416
- return Object.values(value).reduce(
14417
- (sum, current) => sum + countNonNullLeaves(current),
14418
- 0
14861
+ if (process.platform === "linux") {
14862
+ const brand = detectChromiumBrand(location);
14863
+ const keychainStore = createKeychainStore();
14864
+ const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
14865
+ const key = (0, import_node_crypto.pbkdf2Sync)(
14866
+ password || "peanuts",
14867
+ KEY_SALT,
14868
+ LINUX_KEY_ITERATIONS,
14869
+ KEY_LENGTH,
14870
+ "sha1"
14419
14871
  );
14420
- }
14421
- return 1;
14422
- }
14423
- function isPrimitiveLike(value) {
14424
- return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
14425
- }
14426
- function assertValidExtractSchemaRoot(schema) {
14427
- if (!schema || typeof schema !== "object") {
14428
- throw new Error(
14429
- "Invalid extraction schema: expected a JSON object at the top level."
14872
+ return async (row) => decryptChromiumAes128CbcValue(
14873
+ Buffer.from(row.encrypted_value || "", "hex"),
14874
+ key,
14875
+ row.host_key
14430
14876
  );
14431
14877
  }
14432
- if (Array.isArray(schema)) {
14433
- throw new Error(
14434
- 'Invalid extraction schema: top-level arrays are not supported. Wrap array fields in an object (for example {"items":[...]}).'
14878
+ if (process.platform === "win32") {
14879
+ if (!location.localStatePath) {
14880
+ throw new Error(
14881
+ `Unable to locate Chromium Local State for profile: ${location.profileDir}`
14882
+ );
14883
+ }
14884
+ const localState = JSON.parse(
14885
+ await (0, import_promises3.readFile)(location.localStatePath, "utf8")
14435
14886
  );
14436
- }
14437
- }
14438
- function parseAiExtractResponse(response) {
14439
- if (typeof response === "string") {
14440
- const trimmed = stripCodeFence2(response);
14441
- try {
14442
- return JSON.parse(trimmed);
14443
- } catch {
14444
- const preview = summarizeForError(trimmed);
14887
+ const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
14888
+ if (!encryptedKeyBase64) {
14445
14889
  throw new Error(
14446
- `LLM extraction returned a non-JSON response.${preview ? ` Preview: "${preview}"` : ""}`
14890
+ `Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
14447
14891
  );
14448
14892
  }
14893
+ const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
14894
+ const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
14895
+ return async (row) => {
14896
+ const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
14897
+ if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
14898
+ const decrypted = await dpapiUnprotect(encryptedValue);
14899
+ return decrypted.toString("utf8");
14900
+ }
14901
+ return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
14902
+ };
14449
14903
  }
14450
- if (response && typeof response === "object") {
14451
- const candidate = response;
14452
- if (candidate.fields || candidate.paths || candidate.data !== void 0) {
14453
- return candidate;
14454
- }
14904
+ throw new Error(
14905
+ `Local Chromium cookie sync is not supported on ${process.platform}.`
14906
+ );
14907
+ }
14908
+ function buildPlaywrightCookie(row, value) {
14909
+ if (!row.name.trim()) {
14910
+ return null;
14911
+ }
14912
+ if (!row.host_key.trim()) {
14913
+ return null;
14914
+ }
14915
+ const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
14916
+ if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
14917
+ return null;
14455
14918
  }
14456
14919
  return {
14457
- data: response
14920
+ name: row.name,
14921
+ value,
14922
+ domain: row.host_key,
14923
+ path: row.path || "/",
14924
+ expires,
14925
+ httpOnly: row.is_httponly === 1,
14926
+ secure: row.is_secure === 1,
14927
+ sameSite: mapChromiumSameSite(row.samesite)
14458
14928
  };
14459
14929
  }
14460
- function stripCodeFence2(input) {
14461
- const trimmed = input.trim();
14462
- if (!trimmed.startsWith("```")) return trimmed;
14463
- const firstBreak = trimmed.indexOf("\n");
14464
- if (firstBreak === -1) {
14465
- return trimmed.replace(/```/g, "").trim();
14466
- }
14467
- const withoutHeader = trimmed.slice(firstBreak + 1);
14468
- const lastFence = withoutHeader.lastIndexOf("```");
14469
- if (lastFence === -1) return withoutHeader.trim();
14470
- return withoutHeader.slice(0, lastFence).trim();
14471
- }
14472
- function summarizeForError(input, maxLength = 180) {
14473
- const compact = input.replace(/\s+/g, " ").trim();
14474
- if (!compact) return "";
14475
- if (compact.length <= maxLength) return compact;
14476
- return `${compact.slice(0, maxLength)}...`;
14477
- }
14478
- function getScrollDelta2(options) {
14479
- const amount = typeof options.amount === "number" ? options.amount : 600;
14480
- const absoluteAmount = Math.abs(amount);
14481
- switch (options.direction) {
14482
- case "up":
14483
- return { x: 0, y: -absoluteAmount };
14484
- case "left":
14485
- return { x: -absoluteAmount, y: 0 };
14486
- case "right":
14487
- return { x: absoluteAmount, y: 0 };
14488
- case "down":
14489
- default:
14490
- return { x: 0, y: absoluteAmount };
14930
+ async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
14931
+ const location = await resolveChromiumProfileLocation(inputPath);
14932
+ try {
14933
+ return await loadCookiesFromSqlite(location);
14934
+ } catch (error) {
14935
+ if (!isMissingSqliteBinary(error)) {
14936
+ throw error;
14937
+ }
14491
14938
  }
14939
+ return await loadCookiesFromBrowserSnapshot(location, options);
14492
14940
  }
14493
- function isInternalOrBlankPageUrl(url) {
14494
- if (!url) return true;
14495
- if (url === "about:blank") return true;
14496
- return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
14497
- }
14498
- function normalizeCloudBrowserProfilePreference(value, source) {
14499
- if (!value) {
14500
- return void 0;
14501
- }
14502
- const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
14503
- if (!profileId) {
14504
- throw new Error(
14505
- `Invalid cloud browser profile in ${source}: profileId must be a non-empty string.`
14941
+ async function loadCookiesFromSqlite(location) {
14942
+ const snapshot = await createSqliteSnapshot(location.cookieDbPath);
14943
+ try {
14944
+ const rows = await querySqliteJson(
14945
+ snapshot.snapshotPath,
14946
+ [
14947
+ "SELECT",
14948
+ " host_key,",
14949
+ " name,",
14950
+ " value,",
14951
+ " hex(encrypted_value) AS encrypted_value,",
14952
+ " path,",
14953
+ " CAST(expires_utc AS TEXT) AS expires_utc,",
14954
+ " is_secure,",
14955
+ " is_httponly,",
14956
+ " has_expires,",
14957
+ " samesite",
14958
+ "FROM cookies"
14959
+ ].join(" ")
14506
14960
  );
14961
+ const decryptValue = await buildChromiumDecryptor(location);
14962
+ const cookies = [];
14963
+ for (const row of rows) {
14964
+ let value = row.value || "";
14965
+ if (!value && row.encrypted_value) {
14966
+ value = await decryptValue(row);
14967
+ }
14968
+ const cookie = buildPlaywrightCookie(row, value);
14969
+ if (cookie) {
14970
+ cookies.push(cookie);
14971
+ }
14972
+ }
14973
+ return cookies;
14974
+ } finally {
14975
+ await snapshot.cleanup();
14507
14976
  }
14508
- if (value.reuseIfActive !== void 0 && typeof value.reuseIfActive !== "boolean") {
14509
- throw new Error(
14510
- `Invalid cloud browser profile in ${source}: reuseIfActive must be a boolean.`
14511
- );
14977
+ }
14978
+ async function loadCookiesFromBrowserSnapshot(location, options) {
14979
+ const snapshotRootDir = await (0, import_promises3.mkdtemp)((0, import_node_path2.join)((0, import_node_os2.tmpdir)(), "opensteer-profile-"));
14980
+ const snapshotProfileDir = (0, import_node_path2.join)(
14981
+ snapshotRootDir,
14982
+ (0, import_node_path2.basename)(location.profileDir)
14983
+ );
14984
+ let context = null;
14985
+ try {
14986
+ await (0, import_promises3.cp)(location.profileDir, snapshotProfileDir, {
14987
+ recursive: true
14988
+ });
14989
+ if (location.localStatePath) {
14990
+ await (0, import_promises3.copyFile)(location.localStatePath, (0, import_node_path2.join)(snapshotRootDir, "Local State"));
14991
+ }
14992
+ const brand = detectChromiumBrand(location);
14993
+ const args = [`--profile-directory=${(0, import_node_path2.basename)(snapshotProfileDir)}`];
14994
+ context = await import_playwright3.chromium.launchPersistentContext(snapshotRootDir, {
14995
+ channel: brand.playwrightChannel,
14996
+ headless: options.headless ?? true,
14997
+ timeout: options.timeout ?? 12e4,
14998
+ args
14999
+ });
15000
+ return await context.cookies();
15001
+ } finally {
15002
+ await context?.close().catch(() => void 0);
15003
+ await (0, import_promises3.rm)(snapshotRootDir, { recursive: true, force: true });
14512
15004
  }
14513
- return {
14514
- profileId,
14515
- reuseIfActive: value.reuseIfActive
14516
- };
14517
15005
  }
14518
- function buildLocalRunId(namespace) {
14519
- const normalized = namespace.trim() || "default";
14520
- return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
15006
+ function isMissingSqliteBinary(error) {
15007
+ return Boolean(
15008
+ error && typeof error === "object" && "code" in error && error.code === "ENOENT"
15009
+ );
14521
15010
  }
14522
15011
 
14523
15012
  // src/cloud/browser-profile-client.ts
@@ -14691,59 +15180,21 @@ var import_open = __toESM(require("open"), 1);
14691
15180
 
14692
15181
  // src/auth/credential-resolver.ts
14693
15182
  function resolveCloudCredential(options) {
14694
- const flagApiKey = normalizeToken(options.apiKeyFlag);
14695
- const flagAccessToken = normalizeToken(options.accessTokenFlag);
14696
- if (flagApiKey && flagAccessToken) {
14697
- throw new Error("--api-key and --access-token are mutually exclusive.");
14698
- }
14699
- if (flagAccessToken) {
14700
- return {
14701
- kind: "access-token",
14702
- source: "flag",
14703
- token: flagAccessToken,
14704
- authScheme: "bearer"
14705
- };
14706
- }
14707
- if (flagApiKey) {
14708
- return {
14709
- kind: "api-key",
14710
- source: "flag",
14711
- token: flagApiKey,
14712
- authScheme: "api-key"
14713
- };
15183
+ const flagCredential = selectCloudCredential({
15184
+ apiKey: options.apiKeyFlag,
15185
+ accessToken: options.accessTokenFlag
15186
+ });
15187
+ if (flagCredential) {
15188
+ return toResolvedCloudCredential("flag", flagCredential);
14714
15189
  }
14715
15190
  const envAuthScheme = parseEnvAuthScheme(options.env.OPENSTEER_AUTH_SCHEME);
14716
- const envApiKey = normalizeToken(options.env.OPENSTEER_API_KEY);
14717
- const envAccessToken = normalizeToken(options.env.OPENSTEER_ACCESS_TOKEN);
14718
- if (envApiKey && envAccessToken) {
14719
- throw new Error(
14720
- "OPENSTEER_API_KEY and OPENSTEER_ACCESS_TOKEN are mutually exclusive. Set only one."
14721
- );
14722
- }
14723
- if (envAccessToken) {
14724
- return {
14725
- kind: "access-token",
14726
- source: "env",
14727
- token: envAccessToken,
14728
- authScheme: "bearer"
14729
- };
14730
- }
14731
- if (envApiKey) {
14732
- if (envAuthScheme === "bearer") {
14733
- return {
14734
- kind: "access-token",
14735
- source: "env",
14736
- token: envApiKey,
14737
- authScheme: "bearer",
14738
- compatibilityBearerApiKey: true
14739
- };
14740
- }
14741
- return {
14742
- kind: "api-key",
14743
- source: "env",
14744
- token: envApiKey,
14745
- authScheme: envAuthScheme ?? "api-key"
14746
- };
15191
+ const envCredential = selectCloudCredential({
15192
+ apiKey: options.env.OPENSTEER_API_KEY,
15193
+ accessToken: options.env.OPENSTEER_ACCESS_TOKEN,
15194
+ authScheme: envAuthScheme
15195
+ });
15196
+ if (envCredential) {
15197
+ return toResolvedCloudCredential("env", envCredential);
14747
15198
  }
14748
15199
  return null;
14749
15200
  }
@@ -14773,12 +15224,29 @@ function normalizeToken(value) {
14773
15224
  const normalized = value.trim();
14774
15225
  return normalized.length ? normalized : void 0;
14775
15226
  }
15227
+ function toResolvedCloudCredential(source, credential) {
15228
+ if (credential.compatibilityBearerApiKey) {
15229
+ return {
15230
+ kind: credential.kind,
15231
+ source,
15232
+ token: credential.token,
15233
+ authScheme: credential.authScheme,
15234
+ compatibilityBearerApiKey: true
15235
+ };
15236
+ }
15237
+ return {
15238
+ kind: credential.kind,
15239
+ source,
15240
+ token: credential.token,
15241
+ authScheme: credential.authScheme
15242
+ };
15243
+ }
14776
15244
 
14777
15245
  // src/auth/machine-credential-store.ts
14778
15246
  var import_node_crypto2 = require("crypto");
14779
- var import_node_fs2 = __toESM(require("fs"), 1);
14780
- var import_node_os2 = __toESM(require("os"), 1);
14781
- var import_node_path2 = __toESM(require("path"), 1);
15247
+ var import_node_fs3 = __toESM(require("fs"), 1);
15248
+ var import_node_os3 = __toESM(require("os"), 1);
15249
+ var import_node_path3 = __toESM(require("path"), 1);
14782
15250
  var METADATA_VERSION = 2;
14783
15251
  var ACTIVE_TARGET_VERSION = 2;
14784
15252
  var KEYCHAIN_SERVICE = "com.opensteer.cli.cloud";
@@ -14793,7 +15261,7 @@ var MachineCredentialStore = class {
14793
15261
  const appName = options.appName || "opensteer";
14794
15262
  const env = options.env ?? process.env;
14795
15263
  const configDir = resolveConfigDir(appName, env);
14796
- this.authDir = import_node_path2.default.join(configDir, "auth");
15264
+ this.authDir = import_node_path3.default.join(configDir, "auth");
14797
15265
  this.warn = options.warn ?? (() => void 0);
14798
15266
  }
14799
15267
  readCloudCredential(target) {
@@ -14926,15 +15394,15 @@ function resolveCredentialSlot(authDir, target) {
14926
15394
  const storageKey = (0, import_node_crypto2.createHash)("sha256").update(normalizedBaseUrl).digest("hex").slice(0, 24);
14927
15395
  return {
14928
15396
  keychainAccount: `${KEYCHAIN_ACCOUNT_PREFIX}${storageKey}`,
14929
- metadataPath: import_node_path2.default.join(authDir, `cli-login.${storageKey}.json`),
14930
- fallbackSecretPath: import_node_path2.default.join(
15397
+ metadataPath: import_node_path3.default.join(authDir, `cli-login.${storageKey}.json`),
15398
+ fallbackSecretPath: import_node_path3.default.join(
14931
15399
  authDir,
14932
15400
  `cli-login.${storageKey}.secret.json`
14933
15401
  )
14934
15402
  };
14935
15403
  }
14936
15404
  function resolveActiveTargetPath(authDir) {
14937
- return import_node_path2.default.join(authDir, ACTIVE_TARGET_FILE_NAME);
15405
+ return import_node_path3.default.join(authDir, ACTIVE_TARGET_FILE_NAME);
14938
15406
  }
14939
15407
  function matchesCredentialTarget(value, target) {
14940
15408
  return normalizeCredentialUrl(value.baseUrl) === normalizeCredentialUrl(target.baseUrl);
@@ -14953,36 +15421,36 @@ function normalizeCloudCredentialTarget(target) {
14953
15421
  }
14954
15422
  function resolveConfigDir(appName, env) {
14955
15423
  if (process.platform === "win32") {
14956
- const appData = env.APPDATA?.trim() || import_node_path2.default.join(import_node_os2.default.homedir(), "AppData", "Roaming");
14957
- return import_node_path2.default.join(appData, appName);
15424
+ const appData = env.APPDATA?.trim() || import_node_path3.default.join(import_node_os3.default.homedir(), "AppData", "Roaming");
15425
+ return import_node_path3.default.join(appData, appName);
14958
15426
  }
14959
15427
  if (process.platform === "darwin") {
14960
- return import_node_path2.default.join(
14961
- import_node_os2.default.homedir(),
15428
+ return import_node_path3.default.join(
15429
+ import_node_os3.default.homedir(),
14962
15430
  "Library",
14963
15431
  "Application Support",
14964
15432
  appName
14965
15433
  );
14966
15434
  }
14967
- const xdgConfigHome = env.XDG_CONFIG_HOME?.trim() || import_node_path2.default.join(import_node_os2.default.homedir(), ".config");
14968
- return import_node_path2.default.join(xdgConfigHome, appName);
15435
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim() || import_node_path3.default.join(import_node_os3.default.homedir(), ".config");
15436
+ return import_node_path3.default.join(xdgConfigHome, appName);
14969
15437
  }
14970
15438
  function ensureDirectory(directoryPath) {
14971
- import_node_fs2.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
15439
+ import_node_fs3.default.mkdirSync(directoryPath, { recursive: true, mode: 448 });
14972
15440
  }
14973
15441
  function removeFileIfExists(filePath) {
14974
15442
  try {
14975
- import_node_fs2.default.rmSync(filePath, { force: true });
15443
+ import_node_fs3.default.rmSync(filePath, { force: true });
14976
15444
  } catch {
14977
15445
  return;
14978
15446
  }
14979
15447
  }
14980
15448
  function readMetadata(filePath) {
14981
- if (!import_node_fs2.default.existsSync(filePath)) {
15449
+ if (!import_node_fs3.default.existsSync(filePath)) {
14982
15450
  return null;
14983
15451
  }
14984
15452
  try {
14985
- const raw = import_node_fs2.default.readFileSync(filePath, "utf8");
15453
+ const raw = import_node_fs3.default.readFileSync(filePath, "utf8");
14986
15454
  const parsed = JSON.parse(raw);
14987
15455
  if (parsed.version !== METADATA_VERSION) return null;
14988
15456
  if (parsed.secretBackend !== "keychain" && parsed.secretBackend !== "file") {
@@ -15009,11 +15477,11 @@ function readMetadata(filePath) {
15009
15477
  }
15010
15478
  }
15011
15479
  function readActiveCloudTargetMetadata(filePath) {
15012
- if (!import_node_fs2.default.existsSync(filePath)) {
15480
+ if (!import_node_fs3.default.existsSync(filePath)) {
15013
15481
  return null;
15014
15482
  }
15015
15483
  try {
15016
- const raw = import_node_fs2.default.readFileSync(filePath, "utf8");
15484
+ const raw = import_node_fs3.default.readFileSync(filePath, "utf8");
15017
15485
  const parsed = JSON.parse(raw);
15018
15486
  if (parsed.version !== ACTIVE_TARGET_VERSION) {
15019
15487
  return null;
@@ -15043,22 +15511,22 @@ function parseSecretPayload(raw) {
15043
15511
  }
15044
15512
  }
15045
15513
  function readSecretFile(filePath) {
15046
- if (!import_node_fs2.default.existsSync(filePath)) {
15514
+ if (!import_node_fs3.default.existsSync(filePath)) {
15047
15515
  return null;
15048
15516
  }
15049
15517
  try {
15050
- return parseSecretPayload(import_node_fs2.default.readFileSync(filePath, "utf8"));
15518
+ return parseSecretPayload(import_node_fs3.default.readFileSync(filePath, "utf8"));
15051
15519
  } catch {
15052
15520
  return null;
15053
15521
  }
15054
15522
  }
15055
15523
  function writeJsonFile(filePath, value, options = {}) {
15056
- import_node_fs2.default.writeFileSync(filePath, JSON.stringify(value, null, 2), {
15524
+ import_node_fs3.default.writeFileSync(filePath, JSON.stringify(value, null, 2), {
15057
15525
  encoding: "utf8",
15058
15526
  mode: options.mode ?? 384
15059
15527
  });
15060
15528
  if (typeof options.mode === "number") {
15061
- import_node_fs2.default.chmodSync(filePath, options.mode);
15529
+ import_node_fs3.default.chmodSync(filePath, options.mode);
15062
15530
  }
15063
15531
  }
15064
15532
 
@@ -15392,7 +15860,7 @@ async function ensureCloudCredentialsForCommand(options) {
15392
15860
  const writeProgress = options.writeProgress ?? options.writeStdout ?? ((message) => process.stdout.write(message));
15393
15861
  const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
15394
15862
  const fetchFn = options.fetchFn ?? fetch;
15395
- const sleep6 = options.sleep ?? (async (ms) => {
15863
+ const sleep7 = options.sleep ?? (async (ms) => {
15396
15864
  await new Promise((resolve) => setTimeout(resolve, ms));
15397
15865
  });
15398
15866
  const now = options.now ?? Date.now;
@@ -15436,7 +15904,7 @@ async function ensureCloudCredentialsForCommand(options) {
15436
15904
  fetchFn,
15437
15905
  writeProgress,
15438
15906
  openExternalUrl,
15439
- sleep: sleep6,
15907
+ sleep: sleep7,
15440
15908
  now,
15441
15909
  openBrowser: true
15442
15910
  });
@@ -15848,7 +16316,7 @@ function createDefaultDeps() {
15848
16316
  loadLocalProfileCookies: (profileDir, options) => loadCookiesFromLocalProfileDir(profileDir, options),
15849
16317
  isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
15850
16318
  confirm: async (message) => {
15851
- const rl = (0, import_promises3.createInterface)({
16319
+ const rl = (0, import_promises4.createInterface)({
15852
16320
  input: process.stdin,
15853
16321
  output: process.stderr
15854
16322
  });
@@ -16012,7 +16480,7 @@ async function resolveTargetProfileId(args, deps, client) {
16012
16480
  "Sync target is required in non-interactive mode. Use --to-profile-id <id> or --name <name>."
16013
16481
  );
16014
16482
  }
16015
- const defaultName = `Synced ${import_node_path3.default.basename(args.fromProfileDir)}`;
16483
+ const defaultName = `Synced ${import_node_path4.default.basename(args.fromProfileDir)}`;
16016
16484
  const shouldCreate = await deps.confirm(
16017
16485
  `No destination profile provided. Create a new cloud profile named "${defaultName}"?`
16018
16486
  );