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.
package/dist/index.cjs CHANGED
@@ -421,9 +421,12 @@ var init_extractor = __esm({
421
421
  var index_exports = {};
422
422
  __export(index_exports, {
423
423
  ActionWsClient: () => ActionWsClient,
424
+ BrowserProfileClient: () => BrowserProfileClient,
425
+ CdpOverlayCursorRenderer: () => CdpOverlayCursorRenderer,
424
426
  CloudCdpClient: () => CloudCdpClient,
425
427
  CloudSessionClient: () => CloudSessionClient,
426
428
  CounterResolutionError: () => CounterResolutionError,
429
+ CursorController: () => CursorController,
427
430
  ElementPathError: () => ElementPathError,
428
431
  LocalSelectorStorage: () => LocalSelectorStorage,
429
432
  OPENSTEER_HIDDEN_ATTR: () => OPENSTEER_HIDDEN_ATTR,
@@ -445,6 +448,7 @@ __export(index_exports, {
445
448
  OpensteerAgentProviderError: () => OpensteerAgentProviderError,
446
449
  OpensteerCloudError: () => OpensteerCloudError,
447
450
  OpensteerCuaAgentHandler: () => OpensteerCuaAgentHandler,
451
+ SvgCursorRenderer: () => SvgCursorRenderer,
448
452
  buildArrayFieldPathCandidates: () => buildArrayFieldPathCandidates,
449
453
  buildElementPathFromHandle: () => buildElementPathFromHandle,
450
454
  buildElementPathFromSelector: () => buildElementPathFromSelector,
@@ -489,6 +493,7 @@ __export(index_exports, {
489
493
  performInput: () => performInput,
490
494
  performScroll: () => performScroll,
491
495
  performSelect: () => performSelect,
496
+ planSnappyCursorMotion: () => planSnappyCursorMotion,
492
497
  prepareSnapshot: () => prepareSnapshot,
493
498
  pressKey: () => pressKey,
494
499
  queryAllByElementPath: () => queryAllByElementPath,
@@ -510,7 +515,7 @@ module.exports = __toCommonJS(index_exports);
510
515
  var import_crypto = require("crypto");
511
516
 
512
517
  // src/browser/pool.ts
513
- var import_playwright = require("playwright");
518
+ var import_playwright2 = require("playwright");
514
519
 
515
520
  // src/browser/cdp-proxy.ts
516
521
  var import_ws = __toESM(require("ws"), 1);
@@ -849,6 +854,19 @@ function errorMessage(error) {
849
854
  return error instanceof Error ? error.message : String(error);
850
855
  }
851
856
 
857
+ // src/browser/chromium-profile.ts
858
+ var import_node_util = require("util");
859
+ var import_node_child_process2 = require("child_process");
860
+ var import_node_crypto = require("crypto");
861
+ var import_promises = require("fs/promises");
862
+ var import_node_fs = require("fs");
863
+ var import_node_path = require("path");
864
+ var import_node_os = require("os");
865
+ var import_playwright = require("playwright");
866
+
867
+ // src/auth/keychain-store.ts
868
+ var import_node_child_process = require("child_process");
869
+
852
870
  // src/browser/chrome.ts
853
871
  var import_os = require("os");
854
872
  var import_path = require("path");
@@ -859,9 +877,61 @@ function expandHome(p) {
859
877
  return p;
860
878
  }
861
879
 
880
+ // src/browser/chromium-profile.ts
881
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
882
+ function directoryExists(filePath) {
883
+ try {
884
+ return (0, import_node_fs.statSync)(filePath).isDirectory();
885
+ } catch {
886
+ return false;
887
+ }
888
+ }
889
+ function fileExists(filePath) {
890
+ try {
891
+ return (0, import_node_fs.statSync)(filePath).isFile();
892
+ } catch {
893
+ return false;
894
+ }
895
+ }
896
+ function resolveCookieDbPath(profileDir) {
897
+ const candidates = [(0, import_node_path.join)(profileDir, "Network", "Cookies"), (0, import_node_path.join)(profileDir, "Cookies")];
898
+ for (const candidate of candidates) {
899
+ if (fileExists(candidate)) {
900
+ return candidate;
901
+ }
902
+ }
903
+ return null;
904
+ }
905
+ function resolvePersistentChromiumLaunchProfile(inputPath) {
906
+ const expandedPath = expandHome(inputPath.trim());
907
+ if (!expandedPath) {
908
+ return {
909
+ userDataDir: inputPath
910
+ };
911
+ }
912
+ if (fileExists(expandedPath) && (0, import_node_path.basename)(expandedPath) === "Cookies") {
913
+ const directParent = (0, import_node_path.dirname)(expandedPath);
914
+ const profileDir = (0, import_node_path.basename)(directParent) === "Network" ? (0, import_node_path.dirname)(directParent) : directParent;
915
+ return {
916
+ userDataDir: (0, import_node_path.dirname)(profileDir),
917
+ profileDirectory: (0, import_node_path.basename)(profileDir)
918
+ };
919
+ }
920
+ if (directoryExists(expandedPath) && resolveCookieDbPath(expandedPath) && fileExists((0, import_node_path.join)((0, import_node_path.dirname)(expandedPath), "Local State"))) {
921
+ return {
922
+ userDataDir: (0, import_node_path.dirname)(expandedPath),
923
+ profileDirectory: (0, import_node_path.basename)(expandedPath)
924
+ };
925
+ }
926
+ return {
927
+ userDataDir: expandedPath
928
+ };
929
+ }
930
+
862
931
  // src/browser/pool.ts
863
932
  var BrowserPool = class {
864
933
  browser = null;
934
+ persistentContext = null;
865
935
  cdpProxy = null;
866
936
  defaults;
867
937
  constructor(defaults = {}) {
@@ -877,16 +947,23 @@ var BrowserPool = class {
877
947
  if (connectUrl) {
878
948
  return this.connectToRunning(connectUrl, options.timeout);
879
949
  }
880
- if (channel || profileDir) {
881
- return this.launchWithProfile(options, channel, profileDir);
950
+ if (profileDir) {
951
+ return this.launchPersistentProfile(options, channel, profileDir);
952
+ }
953
+ if (channel) {
954
+ return this.launchWithChannel(options, channel);
882
955
  }
883
956
  return this.launchSandbox(options);
884
957
  }
885
958
  async close() {
886
959
  const browser = this.browser;
960
+ const persistentContext = this.persistentContext;
887
961
  this.browser = null;
962
+ this.persistentContext = null;
888
963
  try {
889
- if (browser) {
964
+ if (persistentContext) {
965
+ await persistentContext.close();
966
+ } else if (browser) {
890
967
  await browser.close();
891
968
  }
892
969
  } finally {
@@ -908,10 +985,11 @@ var BrowserPool = class {
908
985
  const target = targets[0];
909
986
  this.cdpProxy = new CDPProxy(browserWsUrl, target.id);
910
987
  const proxyWsUrl = await this.cdpProxy.start();
911
- browser = await import_playwright.chromium.connectOverCDP(proxyWsUrl, {
988
+ browser = await import_playwright2.chromium.connectOverCDP(proxyWsUrl, {
912
989
  timeout: timeout ?? 3e4
913
990
  });
914
991
  this.browser = browser;
992
+ this.persistentContext = null;
915
993
  const contexts = browser.contexts();
916
994
  if (contexts.length === 0) {
917
995
  throw new Error(
@@ -927,46 +1005,66 @@ var BrowserPool = class {
927
1005
  await browser.close().catch(() => void 0);
928
1006
  }
929
1007
  this.browser = null;
1008
+ this.persistentContext = null;
930
1009
  this.cdpProxy?.close();
931
1010
  this.cdpProxy = null;
932
1011
  throw error;
933
1012
  }
934
1013
  }
935
- async launchWithProfile(options, channel, profileDir) {
1014
+ async launchPersistentProfile(options, channel, profileDir) {
936
1015
  const args = [];
937
- if (profileDir) {
938
- args.push(`--user-data-dir=${expandHome(profileDir)}`);
1016
+ const launchProfile = resolvePersistentChromiumLaunchProfile(profileDir);
1017
+ if (launchProfile.profileDirectory) {
1018
+ args.push(`--profile-directory=${launchProfile.profileDirectory}`);
1019
+ }
1020
+ const context = await import_playwright2.chromium.launchPersistentContext(
1021
+ launchProfile.userDataDir,
1022
+ {
1023
+ channel,
1024
+ headless: options.headless ?? this.defaults.headless,
1025
+ executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
1026
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
1027
+ timeout: options.timeout,
1028
+ ...options.context || {},
1029
+ args
1030
+ }
1031
+ );
1032
+ const browser = context.browser();
1033
+ if (!browser) {
1034
+ await context.close().catch(() => void 0);
1035
+ throw new Error("Persistent browser launch did not expose a browser instance.");
939
1036
  }
940
- const browser = await import_playwright.chromium.launch({
1037
+ this.browser = browser;
1038
+ this.persistentContext = context;
1039
+ const pages = context.pages();
1040
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
1041
+ return { browser, context, page, isExternal: false };
1042
+ }
1043
+ async launchWithChannel(options, channel) {
1044
+ const browser = await import_playwright2.chromium.launch({
941
1045
  channel,
942
1046
  headless: options.headless ?? this.defaults.headless,
943
1047
  executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
944
1048
  slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
945
- args
1049
+ timeout: options.timeout
946
1050
  });
947
1051
  this.browser = browser;
948
- const contexts = browser.contexts();
949
- let context;
950
- let page;
951
- if (contexts.length > 0) {
952
- context = contexts[0];
953
- const pages = context.pages();
954
- page = pages.length > 0 ? pages[0] : await context.newPage();
955
- } else {
956
- context = await browser.newContext(options.context || {});
957
- page = await context.newPage();
958
- }
1052
+ this.persistentContext = null;
1053
+ const context = await browser.newContext(options.context || {});
1054
+ const page = await context.newPage();
959
1055
  return { browser, context, page, isExternal: false };
960
1056
  }
961
1057
  async launchSandbox(options) {
962
- const browser = await import_playwright.chromium.launch({
1058
+ const browser = await import_playwright2.chromium.launch({
963
1059
  headless: options.headless ?? this.defaults.headless,
964
1060
  executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
965
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0
1061
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
1062
+ timeout: options.timeout
966
1063
  });
967
1064
  const context = await browser.newContext(options.context || {});
968
1065
  const page = await context.newPage();
969
1066
  this.browser = browser;
1067
+ this.persistentContext = null;
970
1068
  return { browser, context, page, isExternal: false };
971
1069
  }
972
1070
  };
@@ -1248,6 +1346,10 @@ var DEFAULT_CONFIG = {
1248
1346
  storage: {
1249
1347
  rootDir: process.cwd()
1250
1348
  },
1349
+ cursor: {
1350
+ enabled: false,
1351
+ profile: "snappy"
1352
+ },
1251
1353
  model: "gpt-5.1",
1252
1354
  debug: false
1253
1355
  };
@@ -1301,7 +1403,7 @@ function loadDotenvValues(rootDir, baseEnv, options = {}) {
1301
1403
  return values;
1302
1404
  }
1303
1405
  function resolveEnv(rootDir, options = {}) {
1304
- const baseEnv = process.env;
1406
+ const baseEnv = options.baseEnv ?? process.env;
1305
1407
  const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
1306
1408
  return {
1307
1409
  ...dotenvValues,
@@ -1450,6 +1552,11 @@ function resolveOpensteerApiKey(env) {
1450
1552
  if (!value) return void 0;
1451
1553
  return value;
1452
1554
  }
1555
+ function resolveOpensteerAccessToken(env) {
1556
+ const value = env.OPENSTEER_ACCESS_TOKEN?.trim();
1557
+ if (!value) return void 0;
1558
+ return value;
1559
+ }
1453
1560
  function resolveOpensteerBaseUrl(env) {
1454
1561
  const value = env.OPENSTEER_BASE_URL?.trim();
1455
1562
  if (!value) return void 0;
@@ -1458,12 +1565,71 @@ function resolveOpensteerBaseUrl(env) {
1458
1565
  function resolveOpensteerAuthScheme(env) {
1459
1566
  return parseAuthScheme(env.OPENSTEER_AUTH_SCHEME, "OPENSTEER_AUTH_SCHEME");
1460
1567
  }
1568
+ function resolveOpensteerCloudProfileId(env) {
1569
+ const value = env.OPENSTEER_CLOUD_PROFILE_ID?.trim();
1570
+ if (!value) return void 0;
1571
+ return value;
1572
+ }
1573
+ function resolveOpensteerCloudProfileReuseIfActive(env) {
1574
+ return parseBool(env.OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE);
1575
+ }
1576
+ function parseCloudBrowserProfileReuseIfActive(value) {
1577
+ if (value == null) return void 0;
1578
+ if (typeof value !== "boolean") {
1579
+ throw new Error(
1580
+ `Invalid cloud.browserProfile.reuseIfActive value "${String(
1581
+ value
1582
+ )}". Use true or false.`
1583
+ );
1584
+ }
1585
+ return value;
1586
+ }
1587
+ function normalizeCloudBrowserProfileOptions(value, source) {
1588
+ if (value == null) {
1589
+ return void 0;
1590
+ }
1591
+ if (typeof value !== "object" || Array.isArray(value)) {
1592
+ throw new Error(
1593
+ `Invalid ${source} value "${String(value)}". Use an object with profileId and optional reuseIfActive.`
1594
+ );
1595
+ }
1596
+ const record = value;
1597
+ const rawProfileId = record.profileId;
1598
+ if (typeof rawProfileId !== "string" || !rawProfileId.trim()) {
1599
+ throw new Error(
1600
+ `${source}.profileId must be a non-empty string when browserProfile is provided.`
1601
+ );
1602
+ }
1603
+ return {
1604
+ profileId: rawProfileId.trim(),
1605
+ reuseIfActive: parseCloudBrowserProfileReuseIfActive(record.reuseIfActive)
1606
+ };
1607
+ }
1608
+ function resolveEnvCloudBrowserProfile(profileId, reuseIfActive) {
1609
+ if (reuseIfActive !== void 0 && !profileId) {
1610
+ throw new Error(
1611
+ "OPENSTEER_CLOUD_PROFILE_REUSE_IF_ACTIVE requires OPENSTEER_CLOUD_PROFILE_ID."
1612
+ );
1613
+ }
1614
+ if (!profileId) {
1615
+ return void 0;
1616
+ }
1617
+ return {
1618
+ profileId,
1619
+ reuseIfActive
1620
+ };
1621
+ }
1461
1622
  function normalizeCloudOptions(value) {
1462
1623
  if (!value || typeof value !== "object" || Array.isArray(value)) {
1463
1624
  return void 0;
1464
1625
  }
1465
1626
  return value;
1466
1627
  }
1628
+ function normalizeNonEmptyString(value) {
1629
+ if (typeof value !== "string") return void 0;
1630
+ const normalized = value.trim();
1631
+ return normalized.length ? normalized : void 0;
1632
+ }
1467
1633
  function parseCloudEnabled(value, source) {
1468
1634
  if (value == null) return void 0;
1469
1635
  if (typeof value === "boolean") return value;
@@ -1492,8 +1658,8 @@ function resolveCloudSelection(config, env = process.env) {
1492
1658
  source: "default"
1493
1659
  };
1494
1660
  }
1495
- function resolveConfigWithEnv(input = {}) {
1496
- const processEnv = process.env;
1661
+ function resolveConfigWithEnv(input = {}, options = {}) {
1662
+ const processEnv = options.env ?? process.env;
1497
1663
  const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
1498
1664
  const initialRootDir = input.storage?.rootDir ?? process.cwd();
1499
1665
  const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
@@ -1511,7 +1677,8 @@ function resolveConfigWithEnv(input = {}) {
1511
1677
  const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
1512
1678
  const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
1513
1679
  const env = resolveEnv(envRootDir, {
1514
- debug: debugHint
1680
+ debug: debugHint,
1681
+ baseEnv: processEnv
1515
1682
  });
1516
1683
  if (env.OPENSTEER_AI_MODEL) {
1517
1684
  throw new Error(
@@ -1532,6 +1699,9 @@ function resolveConfigWithEnv(input = {}) {
1532
1699
  channel: env.OPENSTEER_CHANNEL || void 0,
1533
1700
  profileDir: env.OPENSTEER_PROFILE_DIR || void 0
1534
1701
  },
1702
+ cursor: {
1703
+ enabled: parseBool(env.OPENSTEER_CURSOR)
1704
+ },
1535
1705
  model: env.OPENSTEER_MODEL || void 0,
1536
1706
  debug: parseBool(env.OPENSTEER_DEBUG)
1537
1707
  };
@@ -1539,13 +1709,27 @@ function resolveConfigWithEnv(input = {}) {
1539
1709
  const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
1540
1710
  const resolved = mergeDeep(mergedWithEnv, input);
1541
1711
  const envApiKey = resolveOpensteerApiKey(env);
1712
+ const envAccessTokenRaw = resolveOpensteerAccessToken(env);
1542
1713
  const envBaseUrl = resolveOpensteerBaseUrl(env);
1543
1714
  const envAuthScheme = resolveOpensteerAuthScheme(env);
1715
+ if (envApiKey && envAccessTokenRaw) {
1716
+ throw new Error(
1717
+ "OPENSTEER_API_KEY and OPENSTEER_ACCESS_TOKEN are mutually exclusive. Set only one."
1718
+ );
1719
+ }
1720
+ const envAccessToken = envAccessTokenRaw || (envAuthScheme === "bearer" ? envApiKey : void 0);
1721
+ const envApiCredential = envAuthScheme === "bearer" && !envAccessTokenRaw ? void 0 : envApiKey;
1722
+ const envCloudProfileId = resolveOpensteerCloudProfileId(env);
1723
+ const envCloudProfileReuseIfActive = resolveOpensteerCloudProfileReuseIfActive(env);
1544
1724
  const envCloudAnnounce = parseCloudAnnounce(
1545
1725
  env.OPENSTEER_REMOTE_ANNOUNCE,
1546
1726
  "OPENSTEER_REMOTE_ANNOUNCE"
1547
1727
  );
1548
1728
  const inputCloudOptions = normalizeCloudOptions(input.cloud);
1729
+ const inputCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1730
+ inputCloudOptions?.browserProfile,
1731
+ "cloud.browserProfile"
1732
+ );
1549
1733
  const inputAuthScheme = parseAuthScheme(
1550
1734
  inputCloudOptions?.authScheme,
1551
1735
  "cloud.authScheme"
@@ -1557,26 +1741,65 @@ function resolveConfigWithEnv(input = {}) {
1557
1741
  const inputHasCloudApiKey = Boolean(
1558
1742
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "apiKey")
1559
1743
  );
1744
+ const inputHasCloudAccessToken = Boolean(
1745
+ inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "accessToken")
1746
+ );
1560
1747
  const inputHasCloudBaseUrl = Boolean(
1561
1748
  inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
1562
1749
  );
1750
+ if (normalizeNonEmptyString(inputCloudOptions?.apiKey) && normalizeNonEmptyString(inputCloudOptions?.accessToken)) {
1751
+ throw new Error(
1752
+ "cloud.apiKey and cloud.accessToken are mutually exclusive. Set only one."
1753
+ );
1754
+ }
1563
1755
  const cloudSelection = resolveCloudSelection({
1564
1756
  cloud: resolved.cloud
1565
1757
  }, env);
1566
1758
  if (cloudSelection.cloud) {
1567
1759
  const resolvedCloud = normalizeCloudOptions(resolved.cloud) ?? {};
1568
- const authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1760
+ const {
1761
+ apiKey: resolvedCloudApiKeyRaw,
1762
+ accessToken: resolvedCloudAccessTokenRaw,
1763
+ ...resolvedCloudRest
1764
+ } = resolvedCloud;
1765
+ if (normalizeNonEmptyString(resolvedCloudApiKeyRaw) && normalizeNonEmptyString(resolvedCloudAccessTokenRaw)) {
1766
+ throw new Error(
1767
+ "Cloud config cannot include both apiKey and accessToken at the same time."
1768
+ );
1769
+ }
1770
+ const resolvedCloudBrowserProfile = normalizeCloudBrowserProfileOptions(
1771
+ resolvedCloud.browserProfile,
1772
+ "resolved.cloud.browserProfile"
1773
+ );
1774
+ const envCloudBrowserProfile = resolveEnvCloudBrowserProfile(
1775
+ envCloudProfileId,
1776
+ envCloudProfileReuseIfActive
1777
+ );
1778
+ const browserProfile = inputCloudBrowserProfile ?? envCloudBrowserProfile ?? resolvedCloudBrowserProfile;
1779
+ let authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
1569
1780
  const announce = inputCloudAnnounce ?? envCloudAnnounce ?? parseCloudAnnounce(resolvedCloud.announce, "cloud.announce") ?? "always";
1781
+ const credentialOverriddenByInput = inputHasCloudApiKey || inputHasCloudAccessToken;
1782
+ let apiKey = normalizeNonEmptyString(resolvedCloudApiKeyRaw);
1783
+ let accessToken = normalizeNonEmptyString(resolvedCloudAccessTokenRaw);
1784
+ if (!credentialOverriddenByInput) {
1785
+ if (envAccessToken) {
1786
+ accessToken = envAccessToken;
1787
+ apiKey = void 0;
1788
+ } else if (envApiCredential) {
1789
+ apiKey = envApiCredential;
1790
+ accessToken = void 0;
1791
+ }
1792
+ }
1793
+ if (accessToken) {
1794
+ authScheme = "bearer";
1795
+ }
1570
1796
  resolved.cloud = {
1571
- ...resolvedCloud,
1797
+ ...resolvedCloudRest,
1798
+ ...inputHasCloudApiKey ? { apiKey: resolvedCloudApiKeyRaw } : apiKey ? { apiKey } : {},
1799
+ ...inputHasCloudAccessToken ? { accessToken: resolvedCloudAccessTokenRaw } : accessToken ? { accessToken } : {},
1572
1800
  authScheme,
1573
- announce
1574
- };
1575
- }
1576
- if (envApiKey && cloudSelection.cloud && !inputHasCloudApiKey) {
1577
- resolved.cloud = {
1578
- ...normalizeCloudOptions(resolved.cloud) ?? {},
1579
- apiKey: envApiKey
1801
+ announce,
1802
+ ...browserProfile ? { browserProfile } : {}
1580
1803
  };
1581
1804
  }
1582
1805
  if (envBaseUrl && cloudSelection.cloud && !inputHasCloudBaseUrl) {
@@ -4969,6 +5192,16 @@ async function probeActionabilityState(element) {
4969
5192
 
4970
5193
  // src/extract-value-normalization.ts
4971
5194
  var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
5195
+ var IFRAME_URL_ATTRIBUTES = /* @__PURE__ */ new Set([
5196
+ "href",
5197
+ "src",
5198
+ "srcset",
5199
+ "imagesrcset",
5200
+ "action",
5201
+ "formaction",
5202
+ "poster",
5203
+ "ping"
5204
+ ]);
4972
5205
  function normalizeExtractedValue(raw, attribute) {
4973
5206
  if (raw == null) return null;
4974
5207
  const rawText = String(raw);
@@ -4984,6 +5217,19 @@ function normalizeExtractedValue(raw, attribute) {
4984
5217
  const text = rawText.replace(/\s+/g, " ").trim();
4985
5218
  return text || null;
4986
5219
  }
5220
+ function resolveExtractedValueInContext(normalizedValue, options) {
5221
+ if (normalizedValue == null) return null;
5222
+ const normalizedAttribute = String(options.attribute || "").trim().toLowerCase();
5223
+ if (!options.insideIframe) return normalizedValue;
5224
+ if (!IFRAME_URL_ATTRIBUTES.has(normalizedAttribute)) return normalizedValue;
5225
+ const baseURI = String(options.baseURI || "").trim();
5226
+ if (!baseURI) return normalizedValue;
5227
+ try {
5228
+ return new URL(normalizedValue, baseURI).href;
5229
+ } catch {
5230
+ return normalizedValue;
5231
+ }
5232
+ }
4987
5233
  function pickSingleListAttributeValue(attribute, raw) {
4988
5234
  if (attribute === "ping") {
4989
5235
  const firstUrl = raw.trim().split(/\s+/)[0] || "";
@@ -5139,6 +5385,36 @@ function readDescriptorToken(value, index) {
5139
5385
  };
5140
5386
  }
5141
5387
 
5388
+ // src/extract-value-reader.ts
5389
+ async function readExtractedValueFromHandle(element, options) {
5390
+ const insideIframe = await isElementInsideIframe(element);
5391
+ const payload = await element.evaluate(
5392
+ (target, browserOptions) => {
5393
+ const ownerDocument = target.ownerDocument;
5394
+ return {
5395
+ raw: browserOptions.attribute ? target.getAttribute(browserOptions.attribute) : target.textContent,
5396
+ baseURI: ownerDocument?.baseURI || null
5397
+ };
5398
+ },
5399
+ {
5400
+ attribute: options.attribute
5401
+ }
5402
+ );
5403
+ const normalizedValue = normalizeExtractedValue(
5404
+ payload.raw,
5405
+ options.attribute
5406
+ );
5407
+ return resolveExtractedValueInContext(normalizedValue, {
5408
+ attribute: options.attribute,
5409
+ baseURI: payload.baseURI,
5410
+ insideIframe
5411
+ });
5412
+ }
5413
+ async function isElementInsideIframe(element) {
5414
+ const ownerFrame = await element.ownerFrame();
5415
+ return !!ownerFrame?.parentFrame();
5416
+ }
5417
+
5142
5418
  // src/html/counter-runtime.ts
5143
5419
  var CounterResolutionError = class extends Error {
5144
5420
  code;
@@ -5161,13 +5437,14 @@ async function resolveCounterElement(page, counter) {
5161
5437
  if (entry.count > 1) {
5162
5438
  throw buildCounterAmbiguousError(counter);
5163
5439
  }
5164
- const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
5165
- const element = handle.asElement();
5166
- if (!element) {
5167
- await handle.dispose();
5440
+ const resolution = await resolveCounterElementInFrame(entry.frame, normalized);
5441
+ if (resolution.status === "ambiguous") {
5442
+ throw buildCounterAmbiguousError(counter);
5443
+ }
5444
+ if (resolution.status === "missing") {
5168
5445
  throw buildCounterNotFoundError(counter);
5169
5446
  }
5170
- return element;
5447
+ return resolution.element;
5171
5448
  }
5172
5449
  async function resolveCountersBatch(page, requests) {
5173
5450
  const out = {};
@@ -5181,41 +5458,57 @@ async function resolveCountersBatch(page, requests) {
5181
5458
  }
5182
5459
  }
5183
5460
  const valueCache = /* @__PURE__ */ new Map();
5184
- for (const request of requests) {
5185
- const normalized = normalizeCounter(request.counter);
5186
- if (normalized == null) {
5187
- out[request.key] = null;
5188
- continue;
5189
- }
5190
- const entry = scan.get(normalized);
5191
- if (!entry || entry.count <= 0 || !entry.frame) {
5192
- out[request.key] = null;
5193
- continue;
5194
- }
5195
- const cacheKey = `${normalized}:${request.attribute || ""}`;
5196
- if (valueCache.has(cacheKey)) {
5197
- out[request.key] = valueCache.get(cacheKey);
5198
- continue;
5199
- }
5200
- const read = await readCounterValueInFrame(
5201
- entry.frame,
5202
- normalized,
5203
- request.attribute
5204
- );
5205
- if (read.status === "ambiguous") {
5206
- throw buildCounterAmbiguousError(normalized);
5207
- }
5208
- if (read.status === "missing") {
5209
- valueCache.set(cacheKey, null);
5210
- out[request.key] = null;
5211
- continue;
5461
+ const elementCache = /* @__PURE__ */ new Map();
5462
+ try {
5463
+ for (const request of requests) {
5464
+ const normalized = normalizeCounter(request.counter);
5465
+ if (normalized == null) {
5466
+ out[request.key] = null;
5467
+ continue;
5468
+ }
5469
+ const entry = scan.get(normalized);
5470
+ if (!entry || entry.count <= 0 || !entry.frame) {
5471
+ out[request.key] = null;
5472
+ continue;
5473
+ }
5474
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
5475
+ if (valueCache.has(cacheKey)) {
5476
+ out[request.key] = valueCache.get(cacheKey);
5477
+ continue;
5478
+ }
5479
+ if (!elementCache.has(normalized)) {
5480
+ elementCache.set(
5481
+ normalized,
5482
+ await resolveCounterElementInFrame(entry.frame, normalized)
5483
+ );
5484
+ }
5485
+ const resolution = elementCache.get(normalized);
5486
+ if (resolution.status === "ambiguous") {
5487
+ throw buildCounterAmbiguousError(normalized);
5488
+ }
5489
+ if (resolution.status === "missing") {
5490
+ valueCache.set(cacheKey, null);
5491
+ out[request.key] = null;
5492
+ continue;
5493
+ }
5494
+ const value = await readCounterValueFromElement(
5495
+ resolution.element,
5496
+ request.attribute
5497
+ );
5498
+ if (value.status === "missing") {
5499
+ await resolution.element.dispose();
5500
+ elementCache.set(normalized, {
5501
+ status: "missing"
5502
+ });
5503
+ valueCache.set(cacheKey, null);
5504
+ out[request.key] = null;
5505
+ continue;
5506
+ }
5507
+ valueCache.set(cacheKey, value.value);
5508
+ out[request.key] = value.value;
5212
5509
  }
5213
- const normalizedValue = normalizeExtractedValue(
5214
- read.value ?? null,
5215
- request.attribute
5216
- );
5217
- valueCache.set(cacheKey, normalizedValue);
5218
- out[request.key] = normalizedValue;
5510
+ } finally {
5511
+ await disposeResolvedCounterElements(elementCache.values());
5219
5512
  }
5220
5513
  return out;
5221
5514
  }
@@ -5287,73 +5580,79 @@ async function scanCounterOccurrences(page, counters) {
5287
5580
  }
5288
5581
  return out;
5289
5582
  }
5290
- async function resolveUniqueHandleInFrame(frame, counter) {
5291
- return frame.evaluateHandle((targetCounter) => {
5292
- const matches = [];
5293
- const walk = (root) => {
5294
- const children = Array.from(root.children);
5295
- for (const child of children) {
5296
- if (child.getAttribute("c") === targetCounter) {
5297
- matches.push(child);
5298
- }
5299
- walk(child);
5300
- if (child.shadowRoot) {
5301
- walk(child.shadowRoot);
5583
+ async function resolveCounterElementInFrame(frame, counter) {
5584
+ try {
5585
+ const handle = await frame.evaluateHandle((targetCounter) => {
5586
+ const matches = [];
5587
+ const walk = (root) => {
5588
+ const children = Array.from(root.children);
5589
+ for (const child of children) {
5590
+ if (child.getAttribute("c") === targetCounter) {
5591
+ matches.push(child);
5592
+ }
5593
+ walk(child);
5594
+ if (child.shadowRoot) {
5595
+ walk(child.shadowRoot);
5596
+ }
5302
5597
  }
5598
+ };
5599
+ walk(document);
5600
+ if (!matches.length) {
5601
+ return "missing";
5303
5602
  }
5304
- };
5305
- walk(document);
5306
- if (matches.length !== 1) {
5307
- return null;
5603
+ if (matches.length > 1) {
5604
+ return "ambiguous";
5605
+ }
5606
+ return matches[0];
5607
+ }, String(counter));
5608
+ const element = handle.asElement();
5609
+ if (element) {
5610
+ return {
5611
+ status: "resolved",
5612
+ element
5613
+ };
5614
+ }
5615
+ const status = await handle.jsonValue();
5616
+ await handle.dispose();
5617
+ return status === "ambiguous" ? { status: "ambiguous" } : { status: "missing" };
5618
+ } catch (error) {
5619
+ if (isRecoverableCounterReadRace(error)) {
5620
+ return {
5621
+ status: "missing"
5622
+ };
5308
5623
  }
5309
- return matches[0];
5310
- }, String(counter));
5624
+ throw error;
5625
+ }
5311
5626
  }
5312
- async function readCounterValueInFrame(frame, counter, attribute) {
5627
+ async function readCounterValueFromElement(element, attribute) {
5313
5628
  try {
5314
- return await frame.evaluate(
5315
- ({ targetCounter, attribute: attribute2 }) => {
5316
- const matches = [];
5317
- const walk = (root) => {
5318
- const children = Array.from(root.children);
5319
- for (const child of children) {
5320
- if (child.getAttribute("c") === targetCounter) {
5321
- matches.push(child);
5322
- }
5323
- walk(child);
5324
- if (child.shadowRoot) {
5325
- walk(child.shadowRoot);
5326
- }
5327
- }
5328
- };
5329
- walk(document);
5330
- if (!matches.length) {
5331
- return {
5332
- status: "missing"
5333
- };
5334
- }
5335
- if (matches.length > 1) {
5336
- return {
5337
- status: "ambiguous"
5338
- };
5339
- }
5340
- const target = matches[0];
5341
- const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
5342
- return {
5343
- status: "ok",
5344
- value
5345
- };
5346
- },
5347
- {
5348
- targetCounter: String(counter),
5349
- attribute
5350
- }
5351
- );
5352
- } catch {
5353
5629
  return {
5354
- status: "missing"
5630
+ status: "ok",
5631
+ value: await readExtractedValueFromHandle(element, {
5632
+ attribute
5633
+ })
5355
5634
  };
5635
+ } catch (error) {
5636
+ if (isRecoverableCounterReadRace(error)) {
5637
+ return {
5638
+ status: "missing"
5639
+ };
5640
+ }
5641
+ throw error;
5642
+ }
5643
+ }
5644
+ async function disposeResolvedCounterElements(resolutions) {
5645
+ const disposals = [];
5646
+ for (const resolution of resolutions) {
5647
+ if (resolution.status !== "resolved") continue;
5648
+ disposals.push(resolution.element.dispose());
5356
5649
  }
5650
+ await Promise.all(disposals);
5651
+ }
5652
+ function isRecoverableCounterReadRace(error) {
5653
+ if (!(error instanceof Error)) return false;
5654
+ const message = error.message;
5655
+ 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");
5357
5656
  }
5358
5657
  function buildCounterNotFoundError(counter) {
5359
5658
  return new CounterResolutionError(
@@ -5969,17 +6268,6 @@ async function performSelect(page, path5, options) {
5969
6268
  }
5970
6269
 
5971
6270
  // src/actions/extract.ts
5972
- async function readFieldValueFromHandle(element, options) {
5973
- const raw = await element.evaluate(
5974
- (target, payload) => {
5975
- return payload.attribute ? target.getAttribute(payload.attribute) : target.textContent;
5976
- },
5977
- {
5978
- attribute: options.attribute
5979
- }
5980
- );
5981
- return normalizeExtractedValue(raw, options.attribute);
5982
- }
5983
6271
  async function extractWithPaths(page, fields) {
5984
6272
  const result = {};
5985
6273
  for (const field of fields) {
@@ -5991,7 +6279,7 @@ async function extractWithPaths(page, fields) {
5991
6279
  continue;
5992
6280
  }
5993
6281
  try {
5994
- result[field.key] = await readFieldValueFromHandle(
6282
+ result[field.key] = await readExtractedValueFromHandle(
5995
6283
  resolved.element,
5996
6284
  {
5997
6285
  attribute: field.attribute
@@ -6031,7 +6319,7 @@ async function extractArrayRowsWithPaths(page, array) {
6031
6319
  field.candidates
6032
6320
  ) : item;
6033
6321
  try {
6034
- const value = target ? await readFieldValueFromHandle(target, {
6322
+ const value = target ? await readExtractedValueFromHandle(target, {
6035
6323
  attribute: field.attribute
6036
6324
  }) : null;
6037
6325
  if (key) {
@@ -6349,7 +6637,7 @@ async function closeTab(context, activePage, index) {
6349
6637
  }
6350
6638
 
6351
6639
  // src/actions/cookies.ts
6352
- var import_promises = require("fs/promises");
6640
+ var import_promises2 = require("fs/promises");
6353
6641
  async function getCookies(context, url) {
6354
6642
  return context.cookies(url ? [url] : void 0);
6355
6643
  }
@@ -6361,10 +6649,10 @@ async function clearCookies(context) {
6361
6649
  }
6362
6650
  async function exportCookies(context, filePath, url) {
6363
6651
  const cookies = await context.cookies(url ? [url] : void 0);
6364
- await (0, import_promises.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
6652
+ await (0, import_promises2.writeFile)(filePath, JSON.stringify(cookies, null, 2), "utf-8");
6365
6653
  }
6366
6654
  async function importCookies(context, filePath) {
6367
- const raw = await (0, import_promises.readFile)(filePath, "utf-8");
6655
+ const raw = await (0, import_promises2.readFile)(filePath, "utf-8");
6368
6656
  const cookies = JSON.parse(raw);
6369
6657
  await context.addCookies(cookies);
6370
6658
  }
@@ -8331,13 +8619,13 @@ function dedupeNewest(entries) {
8331
8619
  }
8332
8620
 
8333
8621
  // src/cloud/cdp-client.ts
8334
- var import_playwright2 = require("playwright");
8622
+ var import_playwright3 = require("playwright");
8335
8623
  var CloudCdpClient = class {
8336
8624
  async connect(args) {
8337
8625
  const endpoint = withTokenQuery2(args.wsUrl, args.token);
8338
8626
  let browser;
8339
8627
  try {
8340
- browser = await import_playwright2.chromium.connectOverCDP(endpoint);
8628
+ browser = await import_playwright3.chromium.connectOverCDP(endpoint);
8341
8629
  } catch (error) {
8342
8630
  const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
8343
8631
  throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
@@ -8392,32 +8680,73 @@ function withTokenQuery2(wsUrl, token) {
8392
8680
  return url.toString();
8393
8681
  }
8394
8682
 
8395
- // src/cloud/session-client.ts
8396
- var CACHE_IMPORT_BATCH_SIZE = 200;
8397
- var CloudSessionClient = class {
8398
- baseUrl;
8399
- key;
8400
- authScheme;
8401
- constructor(baseUrl, key, authScheme = "api-key") {
8402
- this.baseUrl = normalizeBaseUrl(baseUrl);
8403
- this.key = key;
8404
- this.authScheme = authScheme;
8683
+ // src/utils/strip-trailing-slashes.ts
8684
+ function stripTrailingSlashes(value) {
8685
+ let end = value.length;
8686
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
8687
+ end -= 1;
8405
8688
  }
8406
- async create(request) {
8407
- const response = await fetch(`${this.baseUrl}/sessions`, {
8408
- method: "POST",
8409
- headers: {
8410
- "content-type": "application/json",
8411
- ...this.authHeaders()
8412
- },
8413
- body: JSON.stringify(request)
8414
- });
8415
- if (!response.ok) {
8416
- throw await parseHttpError(response);
8417
- }
8418
- let body;
8419
- try {
8420
- body = await response.json();
8689
+ return end === value.length ? value : value.slice(0, end);
8690
+ }
8691
+
8692
+ // src/cloud/http-client.ts
8693
+ function normalizeCloudBaseUrl(baseUrl) {
8694
+ return stripTrailingSlashes(baseUrl);
8695
+ }
8696
+ function cloudAuthHeaders(key, authScheme) {
8697
+ if (authScheme === "bearer") {
8698
+ return {
8699
+ authorization: `Bearer ${key}`
8700
+ };
8701
+ }
8702
+ return {
8703
+ "x-api-key": key
8704
+ };
8705
+ }
8706
+ async function parseCloudHttpError(response) {
8707
+ let body = null;
8708
+ try {
8709
+ body = await response.json();
8710
+ } catch {
8711
+ body = null;
8712
+ }
8713
+ const code = typeof body?.code === "string" ? toCloudErrorCode(body.code) : "CLOUD_TRANSPORT_ERROR";
8714
+ const message = typeof body?.error === "string" ? body.error : `Cloud request failed with status ${response.status}.`;
8715
+ return new OpensteerCloudError(code, message, response.status, body?.details);
8716
+ }
8717
+ function toCloudErrorCode(code) {
8718
+ 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") {
8719
+ return code;
8720
+ }
8721
+ return "CLOUD_TRANSPORT_ERROR";
8722
+ }
8723
+
8724
+ // src/cloud/session-client.ts
8725
+ var CACHE_IMPORT_BATCH_SIZE = 200;
8726
+ var CloudSessionClient = class {
8727
+ baseUrl;
8728
+ key;
8729
+ authScheme;
8730
+ constructor(baseUrl, key, authScheme = "api-key") {
8731
+ this.baseUrl = normalizeCloudBaseUrl(baseUrl);
8732
+ this.key = key;
8733
+ this.authScheme = authScheme;
8734
+ }
8735
+ async create(request) {
8736
+ const response = await fetch(`${this.baseUrl}/sessions`, {
8737
+ method: "POST",
8738
+ headers: {
8739
+ "content-type": "application/json",
8740
+ ...cloudAuthHeaders(this.key, this.authScheme)
8741
+ },
8742
+ body: JSON.stringify(request)
8743
+ });
8744
+ if (!response.ok) {
8745
+ throw await parseCloudHttpError(response);
8746
+ }
8747
+ let body;
8748
+ try {
8749
+ body = await response.json();
8421
8750
  } catch {
8422
8751
  throw new OpensteerCloudError(
8423
8752
  "CLOUD_CONTRACT_MISMATCH",
@@ -8431,14 +8760,14 @@ var CloudSessionClient = class {
8431
8760
  const response = await fetch(`${this.baseUrl}/sessions/${sessionId}`, {
8432
8761
  method: "DELETE",
8433
8762
  headers: {
8434
- ...this.authHeaders()
8763
+ ...cloudAuthHeaders(this.key, this.authScheme)
8435
8764
  }
8436
8765
  });
8437
8766
  if (response.status === 204) {
8438
8767
  return;
8439
8768
  }
8440
8769
  if (!response.ok) {
8441
- throw await parseHttpError(response);
8770
+ throw await parseCloudHttpError(response);
8442
8771
  }
8443
8772
  }
8444
8773
  async importSelectorCache(request) {
@@ -8461,29 +8790,16 @@ var CloudSessionClient = class {
8461
8790
  method: "POST",
8462
8791
  headers: {
8463
8792
  "content-type": "application/json",
8464
- ...this.authHeaders()
8793
+ ...cloudAuthHeaders(this.key, this.authScheme)
8465
8794
  },
8466
8795
  body: JSON.stringify({ entries })
8467
8796
  });
8468
8797
  if (!response.ok) {
8469
- throw await parseHttpError(response);
8798
+ throw await parseCloudHttpError(response);
8470
8799
  }
8471
8800
  return await response.json();
8472
8801
  }
8473
- authHeaders() {
8474
- if (this.authScheme === "bearer") {
8475
- return {
8476
- authorization: `Bearer ${this.key}`
8477
- };
8478
- }
8479
- return {
8480
- "x-api-key": this.key
8481
- };
8482
- }
8483
8802
  };
8484
- function normalizeBaseUrl(baseUrl) {
8485
- return baseUrl.replace(/\/+$/, "");
8486
- }
8487
8803
  function parseCreateResponse(body, status) {
8488
8804
  const root = requireObject(
8489
8805
  body,
@@ -8628,23 +8944,6 @@ function mergeImportResponse(first, second) {
8628
8944
  skipped: first.skipped + second.skipped
8629
8945
  };
8630
8946
  }
8631
- async function parseHttpError(response) {
8632
- let body = null;
8633
- try {
8634
- body = await response.json();
8635
- } catch {
8636
- body = null;
8637
- }
8638
- const code = typeof body?.code === "string" ? toCloudErrorCode(body.code) : "CLOUD_TRANSPORT_ERROR";
8639
- const message = typeof body?.error === "string" ? body.error : `Cloud request failed with status ${response.status}.`;
8640
- return new OpensteerCloudError(code, message, response.status, body?.details);
8641
- }
8642
- function toCloudErrorCode(code) {
8643
- 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") {
8644
- return code;
8645
- }
8646
- return "CLOUD_TRANSPORT_ERROR";
8647
- }
8648
8947
 
8649
8948
  // src/cloud/runtime.ts
8650
8949
  var DEFAULT_CLOUD_BASE_URL = "https://api.opensteer.com";
@@ -8669,9 +8968,6 @@ function resolveCloudBaseUrl() {
8669
8968
  if (!value) return DEFAULT_CLOUD_BASE_URL;
8670
8969
  return normalizeCloudBaseUrl(value);
8671
8970
  }
8672
- function normalizeCloudBaseUrl(value) {
8673
- return value.replace(/\/+$/, "");
8674
- }
8675
8971
  function readCloudActionDescription(payload) {
8676
8972
  const description = payload.description;
8677
8973
  if (typeof description !== "string") return void 0;
@@ -10002,7 +10298,7 @@ function resolveAgentConfig(args) {
10002
10298
  });
10003
10299
  return {
10004
10300
  mode: "cua",
10005
- systemPrompt: normalizeNonEmptyString(agentConfig.systemPrompt) || DEFAULT_SYSTEM_PROMPT,
10301
+ systemPrompt: normalizeNonEmptyString2(agentConfig.systemPrompt) || DEFAULT_SYSTEM_PROMPT,
10006
10302
  waitBetweenActionsMs: normalizeWaitBetween(agentConfig.waitBetweenActionsMs),
10007
10303
  model
10008
10304
  };
@@ -10021,7 +10317,7 @@ function createCuaClient(config) {
10021
10317
  );
10022
10318
  }
10023
10319
  }
10024
- function normalizeNonEmptyString(value) {
10320
+ function normalizeNonEmptyString2(value) {
10025
10321
  if (typeof value !== "string") return void 0;
10026
10322
  const normalized = value.trim();
10027
10323
  return normalized.length ? normalized : void 0;
@@ -10296,24 +10592,22 @@ var OpensteerCuaAgentHandler = class {
10296
10592
  page;
10297
10593
  config;
10298
10594
  client;
10299
- debug;
10595
+ cursorController;
10300
10596
  onMutatingAction;
10301
- cursorOverlayInjected = false;
10302
10597
  constructor(options) {
10303
10598
  this.page = options.page;
10304
10599
  this.config = options.config;
10305
10600
  this.client = options.client;
10306
- this.debug = options.debug;
10601
+ this.cursorController = options.cursorController;
10307
10602
  this.onMutatingAction = options.onMutatingAction;
10308
10603
  }
10309
10604
  async execute(options) {
10310
10605
  const instruction = options.instruction;
10311
10606
  const maxSteps = options.maxSteps ?? 20;
10312
10607
  await this.initializeClient();
10313
- const highlightCursor = options.highlightCursor === true;
10314
10608
  this.client.setActionHandler(async (action) => {
10315
- if (highlightCursor) {
10316
- await this.maybeRenderCursor(action);
10609
+ if (this.cursorController.isEnabled()) {
10610
+ await this.maybePreviewCursor(action);
10317
10611
  }
10318
10612
  await executeAgentAction(this.page, action);
10319
10613
  this.client.setCurrentUrl(this.page.url());
@@ -10344,6 +10638,7 @@ var OpensteerCuaAgentHandler = class {
10344
10638
  const viewport = await this.resolveViewport();
10345
10639
  this.client.setViewport(viewport.width, viewport.height);
10346
10640
  this.client.setCurrentUrl(this.page.url());
10641
+ await this.cursorController.attachPage(this.page);
10347
10642
  this.client.setScreenshotProvider(async () => {
10348
10643
  const buffer = await this.page.screenshot({
10349
10644
  fullPage: false,
@@ -10372,51 +10667,611 @@ var OpensteerCuaAgentHandler = class {
10372
10667
  }
10373
10668
  return DEFAULT_CUA_VIEWPORT;
10374
10669
  }
10375
- async maybeRenderCursor(action) {
10670
+ async maybePreviewCursor(action) {
10376
10671
  const x = typeof action.x === "number" ? action.x : null;
10377
10672
  const y = typeof action.y === "number" ? action.y : null;
10378
10673
  if (x == null || y == null) {
10379
10674
  return;
10380
10675
  }
10676
+ await this.cursorController.preview({ x, y }, "agent");
10677
+ }
10678
+ };
10679
+ function sleep4(ms) {
10680
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10681
+ }
10682
+
10683
+ // src/cursor/motion.ts
10684
+ var DEFAULT_SNAPPY_OPTIONS = {
10685
+ minDurationMs: 46,
10686
+ maxDurationMs: 170,
10687
+ maxPoints: 14
10688
+ };
10689
+ function planSnappyCursorMotion(from, to, options = {}) {
10690
+ const resolved = {
10691
+ ...DEFAULT_SNAPPY_OPTIONS,
10692
+ ...options
10693
+ };
10694
+ const dx = to.x - from.x;
10695
+ const dy = to.y - from.y;
10696
+ const distance = Math.hypot(dx, dy);
10697
+ if (distance < 5) {
10698
+ return {
10699
+ points: [roundPoint(to)],
10700
+ stepDelayMs: 0
10701
+ };
10702
+ }
10703
+ const durationMs = clamp(
10704
+ 44 + distance * 0.26,
10705
+ resolved.minDurationMs,
10706
+ resolved.maxDurationMs
10707
+ );
10708
+ const rawPoints = clamp(
10709
+ Math.round(durationMs / 13),
10710
+ 4,
10711
+ resolved.maxPoints
10712
+ );
10713
+ const sign = deterministicBendSign(from, to);
10714
+ const nx = -dy / distance;
10715
+ const ny = dx / distance;
10716
+ const bend = clamp(distance * 0.12, 8, 28) * sign;
10717
+ const c1 = {
10718
+ x: from.x + dx * 0.28 + nx * bend,
10719
+ y: from.y + dy * 0.28 + ny * bend
10720
+ };
10721
+ const c2 = {
10722
+ x: from.x + dx * 0.74 + nx * bend * 0.58,
10723
+ y: from.y + dy * 0.74 + ny * bend * 0.58
10724
+ };
10725
+ const points = [];
10726
+ for (let i = 1; i <= rawPoints; i += 1) {
10727
+ const t = i / rawPoints;
10728
+ const sampled = cubicBezier(from, c1, c2, to, t);
10729
+ if (i !== rawPoints) {
10730
+ const previous = points[points.length - 1];
10731
+ if (previous && distanceBetween(previous, sampled) < 1.4) {
10732
+ continue;
10733
+ }
10734
+ }
10735
+ points.push(roundPoint(sampled));
10736
+ }
10737
+ if (distance > 220) {
10738
+ const settle = {
10739
+ x: to.x - dx / distance,
10740
+ y: to.y - dy / distance
10741
+ };
10742
+ const last = points[points.length - 1];
10743
+ if (!last || distanceBetween(last, settle) > 1.2) {
10744
+ points.splice(Math.max(0, points.length - 1), 0, roundPoint(settle));
10745
+ }
10746
+ }
10747
+ const deduped = dedupeAdjacent(points);
10748
+ const stepDelayMs = deduped.length > 1 ? Math.round(durationMs / deduped.length) : 0;
10749
+ return {
10750
+ points: deduped.length ? deduped : [roundPoint(to)],
10751
+ stepDelayMs
10752
+ };
10753
+ }
10754
+ function cubicBezier(p0, p1, p2, p3, t) {
10755
+ const inv = 1 - t;
10756
+ const inv2 = inv * inv;
10757
+ const inv3 = inv2 * inv;
10758
+ const t2 = t * t;
10759
+ const t3 = t2 * t;
10760
+ return {
10761
+ x: inv3 * p0.x + 3 * inv2 * t * p1.x + 3 * inv * t2 * p2.x + t3 * p3.x,
10762
+ y: inv3 * p0.y + 3 * inv2 * t * p1.y + 3 * inv * t2 * p2.y + t3 * p3.y
10763
+ };
10764
+ }
10765
+ function deterministicBendSign(from, to) {
10766
+ const seed = Math.sin(
10767
+ from.x * 12.9898 + from.y * 78.233 + to.x * 37.719 + to.y * 19.113
10768
+ ) * 43758.5453;
10769
+ const fractional = seed - Math.floor(seed);
10770
+ return fractional >= 0.5 ? 1 : -1;
10771
+ }
10772
+ function roundPoint(point) {
10773
+ return {
10774
+ x: Math.round(point.x * 100) / 100,
10775
+ y: Math.round(point.y * 100) / 100
10776
+ };
10777
+ }
10778
+ function distanceBetween(a, b) {
10779
+ return Math.hypot(a.x - b.x, a.y - b.y);
10780
+ }
10781
+ function dedupeAdjacent(points) {
10782
+ const out = [];
10783
+ for (const point of points) {
10784
+ const last = out[out.length - 1];
10785
+ if (last && last.x === point.x && last.y === point.y) {
10786
+ continue;
10787
+ }
10788
+ out.push(point);
10789
+ }
10790
+ return out;
10791
+ }
10792
+ function clamp(value, min, max) {
10793
+ return Math.min(max, Math.max(min, value));
10794
+ }
10795
+
10796
+ // src/cursor/renderers/svg-overlay.ts
10797
+ var PULSE_DURATION_MS = 220;
10798
+ var HOST_ELEMENT_ID = "__os_cr";
10799
+ var SvgCursorRenderer = class {
10800
+ page = null;
10801
+ active = false;
10802
+ reason = "disabled";
10803
+ lastMessage;
10804
+ async initialize(page) {
10805
+ this.page = page;
10806
+ if (page.isClosed()) {
10807
+ this.markInactive("page_closed");
10808
+ return;
10809
+ }
10810
+ try {
10811
+ await page.evaluate(injectCursor, HOST_ELEMENT_ID);
10812
+ this.active = true;
10813
+ this.reason = void 0;
10814
+ this.lastMessage = void 0;
10815
+ } catch (error) {
10816
+ const message = error instanceof Error ? error.message : String(error);
10817
+ this.markInactive("renderer_error", message);
10818
+ }
10819
+ }
10820
+ isActive() {
10821
+ return this.active;
10822
+ }
10823
+ status() {
10824
+ return {
10825
+ enabled: true,
10826
+ active: this.active,
10827
+ reason: this.reason ? this.lastMessage ? `${this.reason}: ${this.lastMessage}` : this.reason : void 0
10828
+ };
10829
+ }
10830
+ async move(point, style) {
10831
+ if (!this.active || !this.page || this.page.isClosed()) return;
10381
10832
  try {
10382
- if (!this.cursorOverlayInjected) {
10383
- await this.page.evaluate(() => {
10384
- if (document.getElementById("__opensteer_cua_cursor")) return;
10385
- const cursor = document.createElement("div");
10386
- cursor.id = "__opensteer_cua_cursor";
10387
- cursor.style.position = "fixed";
10388
- cursor.style.width = "14px";
10389
- cursor.style.height = "14px";
10390
- cursor.style.borderRadius = "999px";
10391
- cursor.style.background = "rgba(255, 51, 51, 0.85)";
10392
- cursor.style.border = "2px solid rgba(255, 255, 255, 0.95)";
10393
- cursor.style.boxShadow = "0 0 0 3px rgba(255, 51, 51, 0.25)";
10394
- cursor.style.pointerEvents = "none";
10395
- cursor.style.zIndex = "2147483647";
10396
- cursor.style.transform = "translate(-9999px, -9999px)";
10397
- cursor.style.transition = "transform 80ms linear";
10398
- document.documentElement.appendChild(cursor);
10833
+ const ok = await this.page.evaluate(moveCursor, {
10834
+ id: HOST_ELEMENT_ID,
10835
+ x: point.x,
10836
+ y: point.y,
10837
+ size: style.size,
10838
+ fill: colorToRgba(style.fillColor),
10839
+ outline: colorToRgba(style.outlineColor)
10840
+ });
10841
+ if (!ok) {
10842
+ await this.reinject();
10843
+ await this.page.evaluate(moveCursor, {
10844
+ id: HOST_ELEMENT_ID,
10845
+ x: point.x,
10846
+ y: point.y,
10847
+ size: style.size,
10848
+ fill: colorToRgba(style.fillColor),
10849
+ outline: colorToRgba(style.outlineColor)
10399
10850
  });
10400
- this.cursorOverlayInjected = true;
10401
10851
  }
10402
- await this.page.evaluate(
10403
- ({ px, py }) => {
10404
- const cursor = document.getElementById("__opensteer_cua_cursor");
10405
- if (!cursor) return;
10406
- cursor.style.transform = `translate(${Math.round(px - 7)}px, ${Math.round(py - 7)}px)`;
10407
- },
10408
- { px: x, py: y }
10409
- );
10852
+ } catch (error) {
10853
+ this.handleError(error);
10854
+ }
10855
+ }
10856
+ async pulse(point, style) {
10857
+ if (!this.active || !this.page || this.page.isClosed()) return;
10858
+ try {
10859
+ const ok = await this.page.evaluate(pulseCursor, {
10860
+ id: HOST_ELEMENT_ID,
10861
+ x: point.x,
10862
+ y: point.y,
10863
+ size: style.size,
10864
+ fill: colorToRgba(style.fillColor),
10865
+ outline: colorToRgba(style.outlineColor),
10866
+ halo: colorToRgba(style.haloColor),
10867
+ pulseMs: PULSE_DURATION_MS
10868
+ });
10869
+ if (!ok) {
10870
+ await this.reinject();
10871
+ await this.page.evaluate(pulseCursor, {
10872
+ id: HOST_ELEMENT_ID,
10873
+ x: point.x,
10874
+ y: point.y,
10875
+ size: style.size,
10876
+ fill: colorToRgba(style.fillColor),
10877
+ outline: colorToRgba(style.outlineColor),
10878
+ halo: colorToRgba(style.haloColor),
10879
+ pulseMs: PULSE_DURATION_MS
10880
+ });
10881
+ }
10882
+ } catch (error) {
10883
+ this.handleError(error);
10884
+ }
10885
+ }
10886
+ async clear() {
10887
+ if (!this.page || this.page.isClosed()) return;
10888
+ try {
10889
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
10890
+ } catch {
10891
+ }
10892
+ }
10893
+ async dispose() {
10894
+ if (this.page && !this.page.isClosed()) {
10895
+ try {
10896
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
10897
+ } catch {
10898
+ }
10899
+ }
10900
+ this.active = false;
10901
+ this.reason = "disabled";
10902
+ this.lastMessage = void 0;
10903
+ this.page = null;
10904
+ }
10905
+ async reinject() {
10906
+ if (!this.page || this.page.isClosed()) return;
10907
+ try {
10908
+ await this.page.evaluate(injectCursor, HOST_ELEMENT_ID);
10909
+ } catch {
10910
+ }
10911
+ }
10912
+ markInactive(reason, message) {
10913
+ this.active = false;
10914
+ this.reason = reason;
10915
+ this.lastMessage = message;
10916
+ }
10917
+ handleError(error) {
10918
+ const message = error instanceof Error ? error.message : String(error);
10919
+ if (isPageGone(message)) {
10920
+ this.markInactive("page_closed", message);
10921
+ }
10922
+ }
10923
+ };
10924
+ function injectCursor(hostId) {
10925
+ const win = window;
10926
+ if (win[hostId]) return;
10927
+ const host = document.createElement("div");
10928
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
10929
+ const shadow = host.attachShadow({ mode: "closed" });
10930
+ const wrapper = document.createElement("div");
10931
+ wrapper.style.cssText = "position:fixed;top:0;left:0;pointer-events:none;will-change:transform;display:none;";
10932
+ wrapper.innerHTML = `
10933
+ <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;">
10934
+ <path d="M3 2L3 23L8.5 17.5L13 26L17 24L12.5 15.5L20 15.5L3 2Z"
10935
+ fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
10936
+ </svg>
10937
+ <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>
10938
+ `;
10939
+ shadow.appendChild(wrapper);
10940
+ document.documentElement.appendChild(host);
10941
+ Object.defineProperty(window, hostId, {
10942
+ value: {
10943
+ host,
10944
+ wrapper,
10945
+ path: wrapper.querySelector("path"),
10946
+ pulse: wrapper.querySelector('[data-role="pulse"]')
10947
+ },
10948
+ configurable: true,
10949
+ enumerable: false
10950
+ });
10951
+ }
10952
+ function moveCursor(args) {
10953
+ const refs = window[args.id];
10954
+ if (!refs) return false;
10955
+ const scale = args.size / 20;
10956
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
10957
+ refs.wrapper.style.display = "block";
10958
+ if (refs.path) {
10959
+ refs.path.setAttribute("fill", args.fill);
10960
+ refs.path.setAttribute("stroke", args.outline);
10961
+ }
10962
+ return true;
10963
+ }
10964
+ function pulseCursor(args) {
10965
+ const refs = window[args.id];
10966
+ if (!refs) return false;
10967
+ const scale = args.size / 20;
10968
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
10969
+ refs.wrapper.style.display = "block";
10970
+ if (refs.path) {
10971
+ refs.path.setAttribute("fill", args.fill);
10972
+ refs.path.setAttribute("stroke", args.outline);
10973
+ }
10974
+ const ring = refs.pulse;
10975
+ if (!ring) return true;
10976
+ ring.style.background = args.halo;
10977
+ ring.style.opacity = "0.7";
10978
+ ring.style.width = "24px";
10979
+ ring.style.height = "24px";
10980
+ ring.style.transition = `all ${args.pulseMs}ms ease-out`;
10981
+ ring.offsetHeight;
10982
+ ring.style.width = "48px";
10983
+ ring.style.height = "48px";
10984
+ ring.style.opacity = "0";
10985
+ ring.style.transform = "translate(-20px, -20px)";
10986
+ setTimeout(() => {
10987
+ ring.style.transition = "none";
10988
+ ring.style.width = "24px";
10989
+ ring.style.height = "24px";
10990
+ ring.style.transform = "translate(-8px, -8px)";
10991
+ ring.style.opacity = "0";
10992
+ }, args.pulseMs);
10993
+ return true;
10994
+ }
10995
+ function removeCursor(hostId) {
10996
+ const refs = window[hostId];
10997
+ if (refs) {
10998
+ refs.host.remove();
10999
+ delete window[hostId];
11000
+ }
11001
+ }
11002
+ function colorToRgba(c) {
11003
+ return `rgba(${Math.round(c.r)},${Math.round(c.g)},${Math.round(c.b)},${c.a})`;
11004
+ }
11005
+ function isPageGone(message) {
11006
+ const m = message.toLowerCase();
11007
+ return m.includes("closed") || m.includes("detached") || m.includes("destroyed") || m.includes("target");
11008
+ }
11009
+
11010
+ // src/cursor/controller.ts
11011
+ var DEFAULT_STYLE = {
11012
+ size: 20,
11013
+ fillColor: {
11014
+ r: 255,
11015
+ g: 255,
11016
+ b: 255,
11017
+ a: 0.96
11018
+ },
11019
+ outlineColor: {
11020
+ r: 0,
11021
+ g: 0,
11022
+ b: 0,
11023
+ a: 1
11024
+ },
11025
+ haloColor: {
11026
+ r: 35,
11027
+ g: 162,
11028
+ b: 255,
11029
+ a: 0.38
11030
+ },
11031
+ pulseScale: 2.15
11032
+ };
11033
+ var REINITIALIZE_BACKOFF_MS = 1e3;
11034
+ var FIRST_MOVE_CENTER_DISTANCE_THRESHOLD = 16;
11035
+ var FIRST_MOVE_MAX_TRAVEL = 220;
11036
+ var FIRST_MOVE_NEAR_TARGET_X_OFFSET = 28;
11037
+ var FIRST_MOVE_NEAR_TARGET_Y_OFFSET = 18;
11038
+ var MOTION_PLANNERS = {
11039
+ snappy: planSnappyCursorMotion
11040
+ };
11041
+ var CursorController = class {
11042
+ debug;
11043
+ renderer;
11044
+ page = null;
11045
+ listenerPage = null;
11046
+ lastPoint = null;
11047
+ initializedForPage = false;
11048
+ lastInitializeAttemptAt = 0;
11049
+ enabled;
11050
+ profile;
11051
+ style;
11052
+ onDomContentLoaded = () => {
11053
+ void this.restoreCursorAfterNavigation();
11054
+ };
11055
+ constructor(options = {}) {
11056
+ const config = options.config || {};
11057
+ this.debug = Boolean(options.debug);
11058
+ this.enabled = config.enabled === true;
11059
+ this.profile = config.profile ?? "snappy";
11060
+ this.style = mergeStyle(config.style);
11061
+ this.renderer = options.renderer ?? new SvgCursorRenderer();
11062
+ }
11063
+ setEnabled(enabled) {
11064
+ if (this.enabled && !enabled) {
11065
+ this.lastPoint = null;
11066
+ void this.clear();
11067
+ }
11068
+ this.enabled = enabled;
11069
+ }
11070
+ isEnabled() {
11071
+ return this.enabled;
11072
+ }
11073
+ getStatus() {
11074
+ if (!this.enabled) {
11075
+ return {
11076
+ enabled: false,
11077
+ active: false,
11078
+ reason: "disabled"
11079
+ };
11080
+ }
11081
+ const status = this.renderer.status();
11082
+ if (!this.initializedForPage && !status.active) {
11083
+ return {
11084
+ enabled: true,
11085
+ active: false,
11086
+ reason: "not_initialized"
11087
+ };
11088
+ }
11089
+ return status;
11090
+ }
11091
+ async attachPage(page) {
11092
+ if (this.page !== page) {
11093
+ this.detachPageListeners();
11094
+ this.page = page;
11095
+ this.lastPoint = null;
11096
+ this.initializedForPage = false;
11097
+ this.lastInitializeAttemptAt = 0;
11098
+ }
11099
+ this.attachPageListeners(page);
11100
+ }
11101
+ async preview(point, intent) {
11102
+ if (!this.enabled || !point) return;
11103
+ if (!this.page || this.page.isClosed()) return;
11104
+ try {
11105
+ await this.ensureInitialized();
11106
+ if (!this.renderer.isActive()) {
11107
+ await this.reinitializeIfEligible();
11108
+ }
11109
+ if (!this.renderer.isActive()) return;
11110
+ const start = this.resolveMotionStart(point);
11111
+ const motion = this.planMotion(start, point);
11112
+ for (const step of motion.points) {
11113
+ await this.renderer.move(step, this.style);
11114
+ if (motion.stepDelayMs > 0) {
11115
+ await sleep5(motion.stepDelayMs);
11116
+ }
11117
+ }
11118
+ if (shouldPulse(intent)) {
11119
+ await this.renderer.pulse(point, this.style);
11120
+ }
11121
+ this.lastPoint = point;
11122
+ } catch (error) {
11123
+ if (this.debug) {
11124
+ const message = error instanceof Error ? error.message : String(error);
11125
+ console.warn(`[opensteer] cursor preview failed: ${message}`);
11126
+ }
11127
+ }
11128
+ }
11129
+ async clear() {
11130
+ try {
11131
+ await this.renderer.clear();
11132
+ } catch (error) {
11133
+ if (this.debug) {
11134
+ const message = error instanceof Error ? error.message : String(error);
11135
+ console.warn(`[opensteer] cursor clear failed: ${message}`);
11136
+ }
11137
+ }
11138
+ }
11139
+ async dispose() {
11140
+ this.detachPageListeners();
11141
+ this.lastPoint = null;
11142
+ this.initializedForPage = false;
11143
+ this.lastInitializeAttemptAt = 0;
11144
+ this.page = null;
11145
+ await this.renderer.dispose();
11146
+ }
11147
+ async ensureInitialized() {
11148
+ if (!this.page || this.page.isClosed()) return;
11149
+ if (this.initializedForPage) return;
11150
+ await this.initializeRenderer();
11151
+ }
11152
+ attachPageListeners(page) {
11153
+ if (this.listenerPage === page) {
11154
+ return;
11155
+ }
11156
+ this.detachPageListeners();
11157
+ page.on("domcontentloaded", this.onDomContentLoaded);
11158
+ this.listenerPage = page;
11159
+ }
11160
+ detachPageListeners() {
11161
+ if (!this.listenerPage) {
11162
+ return;
11163
+ }
11164
+ this.listenerPage.off("domcontentloaded", this.onDomContentLoaded);
11165
+ this.listenerPage = null;
11166
+ }
11167
+ planMotion(from, to) {
11168
+ return MOTION_PLANNERS[this.profile](from, to);
11169
+ }
11170
+ async reinitializeIfEligible() {
11171
+ if (!this.page || this.page.isClosed()) return;
11172
+ const elapsed = Date.now() - this.lastInitializeAttemptAt;
11173
+ if (elapsed < REINITIALIZE_BACKOFF_MS) return;
11174
+ await this.initializeRenderer();
11175
+ }
11176
+ async initializeRenderer() {
11177
+ if (!this.page || this.page.isClosed()) return;
11178
+ this.lastInitializeAttemptAt = Date.now();
11179
+ await this.renderer.initialize(this.page);
11180
+ this.initializedForPage = true;
11181
+ }
11182
+ async restoreCursorAfterNavigation() {
11183
+ if (!this.enabled || !this.lastPoint) return;
11184
+ if (!this.page || this.page.isClosed()) return;
11185
+ try {
11186
+ if (!this.renderer.isActive()) {
11187
+ await this.reinitializeIfEligible();
11188
+ }
11189
+ if (!this.renderer.isActive()) {
11190
+ return;
11191
+ }
11192
+ await this.renderer.move(this.lastPoint, this.style);
10410
11193
  } catch (error) {
10411
11194
  if (this.debug) {
10412
11195
  const message = error instanceof Error ? error.message : String(error);
10413
- console.warn(`[opensteer] cursor overlay failed: ${message}`);
11196
+ console.warn(
11197
+ `[opensteer] cursor restore after navigation failed: ${message}`
11198
+ );
11199
+ }
11200
+ }
11201
+ }
11202
+ resolveMotionStart(target) {
11203
+ if (this.lastPoint) {
11204
+ return this.lastPoint;
11205
+ }
11206
+ const viewport = this.page?.viewportSize();
11207
+ if (!viewport?.width || !viewport?.height) {
11208
+ return target;
11209
+ }
11210
+ const centerPoint = {
11211
+ x: viewport.width / 2,
11212
+ y: viewport.height / 2
11213
+ };
11214
+ if (distanceBetween2(centerPoint, target) > FIRST_MOVE_CENTER_DISTANCE_THRESHOLD) {
11215
+ const dx = target.x - centerPoint.x;
11216
+ const dy = target.y - centerPoint.y;
11217
+ const distance = Math.hypot(dx, dy);
11218
+ if (distance > FIRST_MOVE_MAX_TRAVEL) {
11219
+ const ux = dx / distance;
11220
+ const uy = dy / distance;
11221
+ return {
11222
+ x: target.x - ux * FIRST_MOVE_MAX_TRAVEL,
11223
+ y: target.y - uy * FIRST_MOVE_MAX_TRAVEL
11224
+ };
10414
11225
  }
11226
+ return centerPoint;
10415
11227
  }
11228
+ return {
11229
+ x: clamp2(target.x - FIRST_MOVE_NEAR_TARGET_X_OFFSET, 0, viewport.width),
11230
+ y: clamp2(target.y - FIRST_MOVE_NEAR_TARGET_Y_OFFSET, 0, viewport.height)
11231
+ };
10416
11232
  }
10417
11233
  };
10418
- function sleep4(ms) {
10419
- return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
11234
+ function mergeStyle(style) {
11235
+ return {
11236
+ size: normalizeFinite(style?.size, DEFAULT_STYLE.size, 4, 48),
11237
+ pulseScale: normalizeFinite(
11238
+ style?.pulseScale,
11239
+ DEFAULT_STYLE.pulseScale,
11240
+ 1,
11241
+ 3
11242
+ ),
11243
+ fillColor: normalizeColor(style?.fillColor, DEFAULT_STYLE.fillColor),
11244
+ outlineColor: normalizeColor(
11245
+ style?.outlineColor,
11246
+ DEFAULT_STYLE.outlineColor
11247
+ ),
11248
+ haloColor: normalizeColor(style?.haloColor, DEFAULT_STYLE.haloColor)
11249
+ };
11250
+ }
11251
+ function normalizeColor(color, fallback) {
11252
+ if (!color) return { ...fallback };
11253
+ return {
11254
+ r: normalizeFinite(color.r, fallback.r, 0, 255),
11255
+ g: normalizeFinite(color.g, fallback.g, 0, 255),
11256
+ b: normalizeFinite(color.b, fallback.b, 0, 255),
11257
+ a: normalizeFinite(color.a, fallback.a, 0, 1)
11258
+ };
11259
+ }
11260
+ function normalizeFinite(value, fallback, min, max) {
11261
+ const numeric = typeof value === "number" && Number.isFinite(value) ? value : fallback;
11262
+ return Math.min(max, Math.max(min, numeric));
11263
+ }
11264
+ function distanceBetween2(a, b) {
11265
+ return Math.hypot(a.x - b.x, a.y - b.y);
11266
+ }
11267
+ function clamp2(value, min, max) {
11268
+ return Math.min(max, Math.max(min, value));
11269
+ }
11270
+ function shouldPulse(intent) {
11271
+ return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
11272
+ }
11273
+ function sleep5(ms) {
11274
+ return new Promise((resolve) => setTimeout(resolve, ms));
10420
11275
  }
10421
11276
 
10422
11277
  // src/opensteer.ts
@@ -10445,6 +11300,7 @@ var Opensteer = class _Opensteer {
10445
11300
  ownsBrowser = false;
10446
11301
  snapshotCache = null;
10447
11302
  agentExecutionInFlight = false;
11303
+ cursorController = null;
10448
11304
  constructor(config = {}) {
10449
11305
  const resolvedRuntime = resolveConfigWithEnv(config);
10450
11306
  const resolved = resolvedRuntime.config;
@@ -10466,15 +11322,29 @@ var Opensteer = class _Opensteer {
10466
11322
  if (cloudSelection.cloud) {
10467
11323
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
10468
11324
  const apiKey = cloudConfig?.apiKey?.trim();
10469
- if (!apiKey) {
11325
+ const accessToken = cloudConfig?.accessToken?.trim();
11326
+ if (apiKey && accessToken) {
11327
+ throw new Error(
11328
+ "Cloud mode cannot use both cloud.apiKey and cloud.accessToken. Set only one credential."
11329
+ );
11330
+ }
11331
+ let credential = "";
11332
+ let authScheme = cloudConfig?.authScheme ?? "api-key";
11333
+ if (accessToken) {
11334
+ credential = accessToken;
11335
+ authScheme = "bearer";
11336
+ } else if (apiKey) {
11337
+ credential = apiKey;
11338
+ }
11339
+ if (!credential) {
10470
11340
  throw new Error(
10471
- "Cloud mode requires a non-empty API key via cloud.apiKey or OPENSTEER_API_KEY."
11341
+ "Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
10472
11342
  );
10473
11343
  }
10474
11344
  this.cloud = createCloudRuntimeState(
10475
- apiKey,
11345
+ credential,
10476
11346
  cloudConfig?.baseUrl,
10477
- cloudConfig?.authScheme
11347
+ authScheme
10478
11348
  );
10479
11349
  } else {
10480
11350
  this.cloud = null;
@@ -10699,6 +11569,19 @@ var Opensteer = class _Opensteer {
10699
11569
  }
10700
11570
  return true;
10701
11571
  }
11572
+ buildCloudSessionLaunchConfig(options) {
11573
+ const cloudConfig = this.config.cloud && typeof this.config.cloud === "object" ? this.config.cloud : void 0;
11574
+ const browserProfile = normalizeCloudBrowserProfilePreference(
11575
+ options.cloudBrowserProfile ?? cloudConfig?.browserProfile,
11576
+ options.cloudBrowserProfile ? "launch options" : "Opensteer config"
11577
+ );
11578
+ if (!browserProfile) {
11579
+ return void 0;
11580
+ }
11581
+ return {
11582
+ browserProfile
11583
+ };
11584
+ }
10702
11585
  async launch(options = {}) {
10703
11586
  if (this.pageRef && !this.ownsBrowser) {
10704
11587
  throw new Error(
@@ -10721,6 +11604,7 @@ var Opensteer = class _Opensteer {
10721
11604
  }
10722
11605
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
10723
11606
  this.cloud.localRunId = localRunId;
11607
+ const launchConfig = this.buildCloudSessionLaunchConfig(options);
10724
11608
  const session2 = await this.cloud.sessionClient.create({
10725
11609
  cloudSessionContractVersion,
10726
11610
  sourceType: "local-cloud",
@@ -10728,7 +11612,8 @@ var Opensteer = class _Opensteer {
10728
11612
  localRunId,
10729
11613
  name: this.namespace,
10730
11614
  model: this.config.model,
10731
- launchContext: options.context || void 0
11615
+ launchContext: options.context || void 0,
11616
+ launchConfig
10732
11617
  });
10733
11618
  sessionId = session2.sessionId;
10734
11619
  actionClient = await ActionWsClient.connect({
@@ -10746,6 +11631,9 @@ var Opensteer = class _Opensteer {
10746
11631
  this.pageRef = cdpConnection.page;
10747
11632
  this.ownsBrowser = true;
10748
11633
  this.snapshotCache = null;
11634
+ if (this.cursorController) {
11635
+ await this.cursorController.attachPage(this.pageRef);
11636
+ }
10749
11637
  this.cloud.actionClient = actionClient;
10750
11638
  this.cloud.sessionId = sessionId;
10751
11639
  this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
@@ -10786,6 +11674,9 @@ var Opensteer = class _Opensteer {
10786
11674
  this.pageRef = session.page;
10787
11675
  this.ownsBrowser = true;
10788
11676
  this.snapshotCache = null;
11677
+ if (this.cursorController) {
11678
+ await this.cursorController.attachPage(this.pageRef);
11679
+ }
10789
11680
  }
10790
11681
  static from(page, config = {}) {
10791
11682
  const resolvedRuntime = resolveConfigWithEnv(config);
@@ -10830,6 +11721,9 @@ var Opensteer = class _Opensteer {
10830
11721
  if (sessionId) {
10831
11722
  await this.cloud.sessionClient.close(sessionId).catch(() => void 0);
10832
11723
  }
11724
+ if (this.cursorController) {
11725
+ await this.cursorController.dispose().catch(() => void 0);
11726
+ }
10833
11727
  return;
10834
11728
  }
10835
11729
  if (this.ownsBrowser) {
@@ -10839,6 +11733,9 @@ var Opensteer = class _Opensteer {
10839
11733
  this.pageRef = null;
10840
11734
  this.contextRef = null;
10841
11735
  this.ownsBrowser = false;
11736
+ if (this.cursorController) {
11737
+ await this.cursorController.dispose().catch(() => void 0);
11738
+ }
10842
11739
  }
10843
11740
  async syncLocalSelectorCacheToCloud() {
10844
11741
  if (!this.cloud) return;
@@ -10965,12 +11862,22 @@ var Opensteer = class _Opensteer {
10965
11862
  resolution.counter
10966
11863
  );
10967
11864
  }
10968
- await this.runWithPostActionWait("hover", options.wait, async () => {
10969
- await handle.hover({
10970
- force: options.force,
10971
- position: options.position
10972
- });
10973
- });
11865
+ await this.runWithCursorPreview(
11866
+ () => this.resolveHandleTargetPoint(handle, options.position),
11867
+ "hover",
11868
+ async () => {
11869
+ await this.runWithPostActionWait(
11870
+ "hover",
11871
+ options.wait,
11872
+ async () => {
11873
+ await handle.hover({
11874
+ force: options.force,
11875
+ position: options.position
11876
+ });
11877
+ }
11878
+ );
11879
+ }
11880
+ );
10974
11881
  } catch (err) {
10975
11882
  const failure = classifyActionFailure({
10976
11883
  action: "hover",
@@ -11005,25 +11912,31 @@ var Opensteer = class _Opensteer {
11005
11912
  throw new Error("Unable to resolve element path for hover action.");
11006
11913
  }
11007
11914
  const path5 = resolution.path;
11008
- const result = await this.runWithPostActionWait(
11915
+ const result = await this.runWithCursorPreview(
11916
+ () => this.resolvePathTargetPoint(path5, options.position),
11009
11917
  "hover",
11010
- options.wait,
11011
11918
  async () => {
11012
- const actionResult = await performHover(this.page, path5, options);
11013
- if (!actionResult.ok) {
11014
- const failure = actionResult.failure || classifyActionFailure({
11015
- action: "hover",
11016
- error: actionResult.error || defaultActionFailureMessage("hover"),
11017
- fallbackMessage: defaultActionFailureMessage("hover")
11018
- });
11019
- throw this.buildActionError(
11020
- "hover",
11021
- options.description,
11022
- failure,
11023
- actionResult.usedSelector || null
11024
- );
11025
- }
11026
- return actionResult;
11919
+ return await this.runWithPostActionWait(
11920
+ "hover",
11921
+ options.wait,
11922
+ async () => {
11923
+ const actionResult = await performHover(this.page, path5, options);
11924
+ if (!actionResult.ok) {
11925
+ const failure = actionResult.failure || classifyActionFailure({
11926
+ action: "hover",
11927
+ error: actionResult.error || defaultActionFailureMessage("hover"),
11928
+ fallbackMessage: defaultActionFailureMessage("hover")
11929
+ });
11930
+ throw this.buildActionError(
11931
+ "hover",
11932
+ options.description,
11933
+ failure,
11934
+ actionResult.usedSelector || null
11935
+ );
11936
+ }
11937
+ return actionResult;
11938
+ }
11939
+ );
11027
11940
  }
11028
11941
  );
11029
11942
  this.snapshotCache = null;
@@ -11064,16 +11977,26 @@ var Opensteer = class _Opensteer {
11064
11977
  resolution.counter
11065
11978
  );
11066
11979
  }
11067
- await this.runWithPostActionWait("input", options.wait, async () => {
11068
- if (options.clear !== false) {
11069
- await handle.fill(options.text);
11070
- } else {
11071
- await handle.type(options.text);
11072
- }
11073
- if (options.pressEnter) {
11074
- await handle.press("Enter", { noWaitAfter: true });
11980
+ await this.runWithCursorPreview(
11981
+ () => this.resolveHandleTargetPoint(handle),
11982
+ "input",
11983
+ async () => {
11984
+ await this.runWithPostActionWait(
11985
+ "input",
11986
+ options.wait,
11987
+ async () => {
11988
+ if (options.clear !== false) {
11989
+ await handle.fill(options.text);
11990
+ } else {
11991
+ await handle.type(options.text);
11992
+ }
11993
+ if (options.pressEnter) {
11994
+ await handle.press("Enter", { noWaitAfter: true });
11995
+ }
11996
+ }
11997
+ );
11075
11998
  }
11076
- });
11999
+ );
11077
12000
  } catch (err) {
11078
12001
  const failure = classifyActionFailure({
11079
12002
  action: "input",
@@ -11108,25 +12031,31 @@ var Opensteer = class _Opensteer {
11108
12031
  throw new Error("Unable to resolve element path for input action.");
11109
12032
  }
11110
12033
  const path5 = resolution.path;
11111
- const result = await this.runWithPostActionWait(
12034
+ const result = await this.runWithCursorPreview(
12035
+ () => this.resolvePathTargetPoint(path5),
11112
12036
  "input",
11113
- options.wait,
11114
12037
  async () => {
11115
- const actionResult = await performInput(this.page, path5, options);
11116
- if (!actionResult.ok) {
11117
- const failure = actionResult.failure || classifyActionFailure({
11118
- action: "input",
11119
- error: actionResult.error || defaultActionFailureMessage("input"),
11120
- fallbackMessage: defaultActionFailureMessage("input")
11121
- });
11122
- throw this.buildActionError(
11123
- "input",
11124
- options.description,
11125
- failure,
11126
- actionResult.usedSelector || null
11127
- );
11128
- }
11129
- return actionResult;
12038
+ return await this.runWithPostActionWait(
12039
+ "input",
12040
+ options.wait,
12041
+ async () => {
12042
+ const actionResult = await performInput(this.page, path5, options);
12043
+ if (!actionResult.ok) {
12044
+ const failure = actionResult.failure || classifyActionFailure({
12045
+ action: "input",
12046
+ error: actionResult.error || defaultActionFailureMessage("input"),
12047
+ fallbackMessage: defaultActionFailureMessage("input")
12048
+ });
12049
+ throw this.buildActionError(
12050
+ "input",
12051
+ options.description,
12052
+ failure,
12053
+ actionResult.usedSelector || null
12054
+ );
12055
+ }
12056
+ return actionResult;
12057
+ }
12058
+ );
11130
12059
  }
11131
12060
  );
11132
12061
  this.snapshotCache = null;
@@ -11167,21 +12096,27 @@ var Opensteer = class _Opensteer {
11167
12096
  resolution.counter
11168
12097
  );
11169
12098
  }
11170
- await this.runWithPostActionWait(
12099
+ await this.runWithCursorPreview(
12100
+ () => this.resolveHandleTargetPoint(handle),
11171
12101
  "select",
11172
- options.wait,
11173
12102
  async () => {
11174
- if (options.value != null) {
11175
- await handle.selectOption(options.value);
11176
- } else if (options.label != null) {
11177
- await handle.selectOption({ label: options.label });
11178
- } else if (options.index != null) {
11179
- await handle.selectOption({ index: options.index });
11180
- } else {
11181
- throw new Error(
11182
- "Select requires value, label, or index."
11183
- );
11184
- }
12103
+ await this.runWithPostActionWait(
12104
+ "select",
12105
+ options.wait,
12106
+ async () => {
12107
+ if (options.value != null) {
12108
+ await handle.selectOption(options.value);
12109
+ } else if (options.label != null) {
12110
+ await handle.selectOption({ label: options.label });
12111
+ } else if (options.index != null) {
12112
+ await handle.selectOption({ index: options.index });
12113
+ } else {
12114
+ throw new Error(
12115
+ "Select requires value, label, or index."
12116
+ );
12117
+ }
12118
+ }
12119
+ );
11185
12120
  }
11186
12121
  );
11187
12122
  } catch (err) {
@@ -11218,25 +12153,31 @@ var Opensteer = class _Opensteer {
11218
12153
  throw new Error("Unable to resolve element path for select action.");
11219
12154
  }
11220
12155
  const path5 = resolution.path;
11221
- const result = await this.runWithPostActionWait(
12156
+ const result = await this.runWithCursorPreview(
12157
+ () => this.resolvePathTargetPoint(path5),
11222
12158
  "select",
11223
- options.wait,
11224
12159
  async () => {
11225
- const actionResult = await performSelect(this.page, path5, options);
11226
- if (!actionResult.ok) {
11227
- const failure = actionResult.failure || classifyActionFailure({
11228
- action: "select",
11229
- error: actionResult.error || defaultActionFailureMessage("select"),
11230
- fallbackMessage: defaultActionFailureMessage("select")
11231
- });
11232
- throw this.buildActionError(
11233
- "select",
11234
- options.description,
11235
- failure,
11236
- actionResult.usedSelector || null
11237
- );
11238
- }
11239
- return actionResult;
12160
+ return await this.runWithPostActionWait(
12161
+ "select",
12162
+ options.wait,
12163
+ async () => {
12164
+ const actionResult = await performSelect(this.page, path5, options);
12165
+ if (!actionResult.ok) {
12166
+ const failure = actionResult.failure || classifyActionFailure({
12167
+ action: "select",
12168
+ error: actionResult.error || defaultActionFailureMessage("select"),
12169
+ fallbackMessage: defaultActionFailureMessage("select")
12170
+ });
12171
+ throw this.buildActionError(
12172
+ "select",
12173
+ options.description,
12174
+ failure,
12175
+ actionResult.usedSelector || null
12176
+ );
12177
+ }
12178
+ return actionResult;
12179
+ }
12180
+ );
11240
12181
  }
11241
12182
  );
11242
12183
  this.snapshotCache = null;
@@ -11278,13 +12219,23 @@ var Opensteer = class _Opensteer {
11278
12219
  );
11279
12220
  }
11280
12221
  const delta = getScrollDelta2(options);
11281
- await this.runWithPostActionWait("scroll", options.wait, async () => {
11282
- await handle.evaluate((el, value) => {
11283
- if (el instanceof HTMLElement) {
11284
- el.scrollBy(value.x, value.y);
11285
- }
11286
- }, delta);
11287
- });
12222
+ await this.runWithCursorPreview(
12223
+ () => this.resolveHandleTargetPoint(handle),
12224
+ "scroll",
12225
+ async () => {
12226
+ await this.runWithPostActionWait(
12227
+ "scroll",
12228
+ options.wait,
12229
+ async () => {
12230
+ await handle.evaluate((el, value) => {
12231
+ if (el instanceof HTMLElement) {
12232
+ el.scrollBy(value.x, value.y);
12233
+ }
12234
+ }, delta);
12235
+ }
12236
+ );
12237
+ }
12238
+ );
11288
12239
  } catch (err) {
11289
12240
  const failure = classifyActionFailure({
11290
12241
  action: "scroll",
@@ -11315,29 +12266,35 @@ var Opensteer = class _Opensteer {
11315
12266
  `[c="${resolution.counter}"]`
11316
12267
  );
11317
12268
  }
11318
- const result = await this.runWithPostActionWait(
12269
+ const result = await this.runWithCursorPreview(
12270
+ () => resolution.path ? this.resolvePathTargetPoint(resolution.path) : this.resolveViewportAnchorPoint(),
11319
12271
  "scroll",
11320
- options.wait,
11321
12272
  async () => {
11322
- const actionResult = await performScroll(
11323
- this.page,
11324
- resolution.path,
11325
- options
12273
+ return await this.runWithPostActionWait(
12274
+ "scroll",
12275
+ options.wait,
12276
+ async () => {
12277
+ const actionResult = await performScroll(
12278
+ this.page,
12279
+ resolution.path,
12280
+ options
12281
+ );
12282
+ if (!actionResult.ok) {
12283
+ const failure = actionResult.failure || classifyActionFailure({
12284
+ action: "scroll",
12285
+ error: actionResult.error || defaultActionFailureMessage("scroll"),
12286
+ fallbackMessage: defaultActionFailureMessage("scroll")
12287
+ });
12288
+ throw this.buildActionError(
12289
+ "scroll",
12290
+ options.description,
12291
+ failure,
12292
+ actionResult.usedSelector || null
12293
+ );
12294
+ }
12295
+ return actionResult;
12296
+ }
11326
12297
  );
11327
- if (!actionResult.ok) {
11328
- const failure = actionResult.failure || classifyActionFailure({
11329
- action: "scroll",
11330
- error: actionResult.error || defaultActionFailureMessage("scroll"),
11331
- fallbackMessage: defaultActionFailureMessage("scroll")
11332
- });
11333
- throw this.buildActionError(
11334
- "scroll",
11335
- options.description,
11336
- failure,
11337
- actionResult.usedSelector || null
11338
- );
11339
- }
11340
- return actionResult;
11341
12298
  }
11342
12299
  );
11343
12300
  this.snapshotCache = null;
@@ -11623,11 +12580,17 @@ var Opensteer = class _Opensteer {
11623
12580
  resolution.counter
11624
12581
  );
11625
12582
  }
11626
- await this.runWithPostActionWait(
12583
+ await this.runWithCursorPreview(
12584
+ () => this.resolveHandleTargetPoint(handle),
11627
12585
  "uploadFile",
11628
- options.wait,
11629
12586
  async () => {
11630
- await handle.setInputFiles(options.paths);
12587
+ await this.runWithPostActionWait(
12588
+ "uploadFile",
12589
+ options.wait,
12590
+ async () => {
12591
+ await handle.setInputFiles(options.paths);
12592
+ }
12593
+ );
11631
12594
  }
11632
12595
  );
11633
12596
  } catch (err) {
@@ -11664,29 +12627,35 @@ var Opensteer = class _Opensteer {
11664
12627
  throw new Error("Unable to resolve element path for file upload.");
11665
12628
  }
11666
12629
  const path5 = resolution.path;
11667
- const result = await this.runWithPostActionWait(
12630
+ const result = await this.runWithCursorPreview(
12631
+ () => this.resolvePathTargetPoint(path5),
11668
12632
  "uploadFile",
11669
- options.wait,
11670
12633
  async () => {
11671
- const actionResult = await performFileUpload(
11672
- this.page,
11673
- path5,
11674
- options.paths
12634
+ return await this.runWithPostActionWait(
12635
+ "uploadFile",
12636
+ options.wait,
12637
+ async () => {
12638
+ const actionResult = await performFileUpload(
12639
+ this.page,
12640
+ path5,
12641
+ options.paths
12642
+ );
12643
+ if (!actionResult.ok) {
12644
+ const failure = actionResult.failure || classifyActionFailure({
12645
+ action: "uploadFile",
12646
+ error: actionResult.error || defaultActionFailureMessage("uploadFile"),
12647
+ fallbackMessage: defaultActionFailureMessage("uploadFile")
12648
+ });
12649
+ throw this.buildActionError(
12650
+ "uploadFile",
12651
+ options.description,
12652
+ failure,
12653
+ actionResult.usedSelector || null
12654
+ );
12655
+ }
12656
+ return actionResult;
12657
+ }
11675
12658
  );
11676
- if (!actionResult.ok) {
11677
- const failure = actionResult.failure || classifyActionFailure({
11678
- action: "uploadFile",
11679
- error: actionResult.error || defaultActionFailureMessage("uploadFile"),
11680
- fallbackMessage: defaultActionFailureMessage("uploadFile")
11681
- });
11682
- throw this.buildActionError(
11683
- "uploadFile",
11684
- options.description,
11685
- failure,
11686
- actionResult.usedSelector || null
11687
- );
11688
- }
11689
- return actionResult;
11690
12659
  }
11691
12660
  );
11692
12661
  this.snapshotCache = null;
@@ -11822,6 +12791,25 @@ var Opensteer = class _Opensteer {
11822
12791
  getConfig() {
11823
12792
  return this.config;
11824
12793
  }
12794
+ setCursorEnabled(enabled) {
12795
+ this.getCursorController().setEnabled(enabled);
12796
+ }
12797
+ getCursorState() {
12798
+ const controller = this.cursorController;
12799
+ if (!controller) {
12800
+ return {
12801
+ enabled: this.config.cursor?.enabled === true,
12802
+ active: false,
12803
+ reason: this.config.cursor?.enabled === true ? "not_initialized" : "disabled"
12804
+ };
12805
+ }
12806
+ const status = controller.getStatus();
12807
+ return {
12808
+ enabled: status.enabled,
12809
+ active: status.active,
12810
+ reason: status.reason
12811
+ };
12812
+ }
11825
12813
  getStorage() {
11826
12814
  return this.storage;
11827
12815
  }
@@ -11849,24 +12837,107 @@ var Opensteer = class _Opensteer {
11849
12837
  this.agentExecutionInFlight = true;
11850
12838
  try {
11851
12839
  const options = normalizeExecuteOptions(instructionOrOptions);
12840
+ const cursorController = this.getCursorController();
12841
+ const previousCursorEnabled = cursorController.isEnabled();
12842
+ if (options.highlightCursor !== void 0) {
12843
+ cursorController.setEnabled(options.highlightCursor);
12844
+ }
11852
12845
  const handler = new OpensteerCuaAgentHandler({
11853
12846
  page: this.page,
11854
12847
  config: resolvedAgentConfig,
11855
12848
  client: createCuaClient(resolvedAgentConfig),
11856
- debug: Boolean(this.config.debug),
12849
+ cursorController,
11857
12850
  onMutatingAction: () => {
11858
12851
  this.snapshotCache = null;
11859
12852
  }
11860
12853
  });
11861
- const result = await handler.execute(options);
11862
- this.snapshotCache = null;
11863
- return result;
12854
+ try {
12855
+ const result = await handler.execute(options);
12856
+ this.snapshotCache = null;
12857
+ return result;
12858
+ } finally {
12859
+ if (options.highlightCursor !== void 0) {
12860
+ cursorController.setEnabled(previousCursorEnabled);
12861
+ }
12862
+ }
11864
12863
  } finally {
11865
12864
  this.agentExecutionInFlight = false;
11866
12865
  }
11867
12866
  }
11868
12867
  };
11869
12868
  }
12869
+ getCursorController() {
12870
+ if (!this.cursorController) {
12871
+ this.cursorController = new CursorController({
12872
+ config: this.config.cursor,
12873
+ debug: Boolean(this.config.debug)
12874
+ });
12875
+ if (this.pageRef) {
12876
+ void this.cursorController.attachPage(this.pageRef);
12877
+ }
12878
+ }
12879
+ return this.cursorController;
12880
+ }
12881
+ async runWithCursorPreview(pointResolver, intent, execute) {
12882
+ if (this.isCursorPreviewEnabled()) {
12883
+ const point = await pointResolver();
12884
+ await this.previewCursorPoint(point, intent);
12885
+ }
12886
+ return await execute();
12887
+ }
12888
+ isCursorPreviewEnabled() {
12889
+ return this.cursorController ? this.cursorController.isEnabled() : this.config.cursor?.enabled === true;
12890
+ }
12891
+ async previewCursorPoint(point, intent) {
12892
+ const cursor = this.getCursorController();
12893
+ await cursor.attachPage(this.page);
12894
+ await cursor.preview(point, intent);
12895
+ }
12896
+ resolveCursorPointFromBoundingBox(box, position) {
12897
+ if (position) {
12898
+ return {
12899
+ x: box.x + position.x,
12900
+ y: box.y + position.y
12901
+ };
12902
+ }
12903
+ return {
12904
+ x: box.x + box.width / 2,
12905
+ y: box.y + box.height / 2
12906
+ };
12907
+ }
12908
+ async resolveHandleTargetPoint(handle, position) {
12909
+ try {
12910
+ const box = await handle.boundingBox();
12911
+ if (!box) return null;
12912
+ return this.resolveCursorPointFromBoundingBox(box, position);
12913
+ } catch {
12914
+ return null;
12915
+ }
12916
+ }
12917
+ async resolvePathTargetPoint(path5, position) {
12918
+ if (!path5) {
12919
+ return null;
12920
+ }
12921
+ let resolved = null;
12922
+ try {
12923
+ resolved = await resolveElementPath(this.page, path5);
12924
+ return await this.resolveHandleTargetPoint(resolved.element, position);
12925
+ } catch {
12926
+ return null;
12927
+ } finally {
12928
+ await resolved?.element.dispose().catch(() => void 0);
12929
+ }
12930
+ }
12931
+ async resolveViewportAnchorPoint() {
12932
+ const viewport = this.page.viewportSize();
12933
+ if (viewport?.width && viewport?.height) {
12934
+ return {
12935
+ x: viewport.width / 2,
12936
+ y: viewport.height / 2
12937
+ };
12938
+ }
12939
+ return null;
12940
+ }
11870
12941
  async runWithPostActionWait(action, waitOverride, execute) {
11871
12942
  const waitSession = createPostActionWaitSession(
11872
12943
  this.page,
@@ -11899,13 +12970,19 @@ var Opensteer = class _Opensteer {
11899
12970
  resolution.counter
11900
12971
  );
11901
12972
  }
11902
- await this.runWithPostActionWait(method, options.wait, async () => {
11903
- await handle.click({
11904
- button: options.button,
11905
- clickCount: options.clickCount,
11906
- modifiers: options.modifiers
11907
- });
11908
- });
12973
+ await this.runWithCursorPreview(
12974
+ () => this.resolveHandleTargetPoint(handle),
12975
+ method,
12976
+ async () => {
12977
+ await this.runWithPostActionWait(method, options.wait, async () => {
12978
+ await handle.click({
12979
+ button: options.button,
12980
+ clickCount: options.clickCount,
12981
+ modifiers: options.modifiers
12982
+ });
12983
+ });
12984
+ }
12985
+ );
11909
12986
  } catch (err) {
11910
12987
  const failure = classifyActionFailure({
11911
12988
  action: method,
@@ -11940,25 +13017,31 @@ var Opensteer = class _Opensteer {
11940
13017
  throw new Error("Unable to resolve element path for click action.");
11941
13018
  }
11942
13019
  const path5 = resolution.path;
11943
- const result = await this.runWithPostActionWait(
13020
+ const result = await this.runWithCursorPreview(
13021
+ () => this.resolvePathTargetPoint(path5),
11944
13022
  method,
11945
- options.wait,
11946
13023
  async () => {
11947
- const actionResult = await performClick(this.page, path5, options);
11948
- if (!actionResult.ok) {
11949
- const failure = actionResult.failure || classifyActionFailure({
11950
- action: method,
11951
- error: actionResult.error || defaultActionFailureMessage(method),
11952
- fallbackMessage: defaultActionFailureMessage(method)
11953
- });
11954
- throw this.buildActionError(
11955
- method,
11956
- options.description,
11957
- failure,
11958
- actionResult.usedSelector || null
11959
- );
11960
- }
11961
- return actionResult;
13024
+ return await this.runWithPostActionWait(
13025
+ method,
13026
+ options.wait,
13027
+ async () => {
13028
+ const actionResult = await performClick(this.page, path5, options);
13029
+ if (!actionResult.ok) {
13030
+ const failure = actionResult.failure || classifyActionFailure({
13031
+ action: method,
13032
+ error: actionResult.error || defaultActionFailureMessage(method),
13033
+ fallbackMessage: defaultActionFailureMessage(method)
13034
+ });
13035
+ throw this.buildActionError(
13036
+ method,
13037
+ options.description,
13038
+ failure,
13039
+ actionResult.usedSelector || null
13040
+ );
13041
+ }
13042
+ return actionResult;
13043
+ }
13044
+ );
11962
13045
  }
11963
13046
  );
11964
13047
  this.snapshotCache = null;
@@ -12953,6 +14036,26 @@ function isInternalOrBlankPageUrl(url) {
12953
14036
  if (url === "about:blank") return true;
12954
14037
  return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
12955
14038
  }
14039
+ function normalizeCloudBrowserProfilePreference(value, source) {
14040
+ if (!value) {
14041
+ return void 0;
14042
+ }
14043
+ const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
14044
+ if (!profileId) {
14045
+ throw new Error(
14046
+ `Invalid cloud browser profile in ${source}: profileId must be a non-empty string.`
14047
+ );
14048
+ }
14049
+ if (value.reuseIfActive !== void 0 && typeof value.reuseIfActive !== "boolean") {
14050
+ throw new Error(
14051
+ `Invalid cloud browser profile in ${source}: reuseIfActive must be a boolean.`
14052
+ );
14053
+ }
14054
+ return {
14055
+ profileId,
14056
+ reuseIfActive: value.reuseIfActive
14057
+ };
14058
+ }
12956
14059
  function buildLocalRunId(namespace) {
12957
14060
  const normalized = namespace.trim() || "default";
12958
14061
  return `${normalized}-${Date.now().toString(36)}-${(0, import_crypto.randomUUID)().slice(0, 8)}`;
@@ -12962,12 +14065,265 @@ function buildLocalRunId(namespace) {
12962
14065
  init_resolver();
12963
14066
  init_extractor();
12964
14067
  init_model();
14068
+
14069
+ // src/cloud/browser-profile-client.ts
14070
+ var BrowserProfileClient = class {
14071
+ baseUrl;
14072
+ key;
14073
+ authScheme;
14074
+ constructor(baseUrl, key, authScheme = "api-key") {
14075
+ this.baseUrl = normalizeCloudBaseUrl(baseUrl);
14076
+ this.key = key;
14077
+ this.authScheme = authScheme;
14078
+ }
14079
+ async list(request = {}) {
14080
+ const query = new URLSearchParams();
14081
+ if (request.cursor) {
14082
+ query.set("cursor", request.cursor);
14083
+ }
14084
+ if (typeof request.limit === "number" && Number.isFinite(request.limit)) {
14085
+ query.set("limit", String(Math.max(1, Math.trunc(request.limit))));
14086
+ }
14087
+ if (request.status) {
14088
+ query.set("status", request.status);
14089
+ }
14090
+ const querySuffix = query.toString() ? `?${query.toString()}` : "";
14091
+ const response = await fetch(
14092
+ `${this.baseUrl}/browser-profiles${querySuffix}`,
14093
+ {
14094
+ method: "GET",
14095
+ headers: {
14096
+ ...cloudAuthHeaders(this.key, this.authScheme)
14097
+ }
14098
+ }
14099
+ );
14100
+ if (!response.ok) {
14101
+ throw await parseCloudHttpError(response);
14102
+ }
14103
+ return await response.json();
14104
+ }
14105
+ async get(profileId) {
14106
+ const normalized = profileId.trim();
14107
+ const response = await fetch(
14108
+ `${this.baseUrl}/browser-profiles/${encodeURIComponent(normalized)}`,
14109
+ {
14110
+ method: "GET",
14111
+ headers: {
14112
+ ...cloudAuthHeaders(this.key, this.authScheme)
14113
+ }
14114
+ }
14115
+ );
14116
+ if (!response.ok) {
14117
+ throw await parseCloudHttpError(response);
14118
+ }
14119
+ return await response.json();
14120
+ }
14121
+ async create(request) {
14122
+ const response = await fetch(`${this.baseUrl}/browser-profiles`, {
14123
+ method: "POST",
14124
+ headers: {
14125
+ "content-type": "application/json",
14126
+ ...cloudAuthHeaders(this.key, this.authScheme)
14127
+ },
14128
+ body: JSON.stringify(request)
14129
+ });
14130
+ if (!response.ok) {
14131
+ throw await parseCloudHttpError(response);
14132
+ }
14133
+ return await response.json();
14134
+ }
14135
+ };
14136
+
14137
+ // src/cursor/renderers/cdp-overlay.ts
14138
+ var PULSE_DELAY_MS = 30;
14139
+ var CdpOverlayCursorRenderer = class {
14140
+ page = null;
14141
+ session = null;
14142
+ active = false;
14143
+ reason = "disabled";
14144
+ lastMessage;
14145
+ lastPoint = null;
14146
+ async initialize(page) {
14147
+ this.page = page;
14148
+ if (page.isClosed()) {
14149
+ this.markInactive("page_closed");
14150
+ return;
14151
+ }
14152
+ await this.createSession();
14153
+ }
14154
+ isActive() {
14155
+ return this.active;
14156
+ }
14157
+ status() {
14158
+ return {
14159
+ enabled: true,
14160
+ active: this.active,
14161
+ reason: this.reason ? this.lastMessage ? `${this.reason}: ${this.lastMessage}` : this.reason : void 0
14162
+ };
14163
+ }
14164
+ async move(point, style) {
14165
+ await this.sendWithRecovery(async (session) => {
14166
+ await session.send("Overlay.highlightQuad", {
14167
+ quad: buildCursorQuad(point, style.size),
14168
+ color: toProtocolRgba(style.fillColor),
14169
+ outlineColor: toProtocolRgba(style.outlineColor)
14170
+ });
14171
+ });
14172
+ this.lastPoint = point;
14173
+ }
14174
+ async pulse(point, style) {
14175
+ const pulseSize = style.size * style.pulseScale;
14176
+ const pulseFill = {
14177
+ ...style.fillColor,
14178
+ a: Math.min(1, style.fillColor.a * 0.14)
14179
+ };
14180
+ const pulseOutline = {
14181
+ ...style.haloColor,
14182
+ a: Math.min(1, style.haloColor.a * 0.9)
14183
+ };
14184
+ await this.sendWithRecovery(async (session) => {
14185
+ await session.send("Overlay.highlightQuad", {
14186
+ quad: buildCursorQuad(point, pulseSize),
14187
+ color: toProtocolRgba(pulseFill),
14188
+ outlineColor: toProtocolRgba(pulseOutline)
14189
+ });
14190
+ });
14191
+ await sleep6(PULSE_DELAY_MS);
14192
+ await this.move(point, style);
14193
+ }
14194
+ async clear() {
14195
+ if (!this.session) return;
14196
+ try {
14197
+ await this.session.send("Overlay.hideHighlight");
14198
+ } catch {
14199
+ this.markInactive("cdp_detached");
14200
+ }
14201
+ }
14202
+ async dispose() {
14203
+ await this.cleanupSession();
14204
+ this.active = false;
14205
+ this.reason = "disabled";
14206
+ this.lastMessage = void 0;
14207
+ this.lastPoint = null;
14208
+ this.page = null;
14209
+ }
14210
+ async sendWithRecovery(operation) {
14211
+ if (!this.active || !this.session) return;
14212
+ try {
14213
+ await operation(this.session);
14214
+ } catch (error) {
14215
+ const message = error instanceof Error ? error.message : String(error);
14216
+ this.lastMessage = message;
14217
+ if (!isRecoverableProtocolError(message) || !this.page) {
14218
+ this.markInactive("renderer_error", message);
14219
+ return;
14220
+ }
14221
+ await this.createSession();
14222
+ if (!this.active || !this.session) {
14223
+ return;
14224
+ }
14225
+ try {
14226
+ await operation(this.session);
14227
+ } catch (retryError) {
14228
+ const retryMessage = retryError instanceof Error ? retryError.message : String(retryError);
14229
+ this.markInactive("renderer_error", retryMessage);
14230
+ }
14231
+ }
14232
+ }
14233
+ async createSession() {
14234
+ if (!this.page || this.page.isClosed()) {
14235
+ this.markInactive("page_closed");
14236
+ return;
14237
+ }
14238
+ await this.cleanupSession();
14239
+ try {
14240
+ const session = await this.page.context().newCDPSession(this.page);
14241
+ await session.send("DOM.enable");
14242
+ await session.send("Overlay.enable");
14243
+ this.session = session;
14244
+ this.active = true;
14245
+ this.reason = void 0;
14246
+ this.lastMessage = void 0;
14247
+ } catch (error) {
14248
+ const message = error instanceof Error ? error.message : String(error);
14249
+ this.markInactive(inferSetupReason(message), message);
14250
+ await this.cleanupSession();
14251
+ }
14252
+ }
14253
+ async cleanupSession() {
14254
+ const session = this.session;
14255
+ this.session = null;
14256
+ if (!session) return;
14257
+ try {
14258
+ await session.detach();
14259
+ } catch {
14260
+ }
14261
+ }
14262
+ markInactive(reason, message) {
14263
+ this.active = false;
14264
+ this.reason = reason;
14265
+ this.lastMessage = message;
14266
+ }
14267
+ };
14268
+ function buildCursorQuad(point, size) {
14269
+ const x = point.x;
14270
+ const y = point.y;
14271
+ return [
14272
+ // Point 0: Tip (the hotspot)
14273
+ roundPointValue(x),
14274
+ roundPointValue(y),
14275
+ // Point 1: Right shoulder — extends right and down
14276
+ roundPointValue(x + size * 0.45),
14277
+ roundPointValue(y + size * 0.78),
14278
+ // Point 2: Tail — bottom of the cursor shaft
14279
+ roundPointValue(x + size * 0.12),
14280
+ roundPointValue(y + size * 1.3),
14281
+ // Point 3: Left edge — stays close to the shaft
14282
+ roundPointValue(x - size * 0.04),
14283
+ roundPointValue(y + size * 0.62)
14284
+ ];
14285
+ }
14286
+ function inferSetupReason(message) {
14287
+ const lowered = message.toLowerCase();
14288
+ if (lowered.includes("not supported") || lowered.includes("only supported") || lowered.includes("unknown command")) {
14289
+ return "unsupported";
14290
+ }
14291
+ return "cdp_unavailable";
14292
+ }
14293
+ function isRecoverableProtocolError(message) {
14294
+ const lowered = message.toLowerCase();
14295
+ return lowered.includes("session closed") || lowered.includes("target closed") || lowered.includes("has been closed") || lowered.includes("detached");
14296
+ }
14297
+ function toProtocolRgba(color) {
14298
+ return {
14299
+ r: clampColor(color.r),
14300
+ g: clampColor(color.g),
14301
+ b: clampColor(color.b),
14302
+ a: clampAlpha(color.a)
14303
+ };
14304
+ }
14305
+ function clampColor(value) {
14306
+ return Math.min(255, Math.max(0, Math.round(value)));
14307
+ }
14308
+ function clampAlpha(value) {
14309
+ const normalized = Number.isFinite(value) ? value : 1;
14310
+ return Math.min(1, Math.max(0, normalized));
14311
+ }
14312
+ function roundPointValue(value) {
14313
+ return Math.round(value * 100) / 100;
14314
+ }
14315
+ function sleep6(ms) {
14316
+ return new Promise((resolve) => setTimeout(resolve, ms));
14317
+ }
12965
14318
  // Annotate the CommonJS export names for ESM import in node:
12966
14319
  0 && (module.exports = {
12967
14320
  ActionWsClient,
14321
+ BrowserProfileClient,
14322
+ CdpOverlayCursorRenderer,
12968
14323
  CloudCdpClient,
12969
14324
  CloudSessionClient,
12970
14325
  CounterResolutionError,
14326
+ CursorController,
12971
14327
  ElementPathError,
12972
14328
  LocalSelectorStorage,
12973
14329
  OPENSTEER_HIDDEN_ATTR,
@@ -12989,6 +14345,7 @@ init_model();
12989
14345
  OpensteerAgentProviderError,
12990
14346
  OpensteerCloudError,
12991
14347
  OpensteerCuaAgentHandler,
14348
+ SvgCursorRenderer,
12992
14349
  buildArrayFieldPathCandidates,
12993
14350
  buildElementPathFromHandle,
12994
14351
  buildElementPathFromSelector,
@@ -13033,6 +14390,7 @@ init_model();
13033
14390
  performInput,
13034
14391
  performScroll,
13035
14392
  performSelect,
14393
+ planSnappyCursorMotion,
13036
14394
  prepareSnapshot,
13037
14395
  pressKey,
13038
14396
  queryAllByElementPath,