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
@@ -418,13 +418,19 @@ var init_extractor = __esm({
418
418
 
419
419
  // src/cli/server.ts
420
420
  var import_net = require("net");
421
- var import_fs4 = require("fs");
421
+ var import_fs5 = require("fs");
422
422
 
423
423
  // src/opensteer.ts
424
424
  var import_crypto = require("crypto");
425
425
 
426
426
  // src/browser/pool.ts
427
- var import_playwright2 = require("playwright");
427
+ var import_node_child_process = require("child_process");
428
+ var import_node_fs = require("fs");
429
+ var import_promises = require("fs/promises");
430
+ var import_node_net = require("net");
431
+ var import_node_os = require("os");
432
+ var import_node_path = require("path");
433
+ var import_playwright = require("playwright");
428
434
 
429
435
  // src/browser/cdp-proxy.ts
430
436
  var import_ws = __toESM(require("ws"), 1);
@@ -763,209 +769,263 @@ function errorMessage(error) {
763
769
  return error instanceof Error ? error.message : String(error);
764
770
  }
765
771
 
766
- // src/browser/chromium-profile.ts
767
- var import_node_util = require("util");
768
- var import_node_child_process2 = require("child_process");
769
- var import_node_crypto = require("crypto");
770
- var import_promises = require("fs/promises");
771
- var import_node_fs = require("fs");
772
- var import_node_path = require("path");
773
- var import_node_os = require("os");
774
- var import_playwright = require("playwright");
775
-
776
- // src/auth/keychain-store.ts
777
- var import_node_child_process = require("child_process");
778
-
779
772
  // src/browser/chrome.ts
780
773
  var import_os = require("os");
781
774
  var import_path = require("path");
775
+ var import_fs = require("fs");
776
+ function detectChromePaths() {
777
+ const os = (0, import_os.platform)();
778
+ if (os === "darwin") {
779
+ const executable2 = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
780
+ return {
781
+ executable: (0, import_fs.existsSync)(executable2) ? executable2 : null,
782
+ defaultUserDataDir: (0, import_path.join)(
783
+ (0, import_os.homedir)(),
784
+ "Library",
785
+ "Application Support",
786
+ "Google",
787
+ "Chrome"
788
+ )
789
+ };
790
+ }
791
+ if (os === "win32") {
792
+ const executable2 = (0, import_path.join)(
793
+ process.env.PROGRAMFILES || "C:\\Program Files",
794
+ "Google",
795
+ "Chrome",
796
+ "Application",
797
+ "chrome.exe"
798
+ );
799
+ return {
800
+ executable: (0, import_fs.existsSync)(executable2) ? executable2 : null,
801
+ defaultUserDataDir: (0, import_path.join)(
802
+ process.env.LOCALAPPDATA || (0, import_path.join)((0, import_os.homedir)(), "AppData", "Local"),
803
+ "Google",
804
+ "Chrome",
805
+ "User Data"
806
+ )
807
+ };
808
+ }
809
+ const executable = "/usr/bin/google-chrome";
810
+ return {
811
+ executable: (0, import_fs.existsSync)(executable) ? executable : null,
812
+ defaultUserDataDir: (0, import_path.join)((0, import_os.homedir)(), ".config", "google-chrome")
813
+ };
814
+ }
782
815
  function expandHome(p) {
783
816
  if (p.startsWith("~/") || p === "~") {
784
817
  return (0, import_path.join)((0, import_os.homedir)(), p.slice(1));
785
818
  }
786
819
  return p;
787
820
  }
788
-
789
- // src/browser/chromium-profile.ts
790
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
791
- function directoryExists(filePath) {
792
- try {
793
- return (0, import_node_fs.statSync)(filePath).isDirectory();
794
- } catch {
795
- return false;
821
+ function listLocalChromeProfiles(userDataDir = detectChromePaths().defaultUserDataDir) {
822
+ const resolvedUserDataDir = expandHome(userDataDir);
823
+ const localStatePath = (0, import_path.join)(resolvedUserDataDir, "Local State");
824
+ if (!(0, import_fs.existsSync)(localStatePath)) {
825
+ return [];
796
826
  }
797
- }
798
- function fileExists(filePath) {
799
827
  try {
800
- return (0, import_node_fs.statSync)(filePath).isFile();
801
- } catch {
802
- return false;
803
- }
804
- }
805
- function resolveCookieDbPath(profileDir) {
806
- const candidates = [(0, import_node_path.join)(profileDir, "Network", "Cookies"), (0, import_node_path.join)(profileDir, "Cookies")];
807
- for (const candidate of candidates) {
808
- if (fileExists(candidate)) {
809
- return candidate;
828
+ const raw = JSON.parse((0, import_fs.readFileSync)(localStatePath, "utf-8"));
829
+ 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;
830
+ if (!infoCache || typeof infoCache !== "object") {
831
+ return [];
810
832
  }
833
+ return Object.entries(infoCache).map(([directory, info]) => {
834
+ const record = info && typeof info === "object" && !Array.isArray(info) ? info : {};
835
+ const name = typeof record.name === "string" && record.name.trim() ? record.name.trim() : directory;
836
+ return {
837
+ directory,
838
+ name
839
+ };
840
+ }).filter((profile) => profile.directory.trim().length > 0).sort(
841
+ (left, right) => left.directory.localeCompare(right.directory)
842
+ );
843
+ } catch {
844
+ return [];
811
845
  }
812
- return null;
813
- }
814
- function resolvePersistentChromiumLaunchProfile(inputPath) {
815
- const expandedPath = expandHome(inputPath.trim());
816
- if (!expandedPath) {
817
- return {
818
- userDataDir: inputPath
819
- };
820
- }
821
- if (fileExists(expandedPath) && (0, import_node_path.basename)(expandedPath) === "Cookies") {
822
- const directParent = (0, import_node_path.dirname)(expandedPath);
823
- const profileDir = (0, import_node_path.basename)(directParent) === "Network" ? (0, import_node_path.dirname)(directParent) : directParent;
824
- return {
825
- userDataDir: (0, import_node_path.dirname)(profileDir),
826
- profileDirectory: (0, import_node_path.basename)(profileDir)
827
- };
828
- }
829
- if (directoryExists(expandedPath) && resolveCookieDbPath(expandedPath) && fileExists((0, import_node_path.join)((0, import_node_path.dirname)(expandedPath), "Local State"))) {
830
- return {
831
- userDataDir: (0, import_node_path.dirname)(expandedPath),
832
- profileDirectory: (0, import_node_path.basename)(expandedPath)
833
- };
834
- }
835
- return {
836
- userDataDir: expandedPath
837
- };
838
846
  }
839
847
 
840
848
  // src/browser/pool.ts
841
849
  var BrowserPool = class {
842
850
  browser = null;
843
- persistentContext = null;
844
851
  cdpProxy = null;
852
+ launchedProcess = null;
853
+ tempUserDataDir = null;
845
854
  defaults;
846
855
  constructor(defaults = {}) {
847
856
  this.defaults = defaults;
848
857
  }
849
858
  async launch(options = {}) {
850
- if (this.browser || this.cdpProxy) {
859
+ if (this.browser || this.cdpProxy || this.launchedProcess || this.tempUserDataDir) {
851
860
  await this.close();
852
861
  }
853
- const connectUrl = options.connectUrl ?? this.defaults.connectUrl;
854
- const channel = options.channel ?? this.defaults.channel;
855
- const profileDir = options.profileDir ?? this.defaults.profileDir;
856
- if (connectUrl) {
857
- return this.connectToRunning(connectUrl, options.timeout);
862
+ const mode = options.mode ?? this.defaults.mode ?? "chromium";
863
+ const cdpUrl = options.cdpUrl ?? this.defaults.cdpUrl;
864
+ const userDataDir = options.userDataDir ?? this.defaults.userDataDir;
865
+ const profileDirectory = options.profileDirectory ?? this.defaults.profileDirectory;
866
+ const executablePath = options.executablePath ?? this.defaults.executablePath;
867
+ if (cdpUrl) {
868
+ if (mode === "real") {
869
+ throw new Error(
870
+ 'cdpUrl cannot be combined with mode "real". Use one browser launch path at a time.'
871
+ );
872
+ }
873
+ if (userDataDir || profileDirectory) {
874
+ throw new Error(
875
+ "userDataDir/profileDirectory cannot be combined with cdpUrl."
876
+ );
877
+ }
878
+ if (options.context && Object.keys(options.context).length > 0) {
879
+ throw new Error(
880
+ "context launch options are not supported when attaching over CDP."
881
+ );
882
+ }
883
+ return this.connectToRunning(cdpUrl, options.timeout);
858
884
  }
859
- if (profileDir) {
860
- return this.launchPersistentProfile(options, channel, profileDir);
885
+ if (mode !== "real" && (userDataDir || profileDirectory)) {
886
+ throw new Error(
887
+ 'userDataDir/profileDirectory require mode "real".'
888
+ );
861
889
  }
862
- if (channel) {
863
- return this.launchWithChannel(options, channel);
890
+ if (mode === "real") {
891
+ if (options.context && Object.keys(options.context).length > 0) {
892
+ throw new Error(
893
+ "context launch options are not supported for real-browser mode."
894
+ );
895
+ }
896
+ return this.launchOwnedRealBrowser({
897
+ ...options,
898
+ executablePath,
899
+ userDataDir,
900
+ profileDirectory
901
+ });
864
902
  }
865
903
  return this.launchSandbox(options);
866
904
  }
867
905
  async close() {
868
906
  const browser = this.browser;
869
- const persistentContext = this.persistentContext;
907
+ const cdpProxy = this.cdpProxy;
908
+ const launchedProcess = this.launchedProcess;
909
+ const tempUserDataDir = this.tempUserDataDir;
870
910
  this.browser = null;
871
- this.persistentContext = null;
911
+ this.cdpProxy = null;
912
+ this.launchedProcess = null;
913
+ this.tempUserDataDir = null;
872
914
  try {
873
- if (persistentContext) {
874
- await persistentContext.close();
875
- } else if (browser) {
876
- await browser.close();
915
+ if (browser) {
916
+ await browser.close().catch(() => void 0);
877
917
  }
878
918
  } finally {
879
- this.cdpProxy?.close();
880
- this.cdpProxy = null;
919
+ cdpProxy?.close();
920
+ await killProcessTree(launchedProcess);
921
+ if (tempUserDataDir) {
922
+ await (0, import_promises.rm)(tempUserDataDir, {
923
+ recursive: true,
924
+ force: true
925
+ }).catch(() => void 0);
926
+ }
881
927
  }
882
928
  }
883
- async connectToRunning(connectUrl, timeout) {
884
- this.cdpProxy?.close();
885
- this.cdpProxy = null;
929
+ async connectToRunning(cdpUrl, timeout) {
886
930
  let browser = null;
931
+ let cdpProxy = null;
887
932
  try {
888
- const { browserWsUrl, targets } = await discoverTargets(connectUrl);
933
+ const { browserWsUrl, targets } = await discoverTargets(cdpUrl);
889
934
  if (targets.length === 0) {
890
935
  throw new Error(
891
936
  "No page targets found. Is the browser running with an open window?"
892
937
  );
893
938
  }
894
- const target = targets[0];
895
- this.cdpProxy = new CDPProxy(browserWsUrl, target.id);
896
- const proxyWsUrl = await this.cdpProxy.start();
897
- browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
939
+ cdpProxy = new CDPProxy(browserWsUrl, targets[0].id);
940
+ const proxyWsUrl = await cdpProxy.start();
941
+ browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
898
942
  timeout: timeout ?? 3e4
899
943
  });
900
944
  this.browser = browser;
901
- this.persistentContext = null;
902
- const contexts = browser.contexts();
903
- if (contexts.length === 0) {
904
- throw new Error(
905
- "Connection succeeded but no browser contexts found. Is the browser running with an open window?"
906
- );
907
- }
908
- const context = contexts[0];
909
- const pages = context.pages();
910
- const page = pages.length > 0 ? pages[0] : await context.newPage();
945
+ this.cdpProxy = cdpProxy;
946
+ const { context, page } = await pickBrowserContextAndPage(browser);
911
947
  return { browser, context, page, isExternal: true };
912
948
  } catch (error) {
913
949
  if (browser) {
914
950
  await browser.close().catch(() => void 0);
915
951
  }
952
+ cdpProxy?.close();
916
953
  this.browser = null;
917
- this.persistentContext = null;
918
- this.cdpProxy?.close();
919
954
  this.cdpProxy = null;
920
955
  throw error;
921
956
  }
922
957
  }
923
- async launchPersistentProfile(options, channel, profileDir) {
924
- const args = [];
925
- const launchProfile = resolvePersistentChromiumLaunchProfile(profileDir);
926
- if (launchProfile.profileDirectory) {
927
- args.push(`--profile-directory=${launchProfile.profileDirectory}`);
928
- }
929
- const context = await import_playwright2.chromium.launchPersistentContext(
930
- launchProfile.userDataDir,
931
- {
932
- channel,
933
- headless: options.headless ?? this.defaults.headless,
934
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
935
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
936
- timeout: options.timeout,
937
- ...options.context || {},
938
- args
939
- }
940
- );
941
- const browser = context.browser();
942
- if (!browser) {
943
- await context.close().catch(() => void 0);
944
- throw new Error("Persistent browser launch did not expose a browser instance.");
958
+ async launchOwnedRealBrowser(options) {
959
+ const chromePaths = detectChromePaths();
960
+ const executablePath = options.executablePath ?? chromePaths.executable ?? void 0;
961
+ if (!executablePath) {
962
+ throw new Error(
963
+ "Chrome was not found. Set browser.executablePath or install Chrome in a supported location."
964
+ );
945
965
  }
946
- this.browser = browser;
947
- this.persistentContext = context;
948
- const pages = context.pages();
949
- const page = pages.length > 0 ? pages[0] : await context.newPage();
950
- return { browser, context, page, isExternal: false };
951
- }
952
- async launchWithChannel(options, channel) {
953
- const browser = await import_playwright2.chromium.launch({
954
- channel,
955
- headless: options.headless ?? this.defaults.headless,
956
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
957
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
958
- timeout: options.timeout
966
+ const sourceUserDataDir = expandHome(
967
+ options.userDataDir ?? chromePaths.defaultUserDataDir
968
+ );
969
+ const profileDirectory = options.profileDirectory ?? "Default";
970
+ const tempUserDataDir = await cloneProfileToTempDir(
971
+ sourceUserDataDir,
972
+ profileDirectory
973
+ );
974
+ const debugPort = await reserveDebugPort();
975
+ const headless = resolveLaunchHeadless(
976
+ "real",
977
+ options.headless,
978
+ this.defaults.headless
979
+ );
980
+ const launchArgs = buildRealBrowserLaunchArgs({
981
+ userDataDir: tempUserDataDir,
982
+ profileDirectory,
983
+ debugPort,
984
+ headless
959
985
  });
960
- this.browser = browser;
961
- this.persistentContext = null;
962
- const context = await browser.newContext(options.context || {});
963
- const page = await context.newPage();
964
- return { browser, context, page, isExternal: false };
986
+ const processHandle = (0, import_node_child_process.spawn)(executablePath, launchArgs, {
987
+ detached: process.platform !== "win32",
988
+ stdio: "ignore"
989
+ });
990
+ processHandle.unref();
991
+ let browser = null;
992
+ try {
993
+ const wsUrl = await resolveCdpWebSocketUrl(
994
+ `http://127.0.0.1:${debugPort}`,
995
+ options.timeout ?? 3e4
996
+ );
997
+ browser = await import_playwright.chromium.connectOverCDP(wsUrl, {
998
+ timeout: options.timeout ?? 3e4
999
+ });
1000
+ const { context, page } = await createOwnedBrowserContextAndPage(
1001
+ browser,
1002
+ {
1003
+ headless,
1004
+ initialUrl: options.initialUrl,
1005
+ timeoutMs: options.timeout ?? 3e4
1006
+ }
1007
+ );
1008
+ this.browser = browser;
1009
+ this.launchedProcess = processHandle;
1010
+ this.tempUserDataDir = tempUserDataDir;
1011
+ return { browser, context, page, isExternal: false };
1012
+ } catch (error) {
1013
+ await browser?.close().catch(() => void 0);
1014
+ await killProcessTree(processHandle);
1015
+ await (0, import_promises.rm)(tempUserDataDir, {
1016
+ recursive: true,
1017
+ force: true
1018
+ }).catch(() => void 0);
1019
+ throw error;
1020
+ }
965
1021
  }
966
1022
  async launchSandbox(options) {
967
- const browser = await import_playwright2.chromium.launch({
968
- headless: options.headless ?? this.defaults.headless,
1023
+ const browser = await import_playwright.chromium.launch({
1024
+ headless: resolveLaunchHeadless(
1025
+ "chromium",
1026
+ options.headless,
1027
+ this.defaults.headless
1028
+ ),
969
1029
  executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
970
1030
  slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
971
1031
  timeout: options.timeout
@@ -973,13 +1033,271 @@ var BrowserPool = class {
973
1033
  const context = await browser.newContext(options.context || {});
974
1034
  const page = await context.newPage();
975
1035
  this.browser = browser;
976
- this.persistentContext = null;
977
1036
  return { browser, context, page, isExternal: false };
978
1037
  }
979
1038
  };
1039
+ async function pickBrowserContextAndPage(browser) {
1040
+ const context = getPrimaryBrowserContext(browser);
1041
+ const pages = context.pages();
1042
+ const page = pages.find((candidate) => isInspectablePageUrl2(candidate.url())) || pages[0] || await context.newPage();
1043
+ return { context, page };
1044
+ }
1045
+ function resolveLaunchHeadless(mode, requestedHeadless, defaultHeadless) {
1046
+ if (requestedHeadless !== void 0) {
1047
+ return requestedHeadless;
1048
+ }
1049
+ if (defaultHeadless !== void 0) {
1050
+ return defaultHeadless;
1051
+ }
1052
+ return mode === "real";
1053
+ }
1054
+ async function createOwnedBrowserContextAndPage(browser, options) {
1055
+ const context = getPrimaryBrowserContext(browser);
1056
+ const page = await createOwnedBrowserPage(browser, context, options);
1057
+ return { context, page };
1058
+ }
1059
+ async function createOwnedBrowserPage(browser, context, options) {
1060
+ const targetUrl = options.initialUrl ?? "about:blank";
1061
+ const existingPages = new Set(context.pages());
1062
+ const browserSession = await browser.newBrowserCDPSession();
1063
+ try {
1064
+ const { targetId } = await browserSession.send("Target.createTarget", {
1065
+ url: targetUrl,
1066
+ newWindow: !options.headless
1067
+ });
1068
+ await browserSession.send("Target.activateTarget", { targetId }).catch(() => void 0);
1069
+ const page = await waitForOwnedBrowserPage(context, {
1070
+ existingPages,
1071
+ targetUrl,
1072
+ timeoutMs: options.timeoutMs
1073
+ });
1074
+ if (targetUrl !== "about:blank") {
1075
+ await page.waitForLoadState("domcontentloaded", {
1076
+ timeout: options.timeoutMs
1077
+ });
1078
+ }
1079
+ await closeDisposableStartupTargets(browserSession, targetId);
1080
+ return page;
1081
+ } finally {
1082
+ await browserSession.detach().catch(() => void 0);
1083
+ }
1084
+ }
1085
+ async function closeDisposableStartupTargets(browserSession, preservedTargetId) {
1086
+ const response = await browserSession.send("Target.getTargets").catch(() => null);
1087
+ if (!response) {
1088
+ return;
1089
+ }
1090
+ for (const targetInfo of response.targetInfos) {
1091
+ if (targetInfo.targetId === preservedTargetId || targetInfo.type !== "page" || !isDisposableStartupPageUrl(targetInfo.url)) {
1092
+ continue;
1093
+ }
1094
+ await browserSession.send("Target.closeTarget", { targetId: targetInfo.targetId }).catch(() => void 0);
1095
+ }
1096
+ }
1097
+ async function waitForOwnedBrowserPage(context, options) {
1098
+ const deadline = Date.now() + options.timeoutMs;
1099
+ let fallbackPage = null;
1100
+ while (Date.now() < deadline) {
1101
+ for (const candidate of context.pages()) {
1102
+ if (options.existingPages.has(candidate)) {
1103
+ continue;
1104
+ }
1105
+ const url = candidate.url();
1106
+ if (!isInspectablePageUrl2(url)) {
1107
+ continue;
1108
+ }
1109
+ fallbackPage ??= candidate;
1110
+ if (options.targetUrl === "about:blank") {
1111
+ return candidate;
1112
+ }
1113
+ if (pageLooselyMatchesUrl(url, options.targetUrl)) {
1114
+ return candidate;
1115
+ }
1116
+ }
1117
+ await sleep(100);
1118
+ }
1119
+ if (fallbackPage) {
1120
+ return fallbackPage;
1121
+ }
1122
+ throw new Error(
1123
+ `Chrome created a target for ${options.targetUrl}, but Playwright did not expose the page in time.`
1124
+ );
1125
+ }
1126
+ function getPrimaryBrowserContext(browser) {
1127
+ const contexts = browser.contexts();
1128
+ if (contexts.length === 0) {
1129
+ throw new Error(
1130
+ "Connection succeeded but no browser contexts were exposed."
1131
+ );
1132
+ }
1133
+ return contexts[0];
1134
+ }
1135
+ function isInspectablePageUrl2(url) {
1136
+ return url === "about:blank" || url.startsWith("http://") || url.startsWith("https://");
1137
+ }
1138
+ function isDisposableStartupPageUrl(url) {
1139
+ return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/";
1140
+ }
1141
+ function pageLooselyMatchesUrl(currentUrl, initialUrl) {
1142
+ try {
1143
+ const current = new URL(currentUrl);
1144
+ const requested = new URL(initialUrl);
1145
+ if (current.href === requested.href) {
1146
+ return true;
1147
+ }
1148
+ return current.hostname === requested.hostname && current.pathname === requested.pathname;
1149
+ } catch {
1150
+ return currentUrl === initialUrl;
1151
+ }
1152
+ }
1153
+ function normalizeDiscoveryUrl(cdpUrl) {
1154
+ let parsed;
1155
+ try {
1156
+ parsed = new URL(cdpUrl);
1157
+ } catch {
1158
+ throw new Error(
1159
+ `Invalid CDP URL "${cdpUrl}". Use an http(s) or ws(s) endpoint.`
1160
+ );
1161
+ }
1162
+ if (parsed.protocol === "ws:" || parsed.protocol === "wss:") {
1163
+ return parsed;
1164
+ }
1165
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1166
+ throw new Error(
1167
+ `Unsupported CDP URL protocol "${parsed.protocol}". Use http(s) or ws(s).`
1168
+ );
1169
+ }
1170
+ const normalized = new URL(parsed.toString());
1171
+ normalized.pathname = "/json/version";
1172
+ normalized.search = "";
1173
+ normalized.hash = "";
1174
+ return normalized;
1175
+ }
1176
+ async function resolveCdpWebSocketUrl(cdpUrl, timeoutMs) {
1177
+ if (cdpUrl.startsWith("ws://") || cdpUrl.startsWith("wss://")) {
1178
+ return cdpUrl;
1179
+ }
1180
+ const versionUrl = normalizeDiscoveryUrl(cdpUrl);
1181
+ const deadline = Date.now() + timeoutMs;
1182
+ let lastError = "CDP discovery did not respond.";
1183
+ while (Date.now() < deadline) {
1184
+ const remaining = Math.max(deadline - Date.now(), 1e3);
1185
+ try {
1186
+ const response = await fetch(versionUrl, {
1187
+ signal: AbortSignal.timeout(Math.min(remaining, 5e3))
1188
+ });
1189
+ if (!response.ok) {
1190
+ lastError = `${response.status} ${response.statusText}`;
1191
+ } else {
1192
+ const payload = await response.json();
1193
+ const wsUrl = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.webSocketDebuggerUrl === "string" ? payload.webSocketDebuggerUrl : null;
1194
+ if (wsUrl && wsUrl.trim()) {
1195
+ return wsUrl;
1196
+ }
1197
+ lastError = "CDP discovery response did not include webSocketDebuggerUrl.";
1198
+ }
1199
+ } catch (error) {
1200
+ lastError = error instanceof Error ? error.message : "Unknown error";
1201
+ }
1202
+ await sleep(100);
1203
+ }
1204
+ throw new Error(
1205
+ `Failed to resolve a CDP websocket URL from ${versionUrl.toString()}: ${lastError}`
1206
+ );
1207
+ }
1208
+ async function reserveDebugPort() {
1209
+ return await new Promise((resolve, reject) => {
1210
+ const server2 = (0, import_node_net.createServer)();
1211
+ server2.unref();
1212
+ server2.on("error", reject);
1213
+ server2.listen(0, "127.0.0.1", () => {
1214
+ const address = server2.address();
1215
+ if (!address || typeof address === "string") {
1216
+ server2.close();
1217
+ reject(new Error("Failed to reserve a local debug port."));
1218
+ return;
1219
+ }
1220
+ server2.close((error) => {
1221
+ if (error) {
1222
+ reject(error);
1223
+ return;
1224
+ }
1225
+ resolve(address.port);
1226
+ });
1227
+ });
1228
+ });
1229
+ }
1230
+ async function cloneProfileToTempDir(userDataDir, profileDirectory) {
1231
+ const resolvedUserDataDir = expandHome(userDataDir);
1232
+ const tempUserDataDir = await (0, import_promises.mkdtemp)(
1233
+ (0, import_node_path.join)((0, import_node_os.tmpdir)(), "opensteer-real-browser-")
1234
+ );
1235
+ const sourceProfileDir = (0, import_node_path.join)(resolvedUserDataDir, profileDirectory);
1236
+ const targetProfileDir = (0, import_node_path.join)(tempUserDataDir, profileDirectory);
1237
+ if ((0, import_node_fs.existsSync)(sourceProfileDir)) {
1238
+ await (0, import_promises.cp)(sourceProfileDir, targetProfileDir, {
1239
+ recursive: true
1240
+ });
1241
+ } else {
1242
+ await (0, import_promises.mkdir)(targetProfileDir, {
1243
+ recursive: true
1244
+ });
1245
+ }
1246
+ const localStatePath = (0, import_node_path.join)(resolvedUserDataDir, "Local State");
1247
+ if ((0, import_node_fs.existsSync)(localStatePath)) {
1248
+ await (0, import_promises.copyFile)(localStatePath, (0, import_node_path.join)(tempUserDataDir, "Local State"));
1249
+ }
1250
+ return tempUserDataDir;
1251
+ }
1252
+ function buildRealBrowserLaunchArgs(options) {
1253
+ const args = [
1254
+ `--user-data-dir=${options.userDataDir}`,
1255
+ `--profile-directory=${options.profileDirectory}`,
1256
+ `--remote-debugging-port=${options.debugPort}`,
1257
+ "--no-first-run",
1258
+ "--no-default-browser-check",
1259
+ "--disable-background-networking",
1260
+ "--disable-sync",
1261
+ "--disable-popup-blocking"
1262
+ ];
1263
+ if (options.headless) {
1264
+ args.push("--headless=new");
1265
+ }
1266
+ return args;
1267
+ }
1268
+ async function killProcessTree(processHandle) {
1269
+ if (!processHandle || processHandle.pid == null || processHandle.exitCode !== null) {
1270
+ return;
1271
+ }
1272
+ if (process.platform === "win32") {
1273
+ await new Promise((resolve) => {
1274
+ const killer = (0, import_node_child_process.spawn)(
1275
+ "taskkill",
1276
+ ["/pid", String(processHandle.pid), "/t", "/f"],
1277
+ {
1278
+ stdio: "ignore"
1279
+ }
1280
+ );
1281
+ killer.on("error", () => resolve());
1282
+ killer.on("exit", () => resolve());
1283
+ });
1284
+ return;
1285
+ }
1286
+ try {
1287
+ process.kill(-processHandle.pid, "SIGKILL");
1288
+ } catch {
1289
+ try {
1290
+ processHandle.kill("SIGKILL");
1291
+ } catch {
1292
+ }
1293
+ }
1294
+ }
1295
+ async function sleep(ms) {
1296
+ await new Promise((resolve) => setTimeout(resolve, ms));
1297
+ }
980
1298
 
981
1299
  // src/config.ts
982
- var import_fs = __toESM(require("fs"), 1);
1300
+ var import_fs2 = __toESM(require("fs"), 1);
983
1301
  var import_path3 = __toESM(require("path"), 1);
984
1302
  var import_url = require("url");
985
1303
  var import_dotenv = require("dotenv");
@@ -1210,6 +1528,65 @@ function toJsonSafeValue(value, seen) {
1210
1528
  return void 0;
1211
1529
  }
1212
1530
 
1531
+ // src/cloud/credential-selection.ts
1532
+ function selectCloudCredential(options) {
1533
+ const apiKey = normalizeNonEmptyString(options.apiKey);
1534
+ const accessToken = normalizeNonEmptyString(options.accessToken);
1535
+ if (apiKey) {
1536
+ if (options.authScheme === "bearer") {
1537
+ return {
1538
+ apiKey,
1539
+ authScheme: "bearer",
1540
+ kind: "access-token",
1541
+ token: apiKey,
1542
+ compatibilityBearerApiKey: true
1543
+ };
1544
+ }
1545
+ return {
1546
+ apiKey,
1547
+ authScheme: "api-key",
1548
+ kind: "api-key",
1549
+ token: apiKey
1550
+ };
1551
+ }
1552
+ if (accessToken) {
1553
+ return {
1554
+ accessToken,
1555
+ authScheme: "bearer",
1556
+ kind: "access-token",
1557
+ token: accessToken
1558
+ };
1559
+ }
1560
+ return null;
1561
+ }
1562
+ function selectCloudCredentialByPrecedence(layers, authScheme) {
1563
+ for (const layer of layers) {
1564
+ const hasApiKey = layer.hasApiKey ?? Object.prototype.hasOwnProperty.call(layer, "apiKey");
1565
+ const hasAccessToken = layer.hasAccessToken ?? Object.prototype.hasOwnProperty.call(layer, "accessToken");
1566
+ if (!hasApiKey && !hasAccessToken) {
1567
+ continue;
1568
+ }
1569
+ return {
1570
+ source: layer.source,
1571
+ apiKey: layer.apiKey,
1572
+ accessToken: layer.accessToken,
1573
+ hasApiKey,
1574
+ hasAccessToken,
1575
+ credential: selectCloudCredential({
1576
+ apiKey: layer.apiKey,
1577
+ accessToken: layer.accessToken,
1578
+ authScheme: layer.authScheme ?? authScheme
1579
+ })
1580
+ };
1581
+ }
1582
+ return null;
1583
+ }
1584
+ function normalizeNonEmptyString(value) {
1585
+ if (typeof value !== "string") return void 0;
1586
+ const normalized = value.trim();
1587
+ return normalized.length ? normalized : void 0;
1588
+ }
1589
+
1213
1590
  // src/storage/namespace.ts
1214
1591
  var import_path2 = __toESM(require("path"), 1);
1215
1592
  var DEFAULT_NAMESPACE = "default";
@@ -1248,9 +1625,10 @@ var DEFAULT_CONFIG = {
1248
1625
  headless: false,
1249
1626
  executablePath: void 0,
1250
1627
  slowMo: 0,
1251
- connectUrl: void 0,
1252
- channel: void 0,
1253
- profileDir: void 0
1628
+ mode: void 0,
1629
+ cdpUrl: void 0,
1630
+ userDataDir: void 0,
1631
+ profileDirectory: void 0
1254
1632
  },
1255
1633
  storage: {
1256
1634
  rootDir: process.cwd()
@@ -1287,9 +1665,9 @@ function loadDotenvValues(rootDir, baseEnv, options = {}) {
1287
1665
  const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
1288
1666
  for (const filename of dotenvFileOrder(nodeEnv)) {
1289
1667
  const filePath = import_path3.default.join(baseDir, filename);
1290
- if (!import_fs.default.existsSync(filePath)) continue;
1668
+ if (!import_fs2.default.existsSync(filePath)) continue;
1291
1669
  try {
1292
- const raw = import_fs.default.readFileSync(filePath, "utf8");
1670
+ const raw = import_fs2.default.readFileSync(filePath, "utf8");
1293
1671
  const parsed = (0, import_dotenv.parse)(raw);
1294
1672
  for (const [key, value] of Object.entries(parsed)) {
1295
1673
  if (values[key] === void 0) {
@@ -1359,9 +1737,9 @@ function assertNoLegacyRuntimeConfig(source, config) {
1359
1737
  }
1360
1738
  function loadConfigFile(rootDir, options = {}) {
1361
1739
  const configPath = import_path3.default.join(rootDir, ".opensteer", "config.json");
1362
- if (!import_fs.default.existsSync(configPath)) return {};
1740
+ if (!import_fs2.default.existsSync(configPath)) return {};
1363
1741
  try {
1364
- const raw = import_fs.default.readFileSync(configPath, "utf8");
1742
+ const raw = import_fs2.default.readFileSync(configPath, "utf8");
1365
1743
  return JSON.parse(raw);
1366
1744
  } catch (error) {
1367
1745
  const message = extractErrorMessage(
@@ -1534,11 +1912,6 @@ function normalizeCloudOptions(value) {
1534
1912
  }
1535
1913
  return value;
1536
1914
  }
1537
- function normalizeNonEmptyString(value) {
1538
- if (typeof value !== "string") return void 0;
1539
- const normalized = value.trim();
1540
- return normalized.length ? normalized : void 0;
1541
- }
1542
1915
  function parseCloudEnabled(value, source) {
1543
1916
  if (value == null) return void 0;
1544
1917
  if (typeof value === "boolean") return value;
@@ -1547,6 +1920,18 @@ function parseCloudEnabled(value, source) {
1547
1920
  `Invalid ${source} value "${String(value)}". Use true, false, or a cloud options object.`
1548
1921
  );
1549
1922
  }
1923
+ function resolveCloudCredentialFields(selectedLayer) {
1924
+ const credential = selectedLayer?.credential;
1925
+ if (!credential) return {};
1926
+ if (credential.kind === "api-key" || credential.compatibilityBearerApiKey === true && selectedLayer?.source !== "env") {
1927
+ return {
1928
+ apiKey: credential.token
1929
+ };
1930
+ }
1931
+ return {
1932
+ accessToken: credential.token
1933
+ };
1934
+ }
1550
1935
  function resolveCloudSelection(config, env = process.env) {
1551
1936
  const configCloud = parseCloudEnabled(config.cloud, "cloud");
1552
1937
  if (configCloud !== void 0) {
@@ -1583,7 +1968,12 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1583
1968
  });
1584
1969
  assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
1585
1970
  assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
1971
+ const fileCloudOptions = normalizeCloudOptions(fileConfig.cloud);
1972
+ const fileHasCloudApiKey = hasOwn(fileCloudOptions, "apiKey");
1973
+ const fileHasCloudAccessToken = hasOwn(fileCloudOptions, "accessToken");
1586
1974
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1975
+ assertNoRemovedBrowserConfig(input.browser, "Opensteer constructor config");
1976
+ assertNoRemovedBrowserConfig(fileConfig.browser, ".opensteer/config.json");
1587
1977
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1588
1978
  const env = resolveEnv(envRootDir, {
1589
1979
  debug: debugHint,
@@ -1599,14 +1989,30 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1599
1989
  "OPENSTEER_RUNTIME is no longer supported. Use OPENSTEER_MODE instead."
1600
1990
  );
1601
1991
  }
1992
+ if (env.OPENSTEER_CONNECT_URL != null) {
1993
+ throw new Error(
1994
+ "OPENSTEER_CONNECT_URL is no longer supported. Use OPENSTEER_CDP_URL instead."
1995
+ );
1996
+ }
1997
+ if (env.OPENSTEER_CHANNEL != null) {
1998
+ throw new Error(
1999
+ "OPENSTEER_CHANNEL is no longer supported. Use OPENSTEER_BROWSER plus OPENSTEER_BROWSER_PATH when needed."
2000
+ );
2001
+ }
2002
+ if (env.OPENSTEER_PROFILE_DIR != null) {
2003
+ throw new Error(
2004
+ "OPENSTEER_PROFILE_DIR is no longer supported. Use OPENSTEER_USER_DATA_DIR and OPENSTEER_PROFILE_DIRECTORY instead."
2005
+ );
2006
+ }
1602
2007
  const envConfig = {
1603
2008
  browser: {
1604
2009
  headless: parseBool(env.OPENSTEER_HEADLESS),
1605
2010
  executablePath: env.OPENSTEER_BROWSER_PATH || void 0,
1606
2011
  slowMo: parseNumber(env.OPENSTEER_SLOW_MO),
1607
- connectUrl: env.OPENSTEER_CONNECT_URL || void 0,
1608
- channel: env.OPENSTEER_CHANNEL || void 0,
1609
- profileDir: env.OPENSTEER_PROFILE_DIR || void 0
2012
+ mode: env.OPENSTEER_BROWSER === "real" || env.OPENSTEER_BROWSER === "chromium" ? env.OPENSTEER_BROWSER : void 0,
2013
+ cdpUrl: env.OPENSTEER_CDP_URL || void 0,
2014
+ userDataDir: env.OPENSTEER_USER_DATA_DIR || void 0,
2015
+ profileDirectory: env.OPENSTEER_PROFILE_DIRECTORY || void 0
1610
2016
  },
1611
2017
  cursor: {
1612
2018
  enabled: parseBool(env.OPENSTEER_CURSOR)
@@ -1617,17 +2023,38 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1617
2023
  const mergedWithFile = mergeDeep(runtimeDefaults, fileConfig);
1618
2024
  const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
1619
2025
  const resolved = mergeDeep(mergedWithEnv, input);
2026
+ const browserHeadlessExplicit = input.browser?.headless !== void 0 || fileConfig.browser?.headless !== void 0 || envConfig.browser?.headless !== void 0;
2027
+ if (!browserHeadlessExplicit && resolved.browser?.mode === "real") {
2028
+ resolved.browser = {
2029
+ ...resolved.browser,
2030
+ headless: true
2031
+ };
2032
+ }
2033
+ function assertNoRemovedBrowserConfig(value, source) {
2034
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2035
+ return;
2036
+ }
2037
+ const record = value;
2038
+ if (record.connectUrl !== void 0) {
2039
+ throw new Error(
2040
+ `${source}.browser.connectUrl is no longer supported. Use browser.cdpUrl instead.`
2041
+ );
2042
+ }
2043
+ if (record.channel !== void 0) {
2044
+ throw new Error(
2045
+ `${source}.browser.channel is no longer supported. Use browser.mode plus browser.executablePath instead.`
2046
+ );
2047
+ }
2048
+ if (record.profileDir !== void 0) {
2049
+ throw new Error(
2050
+ `${source}.browser.profileDir is no longer supported. Use browser.userDataDir and browser.profileDirectory instead.`
2051
+ );
2052
+ }
2053
+ }
1620
2054
  const envApiKey = resolveOpensteerApiKey(env);
1621
2055
  const envAccessTokenRaw = resolveOpensteerAccessToken(env);
1622
2056
  const envBaseUrl = resolveOpensteerBaseUrl(env);
1623
2057
  const envAuthScheme = resolveOpensteerAuthScheme(env);
1624
- if (envApiKey && envAccessTokenRaw) {
1625
- throw new Error(
1626
- "OPENSTEER_API_KEY and OPENSTEER_ACCESS_TOKEN are mutually exclusive. Set only one."
1627
- );
1628
- }
1629
- const envAccessToken = envAccessTokenRaw || (envAuthScheme === "bearer" ? envApiKey : void 0);
1630
- const envApiCredential = envAuthScheme === "bearer" && !envAccessTokenRaw ? void 0 : envApiKey;
1631
2058
  const envCloudProfileId = resolveOpensteerCloudProfileId(env);
1632
2059
  const envCloudProfileReuseIfActive = resolveOpensteerCloudProfileReuseIfActive(env);
1633
2060
  const envCloudAnnounce = parseCloudAnnounce(
@@ -1656,11 +2083,6 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1656
2083
  const inputHasCloudBaseUrl = Boolean(
1657
2084
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
1658
2085
  );
1659
- if (normalizeNonEmptyString(inputCloudOptions?.apiKey) && normalizeNonEmptyString(inputCloudOptions?.accessToken)) {
1660
- throw new Error(
1661
- "cloud.apiKey and cloud.accessToken are mutually exclusive. Set only one."
1662
- );
1663
- }
1664
2086
  const cloudSelection = resolveCloudSelection({
1665
2087
  cloud: resolved.cloud
1666
2088
  }, env);
@@ -1671,11 +2093,6 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1671
2093
  accessToken: resolvedCloudAccessTokenRaw,
1672
2094
  ...resolvedCloudRest
1673
2095
  } = resolvedCloud;
1674
- if (normalizeNonEmptyString(resolvedCloudApiKeyRaw) && normalizeNonEmptyString(resolvedCloudAccessTokenRaw)) {
1675
- throw new Error(
1676
- "Cloud config cannot include both apiKey and accessToken at the same time."
1677
- );
1678
- }
1679
2096
  const resolvedCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1680
2097
  resolvedCloud.browserProfile,
1681
2098
  "resolved.cloud.browserProfile"
@@ -1687,25 +2104,40 @@ function resolveConfigWithEnv(input = {}, options = {}) {
1687
2104
  const browserProfile = inputCloudBrowserProfile ?? envCloudBrowserProfile ?? resolvedCloudBrowserProfile;
1688
2105
  let authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1689
2106
  const announce = inputCloudAnnounce ?? envCloudAnnounce ?? parseCloudAnnounce(resolvedCloud.announce, "cloud.announce") ?? "always";
1690
- const credentialOverriddenByInput = inputHasCloudApiKey || inputHasCloudAccessToken;
1691
- let apiKey = normalizeNonEmptyString(resolvedCloudApiKeyRaw);
1692
- let accessToken = normalizeNonEmptyString(resolvedCloudAccessTokenRaw);
1693
- if (!credentialOverriddenByInput) {
1694
- if (envAccessToken) {
1695
- accessToken = envAccessToken;
1696
- apiKey = void 0;
1697
- } else if (envApiCredential) {
1698
- apiKey = envApiCredential;
1699
- accessToken = void 0;
1700
- }
1701
- }
2107
+ const selectedCredentialLayer = selectCloudCredentialByPrecedence(
2108
+ [
2109
+ {
2110
+ source: "input",
2111
+ apiKey: inputCloudOptions?.apiKey,
2112
+ accessToken: inputCloudOptions?.accessToken,
2113
+ hasApiKey: inputHasCloudApiKey,
2114
+ hasAccessToken: inputHasCloudAccessToken
2115
+ },
2116
+ {
2117
+ source: "env",
2118
+ apiKey: envApiKey,
2119
+ accessToken: envAccessTokenRaw,
2120
+ hasApiKey: envApiKey !== void 0,
2121
+ hasAccessToken: envAccessTokenRaw !== void 0
2122
+ },
2123
+ {
2124
+ source: "file",
2125
+ apiKey: fileCloudOptions?.apiKey,
2126
+ accessToken: fileCloudOptions?.accessToken,
2127
+ hasApiKey: fileHasCloudApiKey,
2128
+ hasAccessToken: fileHasCloudAccessToken
2129
+ }
2130
+ ],
2131
+ authScheme
2132
+ );
2133
+ const { apiKey, accessToken } = resolveCloudCredentialFields(selectedCredentialLayer);
1702
2134
  if (accessToken) {
1703
2135
  authScheme = "bearer";
1704
2136
  }
1705
2137
  resolved.cloud = {
1706
2138
  ...resolvedCloudRest,
1707
- ...inputHasCloudApiKey ? { apiKey: resolvedCloudApiKeyRaw } : apiKey ? { apiKey } : {},
1708
- ...inputHasCloudAccessToken ? { accessToken: resolvedCloudAccessTokenRaw } : accessToken ? { accessToken } : {},
2139
+ ...apiKey ? { apiKey } : selectedCredentialLayer?.hasApiKey && !accessToken ? { apiKey: selectedCredentialLayer.apiKey } : {},
2140
+ ...accessToken ? { accessToken } : selectedCredentialLayer?.hasAccessToken && !apiKey ? { accessToken: selectedCredentialLayer.accessToken } : {},
1709
2141
  authScheme,
1710
2142
  announce,
1711
2143
  ...browserProfile ? { browserProfile } : {}
@@ -2134,7 +2566,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2134
2566
  TRANSIENT_CONTEXT_RETRY_DELAY_MS,
2135
2567
  Math.max(0, deadline - Date.now())
2136
2568
  );
2137
- await sleep(retryDelay);
2569
+ await sleep2(retryDelay);
2138
2570
  }
2139
2571
  }
2140
2572
  }
@@ -2167,7 +2599,7 @@ var StealthCdpRuntime = class _StealthCdpRuntime {
2167
2599
  () => ({ kind: "resolved" }),
2168
2600
  (error) => ({ kind: "rejected", error })
2169
2601
  );
2170
- const timeoutPromise = sleep(
2602
+ const timeoutPromise = sleep2(
2171
2603
  timeout + FRAME_EVALUATE_GRACE_MS
2172
2604
  ).then(() => ({ kind: "timeout" }));
2173
2605
  const result = await Promise.race([
@@ -2309,14 +2741,14 @@ function isIgnorableFrameError(error) {
2309
2741
  const message = error.message;
2310
2742
  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");
2311
2743
  }
2312
- function sleep(ms) {
2744
+ function sleep2(ms) {
2313
2745
  return new Promise((resolve) => {
2314
2746
  setTimeout(resolve, ms);
2315
2747
  });
2316
2748
  }
2317
2749
 
2318
2750
  // src/storage/local.ts
2319
- var import_fs2 = __toESM(require("fs"), 1);
2751
+ var import_fs3 = __toESM(require("fs"), 1);
2320
2752
  var import_path4 = __toESM(require("path"), 1);
2321
2753
 
2322
2754
  // src/storage/registry.ts
@@ -2360,14 +2792,14 @@ var LocalSelectorStorage = class {
2360
2792
  return import_path4.default.join(this.getNamespaceDir(), this.getSelectorFileName(id));
2361
2793
  }
2362
2794
  ensureDirs() {
2363
- import_fs2.default.mkdirSync(this.getNamespaceDir(), { recursive: true });
2795
+ import_fs3.default.mkdirSync(this.getNamespaceDir(), { recursive: true });
2364
2796
  }
2365
2797
  loadRegistry() {
2366
2798
  this.ensureDirs();
2367
2799
  const file = this.getRegistryPath();
2368
- if (!import_fs2.default.existsSync(file)) return createEmptyRegistry(this.namespace);
2800
+ if (!import_fs3.default.existsSync(file)) return createEmptyRegistry(this.namespace);
2369
2801
  try {
2370
- const raw = import_fs2.default.readFileSync(file, "utf8");
2802
+ const raw = import_fs3.default.readFileSync(file, "utf8");
2371
2803
  return JSON.parse(raw);
2372
2804
  } catch (error) {
2373
2805
  const message = extractErrorMessage(
@@ -2384,16 +2816,16 @@ var LocalSelectorStorage = class {
2384
2816
  }
2385
2817
  saveRegistry(registry) {
2386
2818
  this.ensureDirs();
2387
- import_fs2.default.writeFileSync(
2819
+ import_fs3.default.writeFileSync(
2388
2820
  this.getRegistryPath(),
2389
2821
  JSON.stringify(registry, null, 2)
2390
2822
  );
2391
2823
  }
2392
2824
  readSelector(id) {
2393
2825
  const file = this.getSelectorPath(id);
2394
- if (!import_fs2.default.existsSync(file)) return null;
2826
+ if (!import_fs3.default.existsSync(file)) return null;
2395
2827
  try {
2396
- const raw = import_fs2.default.readFileSync(file, "utf8");
2828
+ const raw = import_fs3.default.readFileSync(file, "utf8");
2397
2829
  return JSON.parse(raw);
2398
2830
  } catch (error) {
2399
2831
  const message = extractErrorMessage(
@@ -2410,15 +2842,15 @@ var LocalSelectorStorage = class {
2410
2842
  }
2411
2843
  writeSelector(payload) {
2412
2844
  this.ensureDirs();
2413
- import_fs2.default.writeFileSync(
2845
+ import_fs3.default.writeFileSync(
2414
2846
  this.getSelectorPath(payload.id),
2415
2847
  JSON.stringify(payload, null, 2)
2416
2848
  );
2417
2849
  }
2418
2850
  clearNamespace() {
2419
2851
  const dir = this.getNamespaceDir();
2420
- if (!import_fs2.default.existsSync(dir)) return;
2421
- import_fs2.default.rmSync(dir, { recursive: true, force: true });
2852
+ if (!import_fs3.default.existsSync(dir)) return;
2853
+ import_fs3.default.rmSync(dir, { recursive: true, force: true });
2422
2854
  }
2423
2855
  };
2424
2856
 
@@ -6862,7 +7294,7 @@ var AdaptiveNetworkTracker = class {
6862
7294
  this.idleSince = 0;
6863
7295
  }
6864
7296
  const remaining = Math.max(1, options.deadline - now);
6865
- await sleep2(Math.min(NETWORK_POLL_MS, remaining));
7297
+ await sleep3(Math.min(NETWORK_POLL_MS, remaining));
6866
7298
  }
6867
7299
  }
6868
7300
  handleRequestStarted = (request) => {
@@ -6907,7 +7339,7 @@ var AdaptiveNetworkTracker = class {
6907
7339
  return false;
6908
7340
  }
6909
7341
  };
6910
- async function sleep2(ms) {
7342
+ async function sleep3(ms) {
6911
7343
  await new Promise((resolve) => {
6912
7344
  setTimeout(resolve, ms);
6913
7345
  });
@@ -8384,15 +8816,15 @@ function withTokenQuery(wsUrl, token) {
8384
8816
  }
8385
8817
 
8386
8818
  // src/cloud/local-cache-sync.ts
8387
- var import_fs3 = __toESM(require("fs"), 1);
8819
+ var import_fs4 = __toESM(require("fs"), 1);
8388
8820
  var import_path5 = __toESM(require("path"), 1);
8389
8821
  function collectLocalSelectorCacheEntries(storage, options = {}) {
8390
8822
  const debug = options.debug === true;
8391
8823
  const namespace = storage.getNamespace();
8392
8824
  const namespaceDir = storage.getNamespaceDir();
8393
- if (!import_fs3.default.existsSync(namespaceDir)) return [];
8825
+ if (!import_fs4.default.existsSync(namespaceDir)) return [];
8394
8826
  const entries = [];
8395
- const fileNames = import_fs3.default.readdirSync(namespaceDir);
8827
+ const fileNames = import_fs4.default.readdirSync(namespaceDir);
8396
8828
  for (const fileName of fileNames) {
8397
8829
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
8398
8830
  const filePath = import_path5.default.join(namespaceDir, fileName);
@@ -8423,7 +8855,7 @@ function collectLocalSelectorCacheEntries(storage, options = {}) {
8423
8855
  }
8424
8856
  function readSelectorFile(filePath, debug) {
8425
8857
  try {
8426
- const raw = import_fs3.default.readFileSync(filePath, "utf8");
8858
+ const raw = import_fs4.default.readFileSync(filePath, "utf8");
8427
8859
  return JSON.parse(raw);
8428
8860
  } catch (error) {
8429
8861
  const message = extractErrorMessage(
@@ -8518,13 +8950,13 @@ function dedupeNewest(entries) {
8518
8950
  }
8519
8951
 
8520
8952
  // src/cloud/cdp-client.ts
8521
- var import_playwright3 = require("playwright");
8953
+ var import_playwright2 = require("playwright");
8522
8954
  var CloudCdpClient = class {
8523
8955
  async connect(args) {
8524
8956
  const endpoint = withTokenQuery2(args.wsUrl, args.token);
8525
8957
  let browser;
8526
8958
  try {
8527
- browser = await import_playwright3.chromium.connectOverCDP(endpoint);
8959
+ browser = await import_playwright2.chromium.connectOverCDP(endpoint);
8528
8960
  } catch (error) {
8529
8961
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
8530
8962
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -10317,7 +10749,7 @@ async function executeAgentAction(page, action) {
10317
10749
  }
10318
10750
  case "wait": {
10319
10751
  const ms = numberOr(action.timeMs, action.time_ms, 1e3);
10320
- await sleep3(ms);
10752
+ await sleep4(ms);
10321
10753
  return;
10322
10754
  }
10323
10755
  case "goto": {
@@ -10482,7 +10914,7 @@ async function pressKeyCombo(page, combo) {
10482
10914
  }
10483
10915
  }
10484
10916
  }
10485
- function sleep3(ms) {
10917
+ function sleep4(ms) {
10486
10918
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10487
10919
  }
10488
10920
 
@@ -10513,7 +10945,7 @@ var OpensteerCuaAgentHandler = class {
10513
10945
  if (isMutatingAgentAction(action)) {
10514
10946
  this.onMutatingAction?.(action);
10515
10947
  }
10516
- await sleep4(this.config.waitBetweenActionsMs);
10948
+ await sleep5(this.config.waitBetweenActionsMs);
10517
10949
  });
10518
10950
  try {
10519
10951
  const result = await this.client.execute({
@@ -10575,7 +11007,7 @@ var OpensteerCuaAgentHandler = class {
10575
11007
  await this.cursorController.preview({ x, y }, "agent");
10576
11008
  }
10577
11009
  };
10578
- function sleep4(ms) {
11010
+ function sleep5(ms) {
10579
11011
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10580
11012
  }
10581
11013
 
@@ -11011,7 +11443,7 @@ var CursorController = class {
11011
11443
  for (const step of motion.points) {
11012
11444
  await this.renderer.move(step, this.style);
11013
11445
  if (motion.stepDelayMs > 0) {
11014
- await sleep5(motion.stepDelayMs);
11446
+ await sleep6(motion.stepDelayMs);
11015
11447
  }
11016
11448
  }
11017
11449
  if (shouldPulse(intent)) {
@@ -11169,7 +11601,7 @@ function clamp2(value, min, max) {
11169
11601
  function shouldPulse(intent) {
11170
11602
  return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
11171
11603
  }
11172
- function sleep5(ms) {
11604
+ function sleep6(ms) {
11173
11605
  return new Promise((resolve) => setTimeout(resolve, ms));
11174
11606
  }
11175
11607
 
@@ -11220,30 +11652,20 @@ var Opensteer = class _Opensteer {
11220
11652
  this.pool = new BrowserPool(resolved.browser || {});
11221
11653
  if (cloudSelection.cloud) {
11222
11654
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
11223
- const apiKey = cloudConfig?.apiKey?.trim();
11224
- const accessToken = cloudConfig?.accessToken?.trim();
11225
- if (apiKey && accessToken) {
11226
- throw new Error(
11227
- "Cloud mode cannot use both cloud.apiKey and cloud.accessToken. Set only one credential."
11228
- );
11229
- }
11230
- let credential = "";
11231
- let authScheme = cloudConfig?.authScheme ?? "api-key";
11232
- if (accessToken) {
11233
- credential = accessToken;
11234
- authScheme = "bearer";
11235
- } else if (apiKey) {
11236
- credential = apiKey;
11237
- }
11655
+ const credential = selectCloudCredential({
11656
+ apiKey: cloudConfig?.apiKey,
11657
+ accessToken: cloudConfig?.accessToken,
11658
+ authScheme: cloudConfig?.authScheme
11659
+ });
11238
11660
  if (!credential) {
11239
11661
  throw new Error(
11240
11662
  "Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
11241
11663
  );
11242
11664
  }
11243
11665
  this.cloud = createCloudRuntimeState(
11244
- credential,
11666
+ credential.token,
11245
11667
  cloudConfig?.baseUrl,
11246
- authScheme
11668
+ credential.authScheme
11247
11669
  );
11248
11670
  } else {
11249
11671
  this.cloud = null;
@@ -11564,9 +11986,11 @@ var Opensteer = class _Opensteer {
11564
11986
  }
11565
11987
  const session2 = await this.pool.launch({
11566
11988
  ...options,
11567
- connectUrl: options.connectUrl ?? this.config.browser?.connectUrl,
11568
- channel: options.channel ?? this.config.browser?.channel,
11569
- profileDir: options.profileDir ?? this.config.browser?.profileDir
11989
+ mode: options.mode ?? this.config.browser?.mode,
11990
+ cdpUrl: options.cdpUrl ?? this.config.browser?.cdpUrl,
11991
+ userDataDir: options.userDataDir ?? this.config.browser?.userDataDir,
11992
+ profileDirectory: options.profileDirectory ?? this.config.browser?.profileDirectory,
11993
+ executablePath: options.executablePath ?? this.config.browser?.executablePath
11570
11994
  });
11571
11995
  this.browser = session2.browser;
11572
11996
  this.contextRef = session2.context;
@@ -11597,6 +12021,32 @@ var Opensteer = class _Opensteer {
11597
12021
  instance2.snapshotCache = null;
11598
12022
  return instance2;
11599
12023
  }
12024
+ static listLocalProfiles(userDataDir) {
12025
+ return listLocalChromeProfiles(userDataDir);
12026
+ }
12027
+ static fromSystemChrome(browser = {}, config = {}) {
12028
+ const chromePaths = detectChromePaths();
12029
+ const executablePath = browser.executablePath ?? config.browser?.executablePath ?? chromePaths.executable ?? void 0;
12030
+ if (!executablePath) {
12031
+ throw new Error(
12032
+ "Chrome was not found. Pass executablePath explicitly or install Chrome in a supported location."
12033
+ );
12034
+ }
12035
+ const userDataDir = browser.userDataDir ?? config.browser?.userDataDir ?? chromePaths.defaultUserDataDir;
12036
+ const autoDetectedProfiles = listLocalChromeProfiles(userDataDir);
12037
+ const profileDirectory = browser.profileDirectory ?? config.browser?.profileDirectory ?? autoDetectedProfiles[0]?.directory ?? "Default";
12038
+ return new _Opensteer({
12039
+ ...config,
12040
+ browser: {
12041
+ ...config.browser || {},
12042
+ mode: "real",
12043
+ headless: browser.headless ?? config.browser?.headless ?? true,
12044
+ executablePath,
12045
+ userDataDir,
12046
+ profileDirectory
12047
+ }
12048
+ });
12049
+ }
11600
12050
  async close() {
11601
12051
  this.snapshotCache = null;
11602
12052
  if (this.cloud) {
@@ -14330,10 +14780,12 @@ function buildServerOpenConfig(options) {
14330
14780
  enabled: options.cursorEnabled
14331
14781
  },
14332
14782
  browser: {
14333
- headless: options.headless ?? false,
14334
- connectUrl: options.connectUrl,
14335
- channel: options.channel,
14336
- profileDir: options.profileDir
14783
+ headless: options.headless,
14784
+ mode: options.mode,
14785
+ cdpUrl: options.cdpUrl,
14786
+ userDataDir: options.userDataDir,
14787
+ profileDirectory: options.profileDirectory,
14788
+ executablePath: options.executablePath
14337
14789
  }
14338
14790
  };
14339
14791
  if (!options.cloudAuth) {
@@ -14385,6 +14837,19 @@ function normalizeAuthScheme(value) {
14385
14837
  );
14386
14838
  }
14387
14839
 
14840
+ // src/cli/open-browser-config.ts
14841
+ function resolveCliBrowserRequestConfig(options) {
14842
+ const mode = options.browser ?? (options.profileDirectory || options.userDataDir || options.executablePath ? "real" : void 0);
14843
+ return {
14844
+ mode,
14845
+ headless: options.headless ?? (mode === "real" ? true : void 0),
14846
+ cdpUrl: options.cdpUrl,
14847
+ profileDirectory: options.profileDirectory,
14848
+ userDataDir: options.userDataDir,
14849
+ executablePath: options.executablePath
14850
+ };
14851
+ }
14852
+
14388
14853
  // src/cli/server.ts
14389
14854
  var instance = null;
14390
14855
  var launchPromise = null;
@@ -14459,15 +14924,15 @@ var socketPath = getSocketPath(session);
14459
14924
  var pidPath = getPidPath(session);
14460
14925
  function cleanup() {
14461
14926
  try {
14462
- (0, import_fs4.unlinkSync)(socketPath);
14927
+ (0, import_fs5.unlinkSync)(socketPath);
14463
14928
  } catch {
14464
14929
  }
14465
14930
  try {
14466
- (0, import_fs4.unlinkSync)(pidPath);
14931
+ (0, import_fs5.unlinkSync)(pidPath);
14467
14932
  } catch {
14468
14933
  }
14469
14934
  try {
14470
- (0, import_fs4.unlinkSync)(getMetadataPath(session));
14935
+ (0, import_fs5.unlinkSync)(getMetadataPath(session));
14471
14936
  } catch {
14472
14937
  }
14473
14938
  }
@@ -14545,9 +15010,11 @@ async function handleRequest(request, socket) {
14545
15010
  try {
14546
15011
  const url = args.url;
14547
15012
  const headless = args.headless;
14548
- const connectUrl = args["connect-url"];
14549
- const channel = args.channel;
14550
- const profileDir = args["profile-dir"];
15013
+ const browser = args.browser === "real" || args.browser === "chromium" ? args.browser : args.browser === void 0 ? void 0 : null;
15014
+ const cdpUrl = args["cdp-url"];
15015
+ const profileDirectory = args.profile;
15016
+ const userDataDir = args["user-data-dir"];
15017
+ const executablePath = args["browser-path"];
14551
15018
  const cloudProfileId = typeof args["cloud-profile-id"] === "string" ? args["cloud-profile-id"].trim() : void 0;
14552
15019
  const cloudProfileReuseIfActive = typeof args["cloud-profile-reuse-if-active"] === "boolean" ? args["cloud-profile-reuse-if-active"] : void 0;
14553
15020
  const requestedCloudProfileBinding = normalizeCloudProfileBinding({
@@ -14562,6 +15029,21 @@ async function handleRequest(request, socket) {
14562
15029
  "--cloud-profile-reuse-if-active requires --cloud-profile-id."
14563
15030
  );
14564
15031
  }
15032
+ if (browser === null) {
15033
+ throw new Error(
15034
+ '--browser must be either "chromium" or "real".'
15035
+ );
15036
+ }
15037
+ if (browser === "chromium" && (profileDirectory || userDataDir || executablePath)) {
15038
+ throw new Error(
15039
+ "--profile, --user-data-dir, and --browser-path require --browser real."
15040
+ );
15041
+ }
15042
+ if (cdpUrl && browser === "real") {
15043
+ throw new Error(
15044
+ "--cdp-url cannot be combined with --browser real."
15045
+ );
15046
+ }
14565
15047
  const requestedCursor = normalizeCursorFlag(args.cursor);
14566
15048
  const requestedName = typeof args.name === "string" && args.name.trim().length > 0 ? sanitizeNamespace(args.name) : null;
14567
15049
  if (requestedCursor !== null) {
@@ -14603,26 +15085,45 @@ async function handleRequest(request, socket) {
14603
15085
  invalidateInstance();
14604
15086
  }
14605
15087
  }
15088
+ const requestedBrowserConfig = resolveCliBrowserRequestConfig({
15089
+ browser: browser ?? void 0,
15090
+ headless,
15091
+ cdpUrl,
15092
+ profileDirectory,
15093
+ userDataDir,
15094
+ executablePath
15095
+ });
14606
15096
  if (instance && !launchPromise) {
14607
15097
  assertCompatibleCloudProfileBinding(
14608
15098
  logicalSession,
14609
15099
  cloudProfileBinding,
14610
15100
  requestedCloudProfileBinding
14611
15101
  );
15102
+ const existingBrowserConfig = instance.getConfig().browser || {};
15103
+ const existingBrowserRecord = existingBrowserConfig;
15104
+ const mismatch = Object.entries(requestedBrowserConfig).find(
15105
+ ([key, value]) => value !== void 0 && existingBrowserRecord[key] !== value
15106
+ );
15107
+ if (mismatch) {
15108
+ const [key, value] = mismatch;
15109
+ throw new Error(
15110
+ `Session '${logicalSession}' is already bound to browser setting "${key}"=${JSON.stringify(existingBrowserRecord[key])}. Requested ${JSON.stringify(value)} does not match. Use the same browser flags for this session or start a different --session.`
15111
+ );
15112
+ }
14612
15113
  }
15114
+ let shouldLaunchInitialUrl = false;
14613
15115
  if (!instance) {
14614
15116
  instance = new Opensteer(
14615
15117
  buildServerOpenConfig({
14616
15118
  scopeDir,
14617
15119
  name: activeNamespace,
14618
15120
  cursorEnabled: effectiveCursorEnabled,
14619
- headless,
14620
- connectUrl,
14621
- channel,
14622
- profileDir,
15121
+ ...requestedBrowserConfig,
14623
15122
  cloudAuth: cloudAuthOverride
14624
15123
  })
14625
15124
  );
15125
+ const resolvedBrowserConfig = instance.getConfig().browser || {};
15126
+ shouldLaunchInitialUrl = Boolean(url) && resolvedBrowserConfig.mode === "real" && !resolvedBrowserConfig.cdpUrl;
14626
15127
  const nextCloudProfileBinding = resolveSessionCloudProfileBinding(
14627
15128
  instance.getConfig(),
14628
15129
  requestedCloudProfileBinding
@@ -14634,12 +15135,13 @@ async function handleRequest(request, socket) {
14634
15135
  );
14635
15136
  }
14636
15137
  launchPromise = instance.launch({
14637
- headless: headless ?? false,
15138
+ initialUrl: shouldLaunchInitialUrl ? url : void 0,
15139
+ ...requestedBrowserConfig,
14638
15140
  cloudBrowserProfile: cloudProfileId ? {
14639
15141
  profileId: cloudProfileId,
14640
15142
  reuseIfActive: cloudProfileReuseIfActive
14641
15143
  } : void 0,
14642
- timeout: connectUrl ? 12e4 : 3e4
15144
+ timeout: cdpUrl ? 12e4 : 3e4
14643
15145
  });
14644
15146
  try {
14645
15147
  await launchPromise;
@@ -14657,7 +15159,7 @@ async function handleRequest(request, socket) {
14657
15159
  } else if (requestedCursor !== null) {
14658
15160
  instance.setCursorEnabled(requestedCursor);
14659
15161
  }
14660
- if (url) {
15162
+ if (url && !shouldLaunchInitialUrl) {
14661
15163
  await instance.goto(url);
14662
15164
  }
14663
15165
  sendResponse(socket, {
@@ -14787,8 +15289,8 @@ async function handleRequest(request, socket) {
14787
15289
  );
14788
15290
  }
14789
15291
  }
14790
- if ((0, import_fs4.existsSync)(socketPath)) {
14791
- (0, import_fs4.unlinkSync)(socketPath);
15292
+ if ((0, import_fs5.existsSync)(socketPath)) {
15293
+ (0, import_fs5.unlinkSync)(socketPath);
14792
15294
  }
14793
15295
  var server = (0, import_net.createServer)((socket) => {
14794
15296
  let buffer = "";
@@ -14818,7 +15320,7 @@ var server = (0, import_net.createServer)((socket) => {
14818
15320
  });
14819
15321
  });
14820
15322
  server.listen(socketPath, () => {
14821
- (0, import_fs4.writeFileSync)(pidPath, String(process.pid));
15323
+ (0, import_fs5.writeFileSync)(pidPath, String(process.pid));
14822
15324
  if (process.send) {
14823
15325
  process.send("ready");
14824
15326
  }