opensteer 0.8.9 → 0.8.10

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.
@@ -10806,7 +10806,8 @@ async function captureActionBoundarySnapshot(engine, pageRef) {
10806
10806
  }
10807
10807
  return {
10808
10808
  pageRef,
10809
- documentRef: mainFrame.documentRef
10809
+ documentRef: mainFrame.documentRef,
10810
+ url: mainFrame.url
10810
10811
  };
10811
10812
  }
10812
10813
  function createActionBoundaryDiagnostics(input) {
@@ -14749,7 +14750,7 @@ async function clearPersistedSessionRecord(rootPath, provider) {
14749
14750
  await rm(resolveLiveSessionRecordPath(rootPath, provider), { force: true });
14750
14751
  }
14751
14752
  function isPersistedCloudSessionRecord(value) {
14752
- return value.layout === OPENSTEER_LIVE_SESSION_LAYOUT && value.version === OPENSTEER_LIVE_SESSION_VERSION && value.provider === "cloud" && typeof value.sessionId === "string" && value.sessionId.length > 0 && typeof value.baseUrl === "string" && value.baseUrl.length > 0 && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
14753
+ return value.layout === OPENSTEER_LIVE_SESSION_LAYOUT && value.version === OPENSTEER_LIVE_SESSION_VERSION && value.provider === "cloud" && typeof value.sessionId === "string" && value.sessionId.length > 0 && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt);
14753
14754
  }
14754
14755
  function isPersistedLocalBrowserSessionRecord(value) {
14755
14756
  return value.layout === OPENSTEER_LIVE_SESSION_LAYOUT && value.version === OPENSTEER_LIVE_SESSION_VERSION && value.provider === "local" && (value.engine === "playwright" || value.engine === "abp") && typeof value.pid === "number" && Number.isFinite(value.pid) && typeof value.startedAt === "number" && Number.isFinite(value.startedAt) && typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) && typeof value.userDataDir === "string" && value.userDataDir.length > 0;
