opensteer 0.6.0 → 0.6.2

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.
@@ -424,7 +424,7 @@ var import_fs4 = require("fs");
424
424
  var import_crypto = require("crypto");
425
425
 
426
426
  // src/browser/pool.ts
427
- var import_playwright = require("playwright");
427
+ var import_playwright2 = require("playwright");
428
428
 
429
429
  // src/browser/cdp-proxy.ts
430
430
  var import_ws = __toESM(require("ws"), 1);
@@ -763,6 +763,19 @@ function errorMessage(error) {
763
763
  return error instanceof Error ? error.message : String(error);
764
764
  }
765
765
 
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
+
766
779
  // src/browser/chrome.ts
767
780
  var import_os = require("os");
768
781
  var import_path = require("path");
@@ -773,9 +786,61 @@ function expandHome(p) {
773
786
  return p;
774
787
  }
775
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;
796
+ }
797
+ }
798
+ function fileExists(filePath) {
799
+ 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;
810
+ }
811
+ }
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
+ }
839
+
776
840
  // src/browser/pool.ts
777
841
  var BrowserPool = class {
778
842
  browser = null;
843
+ persistentContext = null;
779
844
  cdpProxy = null;
780
845
  defaults;
781
846
  constructor(defaults = {}) {
@@ -791,16 +856,23 @@ var BrowserPool = class {
791
856
  if (connectUrl) {
792
857
  return this.connectToRunning(connectUrl, options.timeout);
793
858
  }
794
- if (channel || profileDir) {
795
- return this.launchWithProfile(options, channel, profileDir);
859
+ if (profileDir) {
860
+ return this.launchPersistentProfile(options, channel, profileDir);
861
+ }
862
+ if (channel) {
863
+ return this.launchWithChannel(options, channel);
796
864
  }
797
865
  return this.launchSandbox(options);
798
866
  }
799
867
  async close() {
800
868
  const browser = this.browser;
869
+ const persistentContext = this.persistentContext;
801
870
  this.browser = null;
871
+ this.persistentContext = null;
802
872
  try {
803
- if (browser) {
873
+ if (persistentContext) {
874
+ await persistentContext.close();
875
+ } else if (browser) {
804
876
  await browser.close();
805
877
  }
806
878
  } finally {
@@ -822,10 +894,11 @@ var BrowserPool = class {
822
894
  const target = targets[0];
823
895
  this.cdpProxy = new CDPProxy(browserWsUrl, target.id);
824
896
  const proxyWsUrl = await this.cdpProxy.start();
825
- browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
897
+ browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
826
898
  timeout: timeout ?? 3e4
827
899
  });
828
900
  this.browser = browser;
901
+ this.persistentContext = null;
829
902
  const contexts = browser.contexts();
830
903
  if (contexts.length === 0) {
831
904
  throw new Error(
@@ -841,46 +914,66 @@ var BrowserPool = class {
841
914
  await browser.close().catch(() => void 0);
842
915
  }
843
916
  this.browser = null;
917
+ this.persistentContext = null;
844
918
  this.cdpProxy?.close();
845
919
  this.cdpProxy = null;
846
920
  throw error;
847
921
  }
848
922
  }
849
- async launchWithProfile(options, channel, profileDir) {
923
+ async launchPersistentProfile(options, channel, profileDir) {
850
924
  const args = [];
851
- if (profileDir) {
852
- args.push(`--user-data-dir=${expandHome(profileDir)}`);
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.");
853
945
  }
854
- const browser = await import_playwright.chromium.launch({
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({
855
954
  channel,
856
955
  headless: options.headless ?? this.defaults.headless,
857
956
  executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
858
957
  slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
859
- args
958
+ timeout: options.timeout
860
959
  });
861
960
  this.browser = browser;
862
- const contexts = browser.contexts();
863
- let context;
864
- let page;
865
- if (contexts.length > 0) {
866
- context = contexts[0];
867
- const pages = context.pages();
868
- page = pages.length > 0 ? pages[0] : await context.newPage();
869
- } else {
870
- context = await browser.newContext(options.context || {});
871
- page = await context.newPage();
872
- }
961
+ this.persistentContext = null;
962
+ const context = await browser.newContext(options.context || {});
963
+ const page = await context.newPage();
873
964
  return { browser, context, page, isExternal: false };
874
965
  }
875
966
  async launchSandbox(options) {
876
- const browser = await import_playwright.chromium.launch({
967
+ const browser = await import_playwright2.chromium.launch({
877
968
  headless: options.headless ?? this.defaults.headless,
878
969
  executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
879
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0
970
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
971
+ timeout: options.timeout
880
972
  });
881
973
  const context = await browser.newContext(options.context || {});
882
974
  const page = await context.newPage();
883
975
  this.browser = browser;
976
+ this.persistentContext = null;
884
977
  return { browser, context, page, isExternal: false };
885
978
  }
886
979
  };
@@ -1162,6 +1255,10 @@ var DEFAULT_CONFIG = {
1162
1255
  storage: {
1163
1256
  rootDir: process.cwd()
1164
1257
  },
1258
+ cursor: {
1259
+ enabled: false,
1260
+ profile: "snappy"
1261
+ },
1165
1262
  model: "gpt-5.1",
1166
1263
  debug: false
1167
1264
  };
@@ -1215,7 +1312,7 @@ function loadDotenvValues(rootDir, baseEnv, options = {}) {
1215
1312
  return values;
1216
1313
  }
1217
1314
  function resolveEnv(rootDir, options = {}) {
1218
- const baseEnv = process.env;
1315
+ const baseEnv = options.baseEnv ?? process.env;
1219
1316
  const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
1220
1317
  return {
1221
1318
  ...dotenvValues,
@@ -1364,6 +1461,11 @@ function resolveOpensteerApiKey(env) {
1364
1461
  if (!value) return void 0;
1365
1462
  return value;
1366
1463
  }
1464
+ function resolveOpensteerAccessToken(env) {
1465
+ const value = env.OPENSTEER_ACCESS_TOKEN?.trim();
1466
+ if (!value) return void 0;
1467
+ return value;
1468
+ }
1367
1469
  function resolveOpensteerBaseUrl(env) {
1368
1470
  const value = env.OPENSTEER_BASE_URL?.trim();
1369
1471
  if (!value) return void 0;
@@ -1372,12 +1474,71 @@ function resolveOpensteerBaseUrl(env) {
1372
1474
  function resolveOpensteerAuthScheme(env) {
1373
1475
  return parseAuthScheme(env.OPENSTEER_AUTH_SCHEME, "OPENSTEER_AUTH_SCHEME");
1374
1476
  }
1477
+ function resolveOpensteerCloudProfileId(env) {
1478
+ const value = env.OPENSTEER_CLOUD_PROFILE_ID?.trim();
1479
+ if (!value) return void 0;
1480
+ return value;
1481
+ }
1482
+ function resolveOpensteerCloudProfileReuseIfActive(env) {
1483
+ return parseBool(env.OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE);
1484
+ }
1485
+ function parseCloudBrowserProfileReuseIfActive(value) {
1486
+ if (value == null) return void 0;
1487
+ if (typeof value !== "boolean") {
1488
+ throw new Error(
1489
+ `Invalid cloud.browserProfile.reuseIfActive value "${String(
1490
+ value
1491
+ )}". Use true or false.`
1492
+ );
1493
+ }
1494
+ return value;
1495
+ }
1496
+ function normalizeCloudBrowserProfileOptions(value, source) {
1497
+ if (value == null) {
1498
+ return void 0;
1499
+ }
1500
+ if (typeof value !== "object" || Array.isArray(value)) {
1501
+ throw new Error(
1502
+ `Invalid ${source} value "${String(value)}". Use an object with profileId and optional reuseIfActive.`
1503
+ );
1504
+ }
1505
+ const record = value;
1506
+ const rawProfileId = record.profileId;
1507
+ if (typeof rawProfileId !== "string" || !rawProfileId.trim()) {
1508
+ throw new Error(
1509
+ `${source}.profileId must be a non-empty string when browserProfile is provided.`
1510
+ );
1511
+ }
1512
+ return {
1513
+ profileId: rawProfileId.trim(),
1514
+ reuseIfActive: parseCloudBrowserProfileReuseIfActive(record.reuseIfActive)
1515
+ };
1516
+ }
1517
+ function resolveEnvCloudBrowserProfile(profileId, reuseIfActive) {
1518
+ if (reuseIfActive !== void 0 && !profileId) {
1519
+ throw new Error(
1520
+ "OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE requires OPENSTEER_CLOUD_PROFILE_ID."
1521
+ );
1522
+ }
1523
+ if (!profileId) {
1524
+ return void 0;
1525
+ }
1526
+ return {
1527
+ profileId,
1528
+ reuseIfActive
1529
+ };
1530
+ }
1375
1531
  function normalizeCloudOptions(value) {
1376
1532
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1377
1533
  return void 0;
1378
1534
  }
1379
1535
  return value;
1380
1536
  }
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
+ }
1381
1542
  function parseCloudEnabled(value, source) {
1382
1543
  if (value == null) return void 0;
1383
1544
  if (typeof value === "boolean") return value;
@@ -1406,8 +1567,8 @@ function resolveCloudSelection(config, env = process.env) {
1406
1567
  source: "default"
1407
1568
  };
1408
1569
  }
1409
- function resolveConfigWithEnv(input = {}) {
1410
- const processEnv = process.env;
1570
+ function resolveConfigWithEnv(input = {}, options = {}) {
1571
+ const processEnv = options.env ?? process.env;
1411
1572
  const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
1412
1573
  const initialRootDir = input.storage?.rootDir ?? process.cwd();
1413
1574
  const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
@@ -1425,7 +1586,8 @@ function resolveConfigWithEnv(input = {}) {
1425
1586
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1426
1587
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1427
1588
  const env = resolveEnv(envRootDir, {
1428
- debug: debugHint
1589
+ debug: debugHint,
1590
+ baseEnv: processEnv
1429
1591
  });
1430
1592
  if (env.OPENSTEER_AI_MODEL) {
1431
1593
  throw new Error(
@@ -1446,6 +1608,9 @@ function resolveConfigWithEnv(input = {}) {
1446
1608
  channel: env.OPENSTEER_CHANNEL || void 0,
1447
1609
  profileDir: env.OPENSTEER_PROFILE_DIR || void 0
1448
1610
  },
1611
+ cursor: {
1612
+ enabled: parseBool(env.OPENSTEER_CURSOR)
1613
+ },
1449
1614
  model: env.OPENSTEER_MODEL || void 0,
1450
1615
  debug: parseBool(env.OPENSTEER_DEBUG)
1451
1616
  };
@@ -1453,13 +1618,27 @@ function resolveConfigWithEnv(input = {}) {
1453
1618
  const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
1454
1619
  const resolved = mergeDeep(mergedWithEnv, input);
1455
1620
  const envApiKey = resolveOpensteerApiKey(env);
1621
+ const envAccessTokenRaw = resolveOpensteerAccessToken(env);
1456
1622
  const envBaseUrl = resolveOpensteerBaseUrl(env);
1457
1623
  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
+ const envCloudProfileId = resolveOpensteerCloudProfileId(env);
1632
+ const envCloudProfileReuseIfActive = resolveOpensteerCloudProfileReuseIfActive(env);
1458
1633
  const envCloudAnnounce = parseCloudAnnounce(
1459
1634
  env.OPENSTEER_REMOTE_ANNOUNCE,
1460
1635
  "OPENSTEER_REMOTE_ANNOUNCE"
1461
1636
  );
1462
1637
  const inputCloudOptions = normalizeCloudOptions(input.cloud);
1638
+ const inputCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1639
+ inputCloudOptions?.browserProfile,
1640
+ "cloud.browserProfile"
1641
+ );
1463
1642
  const inputAuthScheme = parseAuthScheme(
1464
1643
  inputCloudOptions?.authScheme,
1465
1644
  "cloud.authScheme"
@@ -1471,26 +1650,65 @@ function resolveConfigWithEnv(input = {}) {
1471
1650
  const inputHasCloudApiKey = Boolean(
1472
1651
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "apiKey")
1473
1652
  );
1653
+ const inputHasCloudAccessToken = Boolean(
1654
+ inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "accessToken")
1655
+ );
1474
1656
  const inputHasCloudBaseUrl = Boolean(
1475
1657
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
1476
1658
  );
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
+ }
1477
1664
  const cloudSelection = resolveCloudSelection({
1478
1665
  cloud: resolved.cloud
1479
1666
  }, env);
1480
1667
  if (cloudSelection.cloud) {
1481
1668
  const resolvedCloud = normalizeCloudOptions(resolved.cloud) ?? {};
1482
- const authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1669
+ const {
1670
+ apiKey: resolvedCloudApiKeyRaw,
1671
+ accessToken: resolvedCloudAccessTokenRaw,
1672
+ ...resolvedCloudRest
1673
+ } = 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
+ const resolvedCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1680
+ resolvedCloud.browserProfile,
1681
+ "resolved.cloud.browserProfile"
1682
+ );
1683
+ const envCloudBrowserProfile = resolveEnvCloudBrowserProfile(
1684
+ envCloudProfileId,
1685
+ envCloudProfileReuseIfActive
1686
+ );
1687
+ const browserProfile = inputCloudBrowserProfile ?? envCloudBrowserProfile ?? resolvedCloudBrowserProfile;
1688
+ let authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1483
1689
  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
+ }
1702
+ if (accessToken) {
1703
+ authScheme = "bearer";
1704
+ }
1484
1705
  resolved.cloud = {
1485
- ...resolvedCloud,
1706
+ ...resolvedCloudRest,
1707
+ ...inputHasCloudApiKey ? { apiKey: resolvedCloudApiKeyRaw } : apiKey ? { apiKey } : {},
1708
+ ...inputHasCloudAccessToken ? { accessToken: resolvedCloudAccessTokenRaw } : accessToken ? { accessToken } : {},
1486
1709
  authScheme,
1487
- announce
1488
- };
1489
- }
1490
- if (envApiKey && cloudSelection.cloud && !inputHasCloudApiKey) {
1491
- resolved.cloud = {
1492
- ...normalizeCloudOptions(resolved.cloud) ?? {},
1493
- apiKey: envApiKey
1710
+ announce,
1711
+ ...browserProfile ? { browserProfile } : {}
1494
1712
  };
1495
1713
  }
1496
1714
  if (envBaseUrl && cloudSelection.cloud && !inputHasCloudBaseUrl) {
@@ -4883,6 +5101,16 @@ async function probeActionabilityState(element) {
4883
5101
 
4884
5102
  // src/extract-value-normalization.ts
4885
5103
  var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
5104
+ var IFRAME_URL_ATTRIBUTES = /* @__PURE__ */ new Set([
5105
+ "href",
5106
+ "src",
5107
+ "srcset",
5108
+ "imagesrcset",
5109
+ "action",
5110
+ "formaction",
5111
+ "poster",
5112
+ "ping"
5113
+ ]);
4886
5114
  function normalizeExtractedValue(raw, attribute) {
4887
5115
  if (raw == null) return null;
4888
5116
  const rawText = String(raw);
@@ -4898,6 +5126,19 @@ function normalizeExtractedValue(raw, attribute) {
4898
5126
  const text = rawText.replace(/\s+/g, " ").trim();
4899
5127
  return text || null;
4900
5128
  }
5129
+ function resolveExtractedValueInContext(normalizedValue, options) {
5130
+ if (normalizedValue == null) return null;
5131
+ const normalizedAttribute = String(options.attribute || "").trim().toLowerCase();
5132
+ if (!options.insideIframe) return normalizedValue;
5133
+ if (!IFRAME_URL_ATTRIBUTES.has(normalizedAttribute)) return normalizedValue;
5134
+ const baseURI = String(options.baseURI || "").trim();
5135
+ if (!baseURI) return normalizedValue;
5136
+ try {
5137
+ return new URL(normalizedValue, baseURI).href;
5138
+ } catch {
5139
+ return normalizedValue;
5140
+ }
5141
+ }
4901
5142
  function pickSingleListAttributeValue(attribute, raw) {
4902
5143
  if (attribute === "ping") {
4903
5144
  const firstUrl = raw.trim().split(/\s+/)[0] || "";
@@ -5053,6 +5294,36 @@ function readDescriptorToken(value, index) {
5053
5294
  };
5054
5295
  }
5055
5296
 
5297
+ // src/extract-value-reader.ts
5298
+ async function readExtractedValueFromHandle(element, options) {
5299
+ const insideIframe = await isElementInsideIframe(element);
5300
+ const payload = await element.evaluate(
5301
+ (target, browserOptions) => {
5302
+ const ownerDocument = target.ownerDocument;
5303
+ return {
5304
+ raw: browserOptions.attribute ? target.getAttribute(browserOptions.attribute) : target.textContent,
5305
+ baseURI: ownerDocument?.baseURI || null
5306
+ };
5307
+ },
5308
+ {
5309
+ attribute: options.attribute
5310
+ }
5311
+ );
5312
+ const normalizedValue = normalizeExtractedValue(
5313
+ payload.raw,
5314
+ options.attribute
5315
+ );
5316
+ return resolveExtractedValueInContext(normalizedValue, {
5317
+ attribute: options.attribute,
5318
+ baseURI: payload.baseURI,
5319
+ insideIframe
5320
+ });
5321
+ }
5322
+ async function isElementInsideIframe(element) {
5323
+ const ownerFrame = await element.ownerFrame();
5324
+ return !!ownerFrame?.parentFrame();
5325
+ }
5326
+
5056
5327
  // src/html/counter-runtime.ts
5057
5328
  var CounterResolutionError = class extends Error {
5058
5329
  code;
@@ -5075,13 +5346,14 @@ async function resolveCounterElement(page, counter) {
5075
5346
  if (entry.count > 1) {
5076
5347
  throw buildCounterAmbiguousError(counter);
5077
5348
  }
5078
- const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
5079
- const element = handle.asElement();
5080
- if (!element) {
5081
- await handle.dispose();
5349
+ const resolution = await resolveCounterElementInFrame(entry.frame, normalized);
5350
+ if (resolution.status === "ambiguous") {
5351
+ throw buildCounterAmbiguousError(counter);
5352
+ }
5353
+ if (resolution.status === "missing") {
5082
5354
  throw buildCounterNotFoundError(counter);
5083
5355
  }
5084
- return element;
5356
+ return resolution.element;
5085
5357
  }
5086
5358
  async function resolveCountersBatch(page, requests) {
5087
5359
  const out = {};
@@ -5095,41 +5367,57 @@ async function resolveCountersBatch(page, requests) {
5095
5367
  }
5096
5368
  }
5097
5369
  const valueCache = /* @__PURE__ */ new Map();
5098
- for (const request of requests) {
5099
- const normalized = normalizeCounter(request.counter);
5100
- if (normalized == null) {
5101
- out[request.key] = null;
5102
- continue;
5103
- }
5104
- const entry = scan.get(normalized);
5105
- if (!entry || entry.count <= 0 || !entry.frame) {
5106
- out[request.key] = null;
5107
- continue;
5108
- }
5109
- const cacheKey = `${normalized}:${request.attribute || ""}`;
5110
- if (valueCache.has(cacheKey)) {
5111
- out[request.key] = valueCache.get(cacheKey);
5112
- continue;
5113
- }
5114
- const read = await readCounterValueInFrame(
5115
- entry.frame,
5116
- normalized,
5117
- request.attribute
5118
- );
5119
- if (read.status === "ambiguous") {
5120
- throw buildCounterAmbiguousError(normalized);
5121
- }
5122
- if (read.status === "missing") {
5123
- valueCache.set(cacheKey, null);
5124
- out[request.key] = null;
5125
- continue;
5370
+ const elementCache = /* @__PURE__ */ new Map();
5371
+ try {
5372
+ for (const request of requests) {
5373
+ const normalized = normalizeCounter(request.counter);
5374
+ if (normalized == null) {
5375
+ out[request.key] = null;
5376
+ continue;
5377
+ }
5378
+ const entry = scan.get(normalized);
5379
+ if (!entry || entry.count <= 0 || !entry.frame) {
5380
+ out[request.key] = null;
5381
+ continue;
5382
+ }
5383
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
5384
+ if (valueCache.has(cacheKey)) {
5385
+ out[request.key] = valueCache.get(cacheKey);
5386
+ continue;
5387
+ }
5388
+ if (!elementCache.has(normalized)) {
5389
+ elementCache.set(
5390
+ normalized,
5391
+ await resolveCounterElementInFrame(entry.frame, normalized)
5392
+ );
5393
+ }
5394
+ const resolution = elementCache.get(normalized);
5395
+ if (resolution.status === "ambiguous") {
5396
+ throw buildCounterAmbiguousError(normalized);
5397
+ }
5398
+ if (resolution.status === "missing") {
5399
+ valueCache.set(cacheKey, null);
5400
+ out[request.key] = null;
5401
+ continue;
5402
+ }
5403
+ const value = await readCounterValueFromElement(
5404
+ resolution.element,
5405
+ request.attribute
5406
+ );
5407
+ if (value.status === "missing") {
5408
+ await resolution.element.dispose();
5409
+ elementCache.set(normalized, {
5410
+ status: "missing"
5411
+ });
5412
+ valueCache.set(cacheKey, null);
5413
+ out[request.key] = null;
5414
+ continue;
5415
+ }
5416
+ valueCache.set(cacheKey, value.value);
5417
+ out[request.key] = value.value;
5126
5418
  }
5127
- const normalizedValue = normalizeExtractedValue(
5128
- read.value ?? null,
5129
- request.attribute
5130
- );
5131
- valueCache.set(cacheKey, normalizedValue);
5132
- out[request.key] = normalizedValue;
5419
+ } finally {
5420
+ await disposeResolvedCounterElements(elementCache.values());
5133
5421
  }
5134
5422
  return out;
5135
5423
  }
@@ -5201,73 +5489,79 @@ async function scanCounterOccurrences(page, counters) {
5201
5489
  }
5202
5490
  return out;
5203
5491
  }
5204
- async function resolveUniqueHandleInFrame(frame, counter) {
5205
- return frame.evaluateHandle((targetCounter) => {
5206
- const matches = [];
5207
- const walk = (root) => {
5208
- const children = Array.from(root.children);
5209
- for (const child of children) {
5210
- if (child.getAttribute("c") === targetCounter) {
5211
- matches.push(child);
5212
- }
5213
- walk(child);
5214
- if (child.shadowRoot) {
5215
- walk(child.shadowRoot);
5492
+ async function resolveCounterElementInFrame(frame, counter) {
5493
+ try {
5494
+ const handle = await frame.evaluateHandle((targetCounter) => {
5495
+ const matches = [];
5496
+ const walk = (root) => {
5497
+ const children = Array.from(root.children);
5498
+ for (const child of children) {
5499
+ if (child.getAttribute("c") === targetCounter) {
5500
+ matches.push(child);
5501
+ }
5502
+ walk(child);
5503
+ if (child.shadowRoot) {
5504
+ walk(child.shadowRoot);
5505
+ }
5216
5506
  }
5507
+ };
5508
+ walk(document);
5509
+ if (!matches.length) {
5510
+ return "missing";
5217
5511
  }
5218
- };
5219
- walk(document);
5220
- if (matches.length !== 1) {
5221
- return null;
5512
+ if (matches.length > 1) {
5513
+ return "ambiguous";
5514
+ }
5515
+ return matches[0];
5516
+ }, String(counter));
5517
+ const element = handle.asElement();
5518
+ if (element) {
5519
+ return {
5520
+ status: "resolved",
5521
+ element
5522
+ };
5523
+ }
5524
+ const status = await handle.jsonValue();
5525
+ await handle.dispose();
5526
+ return status === "ambiguous" ? { status: "ambiguous" } : { status: "missing" };
5527
+ } catch (error) {
5528
+ if (isRecoverableCounterReadRace(error)) {
5529
+ return {
5530
+ status: "missing"
5531
+ };
5222
5532
  }
5223
- return matches[0];
5224
- }, String(counter));
5533
+ throw error;
5534
+ }
5225
5535
  }
5226
- async function readCounterValueInFrame(frame, counter, attribute) {
5536
+ async function readCounterValueFromElement(element, attribute) {
5227
5537
  try {
5228
- return await frame.evaluate(
5229
- ({ targetCounter, attribute: attribute2 }) => {
5230
- const matches = [];
5231
- const walk = (root) => {
5232
- const children = Array.from(root.children);
5233
- for (const child of children) {
5234
- if (child.getAttribute("c") === targetCounter) {
5235
- matches.push(child);
5236
- }
5237
- walk(child);
5238
- if (child.shadowRoot) {
5239
- walk(child.shadowRoot);
5240
- }
5241
- }
5242
- };
5243
- walk(document);
5244
- if (!matches.length) {
5245
- return {
5246
- status: "missing"
5247
- };
5248
- }
5249
- if (matches.length > 1) {
5250
- return {
5251
- status: "ambiguous"
5252
- };
5253
- }
5254
- const target = matches[0];
5255
- const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
5256
- return {
5257
- status: "ok",
5258
- value
5259
- };
5260
- },
5261
- {
5262
- targetCounter: String(counter),
5263
- attribute
5264
- }
5265
- );
5266
- } catch {
5267
5538
  return {
5268
- status: "missing"
5539
+ status: "ok",
5540
+ value: await readExtractedValueFromHandle(element, {
5541
+ attribute
5542
+ })
5269
5543
  };
5544
+ } catch (error) {
5545
+ if (isRecoverableCounterReadRace(error)) {
5546
+ return {
5547
+ status: "missing"
5548
+ };
5549
+ }
5550
+ throw error;
5551
+ }
5552
+ }
5553
+ async function disposeResolvedCounterElements(resolutions) {
5554
+ const disposals = [];
5555
+ for (const resolution of resolutions) {
5556
+ if (resolution.status !== "resolved") continue;
5557
+ disposals.push(resolution.element.dispose());
5270
5558
  }
5559
+ await Promise.all(disposals);
5560
+ }
5561
+ function isRecoverableCounterReadRace(error) {
5562
+ if (!(error instanceof Error)) return false;
5563
+ const message = error.message;
5564
+ return message.includes("Execution context was destroyed") || message.includes("Cannot find context with specified id") || message.includes("Cannot find execution context") || message.includes("Frame was detached") || message.includes("Element is not attached to the DOM") || message.includes("Element is detached");
5271
5565
  }
5272
5566
  function buildCounterNotFoundError(counter) {
5273
5567
  return new CounterResolutionError(
@@ -5883,17 +6177,6 @@ async function performSelect(page, path5, options) {
5883
6177
  }
5884
6178
 
5885
6179
  // src/actions/extract.ts
5886
- async function readFieldValueFromHandle(element, options) {
5887
- const raw = await element.evaluate(
5888
- (target, payload) => {
5889
- return payload.attribute ? target.getAttribute(payload.attribute) : target.textContent;
5890
- },
5891
- {
5892
- attribute: options.attribute
5893
- }
5894
- );
5895
- return normalizeExtractedValue(raw, options.attribute);
5896
- }
5897
6180
  async function extractWithPaths(page, fields) {
5898
6181
  const result = {};
5899
6182
  for (const field of fields) {
@@ -5905,7 +6188,7 @@ async function extractWithPaths(page, fields) {
5905
6188
  continue;
5906
6189
  }
5907
6190
  try {
5908
- result[field.key] = await readFieldValueFromHandle(
6191
+ result[field.key] = await readExtractedValueFromHandle(
5909
6192
  resolved.element,
5910
6193
  {
5911
6194
  attribute: field.attribute
@@ -5941,7 +6224,7 @@ async function extractArrayRowsWithPaths(page, array) {
5941
6224
  field.candidates
5942
6225
  ) : item;
5943
6226
  try {
5944
- const value = target ? await readFieldValueFromHandle(target, {
6227
+ const value = target ? await readExtractedValueFromHandle(target, {
5945
6228
  attribute: field.attribute
5946
6229
  }) : null;
5947
6230
  if (key) {
@@ -6253,7 +6536,7 @@ async function closeTab(context, activePage, index) {
6253
6536
  }
6254
6537
 
6255
6538
  // src/actions/cookies.ts
6256
- var import_promises = require("fs/promises");
6539
+ var import_promises2 = require("fs/promises");
6257
6540
  async function getCookies(context, url) {
6258
6541
  return context.cookies(url ? [url] : void 0);
6259
6542
  }
@@ -6265,10 +6548,10 @@ async function clearCookies(context) {
6265
6548
  }
6266
6549
  async function exportCookies(context, filePath, url) {
6267
6550
  const cookies = await context.cookies(url ? [url] : void 0);
6268
- await (0, import_promises.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
6551
+ await (0, import_promises2.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
6269
6552
  }
6270
6553
  async function importCookies(context, filePath) {
6271
- const raw = await (0, import_promises.readFile)(filePath, "utf-8");
6554
+ const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
6272
6555
  const cookies = JSON.parse(raw);
6273
6556
  await context.addCookies(cookies);
6274
6557
  }
@@ -8235,13 +8518,13 @@ function dedupeNewest(entries) {
8235
8518
  }
8236
8519
 
8237
8520
  // src/cloud/cdp-client.ts
8238
- var import_playwright2 = require("playwright");
8521
+ var import_playwright3 = require("playwright");
8239
8522
  var CloudCdpClient = class {
8240
8523
  async connect(args) {
8241
8524
  const endpoint = withTokenQuery2(args.wsUrl, args.token);
8242
8525
  let browser;
8243
8526
  try {
8244
- browser = await import_playwright2.chromium.connectOverCDP(endpoint);
8527
+ browser = await import_playwright3.chromium.connectOverCDP(endpoint);
8245
8528
  } catch (error) {
8246
8529
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
8247
8530
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -8296,31 +8579,72 @@ function withTokenQuery2(wsUrl, token) {
8296
8579
  return url.toString();
8297
8580
  }
8298
8581
 
8299
- // src/cloud/session-client.ts
8300
- var CACHE_IMPORT_BATCH_SIZE = 200;
8301
- var CloudSessionClient = class {
8302
- baseUrl;
8303
- key;
8304
- authScheme;
8305
- constructor(baseUrl, key, authScheme = "api-key") {
8306
- this.baseUrl = normalizeBaseUrl(baseUrl);
8307
- this.key = key;
8308
- this.authScheme = authScheme;
8582
+ // src/utils/strip-trailing-slashes.ts
8583
+ function stripTrailingSlashes(value) {
8584
+ let end = value.length;
8585
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
8586
+ end -= 1;
8309
8587
  }
8310
- async create(request) {
8311
- const response = await fetch(`${this.baseUrl}/sessions`, {
8312
- method: "POST",
8313
- headers: {
8314
- "content-type": "application/json",
8315
- ...this.authHeaders()
8316
- },
8317
- body: JSON.stringify(request)
8318
- });
8319
- if (!response.ok) {
8320
- throw await parseHttpError(response);
8321
- }
8322
- let body;
8323
- try {
8588
+ return end === value.length ? value : value.slice(0, end);
8589
+ }
8590
+
8591
+ // src/cloud/http-client.ts
8592
+ function normalizeCloudBaseUrl(baseUrl) {
8593
+ return stripTrailingSlashes(baseUrl);
8594
+ }
8595
+ function cloudAuthHeaders(key, authScheme) {
8596
+ if (authScheme === "bearer") {
8597
+ return {
8598
+ authorization: `Bearer ${key}`
8599
+ };
8600
+ }
8601
+ return {
8602
+ "x-api-key": key
8603
+ };
8604
+ }
8605
+ async function parseCloudHttpError(response) {
8606
+ let body = null;
8607
+ try {
8608
+ body = await response.json();
8609
+ } catch {
8610
+ body = null;
8611
+ }
8612
+ const code = typeof body?.code === "string" ? toCloudErrorCode(body.code) : "CLOUD_TRANSPORT_ERROR";
8613
+ const message = typeof body?.error === "string" ? body.error : `Cloud request failed with status ${response.status}.`;
8614
+ return new OpensteerCloudError(code, message, response.status, body?.details);
8615
+ }
8616
+ function toCloudErrorCode(code) {
8617
+ if (code === "CLOUD_AUTH_FAILED" || code === "CLOUD_SESSION_NOT_FOUND" || code === "CLOUD_SESSION_CLOSED" || code === "CLOUD_UNSUPPORTED_METHOD" || code === "CLOUD_INVALID_REQUEST" || code === "CLOUD_MODEL_NOT_ALLOWED" || code === "CLOUD_ACTION_FAILED" || code === "CLOUD_INTERNAL" || code === "CLOUD_CAPACITY_EXHAUSTED" || code === "CLOUD_RUNTIME_UNAVAILABLE" || code === "CLOUD_RUNTIME_MISMATCH" || code === "CLOUD_SESSION_STALE" || code === "CLOUD_CONTRACT_MISMATCH" || code === "CLOUD_CONTROL_PLANE_ERROR" || code === "CLOUD_PROXY_UNAVAILABLE" || code === "CLOUD_PROXY_REQUIRED" || code === "CLOUD_BILLING_LIMIT_REACHED" || code === "CLOUD_RATE_LIMITED" || code === "CLOUD_BROWSER_PROFILE_NOT_FOUND" || code === "CLOUD_BROWSER_PROFILE_BUSY" || code === "CLOUD_BROWSER_PROFILE_DISABLED" || code === "CLOUD_BROWSER_PROFILE_PROXY_UNAVAILABLE" || code === "CLOUD_BROWSER_PROFILE_SYNC_FAILED") {
8618
+ return code;
8619
+ }
8620
+ return "CLOUD_TRANSPORT_ERROR";
8621
+ }
8622
+
8623
+ // src/cloud/session-client.ts
8624
+ var CACHE_IMPORT_BATCH_SIZE = 200;
8625
+ var CloudSessionClient = class {
8626
+ baseUrl;
8627
+ key;
8628
+ authScheme;
8629
+ constructor(baseUrl, key, authScheme = "api-key") {
8630
+ this.baseUrl = normalizeCloudBaseUrl(baseUrl);
8631
+ this.key = key;
8632
+ this.authScheme = authScheme;
8633
+ }
8634
+ async create(request) {
8635
+ const response = await fetch(`${this.baseUrl}/sessions`, {
8636
+ method: "POST",
8637
+ headers: {
8638
+ "content-type": "application/json",
8639
+ ...cloudAuthHeaders(this.key, this.authScheme)
8640
+ },
8641
+ body: JSON.stringify(request)
8642
+ });
8643
+ if (!response.ok) {
8644
+ throw await parseCloudHttpError(response);
8645
+ }
8646
+ let body;
8647
+ try {
8324
8648
  body = await response.json();
8325
8649
  } catch {
8326
8650
  throw new OpensteerCloudError(
@@ -8335,14 +8659,14 @@ var CloudSessionClient = class {
8335
8659
  const response = await fetch(`${this.baseUrl}/sessions/${sessionId}`, {
8336
8660
  method: "DELETE",
8337
8661
  headers: {
8338
- ...this.authHeaders()
8662
+ ...cloudAuthHeaders(this.key, this.authScheme)
8339
8663
  }
8340
8664
  });
8341
8665
  if (response.status === 204) {
8342
8666
  return;
8343
8667
  }
8344
8668
  if (!response.ok) {
8345
- throw await parseHttpError(response);
8669
+ throw await parseCloudHttpError(response);
8346
8670
  }
8347
8671
  }
8348
8672
  async importSelectorCache(request) {
@@ -8365,29 +8689,16 @@ var CloudSessionClient = class {
8365
8689
  method: "POST",
8366
8690
  headers: {
8367
8691
  "content-type": "application/json",
8368
- ...this.authHeaders()
8692
+ ...cloudAuthHeaders(this.key, this.authScheme)
8369
8693
  },
8370
8694
  body: JSON.stringify({ entries })
8371
8695
  });
8372
8696
  if (!response.ok) {
8373
- throw await parseHttpError(response);
8697
+ throw await parseCloudHttpError(response);
8374
8698
  }
8375
8699
  return await response.json();
8376
8700
  }
8377
- authHeaders() {
8378
- if (this.authScheme === "bearer") {
8379
- return {
8380
- authorization: `Bearer ${this.key}`
8381
- };
8382
- }
8383
- return {
8384
- "x-api-key": this.key
8385
- };
8386
- }
8387
8701
  };
8388
- function normalizeBaseUrl(baseUrl) {
8389
- return baseUrl.replace(/\/+$/, "");
8390
- }
8391
8702
  function parseCreateResponse(body, status) {
8392
8703
  const root = requireObject(
8393
8704
  body,
@@ -8532,23 +8843,6 @@ function mergeImportResponse(first, second) {
8532
8843
  skipped: first.skipped + second.skipped
8533
8844
  };
8534
8845
  }
8535
- async function parseHttpError(response) {
8536
- let body = null;
8537
- try {
8538
- body = await response.json();
8539
- } catch {
8540
- body = null;
8541
- }
8542
- const code = typeof body?.code === "string" ? toCloudErrorCode(body.code) : "CLOUD_TRANSPORT_ERROR";
8543
- const message = typeof body?.error === "string" ? body.error : `Cloud request failed with status ${response.status}.`;
8544
- return new OpensteerCloudError(code, message, response.status, body?.details);
8545
- }
8546
- function toCloudErrorCode(code) {
8547
- if (code === "CLOUD_AUTH_FAILED" || code === "CLOUD_SESSION_NOT_FOUND" || code === "CLOUD_SESSION_CLOSED" || code === "CLOUD_UNSUPPORTED_METHOD" || code === "CLOUD_INVALID_REQUEST" || code === "CLOUD_MODEL_NOT_ALLOWED" || code === "CLOUD_ACTION_FAILED" || code === "CLOUD_INTERNAL" || code === "CLOUD_CAPACITY_EXHAUSTED" || code === "CLOUD_RUNTIME_UNAVAILABLE" || code === "CLOUD_RUNTIME_MISMATCH" || code === "CLOUD_SESSION_STALE" || code === "CLOUD_CONTRACT_MISMATCH" || code === "CLOUD_CONTROL_PLANE_ERROR") {
8548
- return code;
8549
- }
8550
- return "CLOUD_TRANSPORT_ERROR";
8551
- }
8552
8846
 
8553
8847
  // src/cloud/runtime.ts
8554
8848
  var DEFAULT_CLOUD_BASE_URL = "https://api.opensteer.com";
@@ -8573,9 +8867,6 @@ function resolveCloudBaseUrl() {
8573
8867
  if (!value) return DEFAULT_CLOUD_BASE_URL;
8574
8868
  return normalizeCloudBaseUrl(value);
8575
8869
  }
8576
- function normalizeCloudBaseUrl(value) {
8577
- return value.replace(/\/+$/, "");
8578
- }
8579
8870
  function readCloudActionDescription(payload) {
8580
8871
  const description = payload.description;
8581
8872
  if (typeof description !== "string") return void 0;
@@ -9906,7 +10197,7 @@ function resolveAgentConfig(args) {
9906
10197
  });
9907
10198
  return {
9908
10199
  mode: "cua",
9909
- systemPrompt: normalizeNonEmptyString(agentConfig.systemPrompt) || DEFAULT_SYSTEM_PROMPT,
10200
+ systemPrompt: normalizeNonEmptyString2(agentConfig.systemPrompt) || DEFAULT_SYSTEM_PROMPT,
9910
10201
  waitBetweenActionsMs: normalizeWaitBetween(agentConfig.waitBetweenActionsMs),
9911
10202
  model
9912
10203
  };
@@ -9925,7 +10216,7 @@ function createCuaClient(config) {
9925
10216
  );
9926
10217
  }
9927
10218
  }
9928
- function normalizeNonEmptyString(value) {
10219
+ function normalizeNonEmptyString2(value) {
9929
10220
  if (typeof value !== "string") return void 0;
9930
10221
  const normalized = value.trim();
9931
10222
  return normalized.length ? normalized : void 0;
@@ -10200,24 +10491,22 @@ var OpensteerCuaAgentHandler = class {
10200
10491
  page;
10201
10492
  config;
10202
10493
  client;
10203
- debug;
10494
+ cursorController;
10204
10495
  onMutatingAction;
10205
- cursorOverlayInjected = false;
10206
10496
  constructor(options) {
10207
10497
  this.page = options.page;
10208
10498
  this.config = options.config;
10209
10499
  this.client = options.client;
10210
- this.debug = options.debug;
10500
+ this.cursorController = options.cursorController;
10211
10501
  this.onMutatingAction = options.onMutatingAction;
10212
10502
  }
10213
10503
  async execute(options) {
10214
10504
  const instruction = options.instruction;
10215
10505
  const maxSteps = options.maxSteps ?? 20;
10216
10506
  await this.initializeClient();
10217
- const highlightCursor = options.highlightCursor === true;
10218
10507
  this.client.setActionHandler(async (action) => {
10219
- if (highlightCursor) {
10220
- await this.maybeRenderCursor(action);
10508
+ if (this.cursorController.isEnabled()) {
10509
+ await this.maybePreviewCursor(action);
10221
10510
  }
10222
10511
  await executeAgentAction(this.page, action);
10223
10512
  this.client.setCurrentUrl(this.page.url());
@@ -10248,6 +10537,7 @@ var OpensteerCuaAgentHandler = class {
10248
10537
  const viewport = await this.resolveViewport();
10249
10538
  this.client.setViewport(viewport.width, viewport.height);
10250
10539
  this.client.setCurrentUrl(this.page.url());
10540
+ await this.cursorController.attachPage(this.page);
10251
10541
  this.client.setScreenshotProvider(async () => {
10252
10542
  const buffer = await this.page.screenshot({
10253
10543
  fullPage: false,
@@ -10276,51 +10566,611 @@ var OpensteerCuaAgentHandler = class {
10276
10566
  }
10277
10567
  return DEFAULT_CUA_VIEWPORT;
10278
10568
  }
10279
- async maybeRenderCursor(action) {
10569
+ async maybePreviewCursor(action) {
10280
10570
  const x = typeof action.x === "number" ? action.x : null;
10281
10571
  const y = typeof action.y === "number" ? action.y : null;
10282
10572
  if (x == null || y == null) {
10283
10573
  return;
10284
10574
  }
10575
+ await this.cursorController.preview({ x, y }, "agent");
10576
+ }
10577
+ };
10578
+ function sleep4(ms) {
10579
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10580
+ }
10581
+
10582
+ // src/cursor/motion.ts
10583
+ var DEFAULT_SNAPPY_OPTIONS = {
10584
+ minDurationMs: 46,
10585
+ maxDurationMs: 170,
10586
+ maxPoints: 14
10587
+ };
10588
+ function planSnappyCursorMotion(from, to, options = {}) {
10589
+ const resolved = {
10590
+ ...DEFAULT_SNAPPY_OPTIONS,
10591
+ ...options
10592
+ };
10593
+ const dx = to.x - from.x;
10594
+ const dy = to.y - from.y;
10595
+ const distance = Math.hypot(dx, dy);
10596
+ if (distance < 5) {
10597
+ return {
10598
+ points: [roundPoint(to)],
10599
+ stepDelayMs: 0
10600
+ };
10601
+ }
10602
+ const durationMs = clamp(
10603
+ 44 + distance * 0.26,
10604
+ resolved.minDurationMs,
10605
+ resolved.maxDurationMs
10606
+ );
10607
+ const rawPoints = clamp(
10608
+ Math.round(durationMs / 13),
10609
+ 4,
10610
+ resolved.maxPoints
10611
+ );
10612
+ const sign = deterministicBendSign(from, to);
10613
+ const nx = -dy / distance;
10614
+ const ny = dx / distance;
10615
+ const bend = clamp(distance * 0.12, 8, 28) * sign;
10616
+ const c1 = {
10617
+ x: from.x + dx * 0.28 + nx * bend,
10618
+ y: from.y + dy * 0.28 + ny * bend
10619
+ };
10620
+ const c2 = {
10621
+ x: from.x + dx * 0.74 + nx * bend * 0.58,
10622
+ y: from.y + dy * 0.74 + ny * bend * 0.58
10623
+ };
10624
+ const points = [];
10625
+ for (let i = 1; i <= rawPoints; i += 1) {
10626
+ const t = i / rawPoints;
10627
+ const sampled = cubicBezier(from, c1, c2, to, t);
10628
+ if (i !== rawPoints) {
10629
+ const previous = points[points.length - 1];
10630
+ if (previous && distanceBetween(previous, sampled) < 1.4) {
10631
+ continue;
10632
+ }
10633
+ }
10634
+ points.push(roundPoint(sampled));
10635
+ }
10636
+ if (distance > 220) {
10637
+ const settle = {
10638
+ x: to.x - dx / distance,
10639
+ y: to.y - dy / distance
10640
+ };
10641
+ const last = points[points.length - 1];
10642
+ if (!last || distanceBetween(last, settle) > 1.2) {
10643
+ points.splice(Math.max(0, points.length - 1), 0, roundPoint(settle));
10644
+ }
10645
+ }
10646
+ const deduped = dedupeAdjacent(points);
10647
+ const stepDelayMs = deduped.length > 1 ? Math.round(durationMs / deduped.length) : 0;
10648
+ return {
10649
+ points: deduped.length ? deduped : [roundPoint(to)],
10650
+ stepDelayMs
10651
+ };
10652
+ }
10653
+ function cubicBezier(p0, p1, p2, p3, t) {
10654
+ const inv = 1 - t;
10655
+ const inv2 = inv * inv;
10656
+ const inv3 = inv2 * inv;
10657
+ const t2 = t * t;
10658
+ const t3 = t2 * t;
10659
+ return {
10660
+ x: inv3 * p0.x + 3 * inv2 * t * p1.x + 3 * inv * t2 * p2.x + t3 * p3.x,
10661
+ y: inv3 * p0.y + 3 * inv2 * t * p1.y + 3 * inv * t2 * p2.y + t3 * p3.y
10662
+ };
10663
+ }
10664
+ function deterministicBendSign(from, to) {
10665
+ const seed = Math.sin(
10666
+ from.x * 12.9898 + from.y * 78.233 + to.x * 37.719 + to.y * 19.113
10667
+ ) * 43758.5453;
10668
+ const fractional = seed - Math.floor(seed);
10669
+ return fractional >= 0.5 ? 1 : -1;
10670
+ }
10671
+ function roundPoint(point) {
10672
+ return {
10673
+ x: Math.round(point.x * 100) / 100,
10674
+ y: Math.round(point.y * 100) / 100
10675
+ };
10676
+ }
10677
+ function distanceBetween(a, b) {
10678
+ return Math.hypot(a.x - b.x, a.y - b.y);
10679
+ }
10680
+ function dedupeAdjacent(points) {
10681
+ const out = [];
10682
+ for (const point of points) {
10683
+ const last = out[out.length - 1];
10684
+ if (last && last.x === point.x && last.y === point.y) {
10685
+ continue;
10686
+ }
10687
+ out.push(point);
10688
+ }
10689
+ return out;
10690
+ }
10691
+ function clamp(value, min, max) {
10692
+ return Math.min(max, Math.max(min, value));
10693
+ }
10694
+
10695
+ // src/cursor/renderers/svg-overlay.ts
10696
+ var PULSE_DURATION_MS = 220;
10697
+ var HOST_ELEMENT_ID = "__os_cr";
10698
+ var SvgCursorRenderer = class {
10699
+ page = null;
10700
+ active = false;
10701
+ reason = "disabled";
10702
+ lastMessage;
10703
+ async initialize(page) {
10704
+ this.page = page;
10705
+ if (page.isClosed()) {
10706
+ this.markInactive("page_closed");
10707
+ return;
10708
+ }
10709
+ try {
10710
+ await page.evaluate(injectCursor, HOST_ELEMENT_ID);
10711
+ this.active = true;
10712
+ this.reason = void 0;
10713
+ this.lastMessage = void 0;
10714
+ } catch (error) {
10715
+ const message = error instanceof Error ? error.message : String(error);
10716
+ this.markInactive("renderer_error", message);
10717
+ }
10718
+ }
10719
+ isActive() {
10720
+ return this.active;
10721
+ }
10722
+ status() {
10723
+ return {
10724
+ enabled: true,
10725
+ active: this.active,
10726
+ reason: this.reason ? this.lastMessage ? `${this.reason}: ${this.lastMessage}` : this.reason : void 0
10727
+ };
10728
+ }
10729
+ async move(point, style) {
10730
+ if (!this.active || !this.page || this.page.isClosed()) return;
10285
10731
  try {
10286
- if (!this.cursorOverlayInjected) {
10287
- await this.page.evaluate(() => {
10288
- if (document.getElementById("__opensteer_cua_cursor")) return;
10289
- const cursor = document.createElement("div");
10290
- cursor.id = "__opensteer_cua_cursor";
10291
- cursor.style.position = "fixed";
10292
- cursor.style.width = "14px";
10293
- cursor.style.height = "14px";
10294
- cursor.style.borderRadius = "999px";
10295
- cursor.style.background = "rgba(255, 51, 51, 0.85)";
10296
- cursor.style.border = "2px solid rgba(255, 255, 255, 0.95)";
10297
- cursor.style.boxShadow = "0 0 0 3px rgba(255, 51, 51, 0.25)";
10298
- cursor.style.pointerEvents = "none";
10299
- cursor.style.zIndex = "2147483647";
10300
- cursor.style.transform = "translate(-9999px, -9999px)";
10301
- cursor.style.transition = "transform 80ms linear";
10302
- document.documentElement.appendChild(cursor);
10732
+ const ok = await this.page.evaluate(moveCursor, {
10733
+ id: HOST_ELEMENT_ID,
10734
+ x: point.x,
10735
+ y: point.y,
10736
+ size: style.size,
10737
+ fill: colorToRgba(style.fillColor),
10738
+ outline: colorToRgba(style.outlineColor)
10739
+ });
10740
+ if (!ok) {
10741
+ await this.reinject();
10742
+ await this.page.evaluate(moveCursor, {
10743
+ id: HOST_ELEMENT_ID,
10744
+ x: point.x,
10745
+ y: point.y,
10746
+ size: style.size,
10747
+ fill: colorToRgba(style.fillColor),
10748
+ outline: colorToRgba(style.outlineColor)
10303
10749
  });
10304
- this.cursorOverlayInjected = true;
10305
10750
  }
10306
- await this.page.evaluate(
10307
- ({ px, py }) => {
10308
- const cursor = document.getElementById("__opensteer_cua_cursor");
10309
- if (!cursor) return;
10310
- cursor.style.transform = `translate(${Math.round(px - 7)}px, ${Math.round(py - 7)}px)`;
10311
- },
10312
- { px: x, py: y }
10313
- );
10751
+ } catch (error) {
10752
+ this.handleError(error);
10753
+ }
10754
+ }
10755
+ async pulse(point, style) {
10756
+ if (!this.active || !this.page || this.page.isClosed()) return;
10757
+ try {
10758
+ const ok = await this.page.evaluate(pulseCursor, {
10759
+ id: HOST_ELEMENT_ID,
10760
+ x: point.x,
10761
+ y: point.y,
10762
+ size: style.size,
10763
+ fill: colorToRgba(style.fillColor),
10764
+ outline: colorToRgba(style.outlineColor),
10765
+ halo: colorToRgba(style.haloColor),
10766
+ pulseMs: PULSE_DURATION_MS
10767
+ });
10768
+ if (!ok) {
10769
+ await this.reinject();
10770
+ await this.page.evaluate(pulseCursor, {
10771
+ id: HOST_ELEMENT_ID,
10772
+ x: point.x,
10773
+ y: point.y,
10774
+ size: style.size,
10775
+ fill: colorToRgba(style.fillColor),
10776
+ outline: colorToRgba(style.outlineColor),
10777
+ halo: colorToRgba(style.haloColor),
10778
+ pulseMs: PULSE_DURATION_MS
10779
+ });
10780
+ }
10781
+ } catch (error) {
10782
+ this.handleError(error);
10783
+ }
10784
+ }
10785
+ async clear() {
10786
+ if (!this.page || this.page.isClosed()) return;
10787
+ try {
10788
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
10789
+ } catch {
10790
+ }
10791
+ }
10792
+ async dispose() {
10793
+ if (this.page && !this.page.isClosed()) {
10794
+ try {
10795
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
10796
+ } catch {
10797
+ }
10798
+ }
10799
+ this.active = false;
10800
+ this.reason = "disabled";
10801
+ this.lastMessage = void 0;
10802
+ this.page = null;
10803
+ }
10804
+ async reinject() {
10805
+ if (!this.page || this.page.isClosed()) return;
10806
+ try {
10807
+ await this.page.evaluate(injectCursor, HOST_ELEMENT_ID);
10808
+ } catch {
10809
+ }
10810
+ }
10811
+ markInactive(reason, message) {
10812
+ this.active = false;
10813
+ this.reason = reason;
10814
+ this.lastMessage = message;
10815
+ }
10816
+ handleError(error) {
10817
+ const message = error instanceof Error ? error.message : String(error);
10818
+ if (isPageGone(message)) {
10819
+ this.markInactive("page_closed", message);
10820
+ }
10821
+ }
10822
+ };
10823
+ function injectCursor(hostId) {
10824
+ const win = window;
10825
+ if (win[hostId]) return;
10826
+ const host = document.createElement("div");
10827
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
10828
+ const shadow = host.attachShadow({ mode: "closed" });
10829
+ const wrapper = document.createElement("div");
10830
+ wrapper.style.cssText = "position:fixed;top:0;left:0;pointer-events:none;will-change:transform;display:none;";
10831
+ wrapper.innerHTML = `
10832
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 1px 2px rgba(0,0,0,0.3));display:block;">
10833
+ <path d="M3 2L3 23L8.5 17.5L13 26L17 24L12.5 15.5L20 15.5L3 2Z"
10834
+ fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
10835
+ </svg>
10836
+ <div data-role="pulse" style="position:absolute;top:0;left:0;width:24px;height:24px;border-radius:50%;pointer-events:none;opacity:0;transform:translate(-8px,-8px);"></div>
10837
+ `;
10838
+ shadow.appendChild(wrapper);
10839
+ document.documentElement.appendChild(host);
10840
+ Object.defineProperty(window, hostId, {
10841
+ value: {
10842
+ host,
10843
+ wrapper,
10844
+ path: wrapper.querySelector("path"),
10845
+ pulse: wrapper.querySelector('[data-role="pulse"]')
10846
+ },
10847
+ configurable: true,
10848
+ enumerable: false
10849
+ });
10850
+ }
10851
+ function moveCursor(args) {
10852
+ const refs = window[args.id];
10853
+ if (!refs) return false;
10854
+ const scale = args.size / 20;
10855
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
10856
+ refs.wrapper.style.display = "block";
10857
+ if (refs.path) {
10858
+ refs.path.setAttribute("fill", args.fill);
10859
+ refs.path.setAttribute("stroke", args.outline);
10860
+ }
10861
+ return true;
10862
+ }
10863
+ function pulseCursor(args) {
10864
+ const refs = window[args.id];
10865
+ if (!refs) return false;
10866
+ const scale = args.size / 20;
10867
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
10868
+ refs.wrapper.style.display = "block";
10869
+ if (refs.path) {
10870
+ refs.path.setAttribute("fill", args.fill);
10871
+ refs.path.setAttribute("stroke", args.outline);
10872
+ }
10873
+ const ring = refs.pulse;
10874
+ if (!ring) return true;
10875
+ ring.style.background = args.halo;
10876
+ ring.style.opacity = "0.7";
10877
+ ring.style.width = "24px";
10878
+ ring.style.height = "24px";
10879
+ ring.style.transition = `all ${args.pulseMs}ms ease-out`;
10880
+ ring.offsetHeight;
10881
+ ring.style.width = "48px";
10882
+ ring.style.height = "48px";
10883
+ ring.style.opacity = "0";
10884
+ ring.style.transform = "translate(-20px, -20px)";
10885
+ setTimeout(() => {
10886
+ ring.style.transition = "none";
10887
+ ring.style.width = "24px";
10888
+ ring.style.height = "24px";
10889
+ ring.style.transform = "translate(-8px, -8px)";
10890
+ ring.style.opacity = "0";
10891
+ }, args.pulseMs);
10892
+ return true;
10893
+ }
10894
+ function removeCursor(hostId) {
10895
+ const refs = window[hostId];
10896
+ if (refs) {
10897
+ refs.host.remove();
10898
+ delete window[hostId];
10899
+ }
10900
+ }
10901
+ function colorToRgba(c) {
10902
+ return `rgba(${Math.round(c.r)},${Math.round(c.g)},${Math.round(c.b)},${c.a})`;
10903
+ }
10904
+ function isPageGone(message) {
10905
+ const m = message.toLowerCase();
10906
+ return m.includes("closed") || m.includes("detached") || m.includes("destroyed") || m.includes("target");
10907
+ }
10908
+
10909
+ // src/cursor/controller.ts
10910
+ var DEFAULT_STYLE = {
10911
+ size: 20,
10912
+ fillColor: {
10913
+ r: 255,
10914
+ g: 255,
10915
+ b: 255,
10916
+ a: 0.96
10917
+ },
10918
+ outlineColor: {
10919
+ r: 0,
10920
+ g: 0,
10921
+ b: 0,
10922
+ a: 1
10923
+ },
10924
+ haloColor: {
10925
+ r: 35,
10926
+ g: 162,
10927
+ b: 255,
10928
+ a: 0.38
10929
+ },
10930
+ pulseScale: 2.15
10931
+ };
10932
+ var REINITIALIZE_BACKOFF_MS = 1e3;
10933
+ var FIRST_MOVE_CENTER_DISTANCE_THRESHOLD = 16;
10934
+ var FIRST_MOVE_MAX_TRAVEL = 220;
10935
+ var FIRST_MOVE_NEAR_TARGET_X_OFFSET = 28;
10936
+ var FIRST_MOVE_NEAR_TARGET_Y_OFFSET = 18;
10937
+ var MOTION_PLANNERS = {
10938
+ snappy: planSnappyCursorMotion
10939
+ };
10940
+ var CursorController = class {
10941
+ debug;
10942
+ renderer;
10943
+ page = null;
10944
+ listenerPage = null;
10945
+ lastPoint = null;
10946
+ initializedForPage = false;
10947
+ lastInitializeAttemptAt = 0;
10948
+ enabled;
10949
+ profile;
10950
+ style;
10951
+ onDomContentLoaded = () => {
10952
+ void this.restoreCursorAfterNavigation();
10953
+ };
10954
+ constructor(options = {}) {
10955
+ const config = options.config || {};
10956
+ this.debug = Boolean(options.debug);
10957
+ this.enabled = config.enabled === true;
10958
+ this.profile = config.profile ?? "snappy";
10959
+ this.style = mergeStyle(config.style);
10960
+ this.renderer = options.renderer ?? new SvgCursorRenderer();
10961
+ }
10962
+ setEnabled(enabled) {
10963
+ if (this.enabled && !enabled) {
10964
+ this.lastPoint = null;
10965
+ void this.clear();
10966
+ }
10967
+ this.enabled = enabled;
10968
+ }
10969
+ isEnabled() {
10970
+ return this.enabled;
10971
+ }
10972
+ getStatus() {
10973
+ if (!this.enabled) {
10974
+ return {
10975
+ enabled: false,
10976
+ active: false,
10977
+ reason: "disabled"
10978
+ };
10979
+ }
10980
+ const status = this.renderer.status();
10981
+ if (!this.initializedForPage && !status.active) {
10982
+ return {
10983
+ enabled: true,
10984
+ active: false,
10985
+ reason: "not_initialized"
10986
+ };
10987
+ }
10988
+ return status;
10989
+ }
10990
+ async attachPage(page) {
10991
+ if (this.page !== page) {
10992
+ this.detachPageListeners();
10993
+ this.page = page;
10994
+ this.lastPoint = null;
10995
+ this.initializedForPage = false;
10996
+ this.lastInitializeAttemptAt = 0;
10997
+ }
10998
+ this.attachPageListeners(page);
10999
+ }
11000
+ async preview(point, intent) {
11001
+ if (!this.enabled || !point) return;
11002
+ if (!this.page || this.page.isClosed()) return;
11003
+ try {
11004
+ await this.ensureInitialized();
11005
+ if (!this.renderer.isActive()) {
11006
+ await this.reinitializeIfEligible();
11007
+ }
11008
+ if (!this.renderer.isActive()) return;
11009
+ const start = this.resolveMotionStart(point);
11010
+ const motion = this.planMotion(start, point);
11011
+ for (const step of motion.points) {
11012
+ await this.renderer.move(step, this.style);
11013
+ if (motion.stepDelayMs > 0) {
11014
+ await sleep5(motion.stepDelayMs);
11015
+ }
11016
+ }
11017
+ if (shouldPulse(intent)) {
11018
+ await this.renderer.pulse(point, this.style);
11019
+ }
11020
+ this.lastPoint = point;
11021
+ } catch (error) {
11022
+ if (this.debug) {
11023
+ const message = error instanceof Error ? error.message : String(error);
11024
+ console.warn(`[opensteer] cursor preview failed: ${message}`);
11025
+ }
11026
+ }
11027
+ }
11028
+ async clear() {
11029
+ try {
11030
+ await this.renderer.clear();
11031
+ } catch (error) {
11032
+ if (this.debug) {
11033
+ const message = error instanceof Error ? error.message : String(error);
11034
+ console.warn(`[opensteer] cursor clear failed: ${message}`);
11035
+ }
11036
+ }
11037
+ }
11038
+ async dispose() {
11039
+ this.detachPageListeners();
11040
+ this.lastPoint = null;
11041
+ this.initializedForPage = false;
11042
+ this.lastInitializeAttemptAt = 0;
11043
+ this.page = null;
11044
+ await this.renderer.dispose();
11045
+ }
11046
+ async ensureInitialized() {
11047
+ if (!this.page || this.page.isClosed()) return;
11048
+ if (this.initializedForPage) return;
11049
+ await this.initializeRenderer();
11050
+ }
11051
+ attachPageListeners(page) {
11052
+ if (this.listenerPage === page) {
11053
+ return;
11054
+ }
11055
+ this.detachPageListeners();
11056
+ page.on("domcontentloaded", this.onDomContentLoaded);
11057
+ this.listenerPage = page;
11058
+ }
11059
+ detachPageListeners() {
11060
+ if (!this.listenerPage) {
11061
+ return;
11062
+ }
11063
+ this.listenerPage.off("domcontentloaded", this.onDomContentLoaded);
11064
+ this.listenerPage = null;
11065
+ }
11066
+ planMotion(from, to) {
11067
+ return MOTION_PLANNERS[this.profile](from, to);
11068
+ }
11069
+ async reinitializeIfEligible() {
11070
+ if (!this.page || this.page.isClosed()) return;
11071
+ const elapsed = Date.now() - this.lastInitializeAttemptAt;
11072
+ if (elapsed < REINITIALIZE_BACKOFF_MS) return;
11073
+ await this.initializeRenderer();
11074
+ }
11075
+ async initializeRenderer() {
11076
+ if (!this.page || this.page.isClosed()) return;
11077
+ this.lastInitializeAttemptAt = Date.now();
11078
+ await this.renderer.initialize(this.page);
11079
+ this.initializedForPage = true;
11080
+ }
11081
+ async restoreCursorAfterNavigation() {
11082
+ if (!this.enabled || !this.lastPoint) return;
11083
+ if (!this.page || this.page.isClosed()) return;
11084
+ try {
11085
+ if (!this.renderer.isActive()) {
11086
+ await this.reinitializeIfEligible();
11087
+ }
11088
+ if (!this.renderer.isActive()) {
11089
+ return;
11090
+ }
11091
+ await this.renderer.move(this.lastPoint, this.style);
10314
11092
  } catch (error) {
10315
11093
  if (this.debug) {
10316
11094
  const message = error instanceof Error ? error.message : String(error);
10317
- console.warn(`[opensteer] cursor overlay failed: ${message}`);
11095
+ console.warn(
11096
+ `[opensteer] cursor restore after navigation failed: ${message}`
11097
+ );
11098
+ }
11099
+ }
11100
+ }
11101
+ resolveMotionStart(target) {
11102
+ if (this.lastPoint) {
11103
+ return this.lastPoint;
11104
+ }
11105
+ const viewport = this.page?.viewportSize();
11106
+ if (!viewport?.width || !viewport?.height) {
11107
+ return target;
11108
+ }
11109
+ const centerPoint = {
11110
+ x: viewport.width / 2,
11111
+ y: viewport.height / 2
11112
+ };
11113
+ if (distanceBetween2(centerPoint, target) > FIRST_MOVE_CENTER_DISTANCE_THRESHOLD) {
11114
+ const dx = target.x - centerPoint.x;
11115
+ const dy = target.y - centerPoint.y;
11116
+ const distance = Math.hypot(dx, dy);
11117
+ if (distance > FIRST_MOVE_MAX_TRAVEL) {
11118
+ const ux = dx / distance;
11119
+ const uy = dy / distance;
11120
+ return {
11121
+ x: target.x - ux * FIRST_MOVE_MAX_TRAVEL,
11122
+ y: target.y - uy * FIRST_MOVE_MAX_TRAVEL
11123
+ };
10318
11124
  }
11125
+ return centerPoint;
10319
11126
  }
11127
+ return {
11128
+ x: clamp2(target.x - FIRST_MOVE_NEAR_TARGET_X_OFFSET, 0, viewport.width),
11129
+ y: clamp2(target.y - FIRST_MOVE_NEAR_TARGET_Y_OFFSET, 0, viewport.height)
11130
+ };
10320
11131
  }
10321
11132
  };
10322
- function sleep4(ms) {
10323
- return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11133
+ function mergeStyle(style) {
11134
+ return {
11135
+ size: normalizeFinite(style?.size, DEFAULT_STYLE.size, 4, 48),
11136
+ pulseScale: normalizeFinite(
11137
+ style?.pulseScale,
11138
+ DEFAULT_STYLE.pulseScale,
11139
+ 1,
11140
+ 3
11141
+ ),
11142
+ fillColor: normalizeColor(style?.fillColor, DEFAULT_STYLE.fillColor),
11143
+ outlineColor: normalizeColor(
11144
+ style?.outlineColor,
11145
+ DEFAULT_STYLE.outlineColor
11146
+ ),
11147
+ haloColor: normalizeColor(style?.haloColor, DEFAULT_STYLE.haloColor)
11148
+ };
11149
+ }
11150
+ function normalizeColor(color, fallback) {
11151
+ if (!color) return { ...fallback };
11152
+ return {
11153
+ r: normalizeFinite(color.r, fallback.r, 0, 255),
11154
+ g: normalizeFinite(color.g, fallback.g, 0, 255),
11155
+ b: normalizeFinite(color.b, fallback.b, 0, 255),
11156
+ a: normalizeFinite(color.a, fallback.a, 0, 1)
11157
+ };
11158
+ }
11159
+ function normalizeFinite(value, fallback, min, max) {
11160
+ const numeric = typeof value === "number" && Number.isFinite(value) ? value : fallback;
11161
+ return Math.min(max, Math.max(min, numeric));
11162
+ }
11163
+ function distanceBetween2(a, b) {
11164
+ return Math.hypot(a.x - b.x, a.y - b.y);
11165
+ }
11166
+ function clamp2(value, min, max) {
11167
+ return Math.min(max, Math.max(min, value));
11168
+ }
11169
+ function shouldPulse(intent) {
11170
+ return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
11171
+ }
11172
+ function sleep5(ms) {
11173
+ return new Promise((resolve) => setTimeout(resolve, ms));
10324
11174
  }
10325
11175
 
10326
11176
  // src/opensteer.ts
@@ -10349,6 +11199,7 @@ var Opensteer = class _Opensteer {
10349
11199
  ownsBrowser = false;
10350
11200
  snapshotCache = null;
10351
11201
  agentExecutionInFlight = false;
11202
+ cursorController = null;
10352
11203
  constructor(config = {}) {
10353
11204
  const resolvedRuntime = resolveConfigWithEnv(config);
10354
11205
  const resolved = resolvedRuntime.config;
@@ -10370,15 +11221,29 @@ var Opensteer = class _Opensteer {
10370
11221
  if (cloudSelection.cloud) {
10371
11222
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
10372
11223
  const apiKey = cloudConfig?.apiKey?.trim();
10373
- if (!apiKey) {
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
+ }
11238
+ if (!credential) {
10374
11239
  throw new Error(
10375
- "Cloud mode requires a non-empty API key via cloud.apiKey or OPENSTEER_API_KEY."
11240
+ "Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
10376
11241
  );
10377
11242
  }
10378
11243
  this.cloud = createCloudRuntimeState(
10379
- apiKey,
11244
+ credential,
10380
11245
  cloudConfig?.baseUrl,
10381
- cloudConfig?.authScheme
11246
+ authScheme
10382
11247
  );
10383
11248
  } else {
10384
11249
  this.cloud = null;
@@ -10603,6 +11468,19 @@ var Opensteer = class _Opensteer {
10603
11468
  }
10604
11469
  return true;
10605
11470
  }
11471
+ buildCloudSessionLaunchConfig(options) {
11472
+ const cloudConfig = this.config.cloud && typeof this.config.cloud === "object" ? this.config.cloud : void 0;
11473
+ const browserProfile = normalizeCloudBrowserProfilePreference(
11474
+ options.cloudBrowserProfile ?? cloudConfig?.browserProfile,
11475
+ options.cloudBrowserProfile ? "launch options" : "Opensteer config"
11476
+ );
11477
+ if (!browserProfile) {
11478
+ return void 0;
11479
+ }
11480
+ return {
11481
+ browserProfile
11482
+ };
11483
+ }
10606
11484
  async launch(options = {}) {
10607
11485
  if (this.pageRef && !this.ownsBrowser) {
10608
11486
  throw new Error(
@@ -10625,6 +11503,7 @@ var Opensteer = class _Opensteer {
10625
11503
  }
10626
11504
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
10627
11505
  this.cloud.localRunId = localRunId;
11506
+ const launchConfig = this.buildCloudSessionLaunchConfig(options);
10628
11507
  const session3 = await this.cloud.sessionClient.create({
10629
11508
  cloudSessionContractVersion,
10630
11509
  sourceType: "local-cloud",
@@ -10632,7 +11511,8 @@ var Opensteer = class _Opensteer {
10632
11511
  localRunId,
10633
11512
  name: this.namespace,
10634
11513
  model: this.config.model,
10635
- launchContext: options.context || void 0
11514
+ launchContext: options.context || void 0,
11515
+ launchConfig
10636
11516
  });
10637
11517
  sessionId = session3.sessionId;
10638
11518
  actionClient = await ActionWsClient.connect({
@@ -10650,6 +11530,9 @@ var Opensteer = class _Opensteer {
10650
11530
  this.pageRef = cdpConnection.page;
10651
11531
  this.ownsBrowser = true;
10652
11532
  this.snapshotCache = null;
11533
+ if (this.cursorController) {
11534
+ await this.cursorController.attachPage(this.pageRef);
11535
+ }
10653
11536
  this.cloud.actionClient = actionClient;
10654
11537
  this.cloud.sessionId = sessionId;
10655
11538
  this.cloud.cloudSessionUrl = session3.cloudSessionUrl;
@@ -10690,6 +11573,9 @@ var Opensteer = class _Opensteer {
10690
11573
  this.pageRef = session2.page;
10691
11574
  this.ownsBrowser = true;
10692
11575
  this.snapshotCache = null;
11576
+ if (this.cursorController) {
11577
+ await this.cursorController.attachPage(this.pageRef);
11578
+ }
10693
11579
  }
10694
11580
  static from(page, config = {}) {
10695
11581
  const resolvedRuntime = resolveConfigWithEnv(config);
@@ -10734,6 +11620,9 @@ var Opensteer = class _Opensteer {
10734
11620
  if (sessionId) {
10735
11621
  await this.cloud.sessionClient.close(sessionId).catch(() => void 0);
10736
11622
  }
11623
+ if (this.cursorController) {
11624
+ await this.cursorController.dispose().catch(() => void 0);
11625
+ }
10737
11626
  return;
10738
11627
  }
10739
11628
  if (this.ownsBrowser) {
@@ -10743,6 +11632,9 @@ var Opensteer = class _Opensteer {
10743
11632
  this.pageRef = null;
10744
11633
  this.contextRef = null;
10745
11634
  this.ownsBrowser = false;
11635
+ if (this.cursorController) {
11636
+ await this.cursorController.dispose().catch(() => void 0);
11637
+ }
10746
11638
  }
10747
11639
  async syncLocalSelectorCacheToCloud() {
10748
11640
  if (!this.cloud) return;
@@ -10869,12 +11761,22 @@ var Opensteer = class _Opensteer {
10869
11761
  resolution.counter
10870
11762
  );
10871
11763
  }
10872
- await this.runWithPostActionWait("hover", options.wait, async () => {
10873
- await handle.hover({
10874
- force: options.force,
10875
- position: options.position
10876
- });
10877
- });
11764
+ await this.runWithCursorPreview(
11765
+ () => this.resolveHandleTargetPoint(handle, options.position),
11766
+ "hover",
11767
+ async () => {
11768
+ await this.runWithPostActionWait(
11769
+ "hover",
11770
+ options.wait,
11771
+ async () => {
11772
+ await handle.hover({
11773
+ force: options.force,
11774
+ position: options.position
11775
+ });
11776
+ }
11777
+ );
11778
+ }
11779
+ );
10878
11780
  } catch (err) {
10879
11781
  const failure = classifyActionFailure({
10880
11782
  action: "hover",
@@ -10909,25 +11811,31 @@ var Opensteer = class _Opensteer {
10909
11811
  throw new Error("Unable to resolve element path for hover action.");
10910
11812
  }
10911
11813
  const path5 = resolution.path;
10912
- const result = await this.runWithPostActionWait(
11814
+ const result = await this.runWithCursorPreview(
11815
+ () => this.resolvePathTargetPoint(path5, options.position),
10913
11816
  "hover",
10914
- options.wait,
10915
11817
  async () => {
10916
- const actionResult = await performHover(this.page, path5, options);
10917
- if (!actionResult.ok) {
10918
- const failure = actionResult.failure || classifyActionFailure({
10919
- action: "hover",
10920
- error: actionResult.error || defaultActionFailureMessage("hover"),
10921
- fallbackMessage: defaultActionFailureMessage("hover")
10922
- });
10923
- throw this.buildActionError(
10924
- "hover",
10925
- options.description,
10926
- failure,
10927
- actionResult.usedSelector || null
10928
- );
10929
- }
10930
- return actionResult;
11818
+ return await this.runWithPostActionWait(
11819
+ "hover",
11820
+ options.wait,
11821
+ async () => {
11822
+ const actionResult = await performHover(this.page, path5, options);
11823
+ if (!actionResult.ok) {
11824
+ const failure = actionResult.failure || classifyActionFailure({
11825
+ action: "hover",
11826
+ error: actionResult.error || defaultActionFailureMessage("hover"),
11827
+ fallbackMessage: defaultActionFailureMessage("hover")
11828
+ });
11829
+ throw this.buildActionError(
11830
+ "hover",
11831
+ options.description,
11832
+ failure,
11833
+ actionResult.usedSelector || null
11834
+ );
11835
+ }
11836
+ return actionResult;
11837
+ }
11838
+ );
10931
11839
  }
10932
11840
  );
10933
11841
  this.snapshotCache = null;
@@ -10968,16 +11876,26 @@ var Opensteer = class _Opensteer {
10968
11876
  resolution.counter
10969
11877
  );
10970
11878
  }
10971
- await this.runWithPostActionWait("input", options.wait, async () => {
10972
- if (options.clear !== false) {
10973
- await handle.fill(options.text);
10974
- } else {
10975
- await handle.type(options.text);
10976
- }
10977
- if (options.pressEnter) {
10978
- await handle.press("Enter", { noWaitAfter: true });
11879
+ await this.runWithCursorPreview(
11880
+ () => this.resolveHandleTargetPoint(handle),
11881
+ "input",
11882
+ async () => {
11883
+ await this.runWithPostActionWait(
11884
+ "input",
11885
+ options.wait,
11886
+ async () => {
11887
+ if (options.clear !== false) {
11888
+ await handle.fill(options.text);
11889
+ } else {
11890
+ await handle.type(options.text);
11891
+ }
11892
+ if (options.pressEnter) {
11893
+ await handle.press("Enter", { noWaitAfter: true });
11894
+ }
11895
+ }
11896
+ );
10979
11897
  }
10980
- });
11898
+ );
10981
11899
  } catch (err) {
10982
11900
  const failure = classifyActionFailure({
10983
11901
  action: "input",
@@ -11012,25 +11930,31 @@ var Opensteer = class _Opensteer {
11012
11930
  throw new Error("Unable to resolve element path for input action.");
11013
11931
  }
11014
11932
  const path5 = resolution.path;
11015
- const result = await this.runWithPostActionWait(
11933
+ const result = await this.runWithCursorPreview(
11934
+ () => this.resolvePathTargetPoint(path5),
11016
11935
  "input",
11017
- options.wait,
11018
11936
  async () => {
11019
- const actionResult = await performInput(this.page, path5, options);
11020
- if (!actionResult.ok) {
11021
- const failure = actionResult.failure || classifyActionFailure({
11022
- action: "input",
11023
- error: actionResult.error || defaultActionFailureMessage("input"),
11024
- fallbackMessage: defaultActionFailureMessage("input")
11025
- });
11026
- throw this.buildActionError(
11027
- "input",
11028
- options.description,
11029
- failure,
11030
- actionResult.usedSelector || null
11031
- );
11032
- }
11033
- return actionResult;
11937
+ return await this.runWithPostActionWait(
11938
+ "input",
11939
+ options.wait,
11940
+ async () => {
11941
+ const actionResult = await performInput(this.page, path5, options);
11942
+ if (!actionResult.ok) {
11943
+ const failure = actionResult.failure || classifyActionFailure({
11944
+ action: "input",
11945
+ error: actionResult.error || defaultActionFailureMessage("input"),
11946
+ fallbackMessage: defaultActionFailureMessage("input")
11947
+ });
11948
+ throw this.buildActionError(
11949
+ "input",
11950
+ options.description,
11951
+ failure,
11952
+ actionResult.usedSelector || null
11953
+ );
11954
+ }
11955
+ return actionResult;
11956
+ }
11957
+ );
11034
11958
  }
11035
11959
  );
11036
11960
  this.snapshotCache = null;
@@ -11071,21 +11995,27 @@ var Opensteer = class _Opensteer {
11071
11995
  resolution.counter
11072
11996
  );
11073
11997
  }
11074
- await this.runWithPostActionWait(
11998
+ await this.runWithCursorPreview(
11999
+ () => this.resolveHandleTargetPoint(handle),
11075
12000
  "select",
11076
- options.wait,
11077
12001
  async () => {
11078
- if (options.value != null) {
11079
- await handle.selectOption(options.value);
11080
- } else if (options.label != null) {
11081
- await handle.selectOption({ label: options.label });
11082
- } else if (options.index != null) {
11083
- await handle.selectOption({ index: options.index });
11084
- } else {
11085
- throw new Error(
11086
- "Select requires value, label, or index."
11087
- );
11088
- }
12002
+ await this.runWithPostActionWait(
12003
+ "select",
12004
+ options.wait,
12005
+ async () => {
12006
+ if (options.value != null) {
12007
+ await handle.selectOption(options.value);
12008
+ } else if (options.label != null) {
12009
+ await handle.selectOption({ label: options.label });
12010
+ } else if (options.index != null) {
12011
+ await handle.selectOption({ index: options.index });
12012
+ } else {
12013
+ throw new Error(
12014
+ "Select requires value, label, or index."
12015
+ );
12016
+ }
12017
+ }
12018
+ );
11089
12019
  }
11090
12020
  );
11091
12021
  } catch (err) {
@@ -11122,25 +12052,31 @@ var Opensteer = class _Opensteer {
11122
12052
  throw new Error("Unable to resolve element path for select action.");
11123
12053
  }
11124
12054
  const path5 = resolution.path;
11125
- const result = await this.runWithPostActionWait(
12055
+ const result = await this.runWithCursorPreview(
12056
+ () => this.resolvePathTargetPoint(path5),
11126
12057
  "select",
11127
- options.wait,
11128
12058
  async () => {
11129
- const actionResult = await performSelect(this.page, path5, options);
11130
- if (!actionResult.ok) {
11131
- const failure = actionResult.failure || classifyActionFailure({
11132
- action: "select",
11133
- error: actionResult.error || defaultActionFailureMessage("select"),
11134
- fallbackMessage: defaultActionFailureMessage("select")
11135
- });
11136
- throw this.buildActionError(
11137
- "select",
11138
- options.description,
11139
- failure,
11140
- actionResult.usedSelector || null
11141
- );
11142
- }
11143
- return actionResult;
12059
+ return await this.runWithPostActionWait(
12060
+ "select",
12061
+ options.wait,
12062
+ async () => {
12063
+ const actionResult = await performSelect(this.page, path5, options);
12064
+ if (!actionResult.ok) {
12065
+ const failure = actionResult.failure || classifyActionFailure({
12066
+ action: "select",
12067
+ error: actionResult.error || defaultActionFailureMessage("select"),
12068
+ fallbackMessage: defaultActionFailureMessage("select")
12069
+ });
12070
+ throw this.buildActionError(
12071
+ "select",
12072
+ options.description,
12073
+ failure,
12074
+ actionResult.usedSelector || null
12075
+ );
12076
+ }
12077
+ return actionResult;
12078
+ }
12079
+ );
11144
12080
  }
11145
12081
  );
11146
12082
  this.snapshotCache = null;
@@ -11182,13 +12118,23 @@ var Opensteer = class _Opensteer {
11182
12118
  );
11183
12119
  }
11184
12120
  const delta = getScrollDelta2(options);
11185
- await this.runWithPostActionWait("scroll", options.wait, async () => {
11186
- await handle.evaluate((el, value) => {
11187
- if (el instanceof HTMLElement) {
11188
- el.scrollBy(value.x, value.y);
11189
- }
11190
- }, delta);
11191
- });
12121
+ await this.runWithCursorPreview(
12122
+ () => this.resolveHandleTargetPoint(handle),
12123
+ "scroll",
12124
+ async () => {
12125
+ await this.runWithPostActionWait(
12126
+ "scroll",
12127
+ options.wait,
12128
+ async () => {
12129
+ await handle.evaluate((el, value) => {
12130
+ if (el instanceof HTMLElement) {
12131
+ el.scrollBy(value.x, value.y);
12132
+ }
12133
+ }, delta);
12134
+ }
12135
+ );
12136
+ }
12137
+ );
11192
12138
  } catch (err) {
11193
12139
  const failure = classifyActionFailure({
11194
12140
  action: "scroll",
@@ -11219,29 +12165,35 @@ var Opensteer = class _Opensteer {
11219
12165
  `[c="${resolution.counter}"]`
11220
12166
  );
11221
12167
  }
11222
- const result = await this.runWithPostActionWait(
12168
+ const result = await this.runWithCursorPreview(
12169
+ () => resolution.path ? this.resolvePathTargetPoint(resolution.path) : this.resolveViewportAnchorPoint(),
11223
12170
  "scroll",
11224
- options.wait,
11225
12171
  async () => {
11226
- const actionResult = await performScroll(
11227
- this.page,
11228
- resolution.path,
11229
- options
12172
+ return await this.runWithPostActionWait(
12173
+ "scroll",
12174
+ options.wait,
12175
+ async () => {
12176
+ const actionResult = await performScroll(
12177
+ this.page,
12178
+ resolution.path,
12179
+ options
12180
+ );
12181
+ if (!actionResult.ok) {
12182
+ const failure = actionResult.failure || classifyActionFailure({
12183
+ action: "scroll",
12184
+ error: actionResult.error || defaultActionFailureMessage("scroll"),
12185
+ fallbackMessage: defaultActionFailureMessage("scroll")
12186
+ });
12187
+ throw this.buildActionError(
12188
+ "scroll",
12189
+ options.description,
12190
+ failure,
12191
+ actionResult.usedSelector || null
12192
+ );
12193
+ }
12194
+ return actionResult;
12195
+ }
11230
12196
  );
11231
- if (!actionResult.ok) {
11232
- const failure = actionResult.failure || classifyActionFailure({
11233
- action: "scroll",
11234
- error: actionResult.error || defaultActionFailureMessage("scroll"),
11235
- fallbackMessage: defaultActionFailureMessage("scroll")
11236
- });
11237
- throw this.buildActionError(
11238
- "scroll",
11239
- options.description,
11240
- failure,
11241
- actionResult.usedSelector || null
11242
- );
11243
- }
11244
- return actionResult;
11245
12197
  }
11246
12198
  );
11247
12199
  this.snapshotCache = null;
@@ -11527,11 +12479,17 @@ var Opensteer = class _Opensteer {
11527
12479
  resolution.counter
11528
12480
  );
11529
12481
  }
11530
- await this.runWithPostActionWait(
12482
+ await this.runWithCursorPreview(
12483
+ () => this.resolveHandleTargetPoint(handle),
11531
12484
  "uploadFile",
11532
- options.wait,
11533
12485
  async () => {
11534
- await handle.setInputFiles(options.paths);
12486
+ await this.runWithPostActionWait(
12487
+ "uploadFile",
12488
+ options.wait,
12489
+ async () => {
12490
+ await handle.setInputFiles(options.paths);
12491
+ }
12492
+ );
11535
12493
  }
11536
12494
  );
11537
12495
  } catch (err) {
@@ -11568,29 +12526,35 @@ var Opensteer = class _Opensteer {
11568
12526
  throw new Error("Unable to resolve element path for file upload.");
11569
12527
  }
11570
12528
  const path5 = resolution.path;
11571
- const result = await this.runWithPostActionWait(
12529
+ const result = await this.runWithCursorPreview(
12530
+ () => this.resolvePathTargetPoint(path5),
11572
12531
  "uploadFile",
11573
- options.wait,
11574
12532
  async () => {
11575
- const actionResult = await performFileUpload(
11576
- this.page,
11577
- path5,
11578
- options.paths
12533
+ return await this.runWithPostActionWait(
12534
+ "uploadFile",
12535
+ options.wait,
12536
+ async () => {
12537
+ const actionResult = await performFileUpload(
12538
+ this.page,
12539
+ path5,
12540
+ options.paths
12541
+ );
12542
+ if (!actionResult.ok) {
12543
+ const failure = actionResult.failure || classifyActionFailure({
12544
+ action: "uploadFile",
12545
+ error: actionResult.error || defaultActionFailureMessage("uploadFile"),
12546
+ fallbackMessage: defaultActionFailureMessage("uploadFile")
12547
+ });
12548
+ throw this.buildActionError(
12549
+ "uploadFile",
12550
+ options.description,
12551
+ failure,
12552
+ actionResult.usedSelector || null
12553
+ );
12554
+ }
12555
+ return actionResult;
12556
+ }
11579
12557
  );
11580
- if (!actionResult.ok) {
11581
- const failure = actionResult.failure || classifyActionFailure({
11582
- action: "uploadFile",
11583
- error: actionResult.error || defaultActionFailureMessage("uploadFile"),
11584
- fallbackMessage: defaultActionFailureMessage("uploadFile")
11585
- });
11586
- throw this.buildActionError(
11587
- "uploadFile",
11588
- options.description,
11589
- failure,
11590
- actionResult.usedSelector || null
11591
- );
11592
- }
11593
- return actionResult;
11594
12558
  }
11595
12559
  );
11596
12560
  this.snapshotCache = null;
@@ -11726,6 +12690,25 @@ var Opensteer = class _Opensteer {
11726
12690
  getConfig() {
11727
12691
  return this.config;
11728
12692
  }
12693
+ setCursorEnabled(enabled) {
12694
+ this.getCursorController().setEnabled(enabled);
12695
+ }
12696
+ getCursorState() {
12697
+ const controller = this.cursorController;
12698
+ if (!controller) {
12699
+ return {
12700
+ enabled: this.config.cursor?.enabled === true,
12701
+ active: false,
12702
+ reason: this.config.cursor?.enabled === true ? "not_initialized" : "disabled"
12703
+ };
12704
+ }
12705
+ const status = controller.getStatus();
12706
+ return {
12707
+ enabled: status.enabled,
12708
+ active: status.active,
12709
+ reason: status.reason
12710
+ };
12711
+ }
11729
12712
  getStorage() {
11730
12713
  return this.storage;
11731
12714
  }
@@ -11753,24 +12736,107 @@ var Opensteer = class _Opensteer {
11753
12736
  this.agentExecutionInFlight = true;
11754
12737
  try {
11755
12738
  const options = normalizeExecuteOptions(instructionOrOptions);
12739
+ const cursorController = this.getCursorController();
12740
+ const previousCursorEnabled = cursorController.isEnabled();
12741
+ if (options.highlightCursor !== void 0) {
12742
+ cursorController.setEnabled(options.highlightCursor);
12743
+ }
11756
12744
  const handler = new OpensteerCuaAgentHandler({
11757
12745
  page: this.page,
11758
12746
  config: resolvedAgentConfig,
11759
12747
  client: createCuaClient(resolvedAgentConfig),
11760
- debug: Boolean(this.config.debug),
12748
+ cursorController,
11761
12749
  onMutatingAction: () => {
11762
12750
  this.snapshotCache = null;
11763
12751
  }
11764
12752
  });
11765
- const result = await handler.execute(options);
11766
- this.snapshotCache = null;
11767
- return result;
12753
+ try {
12754
+ const result = await handler.execute(options);
12755
+ this.snapshotCache = null;
12756
+ return result;
12757
+ } finally {
12758
+ if (options.highlightCursor !== void 0) {
12759
+ cursorController.setEnabled(previousCursorEnabled);
12760
+ }
12761
+ }
11768
12762
  } finally {
11769
12763
  this.agentExecutionInFlight = false;
11770
12764
  }
11771
12765
  }
11772
12766
  };
11773
12767
  }
12768
+ getCursorController() {
12769
+ if (!this.cursorController) {
12770
+ this.cursorController = new CursorController({
12771
+ config: this.config.cursor,
12772
+ debug: Boolean(this.config.debug)
12773
+ });
12774
+ if (this.pageRef) {
12775
+ void this.cursorController.attachPage(this.pageRef);
12776
+ }
12777
+ }
12778
+ return this.cursorController;
12779
+ }
12780
+ async runWithCursorPreview(pointResolver, intent, execute) {
12781
+ if (this.isCursorPreviewEnabled()) {
12782
+ const point = await pointResolver();
12783
+ await this.previewCursorPoint(point, intent);
12784
+ }
12785
+ return await execute();
12786
+ }
12787
+ isCursorPreviewEnabled() {
12788
+ return this.cursorController ? this.cursorController.isEnabled() : this.config.cursor?.enabled === true;
12789
+ }
12790
+ async previewCursorPoint(point, intent) {
12791
+ const cursor = this.getCursorController();
12792
+ await cursor.attachPage(this.page);
12793
+ await cursor.preview(point, intent);
12794
+ }
12795
+ resolveCursorPointFromBoundingBox(box, position) {
12796
+ if (position) {
12797
+ return {
12798
+ x: box.x + position.x,
12799
+ y: box.y + position.y
12800
+ };
12801
+ }
12802
+ return {
12803
+ x: box.x + box.width / 2,
12804
+ y: box.y + box.height / 2
12805
+ };
12806
+ }
12807
+ async resolveHandleTargetPoint(handle, position) {
12808
+ try {
12809
+ const box = await handle.boundingBox();
12810
+ if (!box) return null;
12811
+ return this.resolveCursorPointFromBoundingBox(box, position);
12812
+ } catch {
12813
+ return null;
12814
+ }
12815
+ }
12816
+ async resolvePathTargetPoint(path5, position) {
12817
+ if (!path5) {
12818
+ return null;
12819
+ }
12820
+ let resolved = null;
12821
+ try {
12822
+ resolved = await resolveElementPath(this.page, path5);
12823
+ return await this.resolveHandleTargetPoint(resolved.element, position);
12824
+ } catch {
12825
+ return null;
12826
+ } finally {
12827
+ await resolved?.element.dispose().catch(() => void 0);
12828
+ }
12829
+ }
12830
+ async resolveViewportAnchorPoint() {
12831
+ const viewport = this.page.viewportSize();
12832
+ if (viewport?.width && viewport?.height) {
12833
+ return {
12834
+ x: viewport.width / 2,
12835
+ y: viewport.height / 2
12836
+ };
12837
+ }
12838
+ return null;
12839
+ }
11774
12840
  async runWithPostActionWait(action, waitOverride, execute) {
11775
12841
  const waitSession = createPostActionWaitSession(
11776
12842
  this.page,
@@ -11803,13 +12869,19 @@ var Opensteer = class _Opensteer {
11803
12869
  resolution.counter
11804
12870
  );
11805
12871
  }
11806
- await this.runWithPostActionWait(method, options.wait, async () => {
11807
- await handle.click({
11808
- button: options.button,
11809
- clickCount: options.clickCount,
11810
- modifiers: options.modifiers
11811
- });
11812
- });
12872
+ await this.runWithCursorPreview(
12873
+ () => this.resolveHandleTargetPoint(handle),
12874
+ method,
12875
+ async () => {
12876
+ await this.runWithPostActionWait(method, options.wait, async () => {
12877
+ await handle.click({
12878
+ button: options.button,
12879
+ clickCount: options.clickCount,
12880
+ modifiers: options.modifiers
12881
+ });
12882
+ });
12883
+ }
12884
+ );
11813
12885
  } catch (err) {
11814
12886
  const failure = classifyActionFailure({
11815
12887
  action: method,
@@ -11844,25 +12916,31 @@ var Opensteer = class _Opensteer {
11844
12916
  throw new Error("Unable to resolve element path for click action.");
11845
12917
  }
11846
12918
  const path5 = resolution.path;
11847
- const result = await this.runWithPostActionWait(
12919
+ const result = await this.runWithCursorPreview(
12920
+ () => this.resolvePathTargetPoint(path5),
11848
12921
  method,
11849
- options.wait,
11850
12922
  async () => {
11851
- const actionResult = await performClick(this.page, path5, options);
11852
- if (!actionResult.ok) {
11853
- const failure = actionResult.failure || classifyActionFailure({
11854
- action: method,
11855
- error: actionResult.error || defaultActionFailureMessage(method),
11856
- fallbackMessage: defaultActionFailureMessage(method)
11857
- });
11858
- throw this.buildActionError(
11859
- method,
11860
- options.description,
11861
- failure,
11862
- actionResult.usedSelector || null
11863
- );
11864
- }
11865
- return actionResult;
12923
+ return await this.runWithPostActionWait(
12924
+ method,
12925
+ options.wait,
12926
+ async () => {
12927
+ const actionResult = await performClick(this.page, path5, options);
12928
+ if (!actionResult.ok) {
12929
+ const failure = actionResult.failure || classifyActionFailure({
12930
+ action: method,
12931
+ error: actionResult.error || defaultActionFailureMessage(method),
12932
+ fallbackMessage: defaultActionFailureMessage(method)
12933
+ });
12934
+ throw this.buildActionError(
12935
+ method,
12936
+ options.description,
12937
+ failure,
12938
+ actionResult.usedSelector || null
12939
+ );
12940
+ }
12941
+ return actionResult;
12942
+ }
12943
+ );
11866
12944
  }
11867
12945
  );
11868
12946
  this.snapshotCache = null;
@@ -12857,6 +13935,26 @@ function isInternalOrBlankPageUrl(url) {
12857
13935
  if (url === "about:blank") return true;
12858
13936
  return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
12859
13937
  }
13938
+ function normalizeCloudBrowserProfilePreference(value, source) {
13939
+ if (!value) {
13940
+ return void 0;
13941
+ }
13942
+ const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
13943
+ if (!profileId) {
13944
+ throw new Error(
13945
+ `Invalid cloud browser profile in ${source}: profileId must be a non-empty string.`
13946
+ );
13947
+ }
13948
+ if (value.reuseIfActive !== void 0 && typeof value.reuseIfActive !== "boolean") {
13949
+ throw new Error(
13950
+ `Invalid cloud browser profile in ${source}: reuseIfActive must be a boolean.`
13951
+ );
13952
+ }
13953
+ return {
13954
+ profileId,
13955
+ reuseIfActive: value.reuseIfActive
13956
+ };
13957
+ }
12860
13958
  function buildLocalRunId(namespace) {
12861
13959
  const normalized = namespace.trim() || "default";
12862
13960
  return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
@@ -12879,7 +13977,7 @@ function getMetadataPath(session2) {
12879
13977
  }
12880
13978
 
12881
13979
  // src/cli/commands.ts
12882
- var import_promises2 = require("fs/promises");
13980
+ var import_promises3 = require("fs/promises");
12883
13981
  var commands = {
12884
13982
  async navigate(opensteer, args) {
12885
13983
  const url = args.url;
@@ -12911,6 +14009,21 @@ var commands = {
12911
14009
  async state(opensteer) {
12912
14010
  return await opensteer.state();
12913
14011
  },
14012
+ async cursor(opensteer, args) {
14013
+ const mode = typeof args.mode === "string" ? args.mode : "status";
14014
+ if (mode === "on") {
14015
+ opensteer.setCursorEnabled(true);
14016
+ } else if (mode === "off") {
14017
+ opensteer.setCursorEnabled(false);
14018
+ } else if (mode !== "status") {
14019
+ throw new Error(
14020
+ `Invalid cursor mode "${String(mode)}". Use "on", "off", or "status".`
14021
+ );
14022
+ }
14023
+ return {
14024
+ cursor: opensteer.getCursorState()
14025
+ };
14026
+ },
12914
14027
  async screenshot(opensteer, args) {
12915
14028
  const file = args.file || "screenshot.png";
12916
14029
  const type = file.endsWith(".jpg") || file.endsWith(".jpeg") ? "jpeg" : "png";
@@ -12918,7 +14031,7 @@ var commands = {
12918
14031
  fullPage: args.fullPage,
12919
14032
  type
12920
14033
  });
12921
- await (0, import_promises2.writeFile)(file, buffer);
14034
+ await (0, import_promises3.writeFile)(file, buffer);
12922
14035
  return { file };
12923
14036
  },
12924
14037
  async click(opensteer, args) {
@@ -13110,10 +14223,175 @@ function getCommandHandler(name) {
13110
14223
  return commands[name];
13111
14224
  }
13112
14225
 
14226
+ // src/cli/cloud-profile-binding.ts
14227
+ function normalizeCloudProfileBinding(value) {
14228
+ if (!value) {
14229
+ return null;
14230
+ }
14231
+ const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
14232
+ if (!profileId) {
14233
+ return null;
14234
+ }
14235
+ return {
14236
+ profileId,
14237
+ reuseIfActive: typeof value.reuseIfActive === "boolean" ? value.reuseIfActive : void 0
14238
+ };
14239
+ }
14240
+ function resolveConfiguredCloudProfileBinding(config) {
14241
+ if (!isCloudConfigured(config)) {
14242
+ return null;
14243
+ }
14244
+ return normalizeCloudProfileBinding(config.cloud.browserProfile);
14245
+ }
14246
+ function resolveSessionCloudProfileBinding(config, requested) {
14247
+ if (!isCloudConfigured(config)) {
14248
+ return null;
14249
+ }
14250
+ return requested ?? resolveConfiguredCloudProfileBinding(config);
14251
+ }
14252
+ function assertCompatibleCloudProfileBinding(sessionId, active, requested) {
14253
+ if (!requested) {
14254
+ return;
14255
+ }
14256
+ if (!active) {
14257
+ throw new Error(
14258
+ [
14259
+ `Session '${sessionId}' is already running without a bound cloud browser profile.`,
14260
+ "Cloud browser profile selection only applies when the session is first opened.",
14261
+ "Close this session or use a different --session to target another profile."
14262
+ ].join(" ")
14263
+ );
14264
+ }
14265
+ if (active.profileId === requested.profileId && active.reuseIfActive === requested.reuseIfActive) {
14266
+ return;
14267
+ }
14268
+ throw new Error(
14269
+ [
14270
+ `Session '${sessionId}' is already bound to cloud browser profile ${formatCloudProfileBinding(active)}.`,
14271
+ `Requested ${formatCloudProfileBinding(requested)} does not match.`,
14272
+ "Use the same cloud profile for this session, or start a different --session."
14273
+ ].join(" ")
14274
+ );
14275
+ }
14276
+ function formatCloudProfileBinding(binding) {
14277
+ if (binding.reuseIfActive === void 0) {
14278
+ return `'${binding.profileId}'`;
14279
+ }
14280
+ return `'${binding.profileId}' (reuseIfActive=${String(
14281
+ binding.reuseIfActive
14282
+ )})`;
14283
+ }
14284
+ function isCloudConfigured(config) {
14285
+ return Boolean(
14286
+ config.cloud && typeof config.cloud === "object" && !Array.isArray(config.cloud)
14287
+ );
14288
+ }
14289
+
14290
+ // src/cli/open-cloud-auth.ts
14291
+ function normalizeCliOpenCloudAuth(value) {
14292
+ if (value == null) {
14293
+ return null;
14294
+ }
14295
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
14296
+ throw new Error("Invalid open request cloud auth payload.");
14297
+ }
14298
+ const record = value;
14299
+ const apiKey = normalizeNonEmptyString3(record.apiKey);
14300
+ const accessToken = normalizeNonEmptyString3(record.accessToken);
14301
+ const baseUrl = normalizeNonEmptyString3(record.baseUrl);
14302
+ const authScheme = normalizeAuthScheme(record.authScheme);
14303
+ if (!baseUrl) {
14304
+ throw new Error("Open request cloud auth payload is missing baseUrl.");
14305
+ }
14306
+ if ((apiKey ? 1 : 0) + (accessToken ? 1 : 0) !== 1) {
14307
+ throw new Error(
14308
+ "Open request cloud auth payload must include exactly one credential."
14309
+ );
14310
+ }
14311
+ if (accessToken && authScheme !== "bearer") {
14312
+ throw new Error(
14313
+ 'Open request cloud auth payload must use authScheme "bearer" with accessToken.'
14314
+ );
14315
+ }
14316
+ return {
14317
+ ...apiKey ? { apiKey } : {},
14318
+ ...accessToken ? { accessToken } : {},
14319
+ baseUrl,
14320
+ authScheme
14321
+ };
14322
+ }
14323
+ function buildServerOpenConfig(options) {
14324
+ const config = {
14325
+ name: options.name,
14326
+ storage: {
14327
+ rootDir: options.scopeDir
14328
+ },
14329
+ cursor: {
14330
+ enabled: options.cursorEnabled
14331
+ },
14332
+ browser: {
14333
+ headless: options.headless ?? false,
14334
+ connectUrl: options.connectUrl,
14335
+ channel: options.channel,
14336
+ profileDir: options.profileDir
14337
+ }
14338
+ };
14339
+ if (!options.cloudAuth) {
14340
+ return config;
14341
+ }
14342
+ const resolved = resolveConfigWithEnv(
14343
+ {
14344
+ storage: {
14345
+ rootDir: options.scopeDir
14346
+ }
14347
+ },
14348
+ {
14349
+ env: options.env
14350
+ }
14351
+ );
14352
+ const cloudSelection = resolveCloudSelection(
14353
+ {
14354
+ cloud: resolved.config.cloud
14355
+ },
14356
+ resolved.env
14357
+ );
14358
+ if (!cloudSelection.cloud) {
14359
+ return config;
14360
+ }
14361
+ config.cloud = toOpensteerCloudOptions(options.cloudAuth);
14362
+ return config;
14363
+ }
14364
+ function toOpensteerCloudOptions(auth) {
14365
+ return {
14366
+ ...auth.apiKey ? { apiKey: auth.apiKey } : {},
14367
+ ...auth.accessToken ? { accessToken: auth.accessToken } : {},
14368
+ baseUrl: auth.baseUrl,
14369
+ authScheme: auth.authScheme
14370
+ };
14371
+ }
14372
+ function normalizeNonEmptyString3(value) {
14373
+ if (typeof value !== "string") {
14374
+ return void 0;
14375
+ }
14376
+ const trimmed = value.trim();
14377
+ return trimmed.length > 0 ? trimmed : void 0;
14378
+ }
14379
+ function normalizeAuthScheme(value) {
14380
+ if (value === "api-key" || value === "bearer") {
14381
+ return value;
14382
+ }
14383
+ throw new Error(
14384
+ 'Open request cloud auth payload must use authScheme "api-key" or "bearer".'
14385
+ );
14386
+ }
14387
+
13113
14388
  // src/cli/server.ts
13114
14389
  var instance = null;
13115
14390
  var launchPromise = null;
13116
14391
  var selectorNamespace = null;
14392
+ var cloudProfileBinding = null;
14393
+ var cloudAuthOverride = null;
14394
+ var cursorEnabledPreference = readCursorPreferenceFromEnv();
13117
14395
  var requestQueue = Promise.resolve();
13118
14396
  var shuttingDown = false;
13119
14397
  function sanitizeNamespace(value) {
@@ -13131,6 +14409,36 @@ function invalidateInstance() {
13131
14409
  instance.close().catch(() => {
13132
14410
  });
13133
14411
  instance = null;
14412
+ cloudProfileBinding = null;
14413
+ }
14414
+ function normalizeCursorFlag(value) {
14415
+ if (value === void 0 || value === null) {
14416
+ return null;
14417
+ }
14418
+ if (typeof value === "boolean") {
14419
+ return value;
14420
+ }
14421
+ if (typeof value === "number") {
14422
+ if (value === 1) return true;
14423
+ if (value === 0) return false;
14424
+ }
14425
+ throw new Error(
14426
+ '--cursor must be a boolean value ("true" or "false").'
14427
+ );
14428
+ }
14429
+ function readCursorPreferenceFromEnv() {
14430
+ const value = process.env.OPENSTEER_CURSOR;
14431
+ if (typeof value !== "string") {
14432
+ return null;
14433
+ }
14434
+ const normalized = value.trim().toLowerCase();
14435
+ if (normalized === "true" || normalized === "1") {
14436
+ return true;
14437
+ }
14438
+ if (normalized === "false" || normalized === "0") {
14439
+ return false;
14440
+ }
14441
+ return null;
13134
14442
  }
13135
14443
  function attachLifecycleListeners(inst) {
13136
14444
  try {
@@ -13240,7 +14548,26 @@ async function handleRequest(request, socket) {
13240
14548
  const connectUrl = args["connect-url"];
13241
14549
  const channel = args.channel;
13242
14550
  const profileDir = args["profile-dir"];
14551
+ const cloudProfileId = typeof args["cloud-profile-id"] === "string" ? args["cloud-profile-id"].trim() : void 0;
14552
+ const cloudProfileReuseIfActive = typeof args["cloud-profile-reuse-if-active"] === "boolean" ? args["cloud-profile-reuse-if-active"] : void 0;
14553
+ const requestedCloudProfileBinding = normalizeCloudProfileBinding({
14554
+ profileId: cloudProfileId,
14555
+ reuseIfActive: cloudProfileReuseIfActive
14556
+ });
14557
+ const requestedCloudAuth = normalizeCliOpenCloudAuth(
14558
+ args["cloud-auth"]
14559
+ );
14560
+ if (cloudProfileReuseIfActive !== void 0 && !cloudProfileId) {
14561
+ throw new Error(
14562
+ "--cloud-profile-reuse-if-active requires --cloud-profile-id."
14563
+ );
14564
+ }
14565
+ const requestedCursor = normalizeCursorFlag(args.cursor);
13243
14566
  const requestedName = typeof args.name === "string" && args.name.trim().length > 0 ? sanitizeNamespace(args.name) : null;
14567
+ if (requestedCursor !== null) {
14568
+ cursorEnabledPreference = requestedCursor;
14569
+ }
14570
+ const effectiveCursorEnabled = cursorEnabledPreference !== null ? cursorEnabledPreference : true;
13244
14571
  if (selectorNamespace && requestedName && requestedName !== selectorNamespace) {
13245
14572
  sendResponse(socket, {
13246
14573
  id,
@@ -13264,6 +14591,9 @@ async function handleRequest(request, socket) {
13264
14591
  selectorNamespace = requestedName ?? logicalSession;
13265
14592
  }
13266
14593
  const activeNamespace = selectorNamespace ?? logicalSession;
14594
+ if (requestedCloudAuth) {
14595
+ cloudAuthOverride = requestedCloudAuth;
14596
+ }
13267
14597
  if (instance && !launchPromise) {
13268
14598
  try {
13269
14599
  if (instance.page.isClosed()) {
@@ -13273,31 +14603,59 @@ async function handleRequest(request, socket) {
13273
14603
  invalidateInstance();
13274
14604
  }
13275
14605
  }
14606
+ if (instance && !launchPromise) {
14607
+ assertCompatibleCloudProfileBinding(
14608
+ logicalSession,
14609
+ cloudProfileBinding,
14610
+ requestedCloudProfileBinding
14611
+ );
14612
+ }
13276
14613
  if (!instance) {
13277
- instance = new Opensteer({
13278
- name: activeNamespace,
13279
- browser: {
13280
- headless: headless ?? false,
14614
+ instance = new Opensteer(
14615
+ buildServerOpenConfig({
14616
+ scopeDir,
14617
+ name: activeNamespace,
14618
+ cursorEnabled: effectiveCursorEnabled,
14619
+ headless,
13281
14620
  connectUrl,
13282
14621
  channel,
13283
- profileDir
13284
- }
13285
- });
14622
+ profileDir,
14623
+ cloudAuth: cloudAuthOverride
14624
+ })
14625
+ );
14626
+ const nextCloudProfileBinding = resolveSessionCloudProfileBinding(
14627
+ instance.getConfig(),
14628
+ requestedCloudProfileBinding
14629
+ );
14630
+ if (requestedCloudProfileBinding && !nextCloudProfileBinding) {
14631
+ instance = null;
14632
+ throw new Error(
14633
+ "--cloud-profile-id can only be used when cloud mode is enabled for this session."
14634
+ );
14635
+ }
13286
14636
  launchPromise = instance.launch({
13287
14637
  headless: headless ?? false,
14638
+ cloudBrowserProfile: cloudProfileId ? {
14639
+ profileId: cloudProfileId,
14640
+ reuseIfActive: cloudProfileReuseIfActive
14641
+ } : void 0,
13288
14642
  timeout: connectUrl ? 12e4 : 3e4
13289
14643
  });
13290
14644
  try {
13291
14645
  await launchPromise;
13292
14646
  attachLifecycleListeners(instance);
14647
+ cloudProfileBinding = nextCloudProfileBinding;
13293
14648
  } catch (err) {
13294
14649
  instance = null;
14650
+ cloudProfileBinding = null;
13295
14651
  throw err;
13296
14652
  } finally {
13297
14653
  launchPromise = null;
13298
14654
  }
13299
14655
  } else if (launchPromise) {
13300
14656
  await launchPromise;
14657
+ } else if (requestedCursor !== null) {
14658
+ instance.setCursorEnabled(requestedCursor);
13301
14659
  }
13302
14660
  if (url) {
13303
14661
  await instance.goto(url);
@@ -13312,6 +14670,7 @@ async function handleRequest(request, socket) {
13312
14670
  runtimeSession: session,
13313
14671
  scopeDir,
13314
14672
  name: activeNamespace,
14673
+ cursor: instance.getCursorState(),
13315
14674
  cloudSessionId: instance.getCloudSessionId() ?? void 0,
13316
14675
  cloudSessionUrl: instance.getCloudSessionUrl() ?? void 0
13317
14676
  }
@@ -13324,6 +14683,41 @@ async function handleRequest(request, socket) {
13324
14683
  }
13325
14684
  return;
13326
14685
  }
14686
+ if (command === "cursor") {
14687
+ try {
14688
+ const mode = typeof args.mode === "string" ? args.mode : "status";
14689
+ if (mode === "on") {
14690
+ cursorEnabledPreference = true;
14691
+ instance?.setCursorEnabled(true);
14692
+ } else if (mode === "off") {
14693
+ cursorEnabledPreference = false;
14694
+ instance?.setCursorEnabled(false);
14695
+ } else if (mode !== "status") {
14696
+ throw new Error(
14697
+ `Invalid cursor mode "${mode}". Use "on", "off", or "status".`
14698
+ );
14699
+ }
14700
+ const defaultEnabled = cursorEnabledPreference !== null ? cursorEnabledPreference : true;
14701
+ const cursor = instance ? instance.getCursorState() : {
14702
+ enabled: defaultEnabled,
14703
+ active: false,
14704
+ reason: "session_not_open"
14705
+ };
14706
+ sendResponse(socket, {
14707
+ id,
14708
+ ok: true,
14709
+ result: {
14710
+ cursor
14711
+ }
14712
+ });
14713
+ } catch (err) {
14714
+ sendResponse(
14715
+ socket,
14716
+ buildErrorResponse(id, err, "Failed to update cursor mode.")
14717
+ );
14718
+ }
14719
+ return;
14720
+ }
13327
14721
  if (command === "close") {
13328
14722
  try {
13329
14723
  if (instance) {