@@ -22408,11 +22409,11 @@ var SandboxClock = class {
22408
22409
  performanceNow() {
22409
22410
  return this.mode === "manual" ? this.manualNow - this.startedAt : (globalThis.performance?.now() ?? 0) - this.performanceStartedAt;
22410
22411
  }
22411
- setTimeout(callback, delay2 = 0, ...args) {
22412
- return this.registerTimer(false, callback, delay2, args);
22412
+ setTimeout(callback, delay3 = 0, ...args) {
22413
+ return this.registerTimer(false, callback, delay3, args);
22413
22414
  }
22414
- setInterval(callback, delay2 = 0, ...args) {
22415
- return this.registerTimer(true, callback, delay2, args);
22415
+ setInterval(callback, delay3 = 0, ...args) {
22416
+ return this.registerTimer(true, callback, delay3, args);
22416
22417
  }
22417
22418
  clearTimeout(timerId) {
22418
22419
  this.clearTimer(timerId);
@@ -22433,9 +22434,9 @@ var SandboxClock = class {
22433
22434
  this.clearTimer(timerId);
22434
22435
  }
22435
22436
  }
22436
- registerTimer(repeat, callback, delay2, args) {
22437
+ registerTimer(repeat, callback, delay3, args) {
22437
22438
  const timerId = this.nextTimerId++;
22438
- const normalizedDelay = Math.max(0, delay2);
22439
+ const normalizedDelay = Math.max(0, delay3);
22439
22440
  const record = {
22440
22441
  callback,
22441
22442
  args,
@@ -22942,6 +22943,7 @@ function diffInteractionTraces(left, right) {
22942
22943
  // ../runtime-core/src/sdk/runtime.ts
22943
22944
  var requireForAuthRecipeHook = createRequire(import.meta.url);
22944
22945
  var MUTATION_CAPTURE_FINALIZE_TIMEOUT_MS = 5e3;
22946
+ var PERSISTED_NETWORK_FLUSH_TIMEOUT_MS = 5e3;
22945
22947
  var PENDING_OPERATION_EVENT_CAPTURE_LIMIT = 64;
22946
22948
  var PENDING_OPERATION_EVENT_CAPTURE_SKEW_MS = 1e3;
22947
22949
  var OpensteerSessionRuntime = class {
@@ -29081,6 +29083,29 @@ var OpensteerSessionRuntime = class {
29081
29083
  return snapshot.sessionStorage?.filter((entry) => entry.origin === origin).find((entry) => pageUrl === void 0 || entry.origin === new URL(pageUrl).origin)?.entries.find((entry) => entry.key === key)?.value;
29082
29084
  }
29083
29085
  async flushPersistedNetworkHistory() {
29086
+ if (this.sessionRef === void 0) {
29087
+ return;
29088
+ }
29089
+ const root = await this.ensureRoot();
29090
+ try {
29091
+ await withDetachedTimeoutSignal(PERSISTED_NETWORK_FLUSH_TIMEOUT_MS, async (signal) => {
29092
+ const browserRecords = await this.readBrowserNetworkRecords(
29093
+ {
29094
+ includeBodies: true,
29095
+ includeCurrentPageOnly: false
29096
+ },
29097
+ signal
29098
+ );
29099
+ await this.networkHistory.persist(browserRecords, root.registry.savedNetwork, {
29100
+ bodyWriteMode: "authoritative",
29101
+ redactSecretHeaders: false
29102
+ });
29103
+ });
29104
+ } catch (error) {
29105
+ if (!isIgnorableRuntimeBindingError(error)) {
29106
+ throw error;
29107
+ }
29108
+ }
29084
29109
  }
29085
29110
  toDomTargetRef(target) {
29086
29111
  if (target.kind === "description") {
@@ -29237,6 +29262,12 @@ var OpensteerSessionRuntime = class {
29237
29262
  return "live";
29238
29263
  } catch (error) {
29239
29264
  if (isIgnorableRuntimeBindingError(error)) {
29265
+ const remainingPages = await engine.listPages({ sessionRef }).catch(() => void 0);
29266
+ const replacementPageRef = remainingPages?.[0]?.pageRef;
29267
+ if (replacementPageRef !== void 0) {
29268
+ this.pageRef = replacementPageRef;
29269
+ return "live";
29270
+ }
29240
29271
  return "invalid";
29241
29272
  }
29242
29273
  throw error;
@@ -32291,197 +32322,1798 @@ function screenshotMediaType(format2) {
32291
32322
  }
32292
32323
  }
32293
32324
 
32294
- // src/sdk/runtime.ts
32295
- var OpensteerRuntime = class extends OpensteerSessionRuntime {
32296
- constructor(options = {}) {
32297
- const publicWorkspace = normalizeWorkspace2(options.workspace);
32298
- const rootPath = options.rootPath ?? (publicWorkspace === void 0 ? path7.resolve(options.rootDir ?? process.cwd(), ".opensteer", "temporary", randomUUID()) : resolveFilesystemWorkspacePath({
32299
- rootDir: path7.resolve(options.rootDir ?? process.cwd()),
32300
- workspace: publicWorkspace
32301
- }));
32302
- const cleanupRootOnClose = options.cleanupRootOnClose ?? publicWorkspace === void 0;
32303
- const engineName = options.engineName ?? DEFAULT_OPENSTEER_ENGINE;
32304
- assertSupportedEngineOptions({
32305
- engineName,
32306
- ...options.browser === void 0 ? {} : { browser: options.browser },
32307
- ...options.context === void 0 ? {} : { context: options.context }
32308
- });
32309
- super(
32310
- buildSharedRuntimeOptions({
32311
- name: publicWorkspace ?? "default",
32312
- rootPath,
32313
- ...publicWorkspace === void 0 ? {} : { workspaceName: publicWorkspace },
32314
- ...options.browser === void 0 ? {} : { browser: options.browser },
32315
- ...options.launch === void 0 ? {} : { launch: options.launch },
32316
- ...options.context === void 0 ? {} : { context: options.context },
32317
- engineName,
32318
- ...options.engine === void 0 ? {} : { engine: options.engine },
32319
- ...options.engineFactory === void 0 ? {} : { engineFactory: options.engineFactory },
32320
- ...options.policy === void 0 ? {} : { policy: options.policy },
32321
- ...options.descriptorStore === void 0 ? {} : { descriptorStore: options.descriptorStore },
32322
- ...options.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: options.extractionDescriptorStore },
32323
- ...options.registryOverrides === void 0 ? {} : { registryOverrides: options.registryOverrides },
32324
- cleanupRootOnClose,
32325
- ...options.observability === void 0 ? {} : { observability: options.observability },
32326
- ...options.observationSessionId === void 0 ? {} : { observationSessionId: options.observationSessionId },
32327
- ...options.observationSink === void 0 ? {} : { observationSink: options.observationSink }
32328
- })
32329
- );
32325
+ // ../runtime-core/src/recorder/browser-scripts.ts
32326
+ var SINGLE_ATTRIBUTE_PRIORITY = Array.from(
32327
+ /* @__PURE__ */ new Set(["data-testid", "data-test", "data-qa", "data-cy", "id", ...STABLE_PRIMARY_ATTR_KEYS])
32328
+ );
32329
+ var FLOW_RECORDER_INSTALL_SOURCE = String.raw`(() => {
32330
+ const TOP_LEVEL_ONLY = (() => {
32331
+ try {
32332
+ return window.top === window.self;
32333
+ } catch {
32334
+ return false;
32335
+ }
32336
+ })();
32337
+ if (!TOP_LEVEL_ONLY) {
32338
+ return;
32330
32339
  }
32331
- };
32332
- var OpensteerSessionRuntime2 = class extends OpensteerSessionRuntime {
32333
- constructor(options) {
32334
- const rootPath = options.rootPath ?? path7.resolve(options.rootDir ?? process.cwd());
32335
- const cleanupRootOnClose = options.cleanupRootOnClose ?? false;
32336
- const engineName = options.engineName ?? DEFAULT_OPENSTEER_ENGINE;
32337
- assertSupportedEngineOptions({
32338
- engineName,
32339
- ...options.browser === void 0 ? {} : { browser: options.browser },
32340
- ...options.context === void 0 ? {} : { context: options.context }
32341
- });
32342
- super(
32343
- buildSharedRuntimeOptions({
32344
- name: options.name,
32345
- rootPath,
32346
- ...options.browser === void 0 ? {} : { browser: options.browser },
32347
- ...options.launch === void 0 ? {} : { launch: options.launch },
32348
- ...options.context === void 0 ? {} : { context: options.context },
32349
- engineName,
32350
- ...options.engine === void 0 ? {} : { engine: options.engine },
32351
- ...options.engineFactory === void 0 ? {} : { engineFactory: options.engineFactory },
32352
- ...options.policy === void 0 ? {} : { policy: options.policy },
32353
- ...options.descriptorStore === void 0 ? {} : { descriptorStore: options.descriptorStore },
32354
- ...options.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: options.extractionDescriptorStore },
32355
- ...options.registryOverrides === void 0 ? {} : { registryOverrides: options.registryOverrides },
32356
- cleanupRootOnClose,
32357
- ...options.observability === void 0 ? {} : { observability: options.observability },
32358
- ...options.observationSessionId === void 0 ? {} : { observationSessionId: options.observationSessionId },
32359
- ...options.observationSink === void 0 ? {} : { observationSink: options.observationSink }
32360
- })
32361
- );
32340
+
32341
+ const globalScope = globalThis;
32342
+ const recorderKey = "__opensteerFlowRecorder";
32343
+ const historyStateKey = "__opensteerFlowRecorderHistory";
32344
+ const recorderUiAttribute = "data-opensteer-recorder-ui";
32345
+ const queueLimit = 1000;
32346
+ const singleAttributePriority = ${JSON.stringify(SINGLE_ATTRIBUTE_PRIORITY)};
32347
+ const stablePrimaryAttrKeys = new Set(${JSON.stringify([...STABLE_PRIMARY_ATTR_KEYS])});
32348
+ const matchAttributePriority = ${JSON.stringify([...MATCH_ATTRIBUTE_PRIORITY])};
32349
+ const attributeDenyKeys = new Set(${JSON.stringify([...ATTRIBUTE_DENY_KEYS])});
32350
+ const lazyLoadingMediaTags = new Set(${JSON.stringify([...LAZY_LOADING_MEDIA_TAGS])});
32351
+ const volatileLazyLoadingAttrs = new Set(${JSON.stringify([...VOLATILE_LAZY_LOADING_ATTRS])});
32352
+ const volatileClassTokens = new Set(${JSON.stringify([...VOLATILE_CLASS_TOKENS])});
32353
+ const volatileLazyClassTokens = new Set(${JSON.stringify([...VOLATILE_LAZY_CLASS_TOKENS])});
32354
+
32355
+ const previous = globalScope[recorderKey];
32356
+ if (previous && typeof previous.dispose === "function") {
32357
+ previous.dispose();
32362
32358
  }
32363
- };
32364
- function buildSharedRuntimeOptions(input) {
32365
- const ownership = resolveOwnership(input.browser);
32366
- const engineFactory = input.engineFactory ?? ((factoryOptions) => new OpensteerBrowserManager({
32367
- rootPath: input.rootPath,
32368
- ...input.workspaceName === void 0 ? {} : { workspace: input.workspaceName },
32369
- engineName: input.engineName,
32370
- ...(factoryOptions.browser ?? input.browser) === void 0 ? {} : { browser: factoryOptions.browser ?? input.browser },
32371
- ...(factoryOptions.launch ?? input.launch) === void 0 ? {} : { launch: factoryOptions.launch ?? input.launch },
32372
- ...(factoryOptions.context ?? input.context) === void 0 ? {} : { context: factoryOptions.context ?? input.context }
32373
- }).createEngine());
32374
- return {
32375
- name: input.name,
32376
- ...input.workspaceName === void 0 ? {} : { workspaceName: input.workspaceName },
32377
- rootPath: input.rootPath,
32378
- ...input.engine === void 0 ? {} : { engine: input.engine },
32379
- ...input.engine === void 0 ? { engineFactory } : {},
32380
- ...input.policy === void 0 ? {} : { policy: input.policy },
32381
- ...input.descriptorStore === void 0 ? {} : { descriptorStore: input.descriptorStore },
32382
- ...input.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: input.extractionDescriptorStore },
32383
- ...input.registryOverrides === void 0 ? {} : { registryOverrides: input.registryOverrides },
32384
- cleanupRootOnClose: input.cleanupRootOnClose,
32385
- ...input.observability === void 0 ? {} : { observability: input.observability },
32386
- ...input.observationSessionId === void 0 ? {} : { observationSessionId: input.observationSessionId },
32387
- ...input.observationSink === void 0 ? {} : { observationSink: input.observationSink },
32388
- sessionInfo: {
32389
- provider: {
32390
- mode: "local",
32391
- ownership,
32392
- engine: input.engineName
32393
- },
32394
- ...input.workspaceName === void 0 ? {} : { workspace: input.workspaceName },
32395
- reconnectable: !input.cleanupRootOnClose
32359
+
32360
+ const queue = [];
32361
+ const cleanup = [];
32362
+ const inputFlushTimers = new Map();
32363
+ const pendingInputs = new Map();
32364
+ let stopRequested = false;
32365
+ let pendingWheel = undefined;
32366
+
32367
+ const actionTargetTags = new Set([
32368
+ "a",
32369
+ "button",
32370
+ "input",
32371
+ "label",
32372
+ "option",
32373
+ "select",
32374
+ "summary",
32375
+ "textarea",
32376
+ ]);
32377
+
32378
+ function now() {
32379
+ return Date.now();
32380
+ }
32381
+
32382
+ function enqueue(entry) {
32383
+ queue.push({
32384
+ ...entry,
32385
+ timestamp: typeof entry.timestamp === "number" ? entry.timestamp : now(),
32386
+ });
32387
+ if (queue.length > queueLimit) {
32388
+ queue.splice(0, queue.length - queueLimit);
32396
32389
  }
32397
- };
32398
- }
32399
- function normalizeWorkspace2(workspace) {
32400
- if (workspace === void 0) {
32401
- return void 0;
32402
32390
  }
32403
- const trimmed = workspace.trim();
32404
- return trimmed.length === 0 ? void 0 : trimmed;
32405
- }
32406
- function resolveOwnership(browser) {
32407
- return typeof browser === "object" && browser.mode === "attach" ? "attached" : "owned";
32408
- }
32409
32391
 
32410
- // src/provider/config.ts
32411
- var OPENSTEER_PROVIDER_MODES = ["local", "cloud"];
32412
- function assertProviderSupportsEngine(provider, engine) {
32413
- if (engine !== "abp") {
32414
- return;
32392
+ function isRecorderUiNode(node) {
32393
+ let current = node instanceof Node ? node : null;
32394
+ while (current) {
32395
+ if (current instanceof Element && current.hasAttribute(recorderUiAttribute)) {
32396
+ return true;
32397
+ }
32398
+ const root = typeof current.getRootNode === "function" ? current.getRootNode() : null;
32399
+ if (root instanceof ShadowRoot) {
32400
+ current = root.host;
32401
+ continue;
32402
+ }
32403
+ current = current instanceof Element ? current.parentElement : null;
32404
+ }
32405
+ return false;
32415
32406
  }
32416
- if (provider === "cloud") {
32417
- throw new Error(
32418
- "ABP is not supported for provider=cloud. Cloud provider currently requires Playwright."
32419
- );
32407
+
32408
+ function isValidAttributeName(name) {
32409
+ if (typeof name !== "string") {
32410
+ return false;
32411
+ }
32412
+ const normalized = name.trim();
32413
+ if (normalized.length === 0) {
32414
+ return false;
32415
+ }
32416
+ if (/[\s"'<>/]/.test(normalized)) {
32417
+ return false;
32418
+ }
32419
+ return /^[A-Za-z_][A-Za-z0-9_:\-.]*$/.test(normalized);
32420
32420
  }
32421
- }
32422
- function normalizeOpensteerProviderMode(value, source = "OPENSTEER_PROVIDER") {
32423
- const normalized = value.trim().toLowerCase();
32424
- if (normalized === OPENSTEER_PROVIDER_MODES[0] || normalized === OPENSTEER_PROVIDER_MODES[1]) {
32425
- return normalized;
32421
+
32422
+ function escapeAttributeValue(value) {
32423
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
32426
32424
  }
32427
- throw new Error(
32428
- `${source} must be one of ${OPENSTEER_PROVIDER_MODES.join(", ")}; received "${value}".`
32429
- );
32430
- }
32431
- function resolveOpensteerProvider(input = {}) {
32432
- if (input.provider) {
32433
- return {
32434
- mode: input.provider.mode,
32435
- source: "explicit"
32436
- };
32425
+
32426
+ function escapeIdentifier(value) {
32427
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
32428
+ return CSS.escape(value);
32429
+ }
32430
+ return String(value).replace(/[^A-Za-z0-9_-]/g, (character) => {
32431
+ const codePoint = character.codePointAt(0);
32432
+ return "\\" + (codePoint == null ? "" : codePoint.toString(16)) + " ";
32433
+ });
32437
32434
  }
32438
- if (input.environmentProvider !== void 0 && input.environmentProvider.trim().length > 0) {
32439
- return {
32440
- mode: normalizeOpensteerProviderMode(input.environmentProvider),
32441
- source: "env"
32442
- };
32435
+
32436
+ function normalizeClassValue(element, rawValue) {
32437
+ const tag = element.tagName.toLowerCase();
32438
+ const tokens = String(rawValue)
32439
+ .split(/\s+/u)
32440
+ .map((token) => token.trim())
32441
+ .filter(Boolean)
32442
+ .filter((token) => !volatileClassTokens.has(token))
32443
+ .filter((token) => !(lazyLoadingMediaTags.has(tag) && volatileLazyClassTokens.has(token)));
32444
+ return tokens.join(" ");
32443
32445
  }
32444
- return {
32445
- mode: "local",
32446
- source: "default"
32447
- };
32448
- }
32449
- var execFile2 = promisify(execFile);
32450
- var NODE_SQLITE_SPECIFIER2 = `node:${"sqlite"}`;
32451
- var CHROME_EPOCH_OFFSET = 11644473600000000n;
32452
- var CHROME_HMAC_PREFIX_LENGTH = 32;
32453
- async function readBrowserCookies(input = {}) {
32454
- const brand2 = resolveRequestedBrand(input);
32455
- const userDataDir = resolveBrandUserDataDir(brand2, input.userDataDir);
32456
- const profileDirectory = input.profileDirectory ?? "Default";
32457
- const cookiesPath = join(userDataDir, profileDirectory, "Cookies");
32458
- if (!existsSync(cookiesPath)) {
32459
- throw new Error(
32460
- `Cookies database not found at "${cookiesPath}". Verify the browser brand, user-data-dir, and profile-directory are correct.`
32461
- );
32446
+
32447
+ function shouldKeepAttribute(element, name, value) {
32448
+ const key = String(name || "")
32449
+ .trim()
32450
+ .toLowerCase();
32451
+ if (!key || !String(value || "").trim()) {
32452
+ return false;
32453
+ }
32454
+ if (!isValidAttributeName(key)) {
32455
+ return false;
32456
+ }
32457
+ if (key === "c") {
32458
+ return false;
32459
+ }
32460
+ if (/^on[a-z]/i.test(key)) {
32461
+ return false;
32462
+ }
32463
+ if (attributeDenyKeys.has(key)) {
32464
+ return false;
32465
+ }
32466
+ if (key.startsWith("data-os-") || key.startsWith("data-opensteer-")) {
32467
+ return false;
32468
+ }
32469
+ if (lazyLoadingMediaTags.has(element.tagName.toLowerCase()) && volatileLazyLoadingAttrs.has(key)) {
32470
+ return false;
32471
+ }
32472
+ return true;
32462
32473
  }
32463
- const tempDir = await mkdtemp(join(tmpdir(), "opensteer-cookies-"));
32464
- try {
32465
- await copyCookiesDatabase(cookiesPath, tempDir);
32466
- const decryptionKey = await resolveDecryptionKey(brand2.id, userDataDir);
32467
- const rows = queryAllCookies(join(tempDir, "Cookies"));
32468
- const cookies = decryptCookieRows(rows, decryptionKey);
32469
- return {
32470
- cookies,
32471
- brandId: brand2.id,
32472
- brandDisplayName: brand2.displayName,
32473
- userDataDir,
32474
- profileDirectory
32475
- };
32476
- } finally {
32477
- await rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
32474
+
32475
+ function readAttributeValue(element, key) {
32476
+ if (key === "class") {
32477
+ const normalized = normalizeClassValue(element, element.getAttribute("class") || "");
32478
+ return normalized.length === 0 ? undefined : normalized;
32479
+ }
32480
+ const value = element.getAttribute(key);
32481
+ if (!shouldKeepAttribute(element, key, value || "")) {
32482
+ return undefined;
32483
+ }
32484
+ return value || undefined;
32478
32485
  }
32479
- }
32480
- function resolveRequestedBrand(input) {
32481
- if (input.brandId !== void 0) {
32482
- return getBrowserBrand(input.brandId);
32486
+
32487
+ function buildSingleAttributeSelector(element, key, value) {
32488
+ if (!value) {
32489
+ return undefined;
32490
+ }
32491
+ const tag = element.tagName.toLowerCase();
32492
+ if (key === "id") {
32493
+ const idSelector = "#" + escapeIdentifier(value);
32494
+ return element.matches(idSelector)
32495
+ ? idSelector
32496
+ : tag + '[id="' + escapeAttributeValue(value) + '"]';
32497
+ }
32498
+ return tag + "[" + key + '="' + escapeAttributeValue(value) + '"]';
32483
32499
  }
32484
- const installed = detectInstalledBrowserBrands()[0];
32500
+
32501
+ function isUniqueSelector(selector, element) {
32502
+ if (!selector) {
32503
+ return false;
32504
+ }
32505
+ let matches;
32506
+ try {
32507
+ matches = document.querySelectorAll(selector);
32508
+ } catch {
32509
+ return false;
32510
+ }
32511
+ return matches.length === 1 && matches[0] === element;
32512
+ }
32513
+
32514
+ function nearestRecordTarget(node) {
32515
+ if (isRecorderUiNode(node)) {
32516
+ return null;
32517
+ }
32518
+ let current = node instanceof Element ? node : null;
32519
+ while (current) {
32520
+ if (current.hasAttribute(recorderUiAttribute)) {
32521
+ return null;
32522
+ }
32523
+ const tag = current.tagName.toLowerCase();
32524
+ if (
32525
+ actionTargetTags.has(tag) ||
32526
+ current.hasAttribute("data-testid") ||
32527
+ current.hasAttribute("data-test") ||
32528
+ current.hasAttribute("data-qa") ||
32529
+ current.hasAttribute("data-cy") ||
32530
+ current.hasAttribute("role") ||
32531
+ current.hasAttribute("aria-label") ||
32532
+ current.hasAttribute("name")
32533
+ ) {
32534
+ return current;
32535
+ }
32536
+ current = current.parentElement;
32537
+ }
32538
+ return node instanceof Element ? node : null;
32539
+ }
32540
+
32541
+ function buildSegmentSelector(element) {
32542
+ const tag = element.tagName.toLowerCase();
32543
+ for (const key of matchAttributePriority) {
32544
+ const value = readAttributeValue(element, key);
32545
+ if (!value) {
32546
+ continue;
32547
+ }
32548
+ if (key === "class") {
32549
+ const tokens = value
32550
+ .split(/\s+/u)
32551
+ .map((token) => token.trim())
32552
+ .filter(Boolean)
32553
+ .slice(0, 2);
32554
+ if (tokens.length === 0) {
32555
+ continue;
32556
+ }
32557
+ return tag + tokens.map((token) => "." + escapeIdentifier(token)).join("");
32558
+ }
32559
+ return key === "id"
32560
+ ? tag + '[id="' + escapeAttributeValue(value) + '"]'
32561
+ : tag + "[" + key + '="' + escapeAttributeValue(value) + '"]';
32562
+ }
32563
+ return tag;
32564
+ }
32565
+
32566
+ function nthOfTypeSegment(element, baseSelector) {
32567
+ const parent = element.parentElement;
32568
+ if (!parent) {
32569
+ return baseSelector;
32570
+ }
32571
+ const sameType = Array.from(parent.children).filter(
32572
+ (child) => child.tagName === element.tagName,
32573
+ );
32574
+ if (sameType.length <= 1) {
32575
+ return baseSelector;
32576
+ }
32577
+ const index = sameType.indexOf(element) + 1;
32578
+ return baseSelector + ":nth-of-type(" + String(index) + ")";
32579
+ }
32580
+
32581
+ function buildSelector(node) {
32582
+ const element = nearestRecordTarget(node);
32583
+ if (!(element instanceof Element)) {
32584
+ return undefined;
32585
+ }
32586
+
32587
+ for (const key of singleAttributePriority) {
32588
+ const value = readAttributeValue(element, key);
32589
+ const selector = buildSingleAttributeSelector(element, key, value);
32590
+ if (selector && isUniqueSelector(selector, element)) {
32591
+ return selector;
32592
+ }
32593
+ if (value && !stablePrimaryAttrKeys.has(key) && key !== "id") {
32594
+ const tagQualified =
32595
+ element.tagName.toLowerCase() + "[" + key + '="' + escapeAttributeValue(value) + '"]';
32596
+ if (isUniqueSelector(tagQualified, element)) {
32597
+ return tagQualified;
32598
+ }
32599
+ }
32600
+ }
32601
+
32602
+ const segments = [];
32603
+ let current = element;
32604
+ let depth = 0;
32605
+ while (current && depth < 6) {
32606
+ const segment = nthOfTypeSegment(current, buildSegmentSelector(current));
32607
+ segments.unshift(segment);
32608
+ const selector = segments.join(" > ");
32609
+ if (isUniqueSelector(selector, element)) {
32610
+ return selector;
32611
+ }
32612
+ current = current.parentElement;
32613
+ depth += 1;
32614
+ }
32615
+
32616
+ const fallback = segments.join(" > ");
32617
+ return fallback.length > 0 ? fallback : element.tagName.toLowerCase();
32618
+ }
32619
+
32620
+ function readTargetValue(target) {
32621
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
32622
+ return target.value;
32623
+ }
32624
+ if (target instanceof HTMLSelectElement) {
32625
+ return target.value;
32626
+ }
32627
+ if (target instanceof HTMLElement && target.isContentEditable) {
32628
+ return target.textContent || "";
32629
+ }
32630
+ return undefined;
32631
+ }
32632
+
32633
+ function flushPendingInput(selector) {
32634
+ const pending = pendingInputs.get(selector);
32635
+ if (!pending) {
32636
+ return;
32637
+ }
32638
+ pendingInputs.delete(selector);
32639
+ const timer = inputFlushTimers.get(selector);
32640
+ if (timer !== undefined) {
32641
+ clearTimeout(timer);
32642
+ inputFlushTimers.delete(selector);
32643
+ }
32644
+ enqueue({
32645
+ kind: "type",
32646
+ selector,
32647
+ text: pending.text,
32648
+ timestamp: pending.timestamp,
32649
+ });
32650
+ }
32651
+
32652
+ function flushAllInputs() {
32653
+ for (const selector of Array.from(pendingInputs.keys())) {
32654
+ flushPendingInput(selector);
32655
+ }
32656
+ }
32657
+
32658
+ function flushPendingWheel() {
32659
+ if (!pendingWheel) {
32660
+ return;
32661
+ }
32662
+ clearTimeout(pendingWheel.timerId);
32663
+ enqueue({
32664
+ kind: "scroll",
32665
+ selector: pendingWheel.selector,
32666
+ deltaX: pendingWheel.deltaX,
32667
+ deltaY: pendingWheel.deltaY,
32668
+ timestamp: pendingWheel.timestamp,
32669
+ });
32670
+ pendingWheel = undefined;
32671
+ }
32672
+
32673
+ function scheduleInputFlush(selector) {
32674
+ const existing = inputFlushTimers.get(selector);
32675
+ if (existing !== undefined) {
32676
+ clearTimeout(existing);
32677
+ }
32678
+ const timerId = setTimeout(() => {
32679
+ flushPendingInput(selector);
32680
+ }, 400);
32681
+ inputFlushTimers.set(selector, timerId);
32682
+ }
32683
+
32684
+ function updateHistoryState(mode, nextUrl) {
32685
+ let state;
32686
+ try {
32687
+ const raw = sessionStorage.getItem(historyStateKey);
32688
+ state = raw ? JSON.parse(raw) : undefined;
32689
+ } catch {
32690
+ state = undefined;
32691
+ }
32692
+ if (!state || !Array.isArray(state.entries) || typeof state.index !== "number") {
32693
+ state = { entries: [location.href], index: 0 };
32694
+ }
32695
+ if (mode === "replace") {
32696
+ state.entries[state.index] = nextUrl;
32697
+ } else if (mode === "push") {
32698
+ state.entries = state.entries.slice(0, state.index + 1);
32699
+ state.entries.push(nextUrl);
32700
+ state.index = state.entries.length - 1;
32701
+ } else if (mode === "back") {
32702
+ state.index = Math.max(0, state.index - 1);
32703
+ } else if (mode === "forward") {
32704
+ state.index = Math.min(state.entries.length - 1, state.index + 1);
32705
+ }
32706
+ try {
32707
+ sessionStorage.setItem(historyStateKey, JSON.stringify(state));
32708
+ } catch {}
32709
+ return state;
32710
+ }
32711
+
32712
+ function readHistoryState() {
32713
+ try {
32714
+ const raw = sessionStorage.getItem(historyStateKey);
32715
+ const parsed = raw ? JSON.parse(raw) : undefined;
32716
+ if (
32717
+ parsed &&
32718
+ Array.isArray(parsed.entries) &&
32719
+ parsed.entries.every((entry) => typeof entry === "string") &&
32720
+ typeof parsed.index === "number"
32721
+ ) {
32722
+ return parsed;
32723
+ }
32724
+ } catch {}
32725
+ return undefined;
32726
+ }
32727
+
32728
+ function classifyHistoryTraversal(currentUrl) {
32729
+ const state = readHistoryState();
32730
+ if (!state) {
32731
+ return undefined;
32732
+ }
32733
+ if (state.entries[state.index - 1] === currentUrl) {
32734
+ updateHistoryState("back", currentUrl);
32735
+ return "go-back";
32736
+ }
32737
+ if (state.entries[state.index + 1] === currentUrl) {
32738
+ updateHistoryState("forward", currentUrl);
32739
+ return "go-forward";
32740
+ }
32741
+ const existingIndex = state.entries.lastIndexOf(currentUrl);
32742
+ if (existingIndex !== -1) {
32743
+ if (existingIndex < state.index) {
32744
+ while (readHistoryState()?.index > existingIndex) {
32745
+ updateHistoryState("back", currentUrl);
32746
+ }
32747
+ return "go-back";
32748
+ }
32749
+ if (existingIndex > state.index) {
32750
+ while (readHistoryState()?.index < existingIndex) {
32751
+ updateHistoryState("forward", currentUrl);
32752
+ }
32753
+ return "go-forward";
32754
+ }
32755
+ }
32756
+ return undefined;
32757
+ }
32758
+
32759
+ function onInstall() {
32760
+ const currentUrl = location.href;
32761
+ const navigationEntry =
32762
+ typeof performance.getEntriesByType === "function"
32763
+ ? performance.getEntriesByType("navigation")[0]
32764
+ : undefined;
32765
+ const navigationType =
32766
+ navigationEntry && typeof navigationEntry.type === "string" ? navigationEntry.type : undefined;
32767
+ const existingState = readHistoryState();
32768
+
32769
+ if (!existingState) {
32770
+ updateHistoryState("replace", currentUrl);
32771
+ return;
32772
+ }
32773
+
32774
+ if (navigationType === "reload") {
32775
+ updateHistoryState("replace", currentUrl);
32776
+ enqueue({
32777
+ kind: "reload",
32778
+ url: currentUrl,
32779
+ });
32780
+ return;
32781
+ }
32782
+
32783
+ if (navigationType === "back_forward") {
32784
+ const traversal = classifyHistoryTraversal(currentUrl);
32785
+ if (traversal === "go-back" || traversal === "go-forward") {
32786
+ enqueue({
32787
+ kind: traversal,
32788
+ url: currentUrl,
32789
+ });
32790
+ return;
32791
+ }
32792
+ }
32793
+
32794
+ if (existingState.entries[existingState.index] !== currentUrl) {
32795
+ updateHistoryState("push", currentUrl);
32796
+ enqueue({
32797
+ kind: "navigate",
32798
+ url: currentUrl,
32799
+ source: "full-navigation",
32800
+ });
32801
+ }
32802
+ }
32803
+
32804
+ function modifierKeys(event) {
32805
+ const modifiers = [];
32806
+ if (event.altKey) {
32807
+ modifiers.push("Alt");
32808
+ }
32809
+ if (event.ctrlKey) {
32810
+ modifiers.push("Control");
32811
+ }
32812
+ if (event.metaKey) {
32813
+ modifiers.push("Meta");
32814
+ }
32815
+ if (event.shiftKey) {
32816
+ modifiers.push("Shift");
32817
+ }
32818
+ return modifiers;
32819
+ }
32820
+
32821
+ function addListener(target, type, listener, options) {
32822
+ target.addEventListener(type, listener, options);
32823
+ cleanup.push(() => {
32824
+ target.removeEventListener(type, listener, options);
32825
+ });
32826
+ }
32827
+
32828
+ function requestStop() {
32829
+ stopRequested = true;
32830
+ }
32831
+
32832
+ function mountStopButton() {
32833
+ if (document.querySelector("[" + recorderUiAttribute + "]")) {
32834
+ return true;
32835
+ }
32836
+ if (stopRequested) {
32837
+ return true;
32838
+ }
32839
+ const parent = document.body || document.documentElement;
32840
+ if (!(parent instanceof HTMLElement)) {
32841
+ return false;
32842
+ }
32843
+
32844
+ const host = document.createElement("div");
32845
+ host.setAttribute(recorderUiAttribute, "true");
32846
+ const shadowRoot = host.attachShadow({ mode: "open" });
32847
+ const style = document.createElement("style");
32848
+ style.textContent = [
32849
+ ":host {",
32850
+ " all: initial;",
32851
+ " display: block;",
32852
+ " position: fixed;",
32853
+ " top: 16px;",
32854
+ " right: 16px;",
32855
+ " z-index: 2147483647;",
32856
+ " pointer-events: auto;",
32857
+ " color-scheme: only light;",
32858
+ " contain: content;",
32859
+ " isolation: isolate;",
32860
+ "}",
32861
+ "button {",
32862
+ " appearance: none;",
32863
+ " border: 1px solid rgba(15, 23, 42, 0.2);",
32864
+ " border-radius: 999px;",
32865
+ " background: rgba(15, 23, 42, 0.92);",
32866
+ " color: #ffffff;",
32867
+ " color-scheme: only light;",
32868
+ " cursor: pointer;",
32869
+ " font: 600 12px/1.2 ui-sans-serif, system-ui, sans-serif;",
32870
+ " letter-spacing: 0.01em;",
32871
+ " padding: 10px 14px;",
32872
+ " box-shadow: 0 10px 30px rgba(15, 23, 42, 0.22);",
32873
+ "}",
32874
+ "button:hover {",
32875
+ " background: rgba(15, 23, 42, 0.98);",
32876
+ "}",
32877
+ "button:focus-visible {",
32878
+ " outline: 2px solid #2563eb;",
32879
+ " outline-offset: 2px;",
32880
+ "}",
32881
+ "button:disabled {",
32882
+ " cursor: wait;",
32883
+ " opacity: 0.8;",
32884
+ "}",
32885
+ ].join("\n");
32886
+ const button = document.createElement("button");
32887
+ button.type = "button";
32888
+ button.textContent = "Stop recording";
32889
+ button.setAttribute("aria-label", "Stop recording");
32890
+ const consumePointerEvent = (event) => {
32891
+ event.preventDefault();
32892
+ event.stopPropagation();
32893
+ };
32894
+ for (const type of ["pointerdown", "mousedown", "mouseup", "click"]) {
32895
+ button.addEventListener(type, consumePointerEvent);
32896
+ }
32897
+ button.addEventListener("click", () => {
32898
+ requestStop();
32899
+ host.remove();
32900
+ });
32901
+ shadowRoot.append(style, button);
32902
+ parent.appendChild(host);
32903
+ cleanup.push(() => {
32904
+ host.remove();
32905
+ });
32906
+ return true;
32907
+ }
32908
+
32909
+ function ensureStopButtonMounted() {
32910
+ if (mountStopButton()) {
32911
+ return;
32912
+ }
32913
+
32914
+ const observer = new MutationObserver(() => {
32915
+ if (!mountStopButton()) {
32916
+ return;
32917
+ }
32918
+ observer.disconnect();
32919
+ });
32920
+ observer.observe(document, {
32921
+ childList: true,
32922
+ subtree: true,
32923
+ });
32924
+ cleanup.push(() => {
32925
+ observer.disconnect();
32926
+ });
32927
+ }
32928
+
32929
+ addListener(document, "click", (event) => {
32930
+ if (!event.isTrusted) {
32931
+ return;
32932
+ }
32933
+ const target = nearestRecordTarget(event.target);
32934
+ if (!(target instanceof Element)) {
32935
+ return;
32936
+ }
32937
+ const tag = target.tagName.toLowerCase();
32938
+ if (tag === "select" || tag === "option") {
32939
+ return;
32940
+ }
32941
+ const selector = buildSelector(target);
32942
+ if (!selector) {
32943
+ return;
32944
+ }
32945
+ enqueue({
32946
+ kind: "click",
32947
+ selector,
32948
+ button: event.button,
32949
+ modifiers: modifierKeys(event),
32950
+ });
32951
+ }, true);
32952
+
32953
+ addListener(document, "dblclick", (event) => {
32954
+ if (!event.isTrusted) {
32955
+ return;
32956
+ }
32957
+ const target = nearestRecordTarget(event.target);
32958
+ if (!(target instanceof Element)) {
32959
+ return;
32960
+ }
32961
+ const selector = buildSelector(target);
32962
+ if (!selector) {
32963
+ return;
32964
+ }
32965
+ enqueue({
32966
+ kind: "dblclick",
32967
+ selector,
32968
+ });
32969
+ }, true);
32970
+
32971
+ addListener(document, "input", (event) => {
32972
+ if (!event.isTrusted) {
32973
+ return;
32974
+ }
32975
+ const target = nearestRecordTarget(event.target);
32976
+ if (!(target instanceof Element)) {
32977
+ return;
32978
+ }
32979
+ const selector = buildSelector(target);
32980
+ const text = readTargetValue(target);
32981
+ if (!selector || typeof text !== "string") {
32982
+ return;
32983
+ }
32984
+ pendingInputs.set(selector, {
32985
+ selector,
32986
+ text,
32987
+ timestamp: now(),
32988
+ });
32989
+ scheduleInputFlush(selector);
32990
+ }, true);
32991
+
32992
+ addListener(document, "change", (event) => {
32993
+ if (!event.isTrusted) {
32994
+ return;
32995
+ }
32996
+ const target = nearestRecordTarget(event.target);
32997
+ if (!(target instanceof HTMLSelectElement)) {
32998
+ return;
32999
+ }
33000
+ const selector = buildSelector(target);
33001
+ if (!selector) {
33002
+ return;
33003
+ }
33004
+ const selectedOption = target.selectedOptions && target.selectedOptions.length > 0
33005
+ ? target.selectedOptions[0]
33006
+ : undefined;
33007
+ enqueue({
33008
+ kind: "select-option",
33009
+ selector,
33010
+ value: target.value,
33011
+ ...(selectedOption === undefined ? {} : { label: selectedOption.label || selectedOption.textContent || "" }),
33012
+ });
33013
+ }, true);
33014
+
33015
+ addListener(document, "keydown", (event) => {
33016
+ if (!event.isTrusted) {
33017
+ return;
33018
+ }
33019
+ const allowedKeys = new Set([
33020
+ "ArrowDown",
33021
+ "ArrowLeft",
33022
+ "ArrowRight",
33023
+ "ArrowUp",
33024
+ "Backspace",
33025
+ "Delete",
33026
+ "Enter",
33027
+ "Escape",
33028
+ "Tab",
33029
+ ]);
33030
+ if (!allowedKeys.has(event.key)) {
33031
+ return;
33032
+ }
33033
+ const target = nearestRecordTarget(event.target);
33034
+ const selector = target instanceof Element ? buildSelector(target) : undefined;
33035
+ if (event.key === "Enter" && selector) {
33036
+ flushPendingInput(selector);
33037
+ }
33038
+ enqueue({
33039
+ kind: "keypress",
33040
+ key: event.key,
33041
+ modifiers: modifierKeys(event),
33042
+ ...(selector === undefined ? {} : { selector }),
33043
+ });
33044
+ }, true);
33045
+
33046
+ addListener(document, "wheel", (event) => {
33047
+ if (!event.isTrusted) {
33048
+ return;
33049
+ }
33050
+ const target = nearestRecordTarget(event.target);
33051
+ const selector = target instanceof Element ? buildSelector(target) : undefined;
33052
+ if (pendingWheel && pendingWheel.selector === selector) {
33053
+ pendingWheel.deltaX += event.deltaX;
33054
+ pendingWheel.deltaY += event.deltaY;
33055
+ clearTimeout(pendingWheel.timerId);
33056
+ pendingWheel.timerId = setTimeout(flushPendingWheel, 250);
33057
+ return;
33058
+ }
33059
+ flushPendingWheel();
33060
+ pendingWheel = {
33061
+ selector,
33062
+ deltaX: event.deltaX,
33063
+ deltaY: event.deltaY,
33064
+ timestamp: now(),
33065
+ timerId: setTimeout(flushPendingWheel, 250),
33066
+ };
33067
+ }, true);
33068
+
33069
+ const originalPushState = history.pushState.bind(history);
33070
+ history.pushState = function pushState(state, unused, url) {
33071
+ const beforeUrl = location.href;
33072
+ const output = originalPushState.apply(this, arguments);
33073
+ const nextUrl = location.href;
33074
+ if (nextUrl !== beforeUrl) {
33075
+ updateHistoryState("push", nextUrl);
33076
+ enqueue({
33077
+ kind: "navigate",
33078
+ url: nextUrl,
33079
+ source: "push-state",
33080
+ });
33081
+ }
33082
+ return output;
33083
+ };
33084
+ cleanup.push(() => {
33085
+ history.pushState = originalPushState;
33086
+ });
33087
+
33088
+ const originalReplaceState = history.replaceState.bind(history);
33089
+ history.replaceState = function replaceState(state, unused, url) {
33090
+ const beforeUrl = location.href;
33091
+ const output = originalReplaceState.apply(this, arguments);
33092
+ const nextUrl = location.href;
33093
+ if (nextUrl !== beforeUrl) {
33094
+ updateHistoryState("replace", nextUrl);
33095
+ enqueue({
33096
+ kind: "navigate",
33097
+ url: nextUrl,
33098
+ source: "replace-state",
33099
+ });
33100
+ }
33101
+ return output;
33102
+ };
33103
+ cleanup.push(() => {
33104
+ history.replaceState = originalReplaceState;
33105
+ });
33106
+
33107
+ addListener(globalScope, "popstate", () => {
33108
+ const currentUrl = location.href;
33109
+ const traversal = classifyHistoryTraversal(currentUrl);
33110
+ if (traversal === "go-back" || traversal === "go-forward") {
33111
+ enqueue({
33112
+ kind: traversal,
33113
+ url: currentUrl,
33114
+ });
33115
+ return;
33116
+ }
33117
+ if (readHistoryState()?.entries[readHistoryState().index] !== currentUrl) {
33118
+ updateHistoryState("push", currentUrl);
33119
+ enqueue({
33120
+ kind: "navigate",
33121
+ url: currentUrl,
33122
+ source: "history-traversal",
33123
+ });
33124
+ }
33125
+ });
33126
+
33127
+ addListener(globalScope, "hashchange", () => {
33128
+ const currentUrl = location.href;
33129
+ updateHistoryState("replace", currentUrl);
33130
+ enqueue({
33131
+ kind: "navigate",
33132
+ url: currentUrl,
33133
+ source: "hashchange",
33134
+ });
33135
+ });
33136
+
33137
+ function drain() {
33138
+ flushAllInputs();
33139
+ flushPendingWheel();
33140
+ return {
33141
+ url: location.href,
33142
+ focused: document.hasFocus(),
33143
+ visibilityState: document.visibilityState,
33144
+ stopRequested,
33145
+ events: queue.splice(0, queue.length),
33146
+ };
33147
+ }
33148
+
33149
+ globalScope[recorderKey] = {
33150
+ installed: true,
33151
+ debugSelector(target) {
33152
+ return buildSelector(target);
33153
+ },
33154
+ drain,
33155
+ requestStop,
33156
+ dispose() {
33157
+ flushAllInputs();
33158
+ flushPendingWheel();
33159
+ for (const dispose of cleanup.splice(0, cleanup.length)) {
33160
+ dispose();
33161
+ }
33162
+ for (const timerId of inputFlushTimers.values()) {
33163
+ clearTimeout(timerId);
33164
+ }
33165
+ inputFlushTimers.clear();
33166
+ pendingInputs.clear();
33167
+ delete globalScope[recorderKey];
33168
+ },
33169
+ };
33170
+
33171
+ ensureStopButtonMounted();
33172
+ onInstall();
33173
+ })();`;
33174
+ var FLOW_RECORDER_INSTALL_SCRIPT = FLOW_RECORDER_INSTALL_SOURCE;
33175
+ var FLOW_RECORDER_DRAIN_SCRIPT = String.raw`(() => {
33176
+ const recorder = globalThis.__opensteerFlowRecorder;
33177
+ if (!recorder || typeof recorder.drain !== "function") {
33178
+ return {
33179
+ url: location.href,
33180
+ focused: document.hasFocus(),
33181
+ visibilityState: document.visibilityState,
33182
+ stopRequested: false,
33183
+ events: [],
33184
+ };
33185
+ }
33186
+ return recorder.drain();
33187
+ })();`;
33188
+
33189
+ // ../runtime-core/src/recorder/event-collector.ts
33190
+ var FlowRecorderCollector = class {
33191
+ runtime;
33192
+ pollIntervalMs;
33193
+ onAction;
33194
+ pages = /* @__PURE__ */ new Map();
33195
+ actions = [];
33196
+ nextPageOrdinal = 0;
33197
+ runningLoop;
33198
+ loopStopRequested = false;
33199
+ stopDetected = false;
33200
+ focusedPageId;
33201
+ stopWaiters = [];
33202
+ constructor(runtime, options = {}) {
33203
+ this.runtime = runtime;
33204
+ this.pollIntervalMs = options.pollIntervalMs ?? 250;
33205
+ this.onAction = options.onAction;
33206
+ }
33207
+ async install() {
33208
+ await this.runtime.addInitScript({
33209
+ script: FLOW_RECORDER_INSTALL_SCRIPT
33210
+ });
33211
+ const { pages } = await this.runtime.listPages();
33212
+ for (const page of pages) {
33213
+ this.ensureKnownPage(page.pageRef, page.url, page.openerPageRef);
33214
+ }
33215
+ await Promise.all(
33216
+ pages.map(
33217
+ (page) => this.runtime.evaluate({
33218
+ script: FLOW_RECORDER_INSTALL_SCRIPT,
33219
+ pageRef: page.pageRef
33220
+ }).catch(() => void 0)
33221
+ )
33222
+ );
33223
+ const evaluatedPages = await this.readEvaluatedPages(pages);
33224
+ for (const page of evaluatedPages) {
33225
+ this.updateKnownPage(page.pageRef, page.currentUrl, page.openerPageRef);
33226
+ }
33227
+ this.focusedPageId = evaluatedPages.find((page) => page.focused)?.pageId ?? this.focusedPageId;
33228
+ }
33229
+ start() {
33230
+ if (this.runningLoop !== void 0) {
33231
+ return;
33232
+ }
33233
+ this.loopStopRequested = false;
33234
+ this.runningLoop = this.runLoop();
33235
+ }
33236
+ async stop() {
33237
+ this.loopStopRequested = true;
33238
+ if (this.runningLoop !== void 0) {
33239
+ await this.runningLoop;
33240
+ this.runningLoop = void 0;
33241
+ }
33242
+ if (!this.stopDetected) {
33243
+ await this.pollOnce().catch(() => void 0);
33244
+ }
33245
+ return this.actions.slice();
33246
+ }
33247
+ getActions() {
33248
+ return this.actions.slice();
33249
+ }
33250
+ async waitForStop() {
33251
+ if (this.stopDetected) {
33252
+ return;
33253
+ }
33254
+ await new Promise((resolve4) => {
33255
+ this.stopWaiters.push(resolve4);
33256
+ });
33257
+ }
33258
+ async pollOnce() {
33259
+ const pollTimestamp = Date.now();
33260
+ const { pages } = await this.runtime.listPages();
33261
+ const previousPageRefs = new Set(this.pages.keys());
33262
+ const evaluatedPages = await this.readEvaluatedPages(pages);
33263
+ if (!this.stopDetected && evaluatedPages.some((page) => page.stopRequested)) {
33264
+ this.stopDetected = true;
33265
+ this.loopStopRequested = true;
33266
+ for (const resolve4 of this.stopWaiters.splice(0, this.stopWaiters.length)) {
33267
+ resolve4();
33268
+ }
33269
+ }
33270
+ const actions = this.normalizePoll({
33271
+ pollTimestamp,
33272
+ previousPageRefs,
33273
+ listedPages: pages,
33274
+ evaluatedPages
33275
+ });
33276
+ if (actions.length === 0) {
33277
+ return [];
33278
+ }
33279
+ actions.sort((left, right) => {
33280
+ const timestampOrder = left.timestamp - right.timestamp;
33281
+ if (timestampOrder !== 0) {
33282
+ return timestampOrder;
33283
+ }
33284
+ return actionSortPriority(left.kind) - actionSortPriority(right.kind);
33285
+ });
33286
+ for (const action of actions) {
33287
+ this.actions.push(action);
33288
+ if (this.onAction !== void 0) {
33289
+ await this.onAction(action);
33290
+ }
33291
+ }
33292
+ return actions;
33293
+ }
33294
+ async runLoop() {
33295
+ while (!this.loopStopRequested) {
33296
+ try {
33297
+ await this.pollOnce();
33298
+ } catch {
33299
+ }
33300
+ if (this.loopStopRequested) {
33301
+ break;
33302
+ }
33303
+ await delay(this.pollIntervalMs);
33304
+ }
33305
+ }
33306
+ async readEvaluatedPages(listedPages) {
33307
+ const pages = await Promise.all(
33308
+ listedPages.map(async (page) => {
33309
+ const knownPage = this.ensureKnownPage(page.pageRef, page.url, page.openerPageRef);
33310
+ const snapshot = await this.readSnapshot(page.pageRef, page.url);
33311
+ this.updateKnownPage(page.pageRef, snapshot.url, page.openerPageRef);
33312
+ return {
33313
+ pageRef: page.pageRef,
33314
+ pageId: knownPage.pageId,
33315
+ previousUrl: knownPage.currentUrl,
33316
+ currentUrl: snapshot.url,
33317
+ focused: snapshot.focused || snapshot.visibilityState === "visible",
33318
+ stopRequested: snapshot.stopRequested,
33319
+ events: snapshot.events,
33320
+ ...page.openerPageRef === void 0 ? {} : { openerPageRef: page.openerPageRef },
33321
+ ...knownPage.openerPageId === void 0 ? {} : { openerPageId: knownPage.openerPageId }
33322
+ };
33323
+ })
33324
+ );
33325
+ return pages;
33326
+ }
33327
+ async readSnapshot(pageRef, fallbackUrl) {
33328
+ const value = await this.runtime.evaluate({
33329
+ script: FLOW_RECORDER_DRAIN_SCRIPT,
33330
+ pageRef
33331
+ });
33332
+ return normalizeSnapshot(value, fallbackUrl);
33333
+ }
33334
+ normalizePoll(input) {
33335
+ const listedPageRefs = new Set(input.listedPages.map((page) => page.pageRef));
33336
+ const actions = [];
33337
+ const firstEventTimestampByPage = /* @__PURE__ */ new Map();
33338
+ for (const evaluatedPage of input.evaluatedPages) {
33339
+ const firstTimestamp = evaluatedPage.events[0]?.timestamp;
33340
+ if (firstTimestamp !== void 0) {
33341
+ firstEventTimestampByPage.set(evaluatedPage.pageRef, firstTimestamp);
33342
+ }
33343
+ }
33344
+ for (const listedPage of input.listedPages) {
33345
+ if (!input.previousPageRefs.has(listedPage.pageRef)) {
33346
+ const created = this.ensureKnownPage(listedPage.pageRef, listedPage.url, listedPage.openerPageRef);
33347
+ actions.push({
33348
+ kind: "new-tab",
33349
+ timestamp: Math.max(0, (firstEventTimestampByPage.get(listedPage.pageRef) ?? input.pollTimestamp) - 1),
33350
+ pageId: created.pageId,
33351
+ pageUrl: listedPage.url,
33352
+ detail: {
33353
+ kind: "new-tab",
33354
+ ...created.openerPageId === void 0 ? {} : { openerPageId: created.openerPageId },
33355
+ initialUrl: listedPage.url
33356
+ }
33357
+ });
33358
+ }
33359
+ }
33360
+ for (const [pageRef, knownPage] of [...this.pages.entries()]) {
33361
+ if (listedPageRefs.has(pageRef)) {
33362
+ continue;
33363
+ }
33364
+ actions.push({
33365
+ kind: "close-tab",
33366
+ timestamp: input.pollTimestamp,
33367
+ pageId: knownPage.pageId,
33368
+ pageUrl: knownPage.currentUrl,
33369
+ detail: {
33370
+ kind: "close-tab"
33371
+ }
33372
+ });
33373
+ this.pages.delete(pageRef);
33374
+ }
33375
+ for (const evaluatedPage of input.evaluatedPages) {
33376
+ const knownPage = this.pages.get(evaluatedPage.pageRef);
33377
+ if (!knownPage) {
33378
+ continue;
33379
+ }
33380
+ if (evaluatedPage.previousUrl !== evaluatedPage.currentUrl && !evaluatedPage.events.some(
33381
+ (event) => event.kind === "navigate" || event.kind === "reload" || event.kind === "go-back" || event.kind === "go-forward"
33382
+ )) {
33383
+ actions.push({
33384
+ kind: "navigate",
33385
+ timestamp: Math.max(
33386
+ 0,
33387
+ (firstEventTimestampByPage.get(evaluatedPage.pageRef) ?? input.pollTimestamp) - 1
33388
+ ),
33389
+ pageId: knownPage.pageId,
33390
+ pageUrl: evaluatedPage.currentUrl,
33391
+ detail: {
33392
+ kind: "navigate",
33393
+ url: evaluatedPage.currentUrl,
33394
+ source: "poll"
33395
+ }
33396
+ });
33397
+ }
33398
+ actions.push(
33399
+ ...evaluatedPage.events.flatMap(
33400
+ (event) => this.normalizeRawEvent(event, knownPage, evaluatedPage.currentUrl)
33401
+ )
33402
+ );
33403
+ this.updateKnownPage(evaluatedPage.pageRef, evaluatedPage.currentUrl, evaluatedPage.openerPageRef);
33404
+ }
33405
+ const focusedPage = input.evaluatedPages.find((page) => page.focused);
33406
+ if (focusedPage !== void 0 && focusedPage.pageId !== this.focusedPageId) {
33407
+ actions.push({
33408
+ kind: "switch-tab",
33409
+ timestamp: Math.max(
33410
+ 0,
33411
+ (firstEventTimestampByPage.get(focusedPage.pageRef) ?? input.pollTimestamp) - 1
33412
+ ),
33413
+ pageId: focusedPage.pageId,
33414
+ pageUrl: focusedPage.currentUrl,
33415
+ detail: {
33416
+ kind: "switch-tab",
33417
+ ...this.focusedPageId === void 0 ? {} : { fromPageId: this.focusedPageId },
33418
+ toPageId: focusedPage.pageId
33419
+ }
33420
+ });
33421
+ this.focusedPageId = focusedPage.pageId;
33422
+ }
33423
+ return dedupeConsecutiveSwitchActions(actions);
33424
+ }
33425
+ normalizeRawEvent(event, page, currentUrl) {
33426
+ switch (event.kind) {
33427
+ case "navigate":
33428
+ return [
33429
+ {
33430
+ kind: "navigate",
33431
+ timestamp: event.timestamp,
33432
+ pageId: page.pageId,
33433
+ pageUrl: event.url,
33434
+ detail: {
33435
+ kind: "navigate",
33436
+ url: event.url,
33437
+ source: event.source
33438
+ }
33439
+ }
33440
+ ];
33441
+ case "click":
33442
+ return [
33443
+ {
33444
+ kind: "click",
33445
+ timestamp: event.timestamp,
33446
+ pageId: page.pageId,
33447
+ pageUrl: currentUrl,
33448
+ selector: event.selector,
33449
+ detail: {
33450
+ kind: "click",
33451
+ button: event.button,
33452
+ modifiers: event.modifiers
33453
+ }
33454
+ }
33455
+ ];
33456
+ case "dblclick":
33457
+ return [
33458
+ {
33459
+ kind: "dblclick",
33460
+ timestamp: event.timestamp,
33461
+ pageId: page.pageId,
33462
+ pageUrl: currentUrl,
33463
+ selector: event.selector,
33464
+ detail: {
33465
+ kind: "dblclick"
33466
+ }
33467
+ }
33468
+ ];
33469
+ case "type":
33470
+ return [
33471
+ {
33472
+ kind: "type",
33473
+ timestamp: event.timestamp,
33474
+ pageId: page.pageId,
33475
+ pageUrl: currentUrl,
33476
+ selector: event.selector,
33477
+ detail: {
33478
+ kind: "type",
33479
+ text: event.text
33480
+ }
33481
+ }
33482
+ ];
33483
+ case "keypress":
33484
+ return [
33485
+ {
33486
+ kind: "keypress",
33487
+ timestamp: event.timestamp,
33488
+ pageId: page.pageId,
33489
+ pageUrl: currentUrl,
33490
+ ...event.selector === void 0 ? {} : { selector: event.selector },
33491
+ detail: {
33492
+ kind: "keypress",
33493
+ key: event.key,
33494
+ modifiers: event.modifiers
33495
+ }
33496
+ }
33497
+ ];
33498
+ case "scroll":
33499
+ return [
33500
+ {
33501
+ kind: "scroll",
33502
+ timestamp: event.timestamp,
33503
+ pageId: page.pageId,
33504
+ pageUrl: currentUrl,
33505
+ ...event.selector === void 0 ? {} : { selector: event.selector },
33506
+ detail: {
33507
+ kind: "scroll",
33508
+ deltaX: event.deltaX,
33509
+ deltaY: event.deltaY
33510
+ }
33511
+ }
33512
+ ];
33513
+ case "select-option":
33514
+ return [
33515
+ {
33516
+ kind: "select-option",
33517
+ timestamp: event.timestamp,
33518
+ pageId: page.pageId,
33519
+ pageUrl: currentUrl,
33520
+ selector: event.selector,
33521
+ detail: {
33522
+ kind: "select-option",
33523
+ value: event.value,
33524
+ ...event.label === void 0 ? {} : { label: event.label }
33525
+ }
33526
+ }
33527
+ ];
33528
+ case "reload":
33529
+ return [
33530
+ {
33531
+ kind: "reload",
33532
+ timestamp: event.timestamp,
33533
+ pageId: page.pageId,
33534
+ pageUrl: event.url,
33535
+ detail: {
33536
+ kind: "reload",
33537
+ url: event.url
33538
+ }
33539
+ }
33540
+ ];
33541
+ case "go-back":
33542
+ return [
33543
+ {
33544
+ kind: "go-back",
33545
+ timestamp: event.timestamp,
33546
+ pageId: page.pageId,
33547
+ pageUrl: event.url,
33548
+ detail: {
33549
+ kind: "go-back",
33550
+ url: event.url
33551
+ }
33552
+ }
33553
+ ];
33554
+ case "go-forward":
33555
+ return [
33556
+ {
33557
+ kind: "go-forward",
33558
+ timestamp: event.timestamp,
33559
+ pageId: page.pageId,
33560
+ pageUrl: event.url,
33561
+ detail: {
33562
+ kind: "go-forward",
33563
+ url: event.url
33564
+ }
33565
+ }
33566
+ ];
33567
+ }
33568
+ }
33569
+ ensureKnownPage(pageRef, url, openerPageRef) {
33570
+ const existing = this.pages.get(pageRef);
33571
+ if (existing !== void 0) {
33572
+ return existing;
33573
+ }
33574
+ const openerPageId = openerPageRef === void 0 ? void 0 : this.pages.get(openerPageRef)?.pageId;
33575
+ const page = {
33576
+ pageId: `page${String(this.nextPageOrdinal++)}`,
33577
+ pageRef,
33578
+ currentUrl: url,
33579
+ ...openerPageRef === void 0 ? {} : { openerPageRef },
33580
+ ...openerPageId === void 0 ? {} : { openerPageId }
33581
+ };
33582
+ this.pages.set(pageRef, page);
33583
+ return page;
33584
+ }
33585
+ updateKnownPage(pageRef, url, openerPageRef) {
33586
+ const current = this.pages.get(pageRef);
33587
+ if (current === void 0) {
33588
+ this.ensureKnownPage(pageRef, url, openerPageRef);
33589
+ return;
33590
+ }
33591
+ const openerPageId = openerPageRef === void 0 ? void 0 : this.pages.get(openerPageRef)?.pageId;
33592
+ this.pages.set(pageRef, {
33593
+ ...current,
33594
+ currentUrl: url,
33595
+ ...openerPageRef === void 0 ? {} : { openerPageRef },
33596
+ ...openerPageId === void 0 ? {} : { openerPageId }
33597
+ });
33598
+ }
33599
+ };
33600
+ function normalizeSnapshot(value, fallbackUrl) {
33601
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
33602
+ return {
33603
+ url: fallbackUrl,
33604
+ focused: false,
33605
+ visibilityState: "hidden",
33606
+ stopRequested: false,
33607
+ events: []
33608
+ };
33609
+ }
33610
+ const snapshot = value;
33611
+ return {
33612
+ url: typeof snapshot.url === "string" ? snapshot.url : fallbackUrl,
33613
+ focused: snapshot.focused === true,
33614
+ visibilityState: snapshot.visibilityState === "visible" || snapshot.visibilityState === "prerender" || snapshot.visibilityState === "hidden" ? snapshot.visibilityState : "hidden",
33615
+ stopRequested: snapshot.stopRequested === true,
33616
+ events: Array.isArray(snapshot.events) ? snapshot.events : []
33617
+ };
33618
+ }
33619
+ function dedupeConsecutiveSwitchActions(actions) {
33620
+ const deduped = [];
33621
+ for (const action of actions) {
33622
+ const previous = deduped[deduped.length - 1];
33623
+ if (previous?.kind === "switch-tab" && action.kind === "switch-tab" && previous.pageId === action.pageId) {
33624
+ continue;
33625
+ }
33626
+ deduped.push(action);
33627
+ }
33628
+ return deduped;
33629
+ }
33630
+ function actionSortPriority(kind) {
33631
+ switch (kind) {
33632
+ case "new-tab":
33633
+ return 0;
33634
+ case "switch-tab":
33635
+ return 1;
33636
+ case "navigate":
33637
+ case "go-back":
33638
+ case "go-forward":
33639
+ case "reload":
33640
+ return 2;
33641
+ case "click":
33642
+ case "dblclick":
33643
+ return 3;
33644
+ case "type":
33645
+ case "keypress":
33646
+ case "select-option":
33647
+ return 4;
33648
+ case "scroll":
33649
+ return 5;
33650
+ case "close-tab":
33651
+ return 6;
33652
+ }
33653
+ }
33654
+ function delay(ms) {
33655
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
33656
+ }
33657
+
33658
+ // ../runtime-core/src/recorder/codegen.ts
33659
+ var DISPATCH_KEY_SCRIPT = String.raw`(key) => {
33660
+ const target = document.activeElement;
33661
+ if (!(target instanceof Element)) {
33662
+ throw new Error("No active element is available for key replay.");
33663
+ }
33664
+ const normalizedKey = String(key);
33665
+ const eventInit = {
33666
+ key: normalizedKey,
33667
+ code: normalizedKey,
33668
+ bubbles: true,
33669
+ cancelable: true,
33670
+ };
33671
+ target.dispatchEvent(new KeyboardEvent("keydown", eventInit));
33672
+ target.dispatchEvent(new KeyboardEvent("keyup", eventInit));
33673
+ }`;
33674
+ var SELECT_OPTION_SCRIPT = String.raw`(selector, value) => {
33675
+ const target = document.querySelector(String(selector));
33676
+ if (!(target instanceof HTMLSelectElement)) {
33677
+ throw new Error("Unable to find a <select> element for option replay.");
33678
+ }
33679
+ target.value = String(value);
33680
+ target.dispatchEvent(new Event("input", { bubbles: true }));
33681
+ target.dispatchEvent(new Event("change", { bubbles: true }));
33682
+ }`;
33683
+ var HISTORY_ACTION_SCRIPT = String.raw`(action) => {
33684
+ const normalized = String(action);
33685
+ if (normalized === "back") {
33686
+ history.back();
33687
+ return;
33688
+ }
33689
+ if (normalized === "forward") {
33690
+ history.forward();
33691
+ return;
33692
+ }
33693
+ if (normalized === "reload") {
33694
+ location.reload();
33695
+ return;
33696
+ }
33697
+ throw new Error("Unsupported history action: " + normalized);
33698
+ }`;
33699
+ var WINDOW_SCROLL_SCRIPT = String.raw`(deltaX, deltaY) => {
33700
+ window.scrollBy(Number(deltaX), Number(deltaY));
33701
+ }`;
33702
+ function generateReplayScript(options) {
33703
+ const pageIds = collectPageIds(options.actions);
33704
+ const initialPageId = pageIds[0] ?? "page0";
33705
+ const lines = [
33706
+ `import { Opensteer } from "opensteer";`,
33707
+ ``,
33708
+ `const opensteer = new Opensteer({`,
33709
+ ` workspace: ${JSON.stringify(options.workspace)},`,
33710
+ ` browser: "persistent",`,
33711
+ `});`,
33712
+ ``,
33713
+ `const ${initialPageId} = (await opensteer.open(${JSON.stringify(options.startUrl)})).pageRef;`,
33714
+ `let activePageRef: string | undefined = ${initialPageId};`,
33715
+ ``,
33716
+ `async function ensureActive(pageRef: string): Promise<void> {`,
33717
+ ` if (activePageRef === pageRef) {`,
33718
+ ` return;`,
33719
+ ` }`,
33720
+ ` await opensteer.activatePage({ pageRef });`,
33721
+ ` activePageRef = pageRef;`,
33722
+ `}`,
33723
+ ``,
33724
+ `async function dispatchKey(pageRef: string, key: string): Promise<void> {`,
33725
+ ` await ensureActive(pageRef);`,
33726
+ ` await opensteer.evaluate({`,
33727
+ ` pageRef,`,
33728
+ ` script: ${JSON.stringify(DISPATCH_KEY_SCRIPT)},`,
33729
+ ` args: [key],`,
33730
+ ` });`,
33731
+ `}`,
33732
+ ``,
33733
+ `async function selectOption(pageRef: string, selector: string, value: string): Promise<void> {`,
33734
+ ` await ensureActive(pageRef);`,
33735
+ ` await opensteer.evaluate({`,
33736
+ ` pageRef,`,
33737
+ ` script: ${JSON.stringify(SELECT_OPTION_SCRIPT)},`,
33738
+ ` args: [selector, value],`,
33739
+ ` });`,
33740
+ `}`,
33741
+ ``,
33742
+ `async function runHistoryAction(pageRef: string, action: "back" | "forward" | "reload"): Promise<void> {`,
33743
+ ` await ensureActive(pageRef);`,
33744
+ ` await opensteer.evaluate({`,
33745
+ ` pageRef,`,
33746
+ ` script: ${JSON.stringify(HISTORY_ACTION_SCRIPT)},`,
33747
+ ` args: [action],`,
33748
+ ` });`,
33749
+ `}`,
33750
+ ``,
33751
+ `try {`
33752
+ ];
33753
+ const declaredPages = /* @__PURE__ */ new Set([initialPageId]);
33754
+ for (let index = 0; index < options.actions.length; index += 1) {
33755
+ const action = options.actions[index];
33756
+ const pageVar = action.pageId;
33757
+ if (action.kind === "type") {
33758
+ const nextAction = options.actions[index + 1];
33759
+ const mergedPressEnter = nextAction?.kind === "keypress" && nextAction.pageId === action.pageId && nextAction.selector === action.selector && nextAction.detail.kind === "keypress" && nextAction.detail.key === "Enter" && nextAction.detail.modifiers.length === 0;
33760
+ lines.push(` await ensureActive(${pageVar});`);
33761
+ lines.push(
33762
+ ` await opensteer.input({ selector: ${JSON.stringify(requireSelector(action))}, text: ${JSON.stringify(action.detail.text)}${mergedPressEnter ? `, pressEnter: true` : ``} });`
33763
+ );
33764
+ if (mergedPressEnter) {
33765
+ index += 1;
33766
+ }
33767
+ continue;
33768
+ }
33769
+ if (action.kind === "switch-tab" && options.actions[index - 1]?.kind === "new-tab" && options.actions[index - 1]?.pageId === action.pageId) {
33770
+ continue;
33771
+ }
33772
+ switch (action.kind) {
33773
+ case "navigate":
33774
+ lines.push(` await ensureActive(${pageVar});`);
33775
+ lines.push(` await opensteer.goto(${JSON.stringify(action.detail.url)});`);
33776
+ break;
33777
+ case "click":
33778
+ lines.push(` await ensureActive(${pageVar});`);
33779
+ lines.push(
33780
+ ` await opensteer.click({ selector: ${JSON.stringify(requireSelector(action))} });`
33781
+ );
33782
+ break;
33783
+ case "dblclick":
33784
+ lines.push(` await ensureActive(${pageVar});`);
33785
+ lines.push(
33786
+ ` await opensteer.click({ selector: ${JSON.stringify(requireSelector(action))} });`
33787
+ );
33788
+ lines.push(
33789
+ ` await opensteer.click({ selector: ${JSON.stringify(requireSelector(action))} });`
33790
+ );
33791
+ break;
33792
+ case "keypress":
33793
+ lines.push(
33794
+ ` await dispatchKey(${pageVar}, ${JSON.stringify(action.detail.key)});`
33795
+ );
33796
+ break;
33797
+ case "scroll": {
33798
+ const { direction, amount, isWindowScroll } = normalizeScrollAction(action);
33799
+ lines.push(` await ensureActive(${pageVar});`);
33800
+ if (isWindowScroll) {
33801
+ lines.push(` await opensteer.evaluate({`);
33802
+ lines.push(` pageRef: ${pageVar},`);
33803
+ lines.push(` script: ${JSON.stringify(WINDOW_SCROLL_SCRIPT)},`);
33804
+ lines.push(
33805
+ ` args: [${String(action.detail.deltaX)}, ${String(action.detail.deltaY)}],`
33806
+ );
33807
+ lines.push(` });`);
33808
+ } else {
33809
+ lines.push(
33810
+ ` await opensteer.scroll({ selector: ${JSON.stringify(requireSelector(action))}, direction: ${JSON.stringify(direction)}, amount: ${String(amount)} });`
33811
+ );
33812
+ }
33813
+ break;
33814
+ }
33815
+ case "select-option":
33816
+ lines.push(
33817
+ ` await selectOption(${pageVar}, ${JSON.stringify(requireSelector(action))}, ${JSON.stringify(action.detail.value)});`
33818
+ );
33819
+ break;
33820
+ case "new-tab": {
33821
+ const openerPageVar = action.detail.openerPageId;
33822
+ const shouldUseWaitForPage = shouldUsePopupWait(options.actions, index, openerPageVar);
33823
+ const creationLine = shouldUseWaitForPage && openerPageVar !== void 0 ? ` const ${pageVar} = (await opensteer.waitForPage({ openerPageRef: ${openerPageVar}, timeoutMs: 30_000 })).pageRef;` : ` const ${pageVar} = (await opensteer.newPage({${renderNewPageArguments(action.detail.openerPageId, action.detail.initialUrl)}})).pageRef;`;
33824
+ lines.push(creationLine);
33825
+ lines.push(` activePageRef = ${pageVar};`);
33826
+ declaredPages.add(pageVar);
33827
+ break;
33828
+ }
33829
+ case "close-tab":
33830
+ lines.push(` await opensteer.closePage({ pageRef: ${pageVar} });`);
33831
+ lines.push(` if (activePageRef === ${pageVar}) {`);
33832
+ lines.push(` activePageRef = undefined;`);
33833
+ lines.push(` }`);
33834
+ break;
33835
+ case "switch-tab":
33836
+ lines.push(` await ensureActive(${pageVar});`);
33837
+ break;
33838
+ case "go-back":
33839
+ lines.push(` await runHistoryAction(${pageVar}, "back");`);
33840
+ break;
33841
+ case "go-forward":
33842
+ lines.push(` await runHistoryAction(${pageVar}, "forward");`);
33843
+ break;
33844
+ case "reload":
33845
+ lines.push(` await runHistoryAction(${pageVar}, "reload");`);
33846
+ break;
33847
+ }
33848
+ }
33849
+ lines.push(`} finally {`);
33850
+ lines.push(` await opensteer.close();`);
33851
+ lines.push(`}`);
33852
+ lines.push(``);
33853
+ return `${lines.join("\n")}
33854
+ `;
33855
+ }
33856
+ function collectPageIds(actions) {
33857
+ const orderedIds = /* @__PURE__ */ new Set(["page0"]);
33858
+ for (const action of actions) {
33859
+ orderedIds.add(action.pageId);
33860
+ if (action.kind === "new-tab" && action.detail.openerPageId !== void 0) {
33861
+ orderedIds.add(action.detail.openerPageId);
33862
+ }
33863
+ if (action.kind === "switch-tab" && action.detail.fromPageId !== void 0) {
33864
+ orderedIds.add(action.detail.fromPageId);
33865
+ }
33866
+ if (action.kind === "switch-tab") {
33867
+ orderedIds.add(action.detail.toPageId);
33868
+ }
33869
+ }
33870
+ return [...orderedIds].sort(comparePageIds);
33871
+ }
33872
+ function comparePageIds(left, right) {
33873
+ const leftMatch = /^page(\d+)$/u.exec(left);
33874
+ const rightMatch = /^page(\d+)$/u.exec(right);
33875
+ if (leftMatch && rightMatch) {
33876
+ return Number(leftMatch[1]) - Number(rightMatch[1]);
33877
+ }
33878
+ return left.localeCompare(right);
33879
+ }
33880
+ function requireSelector(action) {
33881
+ if (action.selector === void 0) {
33882
+ throw new Error(`Action "${action.kind}" on ${action.pageId} is missing a selector.`);
33883
+ }
33884
+ return action.selector;
33885
+ }
33886
+ function normalizeScrollAction(action) {
33887
+ const horizontal = Math.abs(action.detail.deltaX);
33888
+ const vertical = Math.abs(action.detail.deltaY);
33889
+ if (vertical >= horizontal) {
33890
+ return {
33891
+ direction: action.detail.deltaY < 0 ? "up" : "down",
33892
+ amount: Math.max(1, Math.round(vertical)),
33893
+ isWindowScroll: action.selector === void 0
33894
+ };
33895
+ }
33896
+ return {
33897
+ direction: action.detail.deltaX < 0 ? "left" : "right",
33898
+ amount: Math.max(1, Math.round(horizontal)),
33899
+ isWindowScroll: action.selector === void 0
33900
+ };
33901
+ }
33902
+ function shouldUsePopupWait(actions, newTabIndex, openerPageId) {
33903
+ if (openerPageId === void 0 || newTabIndex === 0) {
33904
+ return false;
33905
+ }
33906
+ const previousAction = actions[newTabIndex - 1];
33907
+ if (previousAction === void 0 || previousAction.pageId !== openerPageId) {
33908
+ return false;
33909
+ }
33910
+ return previousAction.kind === "click" || previousAction.kind === "dblclick" || previousAction.kind === "keypress";
33911
+ }
33912
+ function renderNewPageArguments(openerPageId, initialUrl) {
33913
+ const argumentsList = [];
33914
+ if (openerPageId !== void 0) {
33915
+ argumentsList.push(` openerPageRef: ${openerPageId}`);
33916
+ }
33917
+ if (initialUrl.length > 0 && initialUrl !== "about:blank") {
33918
+ argumentsList.push(` url: ${JSON.stringify(initialUrl)}`);
33919
+ }
33920
+ if (argumentsList.length === 0) {
33921
+ return ``;
33922
+ }
33923
+ return ` ${argumentsList.join(",")}`.trimStart();
33924
+ }
33925
+
33926
+ // src/sdk/runtime.ts
33927
+ var OpensteerRuntime = class extends OpensteerSessionRuntime {
33928
+ constructor(options = {}) {
33929
+ const publicWorkspace = normalizeWorkspace2(options.workspace);
33930
+ const rootPath = options.rootPath ?? (publicWorkspace === void 0 ? path7.resolve(options.rootDir ?? process.cwd(), ".opensteer", "temporary", randomUUID()) : resolveFilesystemWorkspacePath({
33931
+ rootDir: path7.resolve(options.rootDir ?? process.cwd()),
33932
+ workspace: publicWorkspace
33933
+ }));
33934
+ const cleanupRootOnClose = options.cleanupRootOnClose ?? publicWorkspace === void 0;
33935
+ const engineName = options.engineName ?? DEFAULT_OPENSTEER_ENGINE;
33936
+ assertSupportedEngineOptions({
33937
+ engineName,
33938
+ ...options.browser === void 0 ? {} : { browser: options.browser },
33939
+ ...options.context === void 0 ? {} : { context: options.context }
33940
+ });
33941
+ super(
33942
+ buildSharedRuntimeOptions({
33943
+ name: publicWorkspace ?? "default",
33944
+ rootPath,
33945
+ ...publicWorkspace === void 0 ? {} : { workspaceName: publicWorkspace },
33946
+ ...options.browser === void 0 ? {} : { browser: options.browser },
33947
+ ...options.launch === void 0 ? {} : { launch: options.launch },
33948
+ ...options.context === void 0 ? {} : { context: options.context },
33949
+ engineName,
33950
+ ...options.engine === void 0 ? {} : { engine: options.engine },
33951
+ ...options.engineFactory === void 0 ? {} : { engineFactory: options.engineFactory },
33952
+ ...options.policy === void 0 ? {} : { policy: options.policy },
33953
+ ...options.descriptorStore === void 0 ? {} : { descriptorStore: options.descriptorStore },
33954
+ ...options.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: options.extractionDescriptorStore },
33955
+ ...options.registryOverrides === void 0 ? {} : { registryOverrides: options.registryOverrides },
33956
+ cleanupRootOnClose,
33957
+ ...options.observability === void 0 ? {} : { observability: options.observability },
33958
+ ...options.observationSessionId === void 0 ? {} : { observationSessionId: options.observationSessionId },
33959
+ ...options.observationSink === void 0 ? {} : { observationSink: options.observationSink }
33960
+ })
33961
+ );
33962
+ }
33963
+ };
33964
+ var OpensteerSessionRuntime2 = class extends OpensteerSessionRuntime {
33965
+ constructor(options) {
33966
+ const rootPath = options.rootPath ?? path7.resolve(options.rootDir ?? process.cwd());
33967
+ const cleanupRootOnClose = options.cleanupRootOnClose ?? false;
33968
+ const engineName = options.engineName ?? DEFAULT_OPENSTEER_ENGINE;
33969
+ assertSupportedEngineOptions({
33970
+ engineName,
33971
+ ...options.browser === void 0 ? {} : { browser: options.browser },
33972
+ ...options.context === void 0 ? {} : { context: options.context }
33973
+ });
33974
+ super(
33975
+ buildSharedRuntimeOptions({
33976
+ name: options.name,
33977
+ rootPath,
33978
+ ...options.browser === void 0 ? {} : { browser: options.browser },
33979
+ ...options.launch === void 0 ? {} : { launch: options.launch },
33980
+ ...options.context === void 0 ? {} : { context: options.context },
33981
+ engineName,
33982
+ ...options.engine === void 0 ? {} : { engine: options.engine },
33983
+ ...options.engineFactory === void 0 ? {} : { engineFactory: options.engineFactory },
33984
+ ...options.policy === void 0 ? {} : { policy: options.policy },
33985
+ ...options.descriptorStore === void 0 ? {} : { descriptorStore: options.descriptorStore },
33986
+ ...options.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: options.extractionDescriptorStore },
33987
+ ...options.registryOverrides === void 0 ? {} : { registryOverrides: options.registryOverrides },
33988
+ cleanupRootOnClose,
33989
+ ...options.observability === void 0 ? {} : { observability: options.observability },
33990
+ ...options.observationSessionId === void 0 ? {} : { observationSessionId: options.observationSessionId },
33991
+ ...options.observationSink === void 0 ? {} : { observationSink: options.observationSink }
33992
+ })
33993
+ );
33994
+ }
33995
+ };
33996
+ function buildSharedRuntimeOptions(input) {
33997
+ const ownership = resolveOwnership(input.browser);
33998
+ const engineFactory = input.engineFactory ?? ((factoryOptions) => new OpensteerBrowserManager({
33999
+ rootPath: input.rootPath,
34000
+ ...input.workspaceName === void 0 ? {} : { workspace: input.workspaceName },
34001
+ engineName: input.engineName,
34002
+ ...(factoryOptions.browser ?? input.browser) === void 0 ? {} : { browser: factoryOptions.browser ?? input.browser },
34003
+ ...(factoryOptions.launch ?? input.launch) === void 0 ? {} : { launch: factoryOptions.launch ?? input.launch },
34004
+ ...(factoryOptions.context ?? input.context) === void 0 ? {} : { context: factoryOptions.context ?? input.context }
34005
+ }).createEngine());
34006
+ return {
34007
+ name: input.name,
34008
+ ...input.workspaceName === void 0 ? {} : { workspaceName: input.workspaceName },
34009
+ rootPath: input.rootPath,
34010
+ ...input.engine === void 0 ? {} : { engine: input.engine },
34011
+ ...input.engine === void 0 ? { engineFactory } : {},
34012
+ ...input.policy === void 0 ? {} : { policy: input.policy },
34013
+ ...input.descriptorStore === void 0 ? {} : { descriptorStore: input.descriptorStore },
34014
+ ...input.extractionDescriptorStore === void 0 ? {} : { extractionDescriptorStore: input.extractionDescriptorStore },
34015
+ ...input.registryOverrides === void 0 ? {} : { registryOverrides: input.registryOverrides },
34016
+ cleanupRootOnClose: input.cleanupRootOnClose,
34017
+ ...input.observability === void 0 ? {} : { observability: input.observability },
34018
+ ...input.observationSessionId === void 0 ? {} : { observationSessionId: input.observationSessionId },
34019
+ ...input.observationSink === void 0 ? {} : { observationSink: input.observationSink },
34020
+ sessionInfo: {
34021
+ provider: {
34022
+ mode: "local",
34023
+ ownership,
34024
+ engine: input.engineName
34025
+ },
34026
+ ...input.workspaceName === void 0 ? {} : { workspace: input.workspaceName },
34027
+ reconnectable: !input.cleanupRootOnClose
34028
+ }
34029
+ };
34030
+ }
34031
+ function normalizeWorkspace2(workspace) {
34032
+ if (workspace === void 0) {
34033
+ return void 0;
34034
+ }
34035
+ const trimmed = workspace.trim();
34036
+ return trimmed.length === 0 ? void 0 : trimmed;
34037
+ }
34038
+ function resolveOwnership(browser) {
34039
+ return typeof browser === "object" && browser.mode === "attach" ? "attached" : "owned";
34040
+ }
34041
+
34042
+ // src/provider/config.ts
34043
+ var OPENSTEER_PROVIDER_MODES = ["local", "cloud"];
34044
+ function assertProviderSupportsEngine(provider, engine) {
34045
+ if (engine !== "abp") {
34046
+ return;
34047
+ }
34048
+ if (provider === "cloud") {
34049
+ throw new Error(
34050
+ "ABP is not supported for provider=cloud. Cloud provider currently requires Playwright."
34051
+ );
34052
+ }
34053
+ }
34054
+ function normalizeOpensteerProviderMode(value, source = "OPENSTEER_PROVIDER") {
34055
+ const normalized = value.trim().toLowerCase();
34056
+ if (normalized === OPENSTEER_PROVIDER_MODES[0] || normalized === OPENSTEER_PROVIDER_MODES[1]) {
34057
+ return normalized;
34058
+ }
34059
+ throw new Error(
34060
+ `${source} must be one of ${OPENSTEER_PROVIDER_MODES.join(", ")}; received "${value}".`
34061
+ );
34062
+ }
34063
+ function resolveOpensteerProvider(input = {}) {
34064
+ if (input.provider) {
34065
+ return {
34066
+ mode: input.provider.mode,
34067
+ source: "explicit"
34068
+ };
34069
+ }
34070
+ if (input.environmentProvider !== void 0 && input.environmentProvider.trim().length > 0) {
34071
+ return {
34072
+ mode: normalizeOpensteerProviderMode(input.environmentProvider),
34073
+ source: "env"
34074
+ };
34075
+ }
34076
+ return {
34077
+ mode: "local",
34078
+ source: "default"
34079
+ };
34080
+ }
34081
+ var execFile2 = promisify(execFile);
34082
+ var NODE_SQLITE_SPECIFIER2 = `node:${"sqlite"}`;
34083
+ var CHROME_EPOCH_OFFSET = 11644473600000000n;
34084
+ var CHROME_HMAC_PREFIX_LENGTH = 32;
34085
+ async function readBrowserCookies(input = {}) {
34086
+ const brand2 = resolveRequestedBrand(input);
34087
+ const userDataDir = resolveBrandUserDataDir(brand2, input.userDataDir);
34088
+ const profileDirectory = input.profileDirectory ?? "Default";
34089
+ const cookiesPath = join(userDataDir, profileDirectory, "Cookies");
34090
+ if (!existsSync(cookiesPath)) {
34091
+ throw new Error(
34092
+ `Cookies database not found at "${cookiesPath}". Verify the browser brand, user-data-dir, and profile-directory are correct.`
34093
+ );
34094
+ }
34095
+ const tempDir = await mkdtemp(join(tmpdir(), "opensteer-cookies-"));
34096
+ try {
34097
+ await copyCookiesDatabase(cookiesPath, tempDir);
34098
+ const decryptionKey = await resolveDecryptionKey(brand2.id, userDataDir);
34099
+ const rows = queryAllCookies(join(tempDir, "Cookies"));
34100
+ const cookies = decryptCookieRows(rows, decryptionKey);
34101
+ return {
34102
+ cookies,
34103
+ brandId: brand2.id,
34104
+ brandDisplayName: brand2.displayName,
34105
+ userDataDir,
34106
+ profileDirectory
34107
+ };
34108
+ } finally {
34109
+ await rm(tempDir, { recursive: true, force: true }).catch(() => void 0);
34110
+ }
34111
+ }
34112
+ function resolveRequestedBrand(input) {
34113
+ if (input.brandId !== void 0) {
34114
+ return getBrowserBrand(input.brandId);
34115
+ }
34116
+ const installed = detectInstalledBrowserBrands()[0];
32485
34117
  if (!installed) {
32486
34118
  throw new Error(
32487
34119
  "No Chromium browser found. Install a supported browser or pass brandId explicitly."
@@ -32515,11 +34147,25 @@ function queryAllCookies(dbPath) {
32515
34147
  FROM cookies`
32516
34148
  );
32517
34149
  stmt.setReadBigInts(true);
32518
- return stmt.all();
34150
+ return stmt.all().map(toRawCookieRow);
32519
34151
  } finally {
32520
34152
  database.close();
32521
34153
  }
32522
34154
  }
34155
+ function toRawCookieRow(row) {
34156
+ return {
34157
+ host_key: row.host_key,
34158
+ name: row.name,
34159
+ value: row.value,
34160
+ encrypted_value: row.encrypted_value,
34161
+ path: row.path,
34162
+ expires_utc: row.expires_utc,
34163
+ is_secure: row.is_secure,
34164
+ is_httponly: row.is_httponly,
34165
+ samesite: row.samesite,
34166
+ is_persistent: row.is_persistent
34167
+ };
34168
+ }
32523
34169
  async function resolveDecryptionKey(brandId, userDataDir) {
32524
34170
  if (process.platform === "darwin") {
32525
34171
  const password = await resolveKeychainPassword(brandId);
@@ -32870,7 +34516,11 @@ var OpensteerCloudClient = class {
32870
34516
  ...input.browser === void 0 ? {} : { browser: input.browser },
32871
34517
  ...input.context === void 0 ? {} : { context: input.context },
32872
34518
  ...input.browserProfile === void 0 ? {} : { browserProfile: input.browserProfile },
32873
- ...input.observability === void 0 ? {} : { observability: input.observability }
34519
+ ...input.observability === void 0 ? {} : { observability: input.observability },
34520
+ ...input.sourceType === void 0 ? {} : { sourceType: input.sourceType },
34521
+ ...input.sourceRef === void 0 ? {} : { sourceRef: input.sourceRef },
34522
+ ...input.localWorkspaceRootPath === void 0 ? {} : { localWorkspaceRootPath: input.localWorkspaceRootPath },
34523
+ ...input.locality === void 0 ? {} : { locality: input.locality }
32874
34524
  }
32875
34525
  });
32876
34526
  return await response.json();
@@ -33042,12 +34692,12 @@ var OpensteerCloudClient = class {
33042
34692
  `Unexpected cloud session status "${String(session.status)}" while waiting for close.`
33043
34693
  );
33044
34694
  }
33045
- await delay(CLOUD_CLOSE_POLL_INTERVAL_MS);
34695
+ await delay2(CLOUD_CLOSE_POLL_INTERVAL_MS);
33046
34696
  }
33047
34697
  throw new Error(`Timed out waiting for cloud session ${sessionId} to close.`);
33048
34698
  }
33049
34699
  };
33050
- function delay(ms) {
34700
+ function delay2(ms) {
33051
34701
  return new Promise((resolve4) => {
33052
34702
  setTimeout(resolve4, ms);
33053
34703
  });
@@ -33107,6 +34757,9 @@ var OpensteerSemanticRestClient = class {
33107
34757
  this.connection = connection;
33108
34758
  }
33109
34759
  async invoke(operation, input) {
34760
+ return this.invokeInternal(operation, input, false);
34761
+ }
34762
+ async invokeInternal(operation, input, hasRetried) {
33110
34763
  const endpoint = opensteerSemanticRestEndpoints.find((entry) => entry.name === operation);
33111
34764
  if (!endpoint) {
33112
34765
  throw new Error(`unsupported semantic operation ${operation}`);
@@ -33116,7 +34769,7 @@ var OpensteerSemanticRestClient = class {
33116
34769
  });
33117
34770
  let response;
33118
34771
  try {
33119
- response = await fetch(`${this.connection.baseUrl}${endpoint.path}`, {
34772
+ response = await fetch(`${await this.connection.getBaseUrl()}${endpoint.path}`, {
33120
34773
  method: "POST",
33121
34774
  headers: {
33122
34775
  authorization: await this.connection.getAuthorizationHeader(),
@@ -33138,6 +34791,9 @@ var OpensteerSemanticRestClient = class {
33138
34791
  }
33139
34792
  return envelope.data;
33140
34793
  } catch (error) {
34794
+ if (!hasRetried && this.connection.handleError && await this.connection.handleError(error, { operation })) {
34795
+ return this.invokeInternal(operation, input, true);
34796
+ }
33141
34797
  if (operation === "session.close" && isFetchFailure(error)) {
33142
34798
  return { closed: true };
33143
34799
  }
@@ -33271,7 +34927,10 @@ var OpensteerCloudAutomationClient = class {
33271
34927
  }
33272
34928
  async connect() {
33273
34929
  const grant = await this.issueGrant("automation");
33274
- const wsUrl = new URL(grant.wsUrl);
34930
+ if (grant.transport !== "ws") {
34931
+ throw new Error(`cloud issued an invalid ${grant.kind} grant transport`);
34932
+ }
34933
+ const wsUrl = new URL(grant.url);
33275
34934
  wsUrl.searchParams.set("token", grant.token);
33276
34935
  const socket = new WebSocket2(wsUrl);
33277
34936
  this.socket = socket;
@@ -33636,7 +35295,7 @@ var CloudSessionProxy = class {
33636
35295
  cloud;
33637
35296
  observability;
33638
35297
  sessionId;
33639
- sessionBaseUrl;
35298
+ semanticGrant;
33640
35299
  client;
33641
35300
  automation;
33642
35301
  workspaceStore;
@@ -33687,7 +35346,7 @@ var CloudSessionProxy = class {
33687
35346
  reconnectable: this.workspace !== void 0 || this.sessionId !== void 0 || persisted !== void 0,
33688
35347
  capabilities: {
33689
35348
  semanticOperations: opensteerSemanticOperationNames,
33690
- sessionGrants: ["automation", "view", "cdp"],
35349
+ sessionGrants: ["semantic", "automation", "view", "cdp"],
33691
35350
  instrumentation: {
33692
35351
  route: true,
33693
35352
  interceptScript: true,
@@ -33929,13 +35588,12 @@ var CloudSessionProxy = class {
33929
35588
  return this.requireClient().invoke("computer.execute", input);
33930
35589
  }
33931
35590
  async close() {
33932
- const session = await this.loadPersistedSession() ?? (this.sessionId === void 0 || this.sessionBaseUrl === void 0 ? void 0 : {
35591
+ const session = await this.loadPersistedSession() ?? (this.sessionId === void 0 ? void 0 : {
33933
35592
  layout: "opensteer-session",
33934
35593
  version: 1,
33935
35594
  provider: "cloud",
33936
35595
  ...this.workspace === void 0 ? {} : { workspace: this.workspace },
33937
35596
  sessionId: this.sessionId,
33938
- baseUrl: this.sessionBaseUrl,
33939
35597
  startedAt: Date.now(),
33940
35598
  updatedAt: Date.now()
33941
35599
  });
@@ -33954,7 +35612,7 @@ var CloudSessionProxy = class {
33954
35612
  this.automation = void 0;
33955
35613
  this.client = void 0;
33956
35614
  this.sessionId = void 0;
33957
- this.sessionBaseUrl = void 0;
35615
+ this.semanticGrant = void 0;
33958
35616
  if (this.cleanupRootOnClose) {
33959
35617
  await rm(this.rootPath, { recursive: true, force: true }).catch(() => void 0);
33960
35618
  }
@@ -33970,39 +35628,56 @@ var CloudSessionProxy = class {
33970
35628
  await this.automation?.close().catch(() => void 0);
33971
35629
  this.automation = void 0;
33972
35630
  this.sessionId = void 0;
33973
- this.sessionBaseUrl = void 0;
35631
+ this.semanticGrant = void 0;
33974
35632
  }
33975
35633
  async ensureSession(input = {}) {
33976
35634
  if (this.client) {
33977
35635
  return;
33978
35636
  }
33979
35637
  assertSupportedCloudBrowserMode(input.browser);
35638
+ const localCloud = this.shouldUseLocalCloudTransport();
35639
+ const browserProfile = resolveCloudBrowserProfile(this.cloud, input);
33980
35640
  const persisted = await this.loadPersistedSession();
33981
35641
  if (persisted !== void 0 && await this.isReusableCloudSession(persisted.sessionId)) {
33982
- await this.syncRegistryToCloud();
35642
+ if (localCloud) {
35643
+ void this.syncRegistryToCloud();
35644
+ } else {
35645
+ await this.syncRegistryToCloud();
35646
+ }
33983
35647
  this.bindClient(persisted);
33984
35648
  return;
33985
35649
  }
33986
- await this.syncRegistryToCloud();
33987
- const session = await this.cloud.createSession({
35650
+ if (localCloud) {
35651
+ void this.syncRegistryToCloud();
35652
+ } else {
35653
+ await this.syncRegistryToCloud();
35654
+ }
35655
+ const baseCreateInput = {
33988
35656
  ...this.workspace === void 0 ? {} : { name: this.workspace },
33989
35657
  ...input.launch === void 0 ? {} : { browser: input.launch },
33990
35658
  ...input.context === void 0 ? {} : { context: input.context },
33991
35659
  ...this.observability === void 0 ? {} : { observability: this.observability },
33992
- ...resolveCloudBrowserProfile(this.cloud, input) === void 0 ? {} : { browserProfile: resolveCloudBrowserProfile(this.cloud, input) }
33993
- });
35660
+ ...browserProfile === void 0 ? {} : { browserProfile }
35661
+ };
35662
+ const createInput = localCloud && this.workspace !== void 0 ? {
35663
+ ...baseCreateInput,
35664
+ sourceType: "local-cloud",
35665
+ sourceRef: this.workspace,
35666
+ localWorkspaceRootPath: this.rootPath,
35667
+ locality: "auto"
35668
+ } : baseCreateInput;
35669
+ const session = await this.cloud.createSession(createInput);
33994
35670
  const record = {
33995
35671
  layout: "opensteer-session",
33996
35672
  version: 1,
33997
35673
  provider: "cloud",
33998
35674
  ...this.workspace === void 0 ? {} : { workspace: this.workspace },
33999
35675
  sessionId: session.sessionId,
34000
- baseUrl: session.baseUrl,
34001
35676
  startedAt: Date.now(),
34002
35677
  updatedAt: Date.now()
34003
35678
  };
34004
35679
  await this.writePersistedSession(record);
34005
- this.bindClient(record);
35680
+ this.bindClient(record, session.initialGrants?.semantic);
34006
35681
  }
34007
35682
  async syncRegistryToCloud() {
34008
35683
  if (this.workspace === void 0) {
@@ -34011,15 +35686,16 @@ var CloudSessionProxy = class {
34011
35686
  try {
34012
35687
  const workspaceStore = await this.ensureWorkspaceStore();
34013
35688
  await syncLocalRegistryToCloud(this.cloud, this.workspace, workspaceStore);
34014
- } catch (error) {
35689
+ } catch {
34015
35690
  }
34016
35691
  }
34017
- bindClient(record) {
35692
+ bindClient(record, initialSemanticGrant) {
34018
35693
  this.sessionId = record.sessionId;
34019
- this.sessionBaseUrl = record.baseUrl;
35694
+ this.semanticGrant = initialSemanticGrant?.kind === "semantic" ? initialSemanticGrant : void 0;
34020
35695
  this.client = new OpensteerSemanticRestClient({
34021
- baseUrl: record.baseUrl,
34022
- getAuthorizationHeader: async () => this.cloud.buildAuthorizationHeader()
35696
+ getBaseUrl: async () => (await this.ensureSemanticGrant()).url,
35697
+ getAuthorizationHeader: async () => `Bearer ${(await this.ensureSemanticGrant()).token}`,
35698
+ handleError: (error) => this.handleSemanticClientError(error)
34023
35699
  });
34024
35700
  this.automation = new OpensteerCloudAutomationClient(this.cloud, record.sessionId);
34025
35701
  }
@@ -34068,6 +35744,43 @@ var CloudSessionProxy = class {
34068
35744
  }
34069
35745
  return this.automation;
34070
35746
  }
35747
+ async ensureSemanticGrant(forceRefresh = false) {
35748
+ if (!forceRefresh && this.semanticGrant?.kind === "semantic" && this.semanticGrant.expiresAt > Date.now() + 1e4) {
35749
+ return this.semanticGrant;
35750
+ }
35751
+ if (!this.sessionId) {
35752
+ throw new Error("Cloud session has not been initialized.");
35753
+ }
35754
+ const issued = await this.cloud.issueAccess(this.sessionId, ["semantic"]);
35755
+ const grant = issued.grants.semantic;
35756
+ if (!grant || grant.transport !== "http") {
35757
+ throw new Error("cloud did not issue a valid semantic grant");
35758
+ }
35759
+ this.semanticGrant = grant;
35760
+ return grant;
35761
+ }
35762
+ async handleSemanticClientError(error) {
35763
+ if (!(error instanceof OpensteerSemanticRestError)) {
35764
+ return false;
35765
+ }
35766
+ if (error.statusCode !== 401 && error.statusCode !== 404) {
35767
+ return false;
35768
+ }
35769
+ this.semanticGrant = void 0;
35770
+ try {
35771
+ await this.ensureSemanticGrant(true);
35772
+ return true;
35773
+ } catch {
35774
+ return false;
35775
+ }
35776
+ }
35777
+ shouldUseLocalCloudTransport() {
35778
+ if (this.workspace === void 0) {
35779
+ return false;
35780
+ }
35781
+ const config = this.cloud.getConfig();
35782
+ return isLoopbackBaseUrl(config.baseUrl);
35783
+ }
34071
35784
  };
34072
35785
  function resolveCloudBrowserProfile(cloud, input) {
34073
35786
  return input.browserProfile ?? cloud.getConfig().browserProfile;
@@ -34083,8 +35796,29 @@ function assertSupportedCloudBrowserMode(browser) {
34083
35796
  function isMissingCloudSessionError(error) {
34084
35797
  return error instanceof Error && /\b404\b/.test(error.message);
34085
35798
  }
35799
+ function isLoopbackBaseUrl(baseUrl) {
35800
+ let url;
35801
+ try {
35802
+ url = new URL(baseUrl);
35803
+ } catch {
35804
+ return false;
35805
+ }
35806
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
35807
+ }
34086
35808
 
34087
35809
  // src/sdk/runtime-resolution.ts
35810
+ var LOCAL_ONLY_RUNTIME_OPTION_KEYS = [
35811
+ "launch",
35812
+ "context",
35813
+ "engine",
35814
+ "engineFactory",
35815
+ "policy",
35816
+ "descriptorStore",
35817
+ "extractionDescriptorStore",
35818
+ "registryOverrides",
35819
+ "observationSessionId",
35820
+ "observationSink"
35821
+ ];
34088
35822
  function resolveOpensteerRuntimeConfig(input = {}) {
34089
35823
  const environment = input.environment ?? process.env;
34090
35824
  const provider = resolveOpensteerProvider({
@@ -34109,8 +35843,15 @@ function createOpensteerSemanticRuntime(input = {}) {
34109
35843
  ...input.provider === void 0 ? {} : { provider: input.provider },
34110
35844
  ...input.environment === void 0 ? {} : { environment: input.environment }
34111
35845
  });
34112
- assertProviderSupportsEngine(config.provider.mode, engine);
34113
- if (config.provider.mode === "cloud") {
35846
+ const localOnlyRuntimeOptions = listLocalOnlyRuntimeOptions(runtimeOptions);
35847
+ const providerMode = config.provider.mode === "cloud" && config.provider.source !== "explicit" && localOnlyRuntimeOptions.length > 0 ? "local" : config.provider.mode;
35848
+ if (config.provider.mode === "cloud" && config.provider.source === "explicit" && localOnlyRuntimeOptions.length > 0) {
35849
+ throw new Error(
35850
+ `provider=cloud does not support local runtime options: ${localOnlyRuntimeOptions.join(", ")}.`
35851
+ );
35852
+ }
35853
+ assertProviderSupportsEngine(providerMode, engine);
35854
+ if (providerMode === "cloud") {
34114
35855
  return new CloudSessionProxy(new OpensteerCloudClient(config.cloud), {
34115
35856
  ...runtimeOptions.rootDir === void 0 ? {} : { rootDir: runtimeOptions.rootDir },
34116
35857
  ...runtimeOptions.rootPath === void 0 ? {} : { rootPath: runtimeOptions.rootPath },
@@ -34124,6 +35865,9 @@ function createOpensteerSemanticRuntime(input = {}) {
34124
35865
  engineName: engine
34125
35866
  });
34126
35867
  }
35868
+ function listLocalOnlyRuntimeOptions(runtimeOptions) {
35869
+ return LOCAL_ONLY_RUNTIME_OPTION_KEYS.filter((key) => runtimeOptions[key] !== void 0);
35870
+ }
34127
35871
  var ENV_FILENAMES = [".env", ".env.local"];
34128
35872
  var OPENSTEER_ENV_PREFIX = "OPENSTEER_";
34129
35873
  var opensteerEnvironmentCache = /* @__PURE__ */ new Map();
@@ -34227,6 +35971,6 @@ function isOpensteerEnvironmentKey(key) {
34227
35971
  return key.startsWith(OPENSTEER_ENV_PREFIX);
34228
35972
  }
34229
35973
 
34230
- export { CloudSessionProxy, DEFAULT_OPENSTEER_ENGINE, DEFERRED_MATCH_ATTR_KEYS, ElementPathError, MATCH_ATTRIBUTE_PRIORITY, OPENSTEER_DOM_ACTION_BRIDGE_SYMBOL, OPENSTEER_ENGINE_NAMES, OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT, OPENSTEER_FILESYSTEM_WORKSPACE_VERSION, OpensteerAttachAmbiguousError, OpensteerBrowserManager, OpensteerCloudClient, OpensteerRuntime, OpensteerSessionRuntime2 as OpensteerSessionRuntime, STABLE_PRIMARY_ATTR_KEYS, assertProviderSupportsEngine, buildArrayFieldPathCandidates, buildDomDescriptorKey, buildDomDescriptorPayload, buildDomDescriptorVersion, buildPathCandidates, buildPathSelectorHint, buildSegmentSelector, clearPersistedSessionRecord, cloneElementPath, cloneReplayElementPath, cloneStructuralElementAnchor, createArtifactStore, createDomRuntime, createFilesystemOpensteerWorkspace, createObservationStore, createOpensteerExtractionDescriptorStore, createOpensteerSemanticRuntime, defaultFallbackPolicy, defaultPolicy, defaultRetryPolicy, defaultSettlePolicy, defaultTimeoutPolicy, delayWithSignal, discoverLocalCdpBrowsers, dispatchSemanticOperation, hashDomDescriptorDescription, inspectCdpEndpoint, isCurrentUrlField, isProcessRunning, isValidCssAttributeKey, listLocalChromeProfiles, loadEnvironment, manifestToExternalBinaryLocation, normalizeExtractedValue, normalizeObservabilityConfig, normalizeOpensteerEngineName, normalizeOpensteerProviderMode, normalizeWorkspaceId, parseDomDescriptorRecord, parseExtractionDescriptorRecord, pathExists, readPersistedCloudSessionRecord, readPersistedLocalBrowserSessionRecord, readPersistedSessionRecord, resolveCloudConfig, resolveCloudSessionRecordPath, resolveDomActionBridge, resolveExtractedValueInContext, resolveFilesystemWorkspacePath, resolveLiveSessionRecordPath, resolveLocalSessionRecordPath, resolveOpensteerEngineName, resolveOpensteerEnvironment, resolveOpensteerProvider, resolveOpensteerRuntimeConfig, runWithPolicyTimeout, sanitizeElementPath, sanitizeReplayElementPath, sanitizeStructuralElementAnchor, settleWithPolicy, shouldKeepAttributeForPath, writePersistedSessionRecord };
34231
- //# sourceMappingURL=chunk-RO6WAWWG.js.map
34232
- //# sourceMappingURL=chunk-RO6WAWWG.js.map
35974
+ export { CloudSessionProxy, DEFAULT_OPENSTEER_ENGINE, DEFERRED_MATCH_ATTR_KEYS, ElementPathError, FlowRecorderCollector, MATCH_ATTRIBUTE_PRIORITY, OPENSTEER_DOM_ACTION_BRIDGE_SYMBOL, OPENSTEER_ENGINE_NAMES, OPENSTEER_FILESYSTEM_WORKSPACE_LAYOUT, OPENSTEER_FILESYSTEM_WORKSPACE_VERSION, OpensteerAttachAmbiguousError, OpensteerBrowserManager, OpensteerCloudClient, OpensteerRuntime, OpensteerSessionRuntime2 as OpensteerSessionRuntime, STABLE_PRIMARY_ATTR_KEYS, assertProviderSupportsEngine, buildArrayFieldPathCandidates, buildDomDescriptorKey, buildDomDescriptorPayload, buildDomDescriptorVersion, buildPathCandidates, buildPathSelectorHint, buildSegmentSelector, clearPersistedSessionRecord, cloneElementPath, cloneReplayElementPath, cloneStructuralElementAnchor, createArtifactStore, createDomDescriptorStore, createDomRuntime, createFilesystemOpensteerWorkspace, createObservationStore, createOpensteerExtractionDescriptorStore, createOpensteerSemanticRuntime, defaultFallbackPolicy, defaultPolicy, defaultRetryPolicy, defaultSettlePolicy, defaultTimeoutPolicy, delayWithSignal, discoverLocalCdpBrowsers, dispatchSemanticOperation, generateReplayScript, hashDomDescriptorDescription, inspectCdpEndpoint, isCurrentUrlField, isProcessRunning, isValidCssAttributeKey, listLocalChromeProfiles, loadEnvironment, manifestToExternalBinaryLocation, normalizeExtractedValue, normalizeObservabilityConfig, normalizeOpensteerEngineName, normalizeOpensteerProviderMode, normalizeWorkspaceId, parseDomDescriptorRecord, parseExtractionDescriptorRecord, pathExists, readPersistedCloudSessionRecord, readPersistedLocalBrowserSessionRecord, readPersistedSessionRecord, resolveCloudConfig, resolveCloudSessionRecordPath, resolveDomActionBridge, resolveExtractedValueInContext, resolveFilesystemWorkspacePath, resolveLiveSessionRecordPath, resolveLocalSessionRecordPath, resolveOpensteerEngineName, resolveOpensteerEnvironment, resolveOpensteerProvider, resolveOpensteerRuntimeConfig, runWithPolicyTimeout, sanitizeElementPath, sanitizeReplayElementPath, sanitizeStructuralElementAnchor, settleWithPolicy, shouldKeepAttributeForPath, writePersistedSessionRecord };
35975
+ //# sourceMappingURL=chunk-F3X6UOEN.js.map
35976
+ //# sourceMappingURL=chunk-F3X6UOEN.js.map