opensteer 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,263 +1,515 @@
1
+ import {
2
+ OpensteerCloudError,
3
+ cloudNotLaunchedError,
4
+ cloudUnsupportedMethodError,
5
+ createCloudRuntimeState,
6
+ createKeychainStore,
7
+ extractErrorMessage,
8
+ normalizeError,
9
+ normalizeNamespace,
10
+ readCloudActionDescription,
11
+ resolveCloudSelection,
12
+ resolveConfigWithEnv,
13
+ resolveNamespace,
14
+ resolveNamespaceDir
15
+ } from "./chunk-WDRMHPWL.js";
1
16
  import {
2
17
  flattenExtractionDataToFieldPlan
3
18
  } from "./chunk-3H5RRIMZ.js";
4
19
 
5
- // src/error-normalization.ts
6
- function extractErrorMessage(error, fallback = "Unknown error.") {
7
- if (error instanceof Error) {
8
- const message = error.message.trim();
9
- if (message) return message;
10
- const name = error.name.trim();
11
- if (name) return name;
20
+ // src/browser/chrome.ts
21
+ import { homedir, platform } from "os";
22
+ import { join } from "path";
23
+ function expandHome(p) {
24
+ if (p.startsWith("~/") || p === "~") {
25
+ return join(homedir(), p.slice(1));
12
26
  }
13
- if (typeof error === "string" && error.trim()) {
14
- return error.trim();
27
+ return p;
28
+ }
29
+
30
+ // src/browser/chromium-profile.ts
31
+ import { promisify } from "util";
32
+ import { execFile } from "child_process";
33
+ import { createDecipheriv, createHash, pbkdf2Sync } from "crypto";
34
+ import {
35
+ cp,
36
+ copyFile,
37
+ mkdtemp,
38
+ readdir,
39
+ readFile,
40
+ rm
41
+ } from "fs/promises";
42
+ import { existsSync, statSync } from "fs";
43
+ import { basename, dirname, join as join2 } from "path";
44
+ import { tmpdir } from "os";
45
+ import { chromium } from "playwright";
46
+ var execFileAsync = promisify(execFile);
47
+ var CHROMIUM_EPOCH_MICROS = 11644473600000000n;
48
+ var AES_BLOCK_BYTES = 16;
49
+ var MAC_KEY_ITERATIONS = 1003;
50
+ var LINUX_KEY_ITERATIONS = 1;
51
+ var KEY_LENGTH = 16;
52
+ var KEY_SALT = "saltysalt";
53
+ var DEFAULT_CHROMIUM_BRAND = {
54
+ macService: "Chrome Safe Storage",
55
+ macAccount: "Chrome",
56
+ linuxApplications: ["chrome", "google-chrome"]
57
+ };
58
+ var CHROMIUM_BRANDS = [
59
+ {
60
+ match: ["bravesoftware", "brave-browser"],
61
+ brand: {
62
+ macService: "Brave Safe Storage",
63
+ macAccount: "Brave",
64
+ linuxApplications: ["brave-browser", "brave"]
65
+ }
66
+ },
67
+ {
68
+ match: ["microsoft", "edge"],
69
+ brand: {
70
+ macService: "Microsoft Edge Safe Storage",
71
+ macAccount: "Microsoft Edge",
72
+ linuxApplications: ["microsoft-edge"],
73
+ playwrightChannel: "msedge"
74
+ }
75
+ },
76
+ {
77
+ match: ["google", "chrome beta"],
78
+ brand: {
79
+ macService: "Chrome Beta Safe Storage",
80
+ macAccount: "Chrome Beta",
81
+ linuxApplications: ["chrome-beta"],
82
+ playwrightChannel: "chrome-beta"
83
+ }
84
+ },
85
+ {
86
+ match: ["google", "chrome"],
87
+ brand: {
88
+ macService: "Chrome Safe Storage",
89
+ macAccount: "Chrome",
90
+ linuxApplications: ["chrome", "google-chrome"],
91
+ playwrightChannel: "chrome"
92
+ }
93
+ },
94
+ {
95
+ match: ["chromium"],
96
+ brand: {
97
+ macService: "Chromium Safe Storage",
98
+ macAccount: "Chromium",
99
+ linuxApplications: ["chromium"]
100
+ }
15
101
  }
16
- const record = asRecord(error);
17
- const recordMessage = toNonEmptyString(record?.message) || toNonEmptyString(record?.error);
18
- if (recordMessage) {
19
- return recordMessage;
102
+ ];
103
+ function directoryExists(filePath) {
104
+ try {
105
+ return statSync(filePath).isDirectory();
106
+ } catch {
107
+ return false;
20
108
  }
21
- return fallback;
22
109
  }
23
- function normalizeError(error, fallback = "Unknown error.", maxCauseDepth = 2) {
24
- const seen = /* @__PURE__ */ new WeakSet();
25
- return normalizeErrorInternal(error, fallback, maxCauseDepth, seen);
110
+ function fileExists(filePath) {
111
+ try {
112
+ return statSync(filePath).isFile();
113
+ } catch {
114
+ return false;
115
+ }
26
116
  }
27
- function normalizeErrorInternal(error, fallback, depthRemaining, seen) {
28
- const record = asRecord(error);
29
- if (record) {
30
- if (seen.has(record)) {
31
- return {
32
- message: extractErrorMessage(error, fallback)
33
- };
117
+ function resolveCookieDbPath(profileDir) {
118
+ const candidates = [join2(profileDir, "Network", "Cookies"), join2(profileDir, "Cookies")];
119
+ for (const candidate of candidates) {
120
+ if (fileExists(candidate)) {
121
+ return candidate;
34
122
  }
35
- seen.add(record);
36
- }
37
- const message = extractErrorMessage(error, fallback);
38
- const code = extractCode(error);
39
- const name = extractName(error);
40
- const details = extractDetails(error);
41
- if (depthRemaining <= 0) {
42
- return compactErrorInfo({
43
- message,
44
- ...code ? { code } : {},
45
- ...name ? { name } : {},
46
- ...details ? { details } : {}
47
- });
48
- }
49
- const cause = extractCause(error);
50
- if (!cause) {
51
- return compactErrorInfo({
52
- message,
53
- ...code ? { code } : {},
54
- ...name ? { name } : {},
55
- ...details ? { details } : {}
56
- });
57
123
  }
58
- const normalizedCause = normalizeErrorInternal(
59
- cause,
60
- "Caused by an unknown error.",
61
- depthRemaining - 1,
62
- seen
63
- );
64
- return compactErrorInfo({
65
- message,
66
- ...code ? { code } : {},
67
- ...name ? { name } : {},
68
- ...details ? { details } : {},
69
- cause: normalizedCause
70
- });
124
+ return null;
71
125
  }
72
- function compactErrorInfo(info) {
73
- const safeDetails = toJsonSafeRecord(info.details);
126
+ async function selectProfileDirFromUserDataDir(userDataDir) {
127
+ const entries = await readdir(userDataDir, {
128
+ withFileTypes: true
129
+ }).catch(() => []);
130
+ const candidates = entries.filter((entry) => entry.isDirectory()).map((entry) => join2(userDataDir, entry.name)).filter((entryPath) => resolveCookieDbPath(entryPath));
131
+ return candidates;
132
+ }
133
+ async function resolveChromiumProfileLocation(inputPath) {
134
+ const expandedPath = expandHome(inputPath.trim());
135
+ if (!expandedPath) {
136
+ throw new Error("Profile path cannot be empty.");
137
+ }
138
+ if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
139
+ const directParent = dirname(expandedPath);
140
+ const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
141
+ const userDataDir = dirname(profileDir);
142
+ return {
143
+ userDataDir,
144
+ profileDir,
145
+ profileDirectory: basename(profileDir),
146
+ cookieDbPath: expandedPath,
147
+ localStatePath: fileExists(join2(userDataDir, "Local State")) ? join2(userDataDir, "Local State") : null
148
+ };
149
+ }
150
+ if (fileExists(expandedPath)) {
151
+ throw new Error(
152
+ `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
153
+ );
154
+ }
155
+ if (!directoryExists(expandedPath)) {
156
+ throw new Error(
157
+ `Could not find a Chromium profile at "${inputPath}".`
158
+ );
159
+ }
160
+ const directCookieDb = resolveCookieDbPath(expandedPath);
161
+ if (directCookieDb) {
162
+ const userDataDir = dirname(expandedPath);
163
+ return {
164
+ userDataDir,
165
+ profileDir: expandedPath,
166
+ profileDirectory: basename(expandedPath),
167
+ cookieDbPath: directCookieDb,
168
+ localStatePath: fileExists(join2(userDataDir, "Local State")) ? join2(userDataDir, "Local State") : null
169
+ };
170
+ }
171
+ const localStatePath = join2(expandedPath, "Local State");
172
+ if (!fileExists(localStatePath)) {
173
+ throw new Error(
174
+ `Unsupported profile source "${inputPath}". Pass a Chromium profile directory, user-data dir, or Cookies database path.`
175
+ );
176
+ }
177
+ const profileDirs = await selectProfileDirFromUserDataDir(expandedPath);
178
+ if (profileDirs.length === 0) {
179
+ throw new Error(
180
+ `No Chromium profile with a Cookies database was found under "${inputPath}".`
181
+ );
182
+ }
183
+ if (profileDirs.length > 1) {
184
+ const candidates = profileDirs.map((entry) => basename(entry)).join(", ");
185
+ throw new Error(
186
+ `"${inputPath}" contains multiple Chromium profiles (${candidates}). Pass a specific profile directory such as "${profileDirs[0]}".`
187
+ );
188
+ }
189
+ const selectedProfileDir = profileDirs[0];
190
+ const cookieDbPath = resolveCookieDbPath(selectedProfileDir);
191
+ if (!cookieDbPath) {
192
+ throw new Error(
193
+ `No Chromium Cookies database was found for "${inputPath}".`
194
+ );
195
+ }
74
196
  return {
75
- message: info.message,
76
- ...info.code ? { code: info.code } : {},
77
- ...info.name ? { name: info.name } : {},
78
- ...safeDetails ? { details: safeDetails } : {},
79
- ...info.cause ? { cause: info.cause } : {}
197
+ userDataDir: expandedPath,
198
+ profileDir: selectedProfileDir,
199
+ profileDirectory: basename(selectedProfileDir),
200
+ cookieDbPath,
201
+ localStatePath
80
202
  };
81
203
  }
82
- function extractCode(error) {
83
- const record = asRecord(error);
84
- const raw = record?.code;
85
- if (typeof raw === "string" && raw.trim()) {
86
- return raw.trim();
204
+ function resolvePersistentChromiumLaunchProfile(inputPath) {
205
+ const expandedPath = expandHome(inputPath.trim());
206
+ if (!expandedPath) {
207
+ return {
208
+ userDataDir: inputPath
209
+ };
87
210
  }
88
- if (typeof raw === "number" && Number.isFinite(raw)) {
89
- return String(raw);
211
+ if (fileExists(expandedPath) && basename(expandedPath) === "Cookies") {
212
+ const directParent = dirname(expandedPath);
213
+ const profileDir = basename(directParent) === "Network" ? dirname(directParent) : directParent;
214
+ return {
215
+ userDataDir: dirname(profileDir),
216
+ profileDirectory: basename(profileDir)
217
+ };
90
218
  }
91
- return void 0;
219
+ if (directoryExists(expandedPath) && resolveCookieDbPath(expandedPath) && fileExists(join2(dirname(expandedPath), "Local State"))) {
220
+ return {
221
+ userDataDir: dirname(expandedPath),
222
+ profileDirectory: basename(expandedPath)
223
+ };
224
+ }
225
+ return {
226
+ userDataDir: expandedPath
227
+ };
92
228
  }
93
- function extractName(error) {
94
- if (error instanceof Error && error.name.trim()) {
95
- return error.name.trim();
96
- }
97
- const record = asRecord(error);
98
- return toNonEmptyString(record?.name);
99
- }
100
- function extractDetails(error) {
101
- const record = asRecord(error);
102
- if (!record) return void 0;
103
- const details = {};
104
- const rawDetails = asRecord(record.details);
105
- if (rawDetails) {
106
- Object.assign(details, rawDetails);
107
- }
108
- const action = toNonEmptyString(record.action);
109
- if (action) {
110
- details.action = action;
111
- }
112
- const selectorUsed = toNonEmptyString(record.selectorUsed);
113
- if (selectorUsed) {
114
- details.selectorUsed = selectorUsed;
115
- }
116
- if (typeof record.status === "number" && Number.isFinite(record.status)) {
117
- details.status = record.status;
118
- }
119
- const failure = asRecord(record.failure);
120
- if (failure) {
121
- const failureCode = toNonEmptyString(failure.code);
122
- const classificationSource = toNonEmptyString(
123
- failure.classificationSource
124
- );
125
- const failureDetails = asRecord(failure.details);
126
- if (failureCode || classificationSource || failureDetails) {
127
- details.actionFailure = {
128
- ...failureCode ? { code: failureCode } : {},
129
- ...classificationSource ? { classificationSource } : {},
130
- ...failureDetails ? { details: failureDetails } : {}
131
- };
229
+ function detectChromiumBrand(location) {
230
+ const normalizedPath = location.userDataDir.toLowerCase();
231
+ for (const candidate of CHROMIUM_BRANDS) {
232
+ if (candidate.match.every((fragment) => normalizedPath.includes(fragment))) {
233
+ return candidate.brand;
132
234
  }
133
235
  }
134
- return Object.keys(details).length ? details : void 0;
236
+ return DEFAULT_CHROMIUM_BRAND;
135
237
  }
136
- function extractCause(error) {
137
- if (error instanceof Error) {
138
- return error.cause;
238
+ async function createSqliteSnapshot(dbPath) {
239
+ const snapshotDir = await mkdtemp(join2(tmpdir(), "opensteer-cookie-db-"));
240
+ const snapshotPath = join2(snapshotDir, "Cookies");
241
+ await copyFile(dbPath, snapshotPath);
242
+ for (const suffix of ["-wal", "-shm", "-journal"]) {
243
+ const source = `${dbPath}${suffix}`;
244
+ if (!existsSync(source)) {
245
+ continue;
246
+ }
247
+ await copyFile(source, `${snapshotPath}${suffix}`);
139
248
  }
140
- const record = asRecord(error);
141
- return record?.cause;
249
+ return {
250
+ snapshotPath,
251
+ cleanup: async () => {
252
+ await rm(snapshotDir, { recursive: true, force: true });
253
+ }
254
+ };
142
255
  }
143
- function asRecord(value) {
144
- if (!value || typeof value !== "object" || Array.isArray(value)) {
145
- return null;
256
+ async function querySqliteJson(dbPath, query) {
257
+ const result = await execFileAsync("sqlite3", ["-json", dbPath, query], {
258
+ encoding: "utf8",
259
+ maxBuffer: 64 * 1024 * 1024
260
+ });
261
+ const stdout = result.stdout;
262
+ const trimmed = stdout.trim();
263
+ if (!trimmed) {
264
+ return [];
146
265
  }
147
- return value;
148
- }
149
- function toNonEmptyString(value) {
150
- if (typeof value !== "string") return void 0;
151
- const normalized = value.trim();
152
- return normalized.length ? normalized : void 0;
266
+ return JSON.parse(trimmed);
153
267
  }
154
- function toJsonSafeRecord(value) {
155
- if (!value) return void 0;
156
- const sanitized = toJsonSafeValue(value, /* @__PURE__ */ new WeakSet());
157
- if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) {
158
- return void 0;
268
+ function convertChromiumTimestampToUnixSeconds(value) {
269
+ if (!value || value === "0") {
270
+ return -1;
159
271
  }
160
- const record = sanitized;
161
- return Object.keys(record).length > 0 ? record : void 0;
162
- }
163
- function toJsonSafeValue(value, seen) {
164
- if (value === null) return null;
165
- if (typeof value === "string" || typeof value === "boolean") {
166
- return value;
272
+ const micros = BigInt(value);
273
+ if (micros <= CHROMIUM_EPOCH_MICROS) {
274
+ return -1;
167
275
  }
168
- if (typeof value === "number") {
169
- return Number.isFinite(value) ? value : null;
276
+ return Number((micros - CHROMIUM_EPOCH_MICROS) / 1000000n);
277
+ }
278
+ function mapChromiumSameSite(value) {
279
+ if (value === 2) {
280
+ return "Strict";
170
281
  }
171
- if (typeof value === "bigint") {
172
- return value.toString();
282
+ if (value === 0) {
283
+ return "None";
173
284
  }
174
- if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
175
- return void 0;
285
+ return "Lax";
286
+ }
287
+ function stripChromiumPadding(buffer) {
288
+ const paddingLength = buffer[buffer.length - 1];
289
+ if (paddingLength <= 0 || paddingLength > AES_BLOCK_BYTES) {
290
+ return buffer;
176
291
  }
177
- if (value instanceof Date) {
178
- return Number.isNaN(value.getTime()) ? null : value.toISOString();
292
+ return buffer.subarray(0, buffer.length - paddingLength);
293
+ }
294
+ function stripDomainHashPrefix(buffer, hostKey) {
295
+ if (buffer.length < 32) {
296
+ return buffer;
179
297
  }
180
- if (Array.isArray(value)) {
181
- if (seen.has(value)) return "[Circular]";
182
- seen.add(value);
183
- const output = value.map((item) => {
184
- const next = toJsonSafeValue(item, seen);
185
- return next === void 0 ? null : next;
186
- });
187
- seen.delete(value);
188
- return output;
189
- }
190
- if (value instanceof Set) {
191
- if (seen.has(value)) return "[Circular]";
192
- seen.add(value);
193
- const output = Array.from(value, (item) => {
194
- const next = toJsonSafeValue(item, seen);
195
- return next === void 0 ? null : next;
196
- });
197
- seen.delete(value);
198
- return output;
298
+ const domainHash = createHash("sha256").update(hostKey, "utf8").digest();
299
+ if (buffer.subarray(0, 32).equals(domainHash)) {
300
+ return buffer.subarray(32);
199
301
  }
200
- if (value instanceof Map) {
201
- if (seen.has(value)) return "[Circular]";
202
- seen.add(value);
203
- const output = {};
204
- for (const [key, item] of value.entries()) {
205
- const normalizedKey = String(key);
206
- const next = toJsonSafeValue(item, seen);
207
- if (next !== void 0) {
208
- output[normalizedKey] = next;
209
- }
302
+ return buffer;
303
+ }
304
+ function decryptChromiumAes128CbcValue(encryptedValue, key, hostKey) {
305
+ const ciphertext = encryptedValue.length > 3 && encryptedValue[0] === 118 && encryptedValue[1] === 49 && (encryptedValue[2] === 48 || encryptedValue[2] === 49) ? encryptedValue.subarray(3) : encryptedValue;
306
+ const iv = Buffer.alloc(AES_BLOCK_BYTES, " ");
307
+ const decipher = createDecipheriv("aes-128-cbc", key, iv);
308
+ const plaintext = Buffer.concat([
309
+ decipher.update(ciphertext),
310
+ decipher.final()
311
+ ]);
312
+ return stripDomainHashPrefix(stripChromiumPadding(plaintext), hostKey).toString(
313
+ "utf8"
314
+ );
315
+ }
316
+ function decryptChromiumAes256GcmValue(encryptedValue, key) {
317
+ const nonce = encryptedValue.subarray(3, 15);
318
+ const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16);
319
+ const authTag = encryptedValue.subarray(encryptedValue.length - 16);
320
+ const decipher = createDecipheriv("aes-256-gcm", key, nonce);
321
+ decipher.setAuthTag(authTag);
322
+ return Buffer.concat([
323
+ decipher.update(ciphertext),
324
+ decipher.final()
325
+ ]).toString("utf8");
326
+ }
327
+ async function dpapiUnprotect(buffer) {
328
+ const script = [
329
+ `$inputBytes = [Convert]::FromBase64String('${buffer.toString("base64")}')`,
330
+ "$plainBytes = [System.Security.Cryptography.ProtectedData]::Unprotect(",
331
+ " $inputBytes,",
332
+ " $null,",
333
+ " [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
334
+ ")",
335
+ "[Convert]::ToBase64String($plainBytes)"
336
+ ].join("\n");
337
+ const { stdout } = await execFileAsync(
338
+ "powershell.exe",
339
+ ["-NoProfile", "-NonInteractive", "-Command", script],
340
+ {
341
+ encoding: "utf8",
342
+ maxBuffer: 8 * 1024 * 1024
210
343
  }
211
- seen.delete(value);
212
- return output;
344
+ );
345
+ return Buffer.from(stdout.trim(), "base64");
346
+ }
347
+ async function buildChromiumDecryptor(location) {
348
+ if (process.platform === "darwin") {
349
+ const brand = detectChromiumBrand(location);
350
+ const keychainStore = createKeychainStore();
351
+ const password = keychainStore?.get(brand.macService, brand.macAccount) ?? null;
352
+ if (!password) {
353
+ throw new Error(
354
+ `Unable to read ${brand.macService} from macOS Keychain.`
355
+ );
356
+ }
357
+ const key = pbkdf2Sync(password, KEY_SALT, MAC_KEY_ITERATIONS, KEY_LENGTH, "sha1");
358
+ return async (row) => decryptChromiumAes128CbcValue(
359
+ Buffer.from(row.encrypted_value || "", "hex"),
360
+ key,
361
+ row.host_key
362
+ );
363
+ }
364
+ if (process.platform === "linux") {
365
+ const brand = detectChromiumBrand(location);
366
+ const keychainStore = createKeychainStore();
367
+ const password = keychainStore?.get(brand.macService, brand.macAccount) ?? brand.linuxApplications.map((application) => keychainStore?.get(application, application) ?? null).find(Boolean) ?? null;
368
+ const key = pbkdf2Sync(
369
+ password || "peanuts",
370
+ KEY_SALT,
371
+ LINUX_KEY_ITERATIONS,
372
+ KEY_LENGTH,
373
+ "sha1"
374
+ );
375
+ return async (row) => decryptChromiumAes128CbcValue(
376
+ Buffer.from(row.encrypted_value || "", "hex"),
377
+ key,
378
+ row.host_key
379
+ );
213
380
  }
214
- if (typeof value === "object") {
215
- const objectValue = value;
216
- if (seen.has(objectValue)) return "[Circular]";
217
- seen.add(objectValue);
218
- const output = {};
219
- for (const [key, item] of Object.entries(objectValue)) {
220
- const next = toJsonSafeValue(item, seen);
221
- if (next !== void 0) {
222
- output[key] = next;
381
+ if (process.platform === "win32") {
382
+ if (!location.localStatePath) {
383
+ throw new Error(
384
+ `Unable to locate Chromium Local State for profile: ${location.profileDir}`
385
+ );
386
+ }
387
+ const localState = JSON.parse(
388
+ await readFile(location.localStatePath, "utf8")
389
+ );
390
+ const encryptedKeyBase64 = localState.os_crypt?.encrypted_key;
391
+ if (!encryptedKeyBase64) {
392
+ throw new Error(
393
+ `Local State did not include os_crypt.encrypted_key for ${location.userDataDir}`
394
+ );
395
+ }
396
+ const encryptedKey = Buffer.from(encryptedKeyBase64, "base64");
397
+ const masterKey = await dpapiUnprotect(encryptedKey.subarray(5));
398
+ return async (row) => {
399
+ const encryptedValue = Buffer.from(row.encrypted_value || "", "hex");
400
+ if (encryptedValue.length > 4 && encryptedValue[0] === 1 && encryptedValue[1] === 0 && encryptedValue[2] === 0 && encryptedValue[3] === 0) {
401
+ const decrypted = await dpapiUnprotect(encryptedValue);
402
+ return decrypted.toString("utf8");
223
403
  }
404
+ return decryptChromiumAes256GcmValue(encryptedValue, masterKey);
405
+ };
406
+ }
407
+ throw new Error(
408
+ `Local Chromium cookie sync is not supported on ${process.platform}.`
409
+ );
410
+ }
411
+ function buildPlaywrightCookie(row, value) {
412
+ if (!row.name.trim()) {
413
+ return null;
414
+ }
415
+ if (!row.host_key.trim()) {
416
+ return null;
417
+ }
418
+ const expires = row.has_expires === 1 ? convertChromiumTimestampToUnixSeconds(row.expires_utc) : -1;
419
+ if (expires !== -1 && expires <= Math.floor(Date.now() / 1e3)) {
420
+ return null;
421
+ }
422
+ return {
423
+ name: row.name,
424
+ value,
425
+ domain: row.host_key,
426
+ path: row.path || "/",
427
+ expires,
428
+ httpOnly: row.is_httponly === 1,
429
+ secure: row.is_secure === 1,
430
+ sameSite: mapChromiumSameSite(row.samesite)
431
+ };
432
+ }
433
+ async function loadCookiesFromLocalProfileDir(inputPath, options = {}) {
434
+ const location = await resolveChromiumProfileLocation(inputPath);
435
+ try {
436
+ return await loadCookiesFromSqlite(location);
437
+ } catch (error) {
438
+ if (!isMissingSqliteBinary(error)) {
439
+ throw error;
224
440
  }
225
- seen.delete(objectValue);
226
- return output;
227
441
  }
228
- return void 0;
442
+ return await loadCookiesFromBrowserSnapshot(location, options);
229
443
  }
230
-
231
- // src/storage/namespace.ts
232
- import path from "path";
233
- var DEFAULT_NAMESPACE = "default";
234
- function normalizeNamespace(input) {
235
- const raw = String(input || "").trim().replace(/\\/g, "/");
236
- if (!raw) return DEFAULT_NAMESPACE;
237
- const segments = raw.split("/").map((segment) => sanitizeNamespaceSegment(segment)).filter((segment) => Boolean(segment));
238
- if (!segments.length) return DEFAULT_NAMESPACE;
239
- return segments.join("/");
240
- }
241
- function resolveNamespaceDir(rootDir, namespace) {
242
- const selectorsRoot = path.resolve(rootDir, ".opensteer", "selectors");
243
- const normalizedNamespace = normalizeNamespace(namespace);
244
- const namespaceDir = path.resolve(selectorsRoot, normalizedNamespace);
245
- const relative = path.relative(selectorsRoot, namespaceDir);
246
- if (relative === "" || relative === ".") return namespaceDir;
247
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
248
- throw new Error(
249
- `Namespace "${namespace}" resolves outside selectors root.`
250
- );
444
+ async function loadCookiesFromSqlite(location) {
445
+ const snapshot = await createSqliteSnapshot(location.cookieDbPath);
446
+ try {
447
+ const rows = await querySqliteJson(
448
+ snapshot.snapshotPath,
449
+ [
450
+ "SELECT",
451
+ " host_key,",
452
+ " name,",
453
+ " value,",
454
+ " hex(encrypted_value) AS encrypted_value,",
455
+ " path,",
456
+ " CAST(expires_utc AS TEXT) AS expires_utc,",
457
+ " is_secure,",
458
+ " is_httponly,",
459
+ " has_expires,",
460
+ " samesite",
461
+ "FROM cookies"
462
+ ].join(" ")
463
+ );
464
+ const decryptValue = await buildChromiumDecryptor(location);
465
+ const cookies = [];
466
+ for (const row of rows) {
467
+ let value = row.value || "";
468
+ if (!value && row.encrypted_value) {
469
+ value = await decryptValue(row);
470
+ }
471
+ const cookie = buildPlaywrightCookie(row, value);
472
+ if (cookie) {
473
+ cookies.push(cookie);
474
+ }
475
+ }
476
+ return cookies;
477
+ } finally {
478
+ await snapshot.cleanup();
479
+ }
480
+ }
481
+ async function loadCookiesFromBrowserSnapshot(location, options) {
482
+ const snapshotRootDir = await mkdtemp(join2(tmpdir(), "opensteer-profile-"));
483
+ const snapshotProfileDir = join2(
484
+ snapshotRootDir,
485
+ basename(location.profileDir)
486
+ );
487
+ let context = null;
488
+ try {
489
+ await cp(location.profileDir, snapshotProfileDir, {
490
+ recursive: true
491
+ });
492
+ if (location.localStatePath) {
493
+ await copyFile(location.localStatePath, join2(snapshotRootDir, "Local State"));
494
+ }
495
+ const brand = detectChromiumBrand(location);
496
+ const args = [`--profile-directory=${basename(snapshotProfileDir)}`];
497
+ context = await chromium.launchPersistentContext(snapshotRootDir, {
498
+ channel: brand.playwrightChannel,
499
+ headless: options.headless ?? true,
500
+ timeout: options.timeout ?? 12e4,
501
+ args
502
+ });
503
+ return await context.cookies();
504
+ } finally {
505
+ await context?.close().catch(() => void 0);
506
+ await rm(snapshotRootDir, { recursive: true, force: true });
251
507
  }
252
- return namespaceDir;
253
508
  }
254
- function sanitizeNamespaceSegment(segment) {
255
- const trimmed = String(segment || "").trim();
256
- if (!trimmed || trimmed === "." || trimmed === "..") return "";
257
- const replaced = trimmed.replace(/[^a-zA-Z0-9_-]+/g, "_");
258
- const collapsed = replaced.replace(/_+/g, "_");
259
- const bounded = collapsed.replace(/^_+|_+$/g, "");
260
- return bounded || "";
509
+ function isMissingSqliteBinary(error) {
510
+ return Boolean(
511
+ error && typeof error === "object" && "code" in error && error.code === "ENOENT"
512
+ );
261
513
  }
262
514
 
263
515
  // src/navigation.ts
@@ -829,7 +1081,7 @@ function createEmptyRegistry(name) {
829
1081
 
830
1082
  // src/storage/local.ts
831
1083
  import fs from "fs";
832
- import path2 from "path";
1084
+ import path from "path";
833
1085
  var LocalSelectorStorage = class {
834
1086
  rootDir;
835
1087
  namespace;
@@ -850,16 +1102,16 @@ var LocalSelectorStorage = class {
850
1102
  return `${safe}.json`;
851
1103
  }
852
1104
  getOpensteerDir() {
853
- return path2.join(this.rootDir, ".opensteer");
1105
+ return path.join(this.rootDir, ".opensteer");
854
1106
  }
855
1107
  getNamespaceDir() {
856
1108
  return resolveNamespaceDir(this.rootDir, this.namespace);
857
1109
  }
858
1110
  getRegistryPath() {
859
- return path2.join(this.getNamespaceDir(), "index.json");
1111
+ return path.join(this.getNamespaceDir(), "index.json");
860
1112
  }
861
1113
  getSelectorPath(id) {
862
- return path2.join(this.getNamespaceDir(), this.getSelectorFileName(id));
1114
+ return path.join(this.getNamespaceDir(), this.getSelectorFileName(id));
863
1115
  }
864
1116
  ensureDirs() {
865
1117
  fs.mkdirSync(this.getNamespaceDir(), { recursive: true });
@@ -1195,11 +1447,11 @@ var OS_INSTANCE_TOKEN_KEY = "__opensteerInstanceToken";
1195
1447
 
1196
1448
  // src/element-path/build.ts
1197
1449
  var MAX_ATTRIBUTE_VALUE_LENGTH = 300;
1198
- function cloneElementPath(path5) {
1199
- return JSON.parse(JSON.stringify(path5));
1450
+ function cloneElementPath(path3) {
1451
+ return JSON.parse(JSON.stringify(path3));
1200
1452
  }
1201
- function buildPathSelectorHint(path5) {
1202
- const nodes = path5?.nodes || [];
1453
+ function buildPathSelectorHint(path3) {
1454
+ const nodes = path3?.nodes || [];
1203
1455
  const last = nodes[nodes.length - 1];
1204
1456
  if (!last) return "*";
1205
1457
  return buildSegmentSelector(last);
@@ -1208,9 +1460,9 @@ async function buildElementPathFromSelector(page, selector) {
1208
1460
  try {
1209
1461
  const handle = await page.mainFrame().locator(selector).first().elementHandle({ timeout: 1500 });
1210
1462
  if (!handle) return null;
1211
- const path5 = await buildElementPathFromHandle(handle);
1463
+ const path3 = await buildElementPathFromHandle(handle);
1212
1464
  await handle.dispose();
1213
- return path5;
1465
+ return path3;
1214
1466
  } catch {
1215
1467
  return null;
1216
1468
  }
@@ -1623,11 +1875,11 @@ async function buildElementPathFromHandle(handle) {
1623
1875
  if (!out) return null;
1624
1876
  return sanitizeElementPath(out);
1625
1877
  }
1626
- function sanitizeElementPath(path5) {
1878
+ function sanitizeElementPath(path3) {
1627
1879
  const cleanNodes = (nodes) => (Array.isArray(nodes) ? nodes : []).map(
1628
1880
  (raw) => normalizePathNode(raw)
1629
1881
  );
1630
- const context = (Array.isArray(path5?.context) ? path5.context : []).filter(
1882
+ const context = (Array.isArray(path3?.context) ? path3.context : []).filter(
1631
1883
  (hop) => hop && (hop.kind === "iframe" || hop.kind === "shadow")
1632
1884
  ).map((hop) => ({
1633
1885
  kind: hop.kind,
@@ -1635,7 +1887,7 @@ function sanitizeElementPath(path5) {
1635
1887
  }));
1636
1888
  return {
1637
1889
  context,
1638
- nodes: cleanNodes(path5?.nodes || [])
1890
+ nodes: cleanNodes(path3?.nodes || [])
1639
1891
  };
1640
1892
  }
1641
1893
  function normalizePathNode(raw) {
@@ -2074,16 +2326,16 @@ async function serializeFrameRecursive(frame, baseContext, frameKey) {
2074
2326
  nodeTableIndexByKey.set(key, index);
2075
2327
  return index;
2076
2328
  },
2077
- packDomPath(path5) {
2078
- return path5.map((node) => helpers.internPathNode(node));
2329
+ packDomPath(path3) {
2330
+ return path3.map((node) => helpers.internPathNode(node));
2079
2331
  },
2080
- packElementPath(path5) {
2332
+ packElementPath(path3) {
2081
2333
  return {
2082
- context: (path5.context || []).map((hop) => ({
2334
+ context: (path3.context || []).map((hop) => ({
2083
2335
  kind: "shadow",
2084
2336
  host: helpers.packDomPath(hop.host)
2085
2337
  })),
2086
- nodes: helpers.packDomPath(path5.nodes)
2338
+ nodes: helpers.packDomPath(path3.nodes)
2087
2339
  };
2088
2340
  },
2089
2341
  ensureNodeId(el) {
@@ -2172,7 +2424,7 @@ async function serializeFrameRecursive(frame, baseContext, frameKey) {
2172
2424
  const nodePaths = /* @__PURE__ */ new Map();
2173
2425
  const nodeMeta = /* @__PURE__ */ new Map();
2174
2426
  for (const [index, entry] of frameSnapshot.entries.entries()) {
2175
- const path5 = decodeSerializedElementPath(
2427
+ const path3 = decodeSerializedElementPath(
2176
2428
  frameSnapshot.nodeTable,
2177
2429
  entry.path,
2178
2430
  `entries[${index}].path`
@@ -2180,9 +2432,9 @@ async function serializeFrameRecursive(frame, baseContext, frameKey) {
2180
2432
  nodePaths.set(entry.nodeId, {
2181
2433
  context: [
2182
2434
  ...baseContext,
2183
- ...path5.context || []
2435
+ ...path3.context || []
2184
2436
  ],
2185
- nodes: path5.nodes
2437
+ nodes: path3.nodes
2186
2438
  });
2187
2439
  nodeMeta.set(entry.nodeId, {
2188
2440
  frameToken: frameSnapshot.frameToken,
@@ -2222,8 +2474,8 @@ async function serializeFrameRecursive(frame, baseContext, frameKey) {
2222
2474
  hostEl.after(
2223
2475
  `<${OS_IFRAME_BOUNDARY_TAG} ${OS_BOUNDARY_ATTR}="iframe">${childResult.html}</${OS_IFRAME_BOUNDARY_TAG}>`
2224
2476
  );
2225
- for (const [nodeId, path5] of childResult.nodePaths.entries()) {
2226
- nodePaths.set(nodeId, path5);
2477
+ for (const [nodeId, path3] of childResult.nodePaths.entries()) {
2478
+ nodePaths.set(nodeId, path3);
2227
2479
  }
2228
2480
  for (const [nodeId, meta] of childResult.nodeMeta.entries()) {
2229
2481
  nodeMeta.set(nodeId, meta);
@@ -2235,8 +2487,8 @@ async function serializeFrameRecursive(frame, baseContext, frameKey) {
2235
2487
  }
2236
2488
  }
2237
2489
  const sanitizedPaths = /* @__PURE__ */ new Map();
2238
- for (const [nodeId, path5] of nodePaths.entries()) {
2239
- sanitizedPaths.set(nodeId, sanitizeElementPath(path5));
2490
+ for (const [nodeId, path3] of nodePaths.entries()) {
2491
+ sanitizedPaths.set(nodeId, sanitizeElementPath(path3));
2240
2492
  }
2241
2493
  return {
2242
2494
  html: $.html(),
@@ -3058,11 +3310,11 @@ async function assignCounters(page, html, nodePaths, nodeMeta) {
3058
3310
  if (!nodeId) return;
3059
3311
  const counter = nextCounter++;
3060
3312
  assignedByNodeId.set(nodeId, counter);
3061
- const path5 = nodePaths.get(nodeId);
3313
+ const path3 = nodePaths.get(nodeId);
3062
3314
  el.attr("c", String(counter));
3063
3315
  el.removeAttr(OS_NODE_ID_ATTR);
3064
- if (path5) {
3065
- counterIndex.set(counter, cloneElementPath(path5));
3316
+ if (path3) {
3317
+ counterIndex.set(counter, cloneElementPath(path3));
3066
3318
  }
3067
3319
  });
3068
3320
  try {
@@ -3301,10 +3553,10 @@ var ElementPathError = class extends Error {
3301
3553
 
3302
3554
  // src/element-path/resolver.ts
3303
3555
  async function resolveElementPath(page, rawPath) {
3304
- const path5 = sanitizeElementPath(rawPath);
3556
+ const path3 = sanitizeElementPath(rawPath);
3305
3557
  let frame = page.mainFrame();
3306
3558
  let rootHandle = null;
3307
- for (const hop of path5.context) {
3559
+ for (const hop of path3.context) {
3308
3560
  const host = await resolveDomPath(frame, hop.host, rootHandle);
3309
3561
  if (!host) {
3310
3562
  await disposeHandle(rootHandle);
@@ -3343,17 +3595,17 @@ async function resolveElementPath(page, rawPath) {
3343
3595
  await disposeHandle(rootHandle);
3344
3596
  rootHandle = shadowRoot;
3345
3597
  }
3346
- const target = await resolveDomPath(frame, path5.nodes, rootHandle);
3598
+ const target = await resolveDomPath(frame, path3.nodes, rootHandle);
3347
3599
  if (!target) {
3348
3600
  const diagnostics = await collectCandidateDiagnostics(
3349
3601
  frame,
3350
- path5.nodes,
3602
+ path3.nodes,
3351
3603
  rootHandle
3352
3604
  );
3353
3605
  await disposeHandle(rootHandle);
3354
3606
  throw new ElementPathError(
3355
3607
  "ERR_PATH_TARGET_NOT_FOUND",
3356
- buildTargetNotFoundMessage(path5.nodes, diagnostics)
3608
+ buildTargetNotFoundMessage(path3.nodes, diagnostics)
3357
3609
  );
3358
3610
  }
3359
3611
  await disposeHandle(rootHandle);
@@ -3362,12 +3614,12 @@ async function resolveElementPath(page, rawPath) {
3362
3614
  selector: target.selector,
3363
3615
  mode: target.mode,
3364
3616
  count: target.count,
3365
- targetDepth: path5.nodes.length
3617
+ targetDepth: path3.nodes.length
3366
3618
  });
3367
3619
  }
3368
3620
  return {
3369
3621
  element: target.element,
3370
- usedSelector: target.selector || buildPathSelectorHint(path5)
3622
+ usedSelector: target.selector || buildPathSelectorHint(path3)
3371
3623
  };
3372
3624
  }
3373
3625
  async function resolveDomPath(frame, domPath, rootHandle) {
@@ -3522,6 +3774,16 @@ async function disposeHandle(handle) {
3522
3774
 
3523
3775
  // src/extract-value-normalization.ts
3524
3776
  var URL_LIST_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset", "ping"]);
3777
+ var IFRAME_URL_ATTRIBUTES = /* @__PURE__ */ new Set([
3778
+ "href",
3779
+ "src",
3780
+ "srcset",
3781
+ "imagesrcset",
3782
+ "action",
3783
+ "formaction",
3784
+ "poster",
3785
+ "ping"
3786
+ ]);
3525
3787
  function normalizeExtractedValue(raw, attribute) {
3526
3788
  if (raw == null) return null;
3527
3789
  const rawText = String(raw);
@@ -3537,6 +3799,19 @@ function normalizeExtractedValue(raw, attribute) {
3537
3799
  const text = rawText.replace(/\s+/g, " ").trim();
3538
3800
  return text || null;
3539
3801
  }
3802
+ function resolveExtractedValueInContext(normalizedValue, options) {
3803
+ if (normalizedValue == null) return null;
3804
+ const normalizedAttribute = String(options.attribute || "").trim().toLowerCase();
3805
+ if (!options.insideIframe) return normalizedValue;
3806
+ if (!IFRAME_URL_ATTRIBUTES.has(normalizedAttribute)) return normalizedValue;
3807
+ const baseURI = String(options.baseURI || "").trim();
3808
+ if (!baseURI) return normalizedValue;
3809
+ try {
3810
+ return new URL(normalizedValue, baseURI).href;
3811
+ } catch {
3812
+ return normalizedValue;
3813
+ }
3814
+ }
3540
3815
  function pickSingleListAttributeValue(attribute, raw) {
3541
3816
  if (attribute === "ping") {
3542
3817
  const firstUrl = raw.trim().split(/\s+/)[0] || "";
@@ -3692,6 +3967,36 @@ function readDescriptorToken(value, index) {
3692
3967
  };
3693
3968
  }
3694
3969
 
3970
+ // src/extract-value-reader.ts
3971
+ async function readExtractedValueFromHandle(element, options) {
3972
+ const insideIframe = await isElementInsideIframe(element);
3973
+ const payload = await element.evaluate(
3974
+ (target, browserOptions) => {
3975
+ const ownerDocument = target.ownerDocument;
3976
+ return {
3977
+ raw: browserOptions.attribute ? target.getAttribute(browserOptions.attribute) : target.textContent,
3978
+ baseURI: ownerDocument?.baseURI || null
3979
+ };
3980
+ },
3981
+ {
3982
+ attribute: options.attribute
3983
+ }
3984
+ );
3985
+ const normalizedValue = normalizeExtractedValue(
3986
+ payload.raw,
3987
+ options.attribute
3988
+ );
3989
+ return resolveExtractedValueInContext(normalizedValue, {
3990
+ attribute: options.attribute,
3991
+ baseURI: payload.baseURI,
3992
+ insideIframe
3993
+ });
3994
+ }
3995
+ async function isElementInsideIframe(element) {
3996
+ const ownerFrame = await element.ownerFrame();
3997
+ return !!ownerFrame?.parentFrame();
3998
+ }
3999
+
3695
4000
  // src/html/counter-runtime.ts
3696
4001
  var CounterResolutionError = class extends Error {
3697
4002
  code;
@@ -3714,13 +4019,14 @@ async function resolveCounterElement(page, counter) {
3714
4019
  if (entry.count > 1) {
3715
4020
  throw buildCounterAmbiguousError(counter);
3716
4021
  }
3717
- const handle = await resolveUniqueHandleInFrame(entry.frame, normalized);
3718
- const element = handle.asElement();
3719
- if (!element) {
3720
- await handle.dispose();
4022
+ const resolution = await resolveCounterElementInFrame(entry.frame, normalized);
4023
+ if (resolution.status === "ambiguous") {
4024
+ throw buildCounterAmbiguousError(counter);
4025
+ }
4026
+ if (resolution.status === "missing") {
3721
4027
  throw buildCounterNotFoundError(counter);
3722
4028
  }
3723
- return element;
4029
+ return resolution.element;
3724
4030
  }
3725
4031
  async function resolveCountersBatch(page, requests) {
3726
4032
  const out = {};
@@ -3734,41 +4040,57 @@ async function resolveCountersBatch(page, requests) {
3734
4040
  }
3735
4041
  }
3736
4042
  const valueCache = /* @__PURE__ */ new Map();
3737
- for (const request of requests) {
3738
- const normalized = normalizeCounter(request.counter);
3739
- if (normalized == null) {
3740
- out[request.key] = null;
3741
- continue;
3742
- }
3743
- const entry = scan.get(normalized);
3744
- if (!entry || entry.count <= 0 || !entry.frame) {
3745
- out[request.key] = null;
3746
- continue;
3747
- }
3748
- const cacheKey = `${normalized}:${request.attribute || ""}`;
3749
- if (valueCache.has(cacheKey)) {
3750
- out[request.key] = valueCache.get(cacheKey);
3751
- continue;
3752
- }
3753
- const read = await readCounterValueInFrame(
3754
- entry.frame,
3755
- normalized,
3756
- request.attribute
3757
- );
3758
- if (read.status === "ambiguous") {
3759
- throw buildCounterAmbiguousError(normalized);
3760
- }
3761
- if (read.status === "missing") {
3762
- valueCache.set(cacheKey, null);
3763
- out[request.key] = null;
3764
- continue;
4043
+ const elementCache = /* @__PURE__ */ new Map();
4044
+ try {
4045
+ for (const request of requests) {
4046
+ const normalized = normalizeCounter(request.counter);
4047
+ if (normalized == null) {
4048
+ out[request.key] = null;
4049
+ continue;
4050
+ }
4051
+ const entry = scan.get(normalized);
4052
+ if (!entry || entry.count <= 0 || !entry.frame) {
4053
+ out[request.key] = null;
4054
+ continue;
4055
+ }
4056
+ const cacheKey = `${normalized}:${request.attribute || ""}`;
4057
+ if (valueCache.has(cacheKey)) {
4058
+ out[request.key] = valueCache.get(cacheKey);
4059
+ continue;
4060
+ }
4061
+ if (!elementCache.has(normalized)) {
4062
+ elementCache.set(
4063
+ normalized,
4064
+ await resolveCounterElementInFrame(entry.frame, normalized)
4065
+ );
4066
+ }
4067
+ const resolution = elementCache.get(normalized);
4068
+ if (resolution.status === "ambiguous") {
4069
+ throw buildCounterAmbiguousError(normalized);
4070
+ }
4071
+ if (resolution.status === "missing") {
4072
+ valueCache.set(cacheKey, null);
4073
+ out[request.key] = null;
4074
+ continue;
4075
+ }
4076
+ const value = await readCounterValueFromElement(
4077
+ resolution.element,
4078
+ request.attribute
4079
+ );
4080
+ if (value.status === "missing") {
4081
+ await resolution.element.dispose();
4082
+ elementCache.set(normalized, {
4083
+ status: "missing"
4084
+ });
4085
+ valueCache.set(cacheKey, null);
4086
+ out[request.key] = null;
4087
+ continue;
4088
+ }
4089
+ valueCache.set(cacheKey, value.value);
4090
+ out[request.key] = value.value;
3765
4091
  }
3766
- const normalizedValue = normalizeExtractedValue(
3767
- read.value ?? null,
3768
- request.attribute
3769
- );
3770
- valueCache.set(cacheKey, normalizedValue);
3771
- out[request.key] = normalizedValue;
4092
+ } finally {
4093
+ await disposeResolvedCounterElements(elementCache.values());
3772
4094
  }
3773
4095
  return out;
3774
4096
  }
@@ -3840,73 +4162,79 @@ async function scanCounterOccurrences(page, counters) {
3840
4162
  }
3841
4163
  return out;
3842
4164
  }
3843
- async function resolveUniqueHandleInFrame(frame, counter) {
3844
- return frame.evaluateHandle((targetCounter) => {
3845
- const matches = [];
3846
- const walk = (root) => {
3847
- const children = Array.from(root.children);
3848
- for (const child of children) {
3849
- if (child.getAttribute("c") === targetCounter) {
3850
- matches.push(child);
3851
- }
3852
- walk(child);
3853
- if (child.shadowRoot) {
3854
- walk(child.shadowRoot);
4165
+ async function resolveCounterElementInFrame(frame, counter) {
4166
+ try {
4167
+ const handle = await frame.evaluateHandle((targetCounter) => {
4168
+ const matches = [];
4169
+ const walk = (root) => {
4170
+ const children = Array.from(root.children);
4171
+ for (const child of children) {
4172
+ if (child.getAttribute("c") === targetCounter) {
4173
+ matches.push(child);
4174
+ }
4175
+ walk(child);
4176
+ if (child.shadowRoot) {
4177
+ walk(child.shadowRoot);
4178
+ }
3855
4179
  }
4180
+ };
4181
+ walk(document);
4182
+ if (!matches.length) {
4183
+ return "missing";
3856
4184
  }
3857
- };
3858
- walk(document);
3859
- if (matches.length !== 1) {
3860
- return null;
4185
+ if (matches.length > 1) {
4186
+ return "ambiguous";
4187
+ }
4188
+ return matches[0];
4189
+ }, String(counter));
4190
+ const element = handle.asElement();
4191
+ if (element) {
4192
+ return {
4193
+ status: "resolved",
4194
+ element
4195
+ };
4196
+ }
4197
+ const status = await handle.jsonValue();
4198
+ await handle.dispose();
4199
+ return status === "ambiguous" ? { status: "ambiguous" } : { status: "missing" };
4200
+ } catch (error) {
4201
+ if (isRecoverableCounterReadRace(error)) {
4202
+ return {
4203
+ status: "missing"
4204
+ };
3861
4205
  }
3862
- return matches[0];
3863
- }, String(counter));
4206
+ throw error;
4207
+ }
3864
4208
  }
3865
- async function readCounterValueInFrame(frame, counter, attribute) {
4209
+ async function readCounterValueFromElement(element, attribute) {
3866
4210
  try {
3867
- return await frame.evaluate(
3868
- ({ targetCounter, attribute: attribute2 }) => {
3869
- const matches = [];
3870
- const walk = (root) => {
3871
- const children = Array.from(root.children);
3872
- for (const child of children) {
3873
- if (child.getAttribute("c") === targetCounter) {
3874
- matches.push(child);
3875
- }
3876
- walk(child);
3877
- if (child.shadowRoot) {
3878
- walk(child.shadowRoot);
3879
- }
3880
- }
3881
- };
3882
- walk(document);
3883
- if (!matches.length) {
3884
- return {
3885
- status: "missing"
3886
- };
3887
- }
3888
- if (matches.length > 1) {
3889
- return {
3890
- status: "ambiguous"
3891
- };
3892
- }
3893
- const target = matches[0];
3894
- const value = attribute2 ? target.getAttribute(attribute2) : target.textContent;
3895
- return {
3896
- status: "ok",
3897
- value
3898
- };
3899
- },
3900
- {
3901
- targetCounter: String(counter),
3902
- attribute
3903
- }
3904
- );
3905
- } catch {
3906
4211
  return {
3907
- status: "missing"
4212
+ status: "ok",
4213
+ value: await readExtractedValueFromHandle(element, {
4214
+ attribute
4215
+ })
3908
4216
  };
4217
+ } catch (error) {
4218
+ if (isRecoverableCounterReadRace(error)) {
4219
+ return {
4220
+ status: "missing"
4221
+ };
4222
+ }
4223
+ throw error;
4224
+ }
4225
+ }
4226
+ async function disposeResolvedCounterElements(resolutions) {
4227
+ const disposals = [];
4228
+ for (const resolution of resolutions) {
4229
+ if (resolution.status !== "resolved") continue;
4230
+ disposals.push(resolution.element.dispose());
3909
4231
  }
4232
+ await Promise.all(disposals);
4233
+ }
4234
+ function isRecoverableCounterReadRace(error) {
4235
+ if (!(error instanceof Error)) return false;
4236
+ const message = error.message;
4237
+ return message.includes("Execution context was destroyed") || message.includes("Cannot find context with specified id") || message.includes("Cannot find execution context") || message.includes("Frame was detached") || message.includes("Element is not attached to the DOM") || message.includes("Element is detached");
3910
4238
  }
3911
4239
  function buildCounterNotFoundError(counter) {
3912
4240
  return new CounterResolutionError(
@@ -4391,10 +4719,10 @@ function classifyPathResolutionFailure(action, err) {
4391
4719
  }
4392
4720
 
4393
4721
  // src/actions/click.ts
4394
- async function performClick(page, path5, options) {
4722
+ async function performClick(page, path3, options) {
4395
4723
  let resolved;
4396
4724
  try {
4397
- resolved = await resolveElementPath(page, path5);
4725
+ resolved = await resolveElementPath(page, path3);
4398
4726
  } catch (err) {
4399
4727
  const failure = classifyPathResolutionFailure("click", err);
4400
4728
  return { ok: false, error: failure.message, failure };
@@ -4407,7 +4735,7 @@ async function performClick(page, path5, options) {
4407
4735
  });
4408
4736
  return {
4409
4737
  ok: true,
4410
- path: path5,
4738
+ path: path3,
4411
4739
  usedSelector: resolved.usedSelector
4412
4740
  };
4413
4741
  } catch (err) {
@@ -4424,10 +4752,10 @@ async function performClick(page, path5, options) {
4424
4752
  }
4425
4753
 
4426
4754
  // src/actions/hover.ts
4427
- async function performHover(page, path5, options) {
4755
+ async function performHover(page, path3, options) {
4428
4756
  let resolved;
4429
4757
  try {
4430
- resolved = await resolveElementPath(page, path5);
4758
+ resolved = await resolveElementPath(page, path3);
4431
4759
  } catch (err) {
4432
4760
  const failure = classifyPathResolutionFailure("hover", err);
4433
4761
  return { ok: false, error: failure.message, failure };
@@ -4439,7 +4767,7 @@ async function performHover(page, path5, options) {
4439
4767
  });
4440
4768
  return {
4441
4769
  ok: true,
4442
- path: path5,
4770
+ path: path3,
4443
4771
  usedSelector: resolved.usedSelector
4444
4772
  };
4445
4773
  } catch (err) {
@@ -4456,10 +4784,10 @@ async function performHover(page, path5, options) {
4456
4784
  }
4457
4785
 
4458
4786
  // src/actions/input.ts
4459
- async function performInput(page, path5, options) {
4787
+ async function performInput(page, path3, options) {
4460
4788
  let resolved;
4461
4789
  try {
4462
- resolved = await resolveElementPath(page, path5);
4790
+ resolved = await resolveElementPath(page, path3);
4463
4791
  } catch (err) {
4464
4792
  const failure = classifyPathResolutionFailure("input", err);
4465
4793
  return { ok: false, error: failure.message, failure };
@@ -4475,7 +4803,7 @@ async function performInput(page, path5, options) {
4475
4803
  }
4476
4804
  return {
4477
4805
  ok: true,
4478
- path: path5,
4806
+ path: path3,
4479
4807
  usedSelector: resolved.usedSelector
4480
4808
  };
4481
4809
  } catch (err) {
@@ -4507,9 +4835,9 @@ function getScrollDelta(options) {
4507
4835
  return { x: 0, y: absoluteAmount };
4508
4836
  }
4509
4837
  }
4510
- async function performScroll(page, path5, options) {
4838
+ async function performScroll(page, path3, options) {
4511
4839
  const { x, y } = getScrollDelta(options);
4512
- if (!path5) {
4840
+ if (!path3) {
4513
4841
  await page.evaluate(
4514
4842
  ({ deltaX, deltaY }) => {
4515
4843
  window.scrollBy(deltaX, deltaY);
@@ -4520,7 +4848,7 @@ async function performScroll(page, path5, options) {
4520
4848
  }
4521
4849
  let resolved;
4522
4850
  try {
4523
- resolved = await resolveElementPath(page, path5);
4851
+ resolved = await resolveElementPath(page, path3);
4524
4852
  } catch (err) {
4525
4853
  const failure = classifyPathResolutionFailure("scroll", err);
4526
4854
  return { ok: false, error: failure.message, failure };
@@ -4536,7 +4864,7 @@ async function performScroll(page, path5, options) {
4536
4864
  );
4537
4865
  return {
4538
4866
  ok: true,
4539
- path: path5,
4867
+ path: path3,
4540
4868
  usedSelector: resolved.usedSelector
4541
4869
  };
4542
4870
  } catch (err) {
@@ -4553,10 +4881,10 @@ async function performScroll(page, path5, options) {
4553
4881
  }
4554
4882
 
4555
4883
  // src/actions/select.ts
4556
- async function performSelect(page, path5, options) {
4884
+ async function performSelect(page, path3, options) {
4557
4885
  let resolved;
4558
4886
  try {
4559
- resolved = await resolveElementPath(page, path5);
4887
+ resolved = await resolveElementPath(page, path3);
4560
4888
  } catch (err) {
4561
4889
  const failure = classifyPathResolutionFailure("select", err);
4562
4890
  return { ok: false, error: failure.message, failure };
@@ -4582,7 +4910,7 @@ async function performSelect(page, path5, options) {
4582
4910
  }
4583
4911
  return {
4584
4912
  ok: true,
4585
- path: path5,
4913
+ path: path3,
4586
4914
  usedSelector: resolved.usedSelector
4587
4915
  };
4588
4916
  } catch (err) {
@@ -4599,17 +4927,6 @@ async function performSelect(page, path5, options) {
4599
4927
  }
4600
4928
 
4601
4929
  // src/actions/extract.ts
4602
- async function readFieldValueFromHandle(element, options) {
4603
- const raw = await element.evaluate(
4604
- (target, payload) => {
4605
- return payload.attribute ? target.getAttribute(payload.attribute) : target.textContent;
4606
- },
4607
- {
4608
- attribute: options.attribute
4609
- }
4610
- );
4611
- return normalizeExtractedValue(raw, options.attribute);
4612
- }
4613
4930
  async function extractWithPaths(page, fields) {
4614
4931
  const result = {};
4615
4932
  for (const field of fields) {
@@ -4621,7 +4938,7 @@ async function extractWithPaths(page, fields) {
4621
4938
  continue;
4622
4939
  }
4623
4940
  try {
4624
- result[field.key] = await readFieldValueFromHandle(
4941
+ result[field.key] = await readExtractedValueFromHandle(
4625
4942
  resolved.element,
4626
4943
  {
4627
4944
  attribute: field.attribute
@@ -4661,7 +4978,7 @@ async function extractArrayRowsWithPaths(page, array) {
4661
4978
  field.candidates
4662
4979
  ) : item;
4663
4980
  try {
4664
- const value = target ? await readFieldValueFromHandle(target, {
4981
+ const value = target ? await readExtractedValueFromHandle(target, {
4665
4982
  attribute: field.attribute
4666
4983
  }) : null;
4667
4984
  if (key) {
@@ -4688,8 +5005,8 @@ async function countArrayItemsWithPath(page, itemParentPath) {
4688
5005
  await Promise.all(items.map((item) => item.dispose()));
4689
5006
  return count;
4690
5007
  }
4691
- async function queryAllByElementPath(page, path5) {
4692
- const normalized = sanitizeElementPath(path5);
5008
+ async function queryAllByElementPath(page, path3) {
5009
+ const normalized = sanitizeElementPath(path3);
4693
5010
  const scope = await resolvePathScope(page, normalized.context);
4694
5011
  if (!scope) return [];
4695
5012
  try {
@@ -4861,9 +5178,9 @@ async function queryFirstByCandidates(selectors, query) {
4861
5178
  }
4862
5179
  return fallback;
4863
5180
  }
4864
- function buildArrayFieldPathCandidates(path5) {
4865
- const strict = path5.nodes.length ? buildPathCandidates(path5.nodes) : [];
4866
- const relaxedNodes = stripPositionClauses(path5.nodes);
5181
+ function buildArrayFieldPathCandidates(path3) {
5182
+ const strict = path3.nodes.length ? buildPathCandidates(path3.nodes) : [];
5183
+ const relaxedNodes = stripPositionClauses(path3.nodes);
4867
5184
  const relaxed = relaxedNodes.length ? buildPathCandidates(relaxedNodes) : [];
4868
5185
  return dedupeSelectors([...strict, ...relaxed]);
4869
5186
  }
@@ -4976,7 +5293,7 @@ async function closeTab(context, activePage, index) {
4976
5293
  }
4977
5294
 
4978
5295
  // src/actions/cookies.ts
4979
- import { readFile, writeFile } from "fs/promises";
5296
+ import { readFile as readFile2, writeFile } from "fs/promises";
4980
5297
  async function getCookies(context, url) {
4981
5298
  return context.cookies(url ? [url] : void 0);
4982
5299
  }
@@ -4991,7 +5308,7 @@ async function exportCookies(context, filePath, url) {
4991
5308
  await writeFile(filePath, JSON.stringify(cookies, null, 2), "utf-8");
4992
5309
  }
4993
5310
  async function importCookies(context, filePath) {
4994
- const raw = await readFile(filePath, "utf-8");
5311
+ const raw = await readFile2(filePath, "utf-8");
4995
5312
  const cookies = JSON.parse(raw);
4996
5313
  await context.addCookies(cookies);
4997
5314
  }
@@ -5005,8 +5322,8 @@ async function typeText(page, text) {
5005
5322
  }
5006
5323
 
5007
5324
  // src/actions/element-info.ts
5008
- async function getElementText(page, path5) {
5009
- const resolved = await resolveElementPath(page, path5);
5325
+ async function getElementText(page, path3) {
5326
+ const resolved = await resolveElementPath(page, path3);
5010
5327
  try {
5011
5328
  const text = await resolved.element.textContent();
5012
5329
  return text ?? "";
@@ -5014,16 +5331,16 @@ async function getElementText(page, path5) {
5014
5331
  await resolved.element.dispose();
5015
5332
  }
5016
5333
  }
5017
- async function getElementValue(page, path5) {
5018
- const resolved = await resolveElementPath(page, path5);
5334
+ async function getElementValue(page, path3) {
5335
+ const resolved = await resolveElementPath(page, path3);
5019
5336
  try {
5020
5337
  return await resolved.element.inputValue();
5021
5338
  } finally {
5022
5339
  await resolved.element.dispose();
5023
5340
  }
5024
5341
  }
5025
- async function getElementAttributes(page, path5) {
5026
- const resolved = await resolveElementPath(page, path5);
5342
+ async function getElementAttributes(page, path3) {
5343
+ const resolved = await resolveElementPath(page, path3);
5027
5344
  try {
5028
5345
  return await resolved.element.evaluate((el) => {
5029
5346
  const attrs = {};
@@ -5036,8 +5353,8 @@ async function getElementAttributes(page, path5) {
5036
5353
  await resolved.element.dispose();
5037
5354
  }
5038
5355
  }
5039
- async function getElementBoundingBox(page, path5) {
5040
- const resolved = await resolveElementPath(page, path5);
5356
+ async function getElementBoundingBox(page, path3) {
5357
+ const resolved = await resolveElementPath(page, path3);
5041
5358
  try {
5042
5359
  return await resolved.element.boundingBox();
5043
5360
  } finally {
@@ -5063,10 +5380,10 @@ async function getPageTitle(page) {
5063
5380
  }
5064
5381
 
5065
5382
  // src/actions/file-upload.ts
5066
- async function performFileUpload(page, path5, filePaths) {
5383
+ async function performFileUpload(page, path3, filePaths) {
5067
5384
  let resolved;
5068
5385
  try {
5069
- resolved = await resolveElementPath(page, path5);
5386
+ resolved = await resolveElementPath(page, path3);
5070
5387
  } catch (err) {
5071
5388
  const failure = classifyPathResolutionFailure("uploadFile", err);
5072
5389
  return { ok: false, error: failure.message, failure };
@@ -5075,7 +5392,7 @@ async function performFileUpload(page, path5, filePaths) {
5075
5392
  await resolved.element.setInputFiles(filePaths);
5076
5393
  return {
5077
5394
  ok: true,
5078
- path: path5,
5395
+ path: path3,
5079
5396
  usedSelector: resolved.usedSelector
5080
5397
  };
5081
5398
  } catch (err) {
@@ -5112,32 +5429,6 @@ var OpensteerActionError = class extends Error {
5112
5429
  // src/cloud/contracts.ts
5113
5430
  var cloudSessionContractVersion = "v3";
5114
5431
 
5115
- // src/cloud/errors.ts
5116
- var OpensteerCloudError = class extends Error {
5117
- code;
5118
- status;
5119
- details;
5120
- constructor(code, message, status, details) {
5121
- super(message);
5122
- this.name = "OpensteerCloudError";
5123
- this.code = code;
5124
- this.status = status;
5125
- this.details = details;
5126
- }
5127
- };
5128
- function cloudUnsupportedMethodError(method, message) {
5129
- return new OpensteerCloudError(
5130
- "CLOUD_UNSUPPORTED_METHOD",
5131
- message || `${method} is not supported in cloud mode.`
5132
- );
5133
- }
5134
- function cloudNotLaunchedError() {
5135
- return new OpensteerCloudError(
5136
- "CLOUD_SESSION_NOT_FOUND",
5137
- "Cloud session is not connected. Call launch() first."
5138
- );
5139
- }
5140
-
5141
5432
  // src/cloud/action-ws-client.ts
5142
5433
  import WebSocket from "ws";
5143
5434
  var ActionWsClient = class _ActionWsClient {
@@ -5275,7 +5566,7 @@ function withTokenQuery(wsUrl, token) {
5275
5566
 
5276
5567
  // src/cloud/local-cache-sync.ts
5277
5568
  import fs2 from "fs";
5278
- import path3 from "path";
5569
+ import path2 from "path";
5279
5570
  function collectLocalSelectorCacheEntries(storage, options = {}) {
5280
5571
  const debug = options.debug === true;
5281
5572
  const namespace = storage.getNamespace();
@@ -5285,7 +5576,7 @@ function collectLocalSelectorCacheEntries(storage, options = {}) {
5285
5576
  const fileNames = fs2.readdirSync(namespaceDir);
5286
5577
  for (const fileName of fileNames) {
5287
5578
  if (fileName === "index.json" || !fileName.endsWith(".json")) continue;
5288
- const filePath = path3.join(namespaceDir, fileName);
5579
+ const filePath = path2.join(namespaceDir, fileName);
5289
5580
  const selector = readSelectorFile(filePath, debug);
5290
5581
  if (!selector) continue;
5291
5582
  const descriptionHash = normalizeDescriptionHash(selector.id);
@@ -5407,324 +5698,6 @@ function dedupeNewest(entries) {
5407
5698
  return [...byKey.values()];
5408
5699
  }
5409
5700
 
5410
- // src/cloud/cdp-client.ts
5411
- import {
5412
- chromium
5413
- } from "playwright";
5414
- var CloudCdpClient = class {
5415
- async connect(args) {
5416
- const endpoint = withTokenQuery2(args.wsUrl, args.token);
5417
- let browser;
5418
- try {
5419
- browser = await chromium.connectOverCDP(endpoint);
5420
- } catch (error) {
5421
- const message = error instanceof Error ? error.message : "Failed to connect to cloud CDP endpoint.";
5422
- throw new OpensteerCloudError("CLOUD_TRANSPORT_ERROR", message);
5423
- }
5424
- const contexts = browser.contexts();
5425
- const context = contexts[0];
5426
- if (!context) {
5427
- await browser.close();
5428
- throw new OpensteerCloudError(
5429
- "CLOUD_INTERNAL",
5430
- "Cloud browser returned no context."
5431
- );
5432
- }
5433
- const preferred = selectPreferredContextPage(browser, contexts);
5434
- if (preferred) {
5435
- return preferred;
5436
- }
5437
- const page = context.pages()[0] || await context.newPage();
5438
- return { browser, context, page };
5439
- }
5440
- };
5441
- function selectPreferredContextPage(browser, contexts) {
5442
- let aboutBlankCandidate = null;
5443
- for (const context of contexts) {
5444
- for (const page of context.pages()) {
5445
- const url = safePageUrl(page);
5446
- if (!isInternalOrEmptyUrl(url)) {
5447
- return { browser, context, page };
5448
- }
5449
- if (!aboutBlankCandidate && url === "about:blank") {
5450
- aboutBlankCandidate = { browser, context, page };
5451
- }
5452
- }
5453
- }
5454
- return aboutBlankCandidate;
5455
- }
5456
- function safePageUrl(page) {
5457
- try {
5458
- return page.url();
5459
- } catch {
5460
- return "";
5461
- }
5462
- }
5463
- function isInternalOrEmptyUrl(url) {
5464
- if (!url) return true;
5465
- if (url === "about:blank") return true;
5466
- return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
5467
- }
5468
- function withTokenQuery2(wsUrl, token) {
5469
- const url = new URL(wsUrl);
5470
- url.searchParams.set("token", token);
5471
- return url.toString();
5472
- }
5473
-
5474
- // src/cloud/session-client.ts
5475
- var CACHE_IMPORT_BATCH_SIZE = 200;
5476
- var CloudSessionClient = class {
5477
- baseUrl;
5478
- key;
5479
- authScheme;
5480
- constructor(baseUrl, key, authScheme = "api-key") {
5481
- this.baseUrl = normalizeBaseUrl(baseUrl);
5482
- this.key = key;
5483
- this.authScheme = authScheme;
5484
- }
5485
- async create(request) {
5486
- const response = await fetch(`${this.baseUrl}/sessions`, {
5487
- method: "POST",
5488
- headers: {
5489
- "content-type": "application/json",
5490
- ...this.authHeaders()
5491
- },
5492
- body: JSON.stringify(request)
5493
- });
5494
- if (!response.ok) {
5495
- throw await parseHttpError(response);
5496
- }
5497
- let body;
5498
- try {
5499
- body = await response.json();
5500
- } catch {
5501
- throw new OpensteerCloudError(
5502
- "CLOUD_CONTRACT_MISMATCH",
5503
- "Invalid cloud session create response: expected a JSON object.",
5504
- response.status
5505
- );
5506
- }
5507
- return parseCreateResponse(body, response.status);
5508
- }
5509
- async close(sessionId) {
5510
- const response = await fetch(`${this.baseUrl}/sessions/${sessionId}`, {
5511
- method: "DELETE",
5512
- headers: {
5513
- ...this.authHeaders()
5514
- }
5515
- });
5516
- if (response.status === 204) {
5517
- return;
5518
- }
5519
- if (!response.ok) {
5520
- throw await parseHttpError(response);
5521
- }
5522
- }
5523
- async importSelectorCache(request) {
5524
- if (!request.entries.length) {
5525
- return zeroImportResponse();
5526
- }
5527
- let totals = zeroImportResponse();
5528
- for (let offset = 0; offset < request.entries.length; offset += CACHE_IMPORT_BATCH_SIZE) {
5529
- const batch = request.entries.slice(
5530
- offset,
5531
- offset + CACHE_IMPORT_BATCH_SIZE
5532
- );
5533
- const response = await this.importSelectorCacheBatch(batch);
5534
- totals = mergeImportResponse(totals, response);
5535
- }
5536
- return totals;
5537
- }
5538
- async importSelectorCacheBatch(entries) {
5539
- const response = await fetch(`${this.baseUrl}/selector-cache/import`, {
5540
- method: "POST",
5541
- headers: {
5542
- "content-type": "application/json",
5543
- ...this.authHeaders()
5544
- },
5545
- body: JSON.stringify({ entries })
5546
- });
5547
- if (!response.ok) {
5548
- throw await parseHttpError(response);
5549
- }
5550
- return await response.json();
5551
- }
5552
- authHeaders() {
5553
- if (this.authScheme === "bearer") {
5554
- return {
5555
- authorization: `Bearer ${this.key}`
5556
- };
5557
- }
5558
- return {
5559
- "x-api-key": this.key
5560
- };
5561
- }
5562
- };
5563
- function normalizeBaseUrl(baseUrl) {
5564
- return baseUrl.replace(/\/+$/, "");
5565
- }
5566
- function parseCreateResponse(body, status) {
5567
- const root = requireObject(
5568
- body,
5569
- "Invalid cloud session create response: expected a JSON object.",
5570
- status
5571
- );
5572
- const sessionId = requireString(root, "sessionId", status);
5573
- const actionWsUrl = requireString(root, "actionWsUrl", status);
5574
- const cdpWsUrl = requireString(root, "cdpWsUrl", status);
5575
- const actionToken = requireString(root, "actionToken", status);
5576
- const cdpToken = requireString(root, "cdpToken", status);
5577
- const cloudSessionUrl = requireString(root, "cloudSessionUrl", status);
5578
- const cloudSessionRoot = requireObject(
5579
- root.cloudSession,
5580
- "Invalid cloud session create response: cloudSession must be an object.",
5581
- status
5582
- );
5583
- const cloudSession = {
5584
- sessionId: requireString(cloudSessionRoot, "sessionId", status, "cloudSession"),
5585
- workspaceId: requireString(
5586
- cloudSessionRoot,
5587
- "workspaceId",
5588
- status,
5589
- "cloudSession"
5590
- ),
5591
- state: requireString(cloudSessionRoot, "state", status, "cloudSession"),
5592
- createdAt: requireNumber(cloudSessionRoot, "createdAt", status, "cloudSession"),
5593
- sourceType: requireSourceType(cloudSessionRoot, "sourceType", status, "cloudSession"),
5594
- sourceRef: optionalString(cloudSessionRoot, "sourceRef", status, "cloudSession"),
5595
- label: optionalString(cloudSessionRoot, "label", status, "cloudSession")
5596
- };
5597
- const expiresAt = optionalNumber(root, "expiresAt", status);
5598
- return {
5599
- sessionId,
5600
- actionWsUrl,
5601
- cdpWsUrl,
5602
- actionToken,
5603
- cdpToken,
5604
- expiresAt,
5605
- cloudSessionUrl,
5606
- cloudSession
5607
- };
5608
- }
5609
- function requireObject(value, message, status) {
5610
- if (!value || typeof value !== "object" || Array.isArray(value)) {
5611
- throw new OpensteerCloudError("CLOUD_CONTRACT_MISMATCH", message, status);
5612
- }
5613
- return value;
5614
- }
5615
- function requireString(source, field, status, parent) {
5616
- const value = source[field];
5617
- if (typeof value !== "string" || !value.trim()) {
5618
- throw new OpensteerCloudError(
5619
- "CLOUD_CONTRACT_MISMATCH",
5620
- `Invalid cloud session create response: ${formatFieldPath(
5621
- field,
5622
- parent
5623
- )} must be a non-empty string.`,
5624
- status
5625
- );
5626
- }
5627
- return value;
5628
- }
5629
- function requireNumber(source, field, status, parent) {
5630
- const value = source[field];
5631
- if (typeof value !== "number" || !Number.isFinite(value)) {
5632
- throw new OpensteerCloudError(
5633
- "CLOUD_CONTRACT_MISMATCH",
5634
- `Invalid cloud session create response: ${formatFieldPath(
5635
- field,
5636
- parent
5637
- )} must be a finite number.`,
5638
- status
5639
- );
5640
- }
5641
- return value;
5642
- }
5643
- function optionalString(source, field, status, parent) {
5644
- const value = source[field];
5645
- if (value == null) {
5646
- return void 0;
5647
- }
5648
- if (typeof value !== "string") {
5649
- throw new OpensteerCloudError(
5650
- "CLOUD_CONTRACT_MISMATCH",
5651
- `Invalid cloud session create response: ${formatFieldPath(
5652
- field,
5653
- parent
5654
- )} must be a string when present.`,
5655
- status
5656
- );
5657
- }
5658
- return value;
5659
- }
5660
- function optionalNumber(source, field, status, parent) {
5661
- const value = source[field];
5662
- if (value == null) {
5663
- return void 0;
5664
- }
5665
- if (typeof value !== "number" || !Number.isFinite(value)) {
5666
- throw new OpensteerCloudError(
5667
- "CLOUD_CONTRACT_MISMATCH",
5668
- `Invalid cloud session create response: ${formatFieldPath(
5669
- field,
5670
- parent
5671
- )} must be a finite number when present.`,
5672
- status
5673
- );
5674
- }
5675
- return value;
5676
- }
5677
- function requireSourceType(source, field, status, parent) {
5678
- const value = source[field];
5679
- if (value === "agent-thread" || value === "agent-run" || value === "local-cloud" || value === "manual") {
5680
- return value;
5681
- }
5682
- throw new OpensteerCloudError(
5683
- "CLOUD_CONTRACT_MISMATCH",
5684
- `Invalid cloud session create response: ${formatFieldPath(
5685
- field,
5686
- parent
5687
- )} must be one of "agent-thread", "agent-run", "local-cloud", or "manual".`,
5688
- status
5689
- );
5690
- }
5691
- function formatFieldPath(field, parent) {
5692
- return parent ? `"${parent}.${field}"` : `"${field}"`;
5693
- }
5694
- function zeroImportResponse() {
5695
- return {
5696
- imported: 0,
5697
- inserted: 0,
5698
- updated: 0,
5699
- skipped: 0
5700
- };
5701
- }
5702
- function mergeImportResponse(first, second) {
5703
- return {
5704
- imported: first.imported + second.imported,
5705
- inserted: first.inserted + second.inserted,
5706
- updated: first.updated + second.updated,
5707
- skipped: first.skipped + second.skipped
5708
- };
5709
- }
5710
- async function parseHttpError(response) {
5711
- let body = null;
5712
- try {
5713
- body = await response.json();
5714
- } catch {
5715
- body = null;
5716
- }
5717
- const code = typeof body?.code === "string" ? toCloudErrorCode(body.code) : "CLOUD_TRANSPORT_ERROR";
5718
- const message = typeof body?.error === "string" ? body.error : `Cloud request failed with status ${response.status}.`;
5719
- return new OpensteerCloudError(code, message, response.status, body?.details);
5720
- }
5721
- function toCloudErrorCode(code) {
5722
- if (code === "CLOUD_AUTH_FAILED" || code === "CLOUD_SESSION_NOT_FOUND" || code === "CLOUD_SESSION_CLOSED" || code === "CLOUD_UNSUPPORTED_METHOD" || code === "CLOUD_INVALID_REQUEST" || code === "CLOUD_MODEL_NOT_ALLOWED" || code === "CLOUD_ACTION_FAILED" || code === "CLOUD_INTERNAL" || code === "CLOUD_CAPACITY_EXHAUSTED" || code === "CLOUD_RUNTIME_UNAVAILABLE" || code === "CLOUD_RUNTIME_MISMATCH" || code === "CLOUD_SESSION_STALE" || code === "CLOUD_CONTRACT_MISMATCH" || code === "CLOUD_CONTROL_PLANE_ERROR") {
5723
- return code;
5724
- }
5725
- return "CLOUD_TRANSPORT_ERROR";
5726
- }
5727
-
5728
5701
  // src/agent/errors.ts
5729
5702
  var OpensteerAgentError = class extends Error {
5730
5703
  constructor(message, cause) {
@@ -7149,15 +7122,15 @@ async function executeAgentAction(page, action) {
7149
7122
  return;
7150
7123
  }
7151
7124
  case "drag": {
7152
- const path5 = normalizePath(action.path);
7153
- if (!path5.length) {
7125
+ const path3 = normalizePath(action.path);
7126
+ if (!path3.length) {
7154
7127
  throw new OpensteerAgentActionError(
7155
7128
  "Drag action requires a non-empty path."
7156
7129
  );
7157
7130
  }
7158
- await page.mouse.move(path5[0].x, path5[0].y);
7131
+ await page.mouse.move(path3[0].x, path3[0].y);
7159
7132
  await page.mouse.down();
7160
- for (const point of path5.slice(1)) {
7133
+ for (const point of path3.slice(1)) {
7161
7134
  await page.mouse.move(point.x, point.y);
7162
7135
  }
7163
7136
  await page.mouse.up();
@@ -7253,10 +7226,10 @@ async function maybeFocusPoint(page, action) {
7253
7226
  clickCount: 1
7254
7227
  });
7255
7228
  }
7256
- function normalizePath(path5) {
7257
- if (!Array.isArray(path5)) return [];
7229
+ function normalizePath(path3) {
7230
+ if (!Array.isArray(path3)) return [];
7258
7231
  const points = [];
7259
- for (const entry of path5) {
7232
+ for (const entry of path3) {
7260
7233
  if (!entry || typeof entry !== "object") continue;
7261
7234
  const candidate = entry;
7262
7235
  const x = Number(candidate.x);
@@ -7345,24 +7318,22 @@ var OpensteerCuaAgentHandler = class {
7345
7318
  page;
7346
7319
  config;
7347
7320
  client;
7348
- debug;
7321
+ cursorController;
7349
7322
  onMutatingAction;
7350
- cursorOverlayInjected = false;
7351
7323
  constructor(options) {
7352
7324
  this.page = options.page;
7353
7325
  this.config = options.config;
7354
7326
  this.client = options.client;
7355
- this.debug = options.debug;
7327
+ this.cursorController = options.cursorController;
7356
7328
  this.onMutatingAction = options.onMutatingAction;
7357
7329
  }
7358
7330
  async execute(options) {
7359
7331
  const instruction = options.instruction;
7360
7332
  const maxSteps = options.maxSteps ?? 20;
7361
7333
  await this.initializeClient();
7362
- const highlightCursor = options.highlightCursor === true;
7363
7334
  this.client.setActionHandler(async (action) => {
7364
- if (highlightCursor) {
7365
- await this.maybeRenderCursor(action);
7335
+ if (this.cursorController.isEnabled()) {
7336
+ await this.maybePreviewCursor(action);
7366
7337
  }
7367
7338
  await executeAgentAction(this.page, action);
7368
7339
  this.client.setCurrentUrl(this.page.url());
@@ -7393,6 +7364,7 @@ var OpensteerCuaAgentHandler = class {
7393
7364
  const viewport = await this.resolveViewport();
7394
7365
  this.client.setViewport(viewport.width, viewport.height);
7395
7366
  this.client.setCurrentUrl(this.page.url());
7367
+ await this.cursorController.attachPage(this.page);
7396
7368
  this.client.setScreenshotProvider(async () => {
7397
7369
  const buffer = await this.page.screenshot({
7398
7370
  fullPage: false,
@@ -7421,55 +7393,615 @@ var OpensteerCuaAgentHandler = class {
7421
7393
  }
7422
7394
  return DEFAULT_CUA_VIEWPORT;
7423
7395
  }
7424
- async maybeRenderCursor(action) {
7396
+ async maybePreviewCursor(action) {
7425
7397
  const x = typeof action.x === "number" ? action.x : null;
7426
7398
  const y = typeof action.y === "number" ? action.y : null;
7427
7399
  if (x == null || y == null) {
7428
7400
  return;
7429
7401
  }
7402
+ await this.cursorController.preview({ x, y }, "agent");
7403
+ }
7404
+ };
7405
+ function sleep3(ms) {
7406
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7407
+ }
7408
+
7409
+ // src/cursor/motion.ts
7410
+ var DEFAULT_SNAPPY_OPTIONS = {
7411
+ minDurationMs: 46,
7412
+ maxDurationMs: 170,
7413
+ maxPoints: 14
7414
+ };
7415
+ function planSnappyCursorMotion(from, to, options = {}) {
7416
+ const resolved = {
7417
+ ...DEFAULT_SNAPPY_OPTIONS,
7418
+ ...options
7419
+ };
7420
+ const dx = to.x - from.x;
7421
+ const dy = to.y - from.y;
7422
+ const distance = Math.hypot(dx, dy);
7423
+ if (distance < 5) {
7424
+ return {
7425
+ points: [roundPoint(to)],
7426
+ stepDelayMs: 0
7427
+ };
7428
+ }
7429
+ const durationMs = clamp(
7430
+ 44 + distance * 0.26,
7431
+ resolved.minDurationMs,
7432
+ resolved.maxDurationMs
7433
+ );
7434
+ const rawPoints = clamp(
7435
+ Math.round(durationMs / 13),
7436
+ 4,
7437
+ resolved.maxPoints
7438
+ );
7439
+ const sign = deterministicBendSign(from, to);
7440
+ const nx = -dy / distance;
7441
+ const ny = dx / distance;
7442
+ const bend = clamp(distance * 0.12, 8, 28) * sign;
7443
+ const c1 = {
7444
+ x: from.x + dx * 0.28 + nx * bend,
7445
+ y: from.y + dy * 0.28 + ny * bend
7446
+ };
7447
+ const c2 = {
7448
+ x: from.x + dx * 0.74 + nx * bend * 0.58,
7449
+ y: from.y + dy * 0.74 + ny * bend * 0.58
7450
+ };
7451
+ const points = [];
7452
+ for (let i = 1; i <= rawPoints; i += 1) {
7453
+ const t = i / rawPoints;
7454
+ const sampled = cubicBezier(from, c1, c2, to, t);
7455
+ if (i !== rawPoints) {
7456
+ const previous = points[points.length - 1];
7457
+ if (previous && distanceBetween(previous, sampled) < 1.4) {
7458
+ continue;
7459
+ }
7460
+ }
7461
+ points.push(roundPoint(sampled));
7462
+ }
7463
+ if (distance > 220) {
7464
+ const settle = {
7465
+ x: to.x - dx / distance,
7466
+ y: to.y - dy / distance
7467
+ };
7468
+ const last = points[points.length - 1];
7469
+ if (!last || distanceBetween(last, settle) > 1.2) {
7470
+ points.splice(Math.max(0, points.length - 1), 0, roundPoint(settle));
7471
+ }
7472
+ }
7473
+ const deduped = dedupeAdjacent(points);
7474
+ const stepDelayMs = deduped.length > 1 ? Math.round(durationMs / deduped.length) : 0;
7475
+ return {
7476
+ points: deduped.length ? deduped : [roundPoint(to)],
7477
+ stepDelayMs
7478
+ };
7479
+ }
7480
+ function cubicBezier(p0, p1, p2, p3, t) {
7481
+ const inv = 1 - t;
7482
+ const inv2 = inv * inv;
7483
+ const inv3 = inv2 * inv;
7484
+ const t2 = t * t;
7485
+ const t3 = t2 * t;
7486
+ return {
7487
+ x: inv3 * p0.x + 3 * inv2 * t * p1.x + 3 * inv * t2 * p2.x + t3 * p3.x,
7488
+ y: inv3 * p0.y + 3 * inv2 * t * p1.y + 3 * inv * t2 * p2.y + t3 * p3.y
7489
+ };
7490
+ }
7491
+ function deterministicBendSign(from, to) {
7492
+ const seed = Math.sin(
7493
+ from.x * 12.9898 + from.y * 78.233 + to.x * 37.719 + to.y * 19.113
7494
+ ) * 43758.5453;
7495
+ const fractional = seed - Math.floor(seed);
7496
+ return fractional >= 0.5 ? 1 : -1;
7497
+ }
7498
+ function roundPoint(point) {
7499
+ return {
7500
+ x: Math.round(point.x * 100) / 100,
7501
+ y: Math.round(point.y * 100) / 100
7502
+ };
7503
+ }
7504
+ function distanceBetween(a, b) {
7505
+ return Math.hypot(a.x - b.x, a.y - b.y);
7506
+ }
7507
+ function dedupeAdjacent(points) {
7508
+ const out = [];
7509
+ for (const point of points) {
7510
+ const last = out[out.length - 1];
7511
+ if (last && last.x === point.x && last.y === point.y) {
7512
+ continue;
7513
+ }
7514
+ out.push(point);
7515
+ }
7516
+ return out;
7517
+ }
7518
+ function clamp(value, min, max) {
7519
+ return Math.min(max, Math.max(min, value));
7520
+ }
7521
+
7522
+ // src/cursor/renderers/svg-overlay.ts
7523
+ var PULSE_DURATION_MS = 220;
7524
+ var HOST_ELEMENT_ID = "__os_cr";
7525
+ var SvgCursorRenderer = class {
7526
+ page = null;
7527
+ active = false;
7528
+ reason = "disabled";
7529
+ lastMessage;
7530
+ async initialize(page) {
7531
+ this.page = page;
7532
+ if (page.isClosed()) {
7533
+ this.markInactive("page_closed");
7534
+ return;
7535
+ }
7536
+ try {
7537
+ await page.evaluate(injectCursor, HOST_ELEMENT_ID);
7538
+ this.active = true;
7539
+ this.reason = void 0;
7540
+ this.lastMessage = void 0;
7541
+ } catch (error) {
7542
+ const message = error instanceof Error ? error.message : String(error);
7543
+ this.markInactive("renderer_error", message);
7544
+ }
7545
+ }
7546
+ isActive() {
7547
+ return this.active;
7548
+ }
7549
+ status() {
7550
+ return {
7551
+ enabled: true,
7552
+ active: this.active,
7553
+ reason: this.reason ? this.lastMessage ? `${this.reason}: ${this.lastMessage}` : this.reason : void 0
7554
+ };
7555
+ }
7556
+ async move(point, style) {
7557
+ if (!this.active || !this.page || this.page.isClosed()) return;
7430
7558
  try {
7431
- if (!this.cursorOverlayInjected) {
7432
- await this.page.evaluate(() => {
7433
- if (document.getElementById("__opensteer_cua_cursor")) return;
7434
- const cursor = document.createElement("div");
7435
- cursor.id = "__opensteer_cua_cursor";
7436
- cursor.style.position = "fixed";
7437
- cursor.style.width = "14px";
7438
- cursor.style.height = "14px";
7439
- cursor.style.borderRadius = "999px";
7440
- cursor.style.background = "rgba(255, 51, 51, 0.85)";
7441
- cursor.style.border = "2px solid rgba(255, 255, 255, 0.95)";
7442
- cursor.style.boxShadow = "0 0 0 3px rgba(255, 51, 51, 0.25)";
7443
- cursor.style.pointerEvents = "none";
7444
- cursor.style.zIndex = "2147483647";
7445
- cursor.style.transform = "translate(-9999px, -9999px)";
7446
- cursor.style.transition = "transform 80ms linear";
7447
- document.documentElement.appendChild(cursor);
7559
+ const ok = await this.page.evaluate(moveCursor, {
7560
+ id: HOST_ELEMENT_ID,
7561
+ x: point.x,
7562
+ y: point.y,
7563
+ size: style.size,
7564
+ fill: colorToRgba(style.fillColor),
7565
+ outline: colorToRgba(style.outlineColor)
7566
+ });
7567
+ if (!ok) {
7568
+ await this.reinject();
7569
+ await this.page.evaluate(moveCursor, {
7570
+ id: HOST_ELEMENT_ID,
7571
+ x: point.x,
7572
+ y: point.y,
7573
+ size: style.size,
7574
+ fill: colorToRgba(style.fillColor),
7575
+ outline: colorToRgba(style.outlineColor)
7576
+ });
7577
+ }
7578
+ } catch (error) {
7579
+ this.handleError(error);
7580
+ }
7581
+ }
7582
+ async pulse(point, style) {
7583
+ if (!this.active || !this.page || this.page.isClosed()) return;
7584
+ try {
7585
+ const ok = await this.page.evaluate(pulseCursor, {
7586
+ id: HOST_ELEMENT_ID,
7587
+ x: point.x,
7588
+ y: point.y,
7589
+ size: style.size,
7590
+ fill: colorToRgba(style.fillColor),
7591
+ outline: colorToRgba(style.outlineColor),
7592
+ halo: colorToRgba(style.haloColor),
7593
+ pulseMs: PULSE_DURATION_MS
7594
+ });
7595
+ if (!ok) {
7596
+ await this.reinject();
7597
+ await this.page.evaluate(pulseCursor, {
7598
+ id: HOST_ELEMENT_ID,
7599
+ x: point.x,
7600
+ y: point.y,
7601
+ size: style.size,
7602
+ fill: colorToRgba(style.fillColor),
7603
+ outline: colorToRgba(style.outlineColor),
7604
+ halo: colorToRgba(style.haloColor),
7605
+ pulseMs: PULSE_DURATION_MS
7448
7606
  });
7449
- this.cursorOverlayInjected = true;
7450
7607
  }
7451
- await this.page.evaluate(
7452
- ({ px, py }) => {
7453
- const cursor = document.getElementById("__opensteer_cua_cursor");
7454
- if (!cursor) return;
7455
- cursor.style.transform = `translate(${Math.round(px - 7)}px, ${Math.round(py - 7)}px)`;
7456
- },
7457
- { px: x, py: y }
7458
- );
7608
+ } catch (error) {
7609
+ this.handleError(error);
7610
+ }
7611
+ }
7612
+ async clear() {
7613
+ if (!this.page || this.page.isClosed()) return;
7614
+ try {
7615
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
7616
+ } catch {
7617
+ }
7618
+ }
7619
+ async dispose() {
7620
+ if (this.page && !this.page.isClosed()) {
7621
+ try {
7622
+ await this.page.evaluate(removeCursor, HOST_ELEMENT_ID);
7623
+ } catch {
7624
+ }
7625
+ }
7626
+ this.active = false;
7627
+ this.reason = "disabled";
7628
+ this.lastMessage = void 0;
7629
+ this.page = null;
7630
+ }
7631
+ async reinject() {
7632
+ if (!this.page || this.page.isClosed()) return;
7633
+ try {
7634
+ await this.page.evaluate(injectCursor, HOST_ELEMENT_ID);
7635
+ } catch {
7636
+ }
7637
+ }
7638
+ markInactive(reason, message) {
7639
+ this.active = false;
7640
+ this.reason = reason;
7641
+ this.lastMessage = message;
7642
+ }
7643
+ handleError(error) {
7644
+ const message = error instanceof Error ? error.message : String(error);
7645
+ if (isPageGone(message)) {
7646
+ this.markInactive("page_closed", message);
7647
+ }
7648
+ }
7649
+ };
7650
+ function injectCursor(hostId) {
7651
+ const win = window;
7652
+ if (win[hostId]) return;
7653
+ const host = document.createElement("div");
7654
+ host.style.cssText = "position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
7655
+ const shadow = host.attachShadow({ mode: "closed" });
7656
+ const wrapper = document.createElement("div");
7657
+ wrapper.style.cssText = "position:fixed;top:0;left:0;pointer-events:none;will-change:transform;display:none;";
7658
+ wrapper.innerHTML = `
7659
+ <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 1px 2px rgba(0,0,0,0.3));display:block;">
7660
+ <path d="M3 2L3 23L8.5 17.5L13 26L17 24L12.5 15.5L20 15.5L3 2Z"
7661
+ fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
7662
+ </svg>
7663
+ <div data-role="pulse" style="position:absolute;top:0;left:0;width:24px;height:24px;border-radius:50%;pointer-events:none;opacity:0;transform:translate(-8px,-8px);"></div>
7664
+ `;
7665
+ shadow.appendChild(wrapper);
7666
+ document.documentElement.appendChild(host);
7667
+ Object.defineProperty(window, hostId, {
7668
+ value: {
7669
+ host,
7670
+ wrapper,
7671
+ path: wrapper.querySelector("path"),
7672
+ pulse: wrapper.querySelector('[data-role="pulse"]')
7673
+ },
7674
+ configurable: true,
7675
+ enumerable: false
7676
+ });
7677
+ }
7678
+ function moveCursor(args) {
7679
+ const refs = window[args.id];
7680
+ if (!refs) return false;
7681
+ const scale = args.size / 20;
7682
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
7683
+ refs.wrapper.style.display = "block";
7684
+ if (refs.path) {
7685
+ refs.path.setAttribute("fill", args.fill);
7686
+ refs.path.setAttribute("stroke", args.outline);
7687
+ }
7688
+ return true;
7689
+ }
7690
+ function pulseCursor(args) {
7691
+ const refs = window[args.id];
7692
+ if (!refs) return false;
7693
+ const scale = args.size / 20;
7694
+ refs.wrapper.style.transform = `translate(${args.x}px, ${args.y}px) scale(${scale})`;
7695
+ refs.wrapper.style.display = "block";
7696
+ if (refs.path) {
7697
+ refs.path.setAttribute("fill", args.fill);
7698
+ refs.path.setAttribute("stroke", args.outline);
7699
+ }
7700
+ const ring = refs.pulse;
7701
+ if (!ring) return true;
7702
+ ring.style.background = args.halo;
7703
+ ring.style.opacity = "0.7";
7704
+ ring.style.width = "24px";
7705
+ ring.style.height = "24px";
7706
+ ring.style.transition = `all ${args.pulseMs}ms ease-out`;
7707
+ ring.offsetHeight;
7708
+ ring.style.width = "48px";
7709
+ ring.style.height = "48px";
7710
+ ring.style.opacity = "0";
7711
+ ring.style.transform = "translate(-20px, -20px)";
7712
+ setTimeout(() => {
7713
+ ring.style.transition = "none";
7714
+ ring.style.width = "24px";
7715
+ ring.style.height = "24px";
7716
+ ring.style.transform = "translate(-8px, -8px)";
7717
+ ring.style.opacity = "0";
7718
+ }, args.pulseMs);
7719
+ return true;
7720
+ }
7721
+ function removeCursor(hostId) {
7722
+ const refs = window[hostId];
7723
+ if (refs) {
7724
+ refs.host.remove();
7725
+ delete window[hostId];
7726
+ }
7727
+ }
7728
+ function colorToRgba(c) {
7729
+ return `rgba(${Math.round(c.r)},${Math.round(c.g)},${Math.round(c.b)},${c.a})`;
7730
+ }
7731
+ function isPageGone(message) {
7732
+ const m = message.toLowerCase();
7733
+ return m.includes("closed") || m.includes("detached") || m.includes("destroyed") || m.includes("target");
7734
+ }
7735
+
7736
+ // src/cursor/controller.ts
7737
+ var DEFAULT_STYLE = {
7738
+ size: 20,
7739
+ fillColor: {
7740
+ r: 255,
7741
+ g: 255,
7742
+ b: 255,
7743
+ a: 0.96
7744
+ },
7745
+ outlineColor: {
7746
+ r: 0,
7747
+ g: 0,
7748
+ b: 0,
7749
+ a: 1
7750
+ },
7751
+ haloColor: {
7752
+ r: 35,
7753
+ g: 162,
7754
+ b: 255,
7755
+ a: 0.38
7756
+ },
7757
+ pulseScale: 2.15
7758
+ };
7759
+ var REINITIALIZE_BACKOFF_MS = 1e3;
7760
+ var FIRST_MOVE_CENTER_DISTANCE_THRESHOLD = 16;
7761
+ var FIRST_MOVE_MAX_TRAVEL = 220;
7762
+ var FIRST_MOVE_NEAR_TARGET_X_OFFSET = 28;
7763
+ var FIRST_MOVE_NEAR_TARGET_Y_OFFSET = 18;
7764
+ var MOTION_PLANNERS = {
7765
+ snappy: planSnappyCursorMotion
7766
+ };
7767
+ var CursorController = class {
7768
+ debug;
7769
+ renderer;
7770
+ page = null;
7771
+ listenerPage = null;
7772
+ lastPoint = null;
7773
+ initializedForPage = false;
7774
+ lastInitializeAttemptAt = 0;
7775
+ enabled;
7776
+ profile;
7777
+ style;
7778
+ onDomContentLoaded = () => {
7779
+ void this.restoreCursorAfterNavigation();
7780
+ };
7781
+ constructor(options = {}) {
7782
+ const config = options.config || {};
7783
+ this.debug = Boolean(options.debug);
7784
+ this.enabled = config.enabled === true;
7785
+ this.profile = config.profile ?? "snappy";
7786
+ this.style = mergeStyle(config.style);
7787
+ this.renderer = options.renderer ?? new SvgCursorRenderer();
7788
+ }
7789
+ setEnabled(enabled) {
7790
+ if (this.enabled && !enabled) {
7791
+ this.lastPoint = null;
7792
+ void this.clear();
7793
+ }
7794
+ this.enabled = enabled;
7795
+ }
7796
+ isEnabled() {
7797
+ return this.enabled;
7798
+ }
7799
+ getStatus() {
7800
+ if (!this.enabled) {
7801
+ return {
7802
+ enabled: false,
7803
+ active: false,
7804
+ reason: "disabled"
7805
+ };
7806
+ }
7807
+ const status = this.renderer.status();
7808
+ if (!this.initializedForPage && !status.active) {
7809
+ return {
7810
+ enabled: true,
7811
+ active: false,
7812
+ reason: "not_initialized"
7813
+ };
7814
+ }
7815
+ return status;
7816
+ }
7817
+ async attachPage(page) {
7818
+ if (this.page !== page) {
7819
+ this.detachPageListeners();
7820
+ this.page = page;
7821
+ this.lastPoint = null;
7822
+ this.initializedForPage = false;
7823
+ this.lastInitializeAttemptAt = 0;
7824
+ }
7825
+ this.attachPageListeners(page);
7826
+ }
7827
+ async preview(point, intent) {
7828
+ if (!this.enabled || !point) return;
7829
+ if (!this.page || this.page.isClosed()) return;
7830
+ try {
7831
+ await this.ensureInitialized();
7832
+ if (!this.renderer.isActive()) {
7833
+ await this.reinitializeIfEligible();
7834
+ }
7835
+ if (!this.renderer.isActive()) return;
7836
+ const start = this.resolveMotionStart(point);
7837
+ const motion = this.planMotion(start, point);
7838
+ for (const step of motion.points) {
7839
+ await this.renderer.move(step, this.style);
7840
+ if (motion.stepDelayMs > 0) {
7841
+ await sleep4(motion.stepDelayMs);
7842
+ }
7843
+ }
7844
+ if (shouldPulse(intent)) {
7845
+ await this.renderer.pulse(point, this.style);
7846
+ }
7847
+ this.lastPoint = point;
7459
7848
  } catch (error) {
7460
7849
  if (this.debug) {
7461
7850
  const message = error instanceof Error ? error.message : String(error);
7462
- console.warn(`[opensteer] cursor overlay failed: ${message}`);
7851
+ console.warn(`[opensteer] cursor preview failed: ${message}`);
7463
7852
  }
7464
7853
  }
7465
7854
  }
7855
+ async clear() {
7856
+ try {
7857
+ await this.renderer.clear();
7858
+ } catch (error) {
7859
+ if (this.debug) {
7860
+ const message = error instanceof Error ? error.message : String(error);
7861
+ console.warn(`[opensteer] cursor clear failed: ${message}`);
7862
+ }
7863
+ }
7864
+ }
7865
+ async dispose() {
7866
+ this.detachPageListeners();
7867
+ this.lastPoint = null;
7868
+ this.initializedForPage = false;
7869
+ this.lastInitializeAttemptAt = 0;
7870
+ this.page = null;
7871
+ await this.renderer.dispose();
7872
+ }
7873
+ async ensureInitialized() {
7874
+ if (!this.page || this.page.isClosed()) return;
7875
+ if (this.initializedForPage) return;
7876
+ await this.initializeRenderer();
7877
+ }
7878
+ attachPageListeners(page) {
7879
+ if (this.listenerPage === page) {
7880
+ return;
7881
+ }
7882
+ this.detachPageListeners();
7883
+ page.on("domcontentloaded", this.onDomContentLoaded);
7884
+ this.listenerPage = page;
7885
+ }
7886
+ detachPageListeners() {
7887
+ if (!this.listenerPage) {
7888
+ return;
7889
+ }
7890
+ this.listenerPage.off("domcontentloaded", this.onDomContentLoaded);
7891
+ this.listenerPage = null;
7892
+ }
7893
+ planMotion(from, to) {
7894
+ return MOTION_PLANNERS[this.profile](from, to);
7895
+ }
7896
+ async reinitializeIfEligible() {
7897
+ if (!this.page || this.page.isClosed()) return;
7898
+ const elapsed = Date.now() - this.lastInitializeAttemptAt;
7899
+ if (elapsed < REINITIALIZE_BACKOFF_MS) return;
7900
+ await this.initializeRenderer();
7901
+ }
7902
+ async initializeRenderer() {
7903
+ if (!this.page || this.page.isClosed()) return;
7904
+ this.lastInitializeAttemptAt = Date.now();
7905
+ await this.renderer.initialize(this.page);
7906
+ this.initializedForPage = true;
7907
+ }
7908
+ async restoreCursorAfterNavigation() {
7909
+ if (!this.enabled || !this.lastPoint) return;
7910
+ if (!this.page || this.page.isClosed()) return;
7911
+ try {
7912
+ if (!this.renderer.isActive()) {
7913
+ await this.reinitializeIfEligible();
7914
+ }
7915
+ if (!this.renderer.isActive()) {
7916
+ return;
7917
+ }
7918
+ await this.renderer.move(this.lastPoint, this.style);
7919
+ } catch (error) {
7920
+ if (this.debug) {
7921
+ const message = error instanceof Error ? error.message : String(error);
7922
+ console.warn(
7923
+ `[opensteer] cursor restore after navigation failed: ${message}`
7924
+ );
7925
+ }
7926
+ }
7927
+ }
7928
+ resolveMotionStart(target) {
7929
+ if (this.lastPoint) {
7930
+ return this.lastPoint;
7931
+ }
7932
+ const viewport = this.page?.viewportSize();
7933
+ if (!viewport?.width || !viewport?.height) {
7934
+ return target;
7935
+ }
7936
+ const centerPoint = {
7937
+ x: viewport.width / 2,
7938
+ y: viewport.height / 2
7939
+ };
7940
+ if (distanceBetween2(centerPoint, target) > FIRST_MOVE_CENTER_DISTANCE_THRESHOLD) {
7941
+ const dx = target.x - centerPoint.x;
7942
+ const dy = target.y - centerPoint.y;
7943
+ const distance = Math.hypot(dx, dy);
7944
+ if (distance > FIRST_MOVE_MAX_TRAVEL) {
7945
+ const ux = dx / distance;
7946
+ const uy = dy / distance;
7947
+ return {
7948
+ x: target.x - ux * FIRST_MOVE_MAX_TRAVEL,
7949
+ y: target.y - uy * FIRST_MOVE_MAX_TRAVEL
7950
+ };
7951
+ }
7952
+ return centerPoint;
7953
+ }
7954
+ return {
7955
+ x: clamp2(target.x - FIRST_MOVE_NEAR_TARGET_X_OFFSET, 0, viewport.width),
7956
+ y: clamp2(target.y - FIRST_MOVE_NEAR_TARGET_Y_OFFSET, 0, viewport.height)
7957
+ };
7958
+ }
7466
7959
  };
7467
- function sleep3(ms) {
7468
- return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7960
+ function mergeStyle(style) {
7961
+ return {
7962
+ size: normalizeFinite(style?.size, DEFAULT_STYLE.size, 4, 48),
7963
+ pulseScale: normalizeFinite(
7964
+ style?.pulseScale,
7965
+ DEFAULT_STYLE.pulseScale,
7966
+ 1,
7967
+ 3
7968
+ ),
7969
+ fillColor: normalizeColor(style?.fillColor, DEFAULT_STYLE.fillColor),
7970
+ outlineColor: normalizeColor(
7971
+ style?.outlineColor,
7972
+ DEFAULT_STYLE.outlineColor
7973
+ ),
7974
+ haloColor: normalizeColor(style?.haloColor, DEFAULT_STYLE.haloColor)
7975
+ };
7976
+ }
7977
+ function normalizeColor(color, fallback) {
7978
+ if (!color) return { ...fallback };
7979
+ return {
7980
+ r: normalizeFinite(color.r, fallback.r, 0, 255),
7981
+ g: normalizeFinite(color.g, fallback.g, 0, 255),
7982
+ b: normalizeFinite(color.b, fallback.b, 0, 255),
7983
+ a: normalizeFinite(color.a, fallback.a, 0, 1)
7984
+ };
7985
+ }
7986
+ function normalizeFinite(value, fallback, min, max) {
7987
+ const numeric = typeof value === "number" && Number.isFinite(value) ? value : fallback;
7988
+ return Math.min(max, Math.max(min, numeric));
7989
+ }
7990
+ function distanceBetween2(a, b) {
7991
+ return Math.hypot(a.x - b.x, a.y - b.y);
7992
+ }
7993
+ function clamp2(value, min, max) {
7994
+ return Math.min(max, Math.max(min, value));
7995
+ }
7996
+ function shouldPulse(intent) {
7997
+ return intent === "click" || intent === "dblclick" || intent === "rightclick" || intent === "agent";
7998
+ }
7999
+ function sleep4(ms) {
8000
+ return new Promise((resolve) => setTimeout(resolve, ms));
7469
8001
  }
7470
8002
 
7471
8003
  // src/opensteer.ts
7472
- import { createHash, randomUUID } from "crypto";
8004
+ import { createHash as createHash2, randomUUID } from "crypto";
7473
8005
 
7474
8006
  // src/browser/pool.ts
7475
8007
  import {
@@ -7813,19 +8345,10 @@ function errorMessage(error) {
7813
8345
  return error instanceof Error ? error.message : String(error);
7814
8346
  }
7815
8347
 
7816
- // src/browser/chrome.ts
7817
- import { homedir, platform } from "os";
7818
- import { join } from "path";
7819
- function expandHome(p) {
7820
- if (p.startsWith("~/") || p === "~") {
7821
- return join(homedir(), p.slice(1));
7822
- }
7823
- return p;
7824
- }
7825
-
7826
8348
  // src/browser/pool.ts
7827
8349
  var BrowserPool = class {
7828
8350
  browser = null;
8351
+ persistentContext = null;
7829
8352
  cdpProxy = null;
7830
8353
  defaults;
7831
8354
  constructor(defaults = {}) {
@@ -7841,16 +8364,23 @@ var BrowserPool = class {
7841
8364
  if (connectUrl) {
7842
8365
  return this.connectToRunning(connectUrl, options.timeout);
7843
8366
  }
7844
- if (channel || profileDir) {
7845
- return this.launchWithProfile(options, channel, profileDir);
8367
+ if (profileDir) {
8368
+ return this.launchPersistentProfile(options, channel, profileDir);
8369
+ }
8370
+ if (channel) {
8371
+ return this.launchWithChannel(options, channel);
7846
8372
  }
7847
8373
  return this.launchSandbox(options);
7848
8374
  }
7849
8375
  async close() {
7850
8376
  const browser = this.browser;
8377
+ const persistentContext = this.persistentContext;
7851
8378
  this.browser = null;
8379
+ this.persistentContext = null;
7852
8380
  try {
7853
- if (browser) {
8381
+ if (persistentContext) {
8382
+ await persistentContext.close();
8383
+ } else if (browser) {
7854
8384
  await browser.close();
7855
8385
  }
7856
8386
  } finally {
@@ -7876,6 +8406,7 @@ var BrowserPool = class {
7876
8406
  timeout: timeout ?? 3e4
7877
8407
  });
7878
8408
  this.browser = browser;
8409
+ this.persistentContext = null;
7879
8410
  const contexts = browser.contexts();
7880
8411
  if (contexts.length === 0) {
7881
8412
  throw new Error(
@@ -7891,442 +8422,69 @@ var BrowserPool = class {
7891
8422
  await browser.close().catch(() => void 0);
7892
8423
  }
7893
8424
  this.browser = null;
7894
- this.cdpProxy?.close();
7895
- this.cdpProxy = null;
7896
- throw error;
7897
- }
7898
- }
7899
- async launchWithProfile(options, channel, profileDir) {
7900
- const args = [];
7901
- if (profileDir) {
7902
- args.push(`--user-data-dir=${expandHome(profileDir)}`);
7903
- }
7904
- const browser = await chromium2.launch({
7905
- channel,
7906
- headless: options.headless ?? this.defaults.headless,
7907
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
7908
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
7909
- args
7910
- });
7911
- this.browser = browser;
7912
- const contexts = browser.contexts();
7913
- let context;
7914
- let page;
7915
- if (contexts.length > 0) {
7916
- context = contexts[0];
7917
- const pages = context.pages();
7918
- page = pages.length > 0 ? pages[0] : await context.newPage();
7919
- } else {
7920
- context = await browser.newContext(options.context || {});
7921
- page = await context.newPage();
7922
- }
7923
- return { browser, context, page, isExternal: false };
7924
- }
7925
- async launchSandbox(options) {
7926
- const browser = await chromium2.launch({
7927
- headless: options.headless ?? this.defaults.headless,
7928
- executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
7929
- slowMo: options.slowMo ?? this.defaults.slowMo ?? 0
7930
- });
7931
- const context = await browser.newContext(options.context || {});
7932
- const page = await context.newPage();
7933
- this.browser = browser;
7934
- return { browser, context, page, isExternal: false };
7935
- }
7936
- };
7937
-
7938
- // src/config.ts
7939
- import fs3 from "fs";
7940
- import path4 from "path";
7941
- import { fileURLToPath } from "url";
7942
- import { parse as parseDotenv } from "dotenv";
7943
- var DEFAULT_CONFIG = {
7944
- browser: {
7945
- headless: false,
7946
- executablePath: void 0,
7947
- slowMo: 0,
7948
- connectUrl: void 0,
7949
- channel: void 0,
7950
- profileDir: void 0
7951
- },
7952
- storage: {
7953
- rootDir: process.cwd()
7954
- },
7955
- model: "gpt-5.1",
7956
- debug: false
7957
- };
7958
- function dotenvFileOrder(nodeEnv) {
7959
- const normalized = nodeEnv?.trim() || "";
7960
- const files = [];
7961
- if (normalized) {
7962
- files.push(`.env.${normalized}.local`);
7963
- }
7964
- if (normalized !== "test") {
7965
- files.push(".env.local");
7966
- }
7967
- if (normalized) {
7968
- files.push(`.env.${normalized}`);
7969
- }
7970
- files.push(".env");
7971
- return files;
7972
- }
7973
- function loadDotenvValues(rootDir, baseEnv, options = {}) {
7974
- const values = {};
7975
- if (parseBool(baseEnv.OPENSTEER_DISABLE_DOTENV_AUTOLOAD) === true) {
7976
- return values;
7977
- }
7978
- const debug = options.debug ?? parseBool(baseEnv.OPENSTEER_DEBUG) === true;
7979
- const baseDir = path4.resolve(rootDir);
7980
- const nodeEnv = baseEnv.NODE_ENV?.trim() || "";
7981
- for (const filename of dotenvFileOrder(nodeEnv)) {
7982
- const filePath = path4.join(baseDir, filename);
7983
- if (!fs3.existsSync(filePath)) continue;
7984
- try {
7985
- const raw = fs3.readFileSync(filePath, "utf8");
7986
- const parsed = parseDotenv(raw);
7987
- for (const [key, value] of Object.entries(parsed)) {
7988
- if (values[key] === void 0) {
7989
- values[key] = value;
7990
- }
7991
- }
7992
- } catch (error) {
7993
- const message = extractErrorMessage(
7994
- error,
7995
- "Unable to read or parse dotenv file."
7996
- );
7997
- if (debug) {
7998
- console.warn(
7999
- `[opensteer] failed to load dotenv file "${filePath}": ${message}`
8000
- );
8001
- }
8002
- continue;
8003
- }
8004
- }
8005
- return values;
8006
- }
8007
- function resolveEnv(rootDir, options = {}) {
8008
- const baseEnv = process.env;
8009
- const dotenvValues = loadDotenvValues(rootDir, baseEnv, options);
8010
- return {
8011
- ...dotenvValues,
8012
- ...baseEnv
8013
- };
8014
- }
8015
- function hasOwn(config, key) {
8016
- if (!config || typeof config !== "object") return false;
8017
- return Object.prototype.hasOwnProperty.call(config, key);
8018
- }
8019
- function hasLegacyAiConfig(config) {
8020
- return hasOwn(config, "ai");
8021
- }
8022
- function assertNoLegacyAiConfig(source, config) {
8023
- if (hasLegacyAiConfig(config)) {
8024
- throw new Error(
8025
- `Legacy "ai" config is no longer supported in ${source}. Use top-level "model" instead.`
8026
- );
8027
- }
8028
- }
8029
- function assertNoLegacyRuntimeConfig(source, config) {
8030
- if (!config || typeof config !== "object") return;
8031
- const configRecord = config;
8032
- if (hasOwn(configRecord, "runtime")) {
8033
- throw new Error(
8034
- `Legacy "runtime" config is no longer supported in ${source}. Use top-level "cloud" instead.`
8035
- );
8036
- }
8037
- if (hasOwn(configRecord, "mode")) {
8038
- throw new Error(
8039
- `Top-level "mode" config is no longer supported in ${source}. Use "cloud: true" to enable cloud mode.`
8040
- );
8041
- }
8042
- if (hasOwn(configRecord, "remote")) {
8043
- throw new Error(
8044
- `Top-level "remote" config is no longer supported in ${source}. Use "cloud" options instead.`
8045
- );
8046
- }
8047
- if (hasOwn(configRecord, "apiKey")) {
8048
- throw new Error(
8049
- `Top-level "apiKey" config is not supported in ${source}. Use "cloud.apiKey" instead.`
8050
- );
8051
- }
8052
- }
8053
- function loadConfigFile(rootDir, options = {}) {
8054
- const configPath = path4.join(rootDir, ".opensteer", "config.json");
8055
- if (!fs3.existsSync(configPath)) return {};
8056
- try {
8057
- const raw = fs3.readFileSync(configPath, "utf8");
8058
- return JSON.parse(raw);
8059
- } catch (error) {
8060
- const message = extractErrorMessage(
8061
- error,
8062
- "Unable to read or parse config file."
8063
- );
8064
- if (options.debug) {
8065
- console.warn(
8066
- `[opensteer] failed to load config file "${configPath}": ${message}`
8067
- );
8068
- }
8069
- return {};
8070
- }
8071
- }
8072
- function mergeDeep(base, patch) {
8073
- const out = {
8074
- ...base
8075
- };
8076
- for (const [key, value] of Object.entries(patch || {})) {
8077
- const currentValue = out[key];
8078
- if (value && typeof value === "object" && !Array.isArray(value) && currentValue && typeof currentValue === "object" && !Array.isArray(currentValue)) {
8079
- out[key] = mergeDeep(
8080
- currentValue,
8081
- value
8082
- );
8083
- continue;
8084
- }
8085
- if (value !== void 0) {
8086
- out[key] = value;
8087
- }
8088
- }
8089
- return out;
8090
- }
8091
- function parseBool(value) {
8092
- if (value == null) return void 0;
8093
- const normalized = value.trim().toLowerCase();
8094
- if (normalized === "true" || normalized === "1") return true;
8095
- if (normalized === "false" || normalized === "0") return false;
8096
- return void 0;
8097
- }
8098
- function parseNumber(value) {
8099
- if (value == null || value.trim() === "") return void 0;
8100
- const parsed = Number(value);
8101
- if (!Number.isFinite(parsed)) return void 0;
8102
- return parsed;
8103
- }
8104
- function parseRuntimeMode(value, source) {
8105
- if (value == null) return void 0;
8106
- if (typeof value !== "string") {
8107
- throw new Error(
8108
- `Invalid ${source} value "${String(value)}". Use "local" or "cloud".`
8109
- );
8110
- }
8111
- const normalized = value.trim().toLowerCase();
8112
- if (!normalized) return void 0;
8113
- if (normalized === "local" || normalized === "cloud") {
8114
- return normalized;
8115
- }
8116
- throw new Error(
8117
- `Invalid ${source} value "${value}". Use "local" or "cloud".`
8118
- );
8119
- }
8120
- function parseAuthScheme(value, source) {
8121
- if (value == null) return void 0;
8122
- if (typeof value !== "string") {
8123
- throw new Error(
8124
- `Invalid ${source} value "${String(value)}". Use "api-key" or "bearer".`
8125
- );
8126
- }
8127
- const normalized = value.trim().toLowerCase();
8128
- if (!normalized) return void 0;
8129
- if (normalized === "api-key" || normalized === "bearer") {
8130
- return normalized;
8131
- }
8132
- throw new Error(
8133
- `Invalid ${source} value "${value}". Use "api-key" or "bearer".`
8134
- );
8135
- }
8136
- function parseCloudAnnounce(value, source) {
8137
- if (value == null) return void 0;
8138
- if (typeof value !== "string") {
8139
- throw new Error(
8140
- `Invalid ${source} value "${String(value)}". Use "always", "off", or "tty".`
8141
- );
8142
- }
8143
- const normalized = value.trim().toLowerCase();
8144
- if (!normalized) return void 0;
8145
- if (normalized === "always" || normalized === "off" || normalized === "tty") {
8146
- return normalized;
8147
- }
8148
- throw new Error(
8149
- `Invalid ${source} value "${value}". Use "always", "off", or "tty".`
8150
- );
8151
- }
8152
- function resolveOpensteerApiKey(env) {
8153
- const value = env.OPENSTEER_API_KEY?.trim();
8154
- if (!value) return void 0;
8155
- return value;
8156
- }
8157
- function resolveOpensteerBaseUrl(env) {
8158
- const value = env.OPENSTEER_BASE_URL?.trim();
8159
- if (!value) return void 0;
8160
- return value;
8161
- }
8162
- function resolveOpensteerAuthScheme(env) {
8163
- return parseAuthScheme(env.OPENSTEER_AUTH_SCHEME, "OPENSTEER_AUTH_SCHEME");
8164
- }
8165
- function normalizeCloudOptions(value) {
8166
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8167
- return void 0;
8168
- }
8169
- return value;
8170
- }
8171
- function parseCloudEnabled(value, source) {
8172
- if (value == null) return void 0;
8173
- if (typeof value === "boolean") return value;
8174
- if (typeof value === "object" && !Array.isArray(value)) return true;
8175
- throw new Error(
8176
- `Invalid ${source} value "${String(value)}". Use true, false, or a cloud options object.`
8177
- );
8178
- }
8179
- function resolveCloudSelection(config, env = process.env) {
8180
- const configCloud = parseCloudEnabled(config.cloud, "cloud");
8181
- if (configCloud !== void 0) {
8182
- return {
8183
- cloud: configCloud,
8184
- source: "config.cloud"
8185
- };
8186
- }
8187
- const envMode = parseRuntimeMode(env.OPENSTEER_MODE, "OPENSTEER_MODE");
8188
- if (envMode) {
8189
- return {
8190
- cloud: envMode === "cloud",
8191
- source: "env.OPENSTEER_MODE"
8192
- };
8193
- }
8194
- return {
8195
- cloud: false,
8196
- source: "default"
8197
- };
8198
- }
8199
- function resolveConfigWithEnv(input = {}) {
8200
- const processEnv = process.env;
8201
- const debugHint = typeof input.debug === "boolean" ? input.debug : parseBool(processEnv.OPENSTEER_DEBUG) === true;
8202
- const initialRootDir = input.storage?.rootDir ?? process.cwd();
8203
- const runtimeDefaults = mergeDeep(DEFAULT_CONFIG, {
8204
- storage: {
8205
- rootDir: initialRootDir
8206
- }
8207
- });
8208
- assertNoLegacyAiConfig("Opensteer constructor config", input);
8209
- assertNoLegacyRuntimeConfig("Opensteer constructor config", input);
8210
- const fileConfig = loadConfigFile(initialRootDir, {
8211
- debug: debugHint
8212
- });
8213
- assertNoLegacyAiConfig(".opensteer/config.json", fileConfig);
8214
- assertNoLegacyRuntimeConfig(".opensteer/config.json", fileConfig);
8215
- const fileRootDir = typeof fileConfig.storage?.rootDir === "string" ? fileConfig.storage.rootDir : void 0;
8216
- const envRootDir = input.storage?.rootDir ?? fileRootDir ?? initialRootDir;
8217
- const env = resolveEnv(envRootDir, {
8218
- debug: debugHint
8219
- });
8220
- if (env.OPENSTEER_AI_MODEL) {
8221
- throw new Error(
8222
- "OPENSTEER_AI_MODEL is no longer supported. Use OPENSTEER_MODEL instead."
8223
- );
8425
+ this.persistentContext = null;
8426
+ this.cdpProxy?.close();
8427
+ this.cdpProxy = null;
8428
+ throw error;
8429
+ }
8224
8430
  }
8225
- if (env.OPENSTEER_RUNTIME != null) {
8226
- throw new Error(
8227
- "OPENSTEER_RUNTIME is no longer supported. Use OPENSTEER_MODE instead."
8431
+ async launchPersistentProfile(options, channel, profileDir) {
8432
+ const args = [];
8433
+ const launchProfile = resolvePersistentChromiumLaunchProfile(profileDir);
8434
+ if (launchProfile.profileDirectory) {
8435
+ args.push(`--profile-directory=${launchProfile.profileDirectory}`);
8436
+ }
8437
+ const context = await chromium2.launchPersistentContext(
8438
+ launchProfile.userDataDir,
8439
+ {
8440
+ channel,
8441
+ headless: options.headless ?? this.defaults.headless,
8442
+ executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
8443
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
8444
+ timeout: options.timeout,
8445
+ ...options.context || {},
8446
+ args
8447
+ }
8228
8448
  );
8449
+ const browser = context.browser();
8450
+ if (!browser) {
8451
+ await context.close().catch(() => void 0);
8452
+ throw new Error("Persistent browser launch did not expose a browser instance.");
8453
+ }
8454
+ this.browser = browser;
8455
+ this.persistentContext = context;
8456
+ const pages = context.pages();
8457
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
8458
+ return { browser, context, page, isExternal: false };
8229
8459
  }
8230
- const envConfig = {
8231
- browser: {
8232
- headless: parseBool(env.OPENSTEER_HEADLESS),
8233
- executablePath: env.OPENSTEER_BROWSER_PATH || void 0,
8234
- slowMo: parseNumber(env.OPENSTEER_SLOW_MO),
8235
- connectUrl: env.OPENSTEER_CONNECT_URL || void 0,
8236
- channel: env.OPENSTEER_CHANNEL || void 0,
8237
- profileDir: env.OPENSTEER_PROFILE_DIR || void 0
8238
- },
8239
- model: env.OPENSTEER_MODEL || void 0,
8240
- debug: parseBool(env.OPENSTEER_DEBUG)
8241
- };
8242
- const mergedWithFile = mergeDeep(runtimeDefaults, fileConfig);
8243
- const mergedWithEnv = mergeDeep(mergedWithFile, envConfig);
8244
- const resolved = mergeDeep(mergedWithEnv, input);
8245
- const envApiKey = resolveOpensteerApiKey(env);
8246
- const envBaseUrl = resolveOpensteerBaseUrl(env);
8247
- const envAuthScheme = resolveOpensteerAuthScheme(env);
8248
- const envCloudAnnounce = parseCloudAnnounce(
8249
- env.OPENSTEER_REMOTE_ANNOUNCE,
8250
- "OPENSTEER_REMOTE_ANNOUNCE"
8251
- );
8252
- const inputCloudOptions = normalizeCloudOptions(input.cloud);
8253
- const inputAuthScheme = parseAuthScheme(
8254
- inputCloudOptions?.authScheme,
8255
- "cloud.authScheme"
8256
- );
8257
- const inputCloudAnnounce = parseCloudAnnounce(
8258
- inputCloudOptions?.announce,
8259
- "cloud.announce"
8260
- );
8261
- const inputHasCloudApiKey = Boolean(
8262
- inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "apiKey")
8263
- );
8264
- const inputHasCloudBaseUrl = Boolean(
8265
- inputCloudOptions && Object.prototype.hasOwnProperty.call(inputCloudOptions, "baseUrl")
8266
- );
8267
- const cloudSelection = resolveCloudSelection({
8268
- cloud: resolved.cloud
8269
- }, env);
8270
- if (cloudSelection.cloud) {
8271
- const resolvedCloud = normalizeCloudOptions(resolved.cloud) ?? {};
8272
- const authScheme = inputAuthScheme ?? envAuthScheme ?? parseAuthScheme(resolvedCloud.authScheme, "cloud.authScheme") ?? "api-key";
8273
- const announce = inputCloudAnnounce ?? envCloudAnnounce ?? parseCloudAnnounce(resolvedCloud.announce, "cloud.announce") ?? "always";
8274
- resolved.cloud = {
8275
- ...resolvedCloud,
8276
- authScheme,
8277
- announce
8278
- };
8279
- }
8280
- if (envApiKey && cloudSelection.cloud && !inputHasCloudApiKey) {
8281
- resolved.cloud = {
8282
- ...normalizeCloudOptions(resolved.cloud) ?? {},
8283
- apiKey: envApiKey
8284
- };
8285
- }
8286
- if (envBaseUrl && cloudSelection.cloud && !inputHasCloudBaseUrl) {
8287
- resolved.cloud = {
8288
- ...normalizeCloudOptions(resolved.cloud) ?? {},
8289
- baseUrl: envBaseUrl
8290
- };
8460
+ async launchWithChannel(options, channel) {
8461
+ const browser = await chromium2.launch({
8462
+ channel,
8463
+ headless: options.headless ?? this.defaults.headless,
8464
+ executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
8465
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
8466
+ timeout: options.timeout
8467
+ });
8468
+ this.browser = browser;
8469
+ this.persistentContext = null;
8470
+ const context = await browser.newContext(options.context || {});
8471
+ const page = await context.newPage();
8472
+ return { browser, context, page, isExternal: false };
8291
8473
  }
8292
- return {
8293
- config: resolved,
8294
- env
8295
- };
8296
- }
8297
- function resolveNamespace(config, rootDir) {
8298
- if (config.name && config.name.trim()) {
8299
- return normalizeNamespace(config.name);
8300
- }
8301
- const caller = getCallerFilePath();
8302
- if (!caller) return normalizeNamespace("default");
8303
- const relative = path4.relative(rootDir, caller);
8304
- const cleaned = relative.replace(/\\/g, "/").replace(/\.(ts|tsx|js|mjs|cjs)$/, "");
8305
- return normalizeNamespace(cleaned || "default");
8306
- }
8307
- function getCallerFilePath() {
8308
- const stack = new Error().stack;
8309
- if (!stack) return null;
8310
- const lines = stack.split("\n").slice(2);
8311
- for (const line of lines) {
8312
- const match = line.match(/\((.*):(\d+):(\d+)\)/) || line.match(/at\s+(.*):(\d+):(\d+)/);
8313
- if (!match) continue;
8314
- const rawPath = match[1];
8315
- if (!rawPath) continue;
8316
- if (rawPath.includes("node:internal")) continue;
8317
- if (rawPath.includes("node_modules")) continue;
8318
- if (rawPath.includes("/opensteer-oss/src/")) continue;
8319
- try {
8320
- if (rawPath.startsWith("file://")) {
8321
- return fileURLToPath(rawPath);
8322
- }
8323
- return rawPath;
8324
- } catch {
8325
- continue;
8326
- }
8474
+ async launchSandbox(options) {
8475
+ const browser = await chromium2.launch({
8476
+ headless: options.headless ?? this.defaults.headless,
8477
+ executablePath: options.executablePath ?? this.defaults.executablePath ?? void 0,
8478
+ slowMo: options.slowMo ?? this.defaults.slowMo ?? 0,
8479
+ timeout: options.timeout
8480
+ });
8481
+ const context = await browser.newContext(options.context || {});
8482
+ const page = await context.newPage();
8483
+ this.browser = browser;
8484
+ this.persistentContext = null;
8485
+ return { browser, context, page, isExternal: false };
8327
8486
  }
8328
- return null;
8329
- }
8487
+ };
8330
8488
 
8331
8489
  // src/action-wait.ts
8332
8490
  var ROBUST_PROFILE = {
@@ -8521,7 +8679,7 @@ var AdaptiveNetworkTracker = class {
8521
8679
  this.idleSince = 0;
8522
8680
  }
8523
8681
  const remaining = Math.max(1, options.deadline - now);
8524
- await sleep4(Math.min(NETWORK_POLL_MS, remaining));
8682
+ await sleep5(Math.min(NETWORK_POLL_MS, remaining));
8525
8683
  }
8526
8684
  }
8527
8685
  handleRequestStarted = (request) => {
@@ -8566,7 +8724,7 @@ var AdaptiveNetworkTracker = class {
8566
8724
  return false;
8567
8725
  }
8568
8726
  };
8569
- async function sleep4(ms) {
8727
+ async function sleep5(ms) {
8570
8728
  await new Promise((resolve) => {
8571
8729
  setTimeout(resolve, ms);
8572
8730
  });
@@ -8590,8 +8748,8 @@ function stableStringify(value) {
8590
8748
  }
8591
8749
 
8592
8750
  // src/extraction/data-path.ts
8593
- function parseDataPath(path5) {
8594
- const input = String(path5 || "").trim();
8751
+ function parseDataPath(path3) {
8752
+ const input = String(path3 || "").trim();
8595
8753
  if (!input) return [];
8596
8754
  if (input.includes("..")) return null;
8597
8755
  if (input.startsWith(".") || input.endsWith(".")) return null;
@@ -8647,8 +8805,8 @@ function joinDataPath(base, key) {
8647
8805
  function inflateDataPathObject(flat) {
8648
8806
  let root = {};
8649
8807
  let initialized = false;
8650
- for (const [path5, value] of Object.entries(flat || {})) {
8651
- const tokens = parseDataPath(path5);
8808
+ for (const [path3, value] of Object.entries(flat || {})) {
8809
+ const tokens = parseDataPath(path3);
8652
8810
  if (!tokens || !tokens.length) continue;
8653
8811
  if (!initialized) {
8654
8812
  root = tokens[0]?.kind === "index" ? [] : {};
@@ -9023,8 +9181,8 @@ function buildVariantDescriptorFromCluster(descriptors) {
9023
9181
  fields: mergedFields
9024
9182
  };
9025
9183
  }
9026
- function minimizePathMatchClauses(path5, mode) {
9027
- const normalized = sanitizeElementPath(path5);
9184
+ function minimizePathMatchClauses(path3, mode) {
9185
+ const normalized = sanitizeElementPath(path3);
9028
9186
  const nodes = normalized.nodes.map((node, index) => {
9029
9187
  const isLast = index === normalized.nodes.length - 1;
9030
9188
  const attrs = node.attrs || {};
@@ -9127,8 +9285,8 @@ function seedMinimalAttrClause(attrs) {
9127
9285
  }
9128
9286
  return null;
9129
9287
  }
9130
- function relaxPathForSingleSample(path5, mode) {
9131
- const normalized = sanitizeElementPath(path5);
9288
+ function relaxPathForSingleSample(path3, mode) {
9289
+ const normalized = sanitizeElementPath(path3);
9132
9290
  const relaxedNodes = normalized.nodes.map((node, index) => {
9133
9291
  const isLast = index === normalized.nodes.length - 1;
9134
9292
  const attrs = normalizeAttrsForSingleSample(node.attrs || {});
@@ -9198,8 +9356,8 @@ function shouldKeepAttrForSingleSample(key) {
9198
9356
  }
9199
9357
  return true;
9200
9358
  }
9201
- function buildPathStructureKey(path5) {
9202
- const normalized = sanitizeElementPath(path5);
9359
+ function buildPathStructureKey(path3) {
9360
+ const normalized = sanitizeElementPath(path3);
9203
9361
  return stableStringify({
9204
9362
  context: (normalized.context || []).map((hop) => ({
9205
9363
  kind: hop.kind,
@@ -9313,16 +9471,16 @@ function buildArrayItemNode(fields) {
9313
9471
  }
9314
9472
  return node;
9315
9473
  }
9316
- function insertNodeAtPath(root, path5, node) {
9317
- const tokens = parseDataPath(path5);
9474
+ function insertNodeAtPath(root, path3, node) {
9475
+ const tokens = parseDataPath(path3);
9318
9476
  if (!tokens || !tokens.length) {
9319
9477
  throw new Error(
9320
- `Invalid persisted extraction path "${path5}": expected a non-empty object path.`
9478
+ `Invalid persisted extraction path "${path3}": expected a non-empty object path.`
9321
9479
  );
9322
9480
  }
9323
9481
  if (tokens.some((token) => token.kind === "index")) {
9324
9482
  throw new Error(
9325
- `Invalid persisted extraction path "${path5}": nested array indices are not supported in cached descriptors.`
9483
+ `Invalid persisted extraction path "${path3}": nested array indices are not supported in cached descriptors.`
9326
9484
  );
9327
9485
  }
9328
9486
  let current = root;
@@ -9330,7 +9488,7 @@ function insertNodeAtPath(root, path5, node) {
9330
9488
  const token = tokens[i];
9331
9489
  if (token.kind !== "prop") {
9332
9490
  throw new Error(
9333
- `Invalid persisted extraction path "${path5}": expected object segment.`
9491
+ `Invalid persisted extraction path "${path3}": expected object segment.`
9334
9492
  );
9335
9493
  }
9336
9494
  const isLast = i === tokens.length - 1;
@@ -9338,7 +9496,7 @@ function insertNodeAtPath(root, path5, node) {
9338
9496
  const existing = current[token.key];
9339
9497
  if (existing) {
9340
9498
  throw new Error(
9341
- `Conflicting persisted extraction path "${path5}" detected while building descriptor tree.`
9499
+ `Conflicting persisted extraction path "${path3}" detected while building descriptor tree.`
9342
9500
  );
9343
9501
  }
9344
9502
  current[token.key] = node;
@@ -9353,7 +9511,7 @@ function insertNodeAtPath(root, path5, node) {
9353
9511
  }
9354
9512
  if (!isPersistedObjectNode(next)) {
9355
9513
  throw new Error(
9356
- `Conflicting persisted extraction path "${path5}" detected at "${token.key}".`
9514
+ `Conflicting persisted extraction path "${path3}" detected at "${token.key}".`
9357
9515
  );
9358
9516
  }
9359
9517
  current = next;
@@ -9379,7 +9537,7 @@ function buildItemRootForArrayIndex(entries) {
9379
9537
  if (!entries.length) return null;
9380
9538
  const paths = entries.map(
9381
9539
  (entry) => isPersistablePathField(entry.source) ? sanitizeElementPath(entry.source.path) : null
9382
- ).filter((path5) => !!path5);
9540
+ ).filter((path3) => !!path3);
9383
9541
  if (!paths.length) return null;
9384
9542
  const prefixLength = getCommonPathPrefixLength(paths);
9385
9543
  if (prefixLength <= 0) return null;
@@ -9392,7 +9550,7 @@ function buildItemRootForArrayIndex(entries) {
9392
9550
  }
9393
9551
  function getCommonPathPrefixLength(paths) {
9394
9552
  if (!paths.length) return 0;
9395
- const nodeChains = paths.map((path5) => path5.nodes);
9553
+ const nodeChains = paths.map((path3) => path3.nodes);
9396
9554
  const minLength = Math.min(...nodeChains.map((nodes) => nodes.length));
9397
9555
  if (!Number.isFinite(minLength) || minLength <= 0) return 0;
9398
9556
  for (let i = 0; i < minLength; i++) {
@@ -9449,28 +9607,28 @@ function toRelativeElementPath(absolute, root) {
9449
9607
  }
9450
9608
  function mergeElementPathsByMajority(paths) {
9451
9609
  if (!paths.length) return null;
9452
- const normalized = paths.map((path5) => sanitizeElementPath(path5));
9610
+ const normalized = paths.map((path3) => sanitizeElementPath(path3));
9453
9611
  const contextKey = pickModeString(
9454
- normalized.map((path5) => stableStringify(path5.context)),
9612
+ normalized.map((path3) => stableStringify(path3.context)),
9455
9613
  1
9456
9614
  );
9457
9615
  if (!contextKey) return null;
9458
9616
  const sameContext = normalized.filter(
9459
- (path5) => stableStringify(path5.context) === contextKey
9617
+ (path3) => stableStringify(path3.context) === contextKey
9460
9618
  );
9461
9619
  if (!sameContext.length) return null;
9462
9620
  const targetLength = pickModeNumber(
9463
- sameContext.map((path5) => path5.nodes.length),
9621
+ sameContext.map((path3) => path3.nodes.length),
9464
9622
  1
9465
9623
  ) ?? sameContext[0]?.nodes.length ?? 0;
9466
9624
  const aligned = sameContext.filter(
9467
- (path5) => path5.nodes.length === targetLength
9625
+ (path3) => path3.nodes.length === targetLength
9468
9626
  );
9469
9627
  if (!aligned.length) return null;
9470
9628
  const threshold = majorityThreshold(aligned.length);
9471
9629
  const nodes = [];
9472
9630
  for (let i = 0; i < targetLength; i++) {
9473
- const nodesAtIndex = aligned.map((path5) => path5.nodes[i]).filter((node) => !!node);
9631
+ const nodesAtIndex = aligned.map((path3) => path3.nodes[i]).filter((node) => !!node);
9474
9632
  if (!nodesAtIndex.length) return null;
9475
9633
  nodes.push(mergePathNodeByMajority(nodesAtIndex, threshold));
9476
9634
  }
@@ -9720,8 +9878,8 @@ function collectValueNodes(node) {
9720
9878
  return [
9721
9879
  {
9722
9880
  path: node.$path,
9723
- replacePath(path5) {
9724
- node.$path = path5;
9881
+ replacePath(path3) {
9882
+ node.$path = path3;
9725
9883
  }
9726
9884
  }
9727
9885
  ];
@@ -9733,10 +9891,10 @@ function collectValueNodes(node) {
9733
9891
  if (isPersistedValueNode(child)) {
9734
9892
  refs.push({
9735
9893
  path: child.$path,
9736
- replacePath(path5) {
9894
+ replacePath(path3) {
9737
9895
  const next = current[key];
9738
9896
  if (!isPersistedValueNode(next)) return;
9739
- next.$path = path5;
9897
+ next.$path = path3;
9740
9898
  }
9741
9899
  });
9742
9900
  continue;
@@ -9749,8 +9907,8 @@ function collectValueNodes(node) {
9749
9907
  visit(node);
9750
9908
  return refs;
9751
9909
  }
9752
- function hasPositionClause(path5) {
9753
- return path5.nodes.some(
9910
+ function hasPositionClause(path3) {
9911
+ return path3.nodes.some(
9754
9912
  (node) => (node.match || []).some((clause) => clause.kind === "position")
9755
9913
  );
9756
9914
  }
@@ -9866,49 +10024,16 @@ async function validateRowSelectors(item, fields) {
9866
10024
  return out;
9867
10025
  }, fields);
9868
10026
  }
9869
- function stripPositionClauses2(path5) {
10027
+ function stripPositionClauses2(path3) {
9870
10028
  return {
9871
- context: path5.context,
9872
- nodes: path5.nodes.map((node) => ({
10029
+ context: path3.context,
10030
+ nodes: path3.nodes.map((node) => ({
9873
10031
  ...node,
9874
10032
  match: (node.match || []).filter((clause) => clause.kind !== "position")
9875
10033
  }))
9876
10034
  };
9877
10035
  }
9878
10036
 
9879
- // src/cloud/runtime.ts
9880
- var DEFAULT_CLOUD_BASE_URL = "https://api.opensteer.com";
9881
- function createCloudRuntimeState(key, baseUrl = resolveCloudBaseUrl(), authScheme = "api-key") {
9882
- const normalizedBaseUrl = normalizeCloudBaseUrl(baseUrl);
9883
- return {
9884
- sessionClient: new CloudSessionClient(
9885
- normalizedBaseUrl,
9886
- key,
9887
- authScheme
9888
- ),
9889
- cdpClient: new CloudCdpClient(),
9890
- baseUrl: normalizedBaseUrl,
9891
- actionClient: null,
9892
- sessionId: null,
9893
- localRunId: null,
9894
- cloudSessionUrl: null
9895
- };
9896
- }
9897
- function resolveCloudBaseUrl() {
9898
- const value = process.env.OPENSTEER_BASE_URL?.trim();
9899
- if (!value) return DEFAULT_CLOUD_BASE_URL;
9900
- return normalizeCloudBaseUrl(value);
9901
- }
9902
- function normalizeCloudBaseUrl(value) {
9903
- return value.replace(/\/+$/, "");
9904
- }
9905
- function readCloudActionDescription(payload) {
9906
- const description = payload.description;
9907
- if (typeof description !== "string") return void 0;
9908
- const normalized = description.trim();
9909
- return normalized.length ? normalized : void 0;
9910
- }
9911
-
9912
10037
  // src/opensteer.ts
9913
10038
  var CLOUD_INTERACTION_METHODS = /* @__PURE__ */ new Set([
9914
10039
  "click",
@@ -9935,6 +10060,7 @@ var Opensteer = class _Opensteer {
9935
10060
  ownsBrowser = false;
9936
10061
  snapshotCache = null;
9937
10062
  agentExecutionInFlight = false;
10063
+ cursorController = null;
9938
10064
  constructor(config = {}) {
9939
10065
  const resolvedRuntime = resolveConfigWithEnv(config);
9940
10066
  const resolved = resolvedRuntime.config;
@@ -9956,15 +10082,29 @@ var Opensteer = class _Opensteer {
9956
10082
  if (cloudSelection.cloud) {
9957
10083
  const cloudConfig = resolved.cloud && typeof resolved.cloud === "object" ? resolved.cloud : void 0;
9958
10084
  const apiKey = cloudConfig?.apiKey?.trim();
9959
- if (!apiKey) {
10085
+ const accessToken = cloudConfig?.accessToken?.trim();
10086
+ if (apiKey && accessToken) {
10087
+ throw new Error(
10088
+ "Cloud mode cannot use both cloud.apiKey and cloud.accessToken. Set only one credential."
10089
+ );
10090
+ }
10091
+ let credential = "";
10092
+ let authScheme = cloudConfig?.authScheme ?? "api-key";
10093
+ if (accessToken) {
10094
+ credential = accessToken;
10095
+ authScheme = "bearer";
10096
+ } else if (apiKey) {
10097
+ credential = apiKey;
10098
+ }
10099
+ if (!credential) {
9960
10100
  throw new Error(
9961
- "Cloud mode requires a non-empty API key via cloud.apiKey or OPENSTEER_API_KEY."
10101
+ "Cloud mode requires credentials via cloud.apiKey/cloud.accessToken or OPENSTEER_API_KEY/OPENSTEER_ACCESS_TOKEN."
9962
10102
  );
9963
10103
  }
9964
10104
  this.cloud = createCloudRuntimeState(
9965
- apiKey,
10105
+ credential,
9966
10106
  cloudConfig?.baseUrl,
9967
- cloudConfig?.authScheme
10107
+ authScheme
9968
10108
  );
9969
10109
  } else {
9970
10110
  this.cloud = null;
@@ -10189,6 +10329,19 @@ var Opensteer = class _Opensteer {
10189
10329
  }
10190
10330
  return true;
10191
10331
  }
10332
+ buildCloudSessionLaunchConfig(options) {
10333
+ const cloudConfig = this.config.cloud && typeof this.config.cloud === "object" ? this.config.cloud : void 0;
10334
+ const browserProfile = normalizeCloudBrowserProfilePreference(
10335
+ options.cloudBrowserProfile ?? cloudConfig?.browserProfile,
10336
+ options.cloudBrowserProfile ? "launch options" : "Opensteer config"
10337
+ );
10338
+ if (!browserProfile) {
10339
+ return void 0;
10340
+ }
10341
+ return {
10342
+ browserProfile
10343
+ };
10344
+ }
10192
10345
  async launch(options = {}) {
10193
10346
  if (this.pageRef && !this.ownsBrowser) {
10194
10347
  throw new Error(
@@ -10211,6 +10364,7 @@ var Opensteer = class _Opensteer {
10211
10364
  }
10212
10365
  localRunId = this.cloud.localRunId || buildLocalRunId(this.namespace);
10213
10366
  this.cloud.localRunId = localRunId;
10367
+ const launchConfig = this.buildCloudSessionLaunchConfig(options);
10214
10368
  const session2 = await this.cloud.sessionClient.create({
10215
10369
  cloudSessionContractVersion,
10216
10370
  sourceType: "local-cloud",
@@ -10218,7 +10372,8 @@ var Opensteer = class _Opensteer {
10218
10372
  localRunId,
10219
10373
  name: this.namespace,
10220
10374
  model: this.config.model,
10221
- launchContext: options.context || void 0
10375
+ launchContext: options.context || void 0,
10376
+ launchConfig
10222
10377
  });
10223
10378
  sessionId = session2.sessionId;
10224
10379
  actionClient = await ActionWsClient.connect({
@@ -10236,6 +10391,9 @@ var Opensteer = class _Opensteer {
10236
10391
  this.pageRef = cdpConnection.page;
10237
10392
  this.ownsBrowser = true;
10238
10393
  this.snapshotCache = null;
10394
+ if (this.cursorController) {
10395
+ await this.cursorController.attachPage(this.pageRef);
10396
+ }
10239
10397
  this.cloud.actionClient = actionClient;
10240
10398
  this.cloud.sessionId = sessionId;
10241
10399
  this.cloud.cloudSessionUrl = session2.cloudSessionUrl;
@@ -10276,6 +10434,9 @@ var Opensteer = class _Opensteer {
10276
10434
  this.pageRef = session.page;
10277
10435
  this.ownsBrowser = true;
10278
10436
  this.snapshotCache = null;
10437
+ if (this.cursorController) {
10438
+ await this.cursorController.attachPage(this.pageRef);
10439
+ }
10279
10440
  }
10280
10441
  static from(page, config = {}) {
10281
10442
  const resolvedRuntime = resolveConfigWithEnv(config);
@@ -10320,6 +10481,9 @@ var Opensteer = class _Opensteer {
10320
10481
  if (sessionId) {
10321
10482
  await this.cloud.sessionClient.close(sessionId).catch(() => void 0);
10322
10483
  }
10484
+ if (this.cursorController) {
10485
+ await this.cursorController.dispose().catch(() => void 0);
10486
+ }
10323
10487
  return;
10324
10488
  }
10325
10489
  if (this.ownsBrowser) {
@@ -10329,6 +10493,9 @@ var Opensteer = class _Opensteer {
10329
10493
  this.pageRef = null;
10330
10494
  this.contextRef = null;
10331
10495
  this.ownsBrowser = false;
10496
+ if (this.cursorController) {
10497
+ await this.cursorController.dispose().catch(() => void 0);
10498
+ }
10332
10499
  }
10333
10500
  async syncLocalSelectorCacheToCloud() {
10334
10501
  if (!this.cloud) return;
@@ -10455,12 +10622,22 @@ var Opensteer = class _Opensteer {
10455
10622
  resolution.counter
10456
10623
  );
10457
10624
  }
10458
- await this.runWithPostActionWait("hover", options.wait, async () => {
10459
- await handle.hover({
10460
- force: options.force,
10461
- position: options.position
10462
- });
10463
- });
10625
+ await this.runWithCursorPreview(
10626
+ () => this.resolveHandleTargetPoint(handle, options.position),
10627
+ "hover",
10628
+ async () => {
10629
+ await this.runWithPostActionWait(
10630
+ "hover",
10631
+ options.wait,
10632
+ async () => {
10633
+ await handle.hover({
10634
+ force: options.force,
10635
+ position: options.position
10636
+ });
10637
+ }
10638
+ );
10639
+ }
10640
+ );
10464
10641
  } catch (err) {
10465
10642
  const failure = classifyActionFailure({
10466
10643
  action: "hover",
@@ -10494,26 +10671,32 @@ var Opensteer = class _Opensteer {
10494
10671
  if (!resolution.path) {
10495
10672
  throw new Error("Unable to resolve element path for hover action.");
10496
10673
  }
10497
- const path5 = resolution.path;
10498
- const result = await this.runWithPostActionWait(
10674
+ const path3 = resolution.path;
10675
+ const result = await this.runWithCursorPreview(
10676
+ () => this.resolvePathTargetPoint(path3, options.position),
10499
10677
  "hover",
10500
- options.wait,
10501
10678
  async () => {
10502
- const actionResult = await performHover(this.page, path5, options);
10503
- if (!actionResult.ok) {
10504
- const failure = actionResult.failure || classifyActionFailure({
10505
- action: "hover",
10506
- error: actionResult.error || defaultActionFailureMessage("hover"),
10507
- fallbackMessage: defaultActionFailureMessage("hover")
10508
- });
10509
- throw this.buildActionError(
10510
- "hover",
10511
- options.description,
10512
- failure,
10513
- actionResult.usedSelector || null
10514
- );
10515
- }
10516
- return actionResult;
10679
+ return await this.runWithPostActionWait(
10680
+ "hover",
10681
+ options.wait,
10682
+ async () => {
10683
+ const actionResult = await performHover(this.page, path3, options);
10684
+ if (!actionResult.ok) {
10685
+ const failure = actionResult.failure || classifyActionFailure({
10686
+ action: "hover",
10687
+ error: actionResult.error || defaultActionFailureMessage("hover"),
10688
+ fallbackMessage: defaultActionFailureMessage("hover")
10689
+ });
10690
+ throw this.buildActionError(
10691
+ "hover",
10692
+ options.description,
10693
+ failure,
10694
+ actionResult.usedSelector || null
10695
+ );
10696
+ }
10697
+ return actionResult;
10698
+ }
10699
+ );
10517
10700
  }
10518
10701
  );
10519
10702
  this.snapshotCache = null;
@@ -10554,16 +10737,26 @@ var Opensteer = class _Opensteer {
10554
10737
  resolution.counter
10555
10738
  );
10556
10739
  }
10557
- await this.runWithPostActionWait("input", options.wait, async () => {
10558
- if (options.clear !== false) {
10559
- await handle.fill(options.text);
10560
- } else {
10561
- await handle.type(options.text);
10562
- }
10563
- if (options.pressEnter) {
10564
- await handle.press("Enter", { noWaitAfter: true });
10740
+ await this.runWithCursorPreview(
10741
+ () => this.resolveHandleTargetPoint(handle),
10742
+ "input",
10743
+ async () => {
10744
+ await this.runWithPostActionWait(
10745
+ "input",
10746
+ options.wait,
10747
+ async () => {
10748
+ if (options.clear !== false) {
10749
+ await handle.fill(options.text);
10750
+ } else {
10751
+ await handle.type(options.text);
10752
+ }
10753
+ if (options.pressEnter) {
10754
+ await handle.press("Enter", { noWaitAfter: true });
10755
+ }
10756
+ }
10757
+ );
10565
10758
  }
10566
- });
10759
+ );
10567
10760
  } catch (err) {
10568
10761
  const failure = classifyActionFailure({
10569
10762
  action: "input",
@@ -10597,26 +10790,32 @@ var Opensteer = class _Opensteer {
10597
10790
  if (!resolution.path) {
10598
10791
  throw new Error("Unable to resolve element path for input action.");
10599
10792
  }
10600
- const path5 = resolution.path;
10601
- const result = await this.runWithPostActionWait(
10793
+ const path3 = resolution.path;
10794
+ const result = await this.runWithCursorPreview(
10795
+ () => this.resolvePathTargetPoint(path3),
10602
10796
  "input",
10603
- options.wait,
10604
10797
  async () => {
10605
- const actionResult = await performInput(this.page, path5, options);
10606
- if (!actionResult.ok) {
10607
- const failure = actionResult.failure || classifyActionFailure({
10608
- action: "input",
10609
- error: actionResult.error || defaultActionFailureMessage("input"),
10610
- fallbackMessage: defaultActionFailureMessage("input")
10611
- });
10612
- throw this.buildActionError(
10613
- "input",
10614
- options.description,
10615
- failure,
10616
- actionResult.usedSelector || null
10617
- );
10618
- }
10619
- return actionResult;
10798
+ return await this.runWithPostActionWait(
10799
+ "input",
10800
+ options.wait,
10801
+ async () => {
10802
+ const actionResult = await performInput(this.page, path3, options);
10803
+ if (!actionResult.ok) {
10804
+ const failure = actionResult.failure || classifyActionFailure({
10805
+ action: "input",
10806
+ error: actionResult.error || defaultActionFailureMessage("input"),
10807
+ fallbackMessage: defaultActionFailureMessage("input")
10808
+ });
10809
+ throw this.buildActionError(
10810
+ "input",
10811
+ options.description,
10812
+ failure,
10813
+ actionResult.usedSelector || null
10814
+ );
10815
+ }
10816
+ return actionResult;
10817
+ }
10818
+ );
10620
10819
  }
10621
10820
  );
10622
10821
  this.snapshotCache = null;
@@ -10657,21 +10856,27 @@ var Opensteer = class _Opensteer {
10657
10856
  resolution.counter
10658
10857
  );
10659
10858
  }
10660
- await this.runWithPostActionWait(
10859
+ await this.runWithCursorPreview(
10860
+ () => this.resolveHandleTargetPoint(handle),
10661
10861
  "select",
10662
- options.wait,
10663
10862
  async () => {
10664
- if (options.value != null) {
10665
- await handle.selectOption(options.value);
10666
- } else if (options.label != null) {
10667
- await handle.selectOption({ label: options.label });
10668
- } else if (options.index != null) {
10669
- await handle.selectOption({ index: options.index });
10670
- } else {
10671
- throw new Error(
10672
- "Select requires value, label, or index."
10673
- );
10674
- }
10863
+ await this.runWithPostActionWait(
10864
+ "select",
10865
+ options.wait,
10866
+ async () => {
10867
+ if (options.value != null) {
10868
+ await handle.selectOption(options.value);
10869
+ } else if (options.label != null) {
10870
+ await handle.selectOption({ label: options.label });
10871
+ } else if (options.index != null) {
10872
+ await handle.selectOption({ index: options.index });
10873
+ } else {
10874
+ throw new Error(
10875
+ "Select requires value, label, or index."
10876
+ );
10877
+ }
10878
+ }
10879
+ );
10675
10880
  }
10676
10881
  );
10677
10882
  } catch (err) {
@@ -10707,26 +10912,32 @@ var Opensteer = class _Opensteer {
10707
10912
  if (!resolution.path) {
10708
10913
  throw new Error("Unable to resolve element path for select action.");
10709
10914
  }
10710
- const path5 = resolution.path;
10711
- const result = await this.runWithPostActionWait(
10915
+ const path3 = resolution.path;
10916
+ const result = await this.runWithCursorPreview(
10917
+ () => this.resolvePathTargetPoint(path3),
10712
10918
  "select",
10713
- options.wait,
10714
10919
  async () => {
10715
- const actionResult = await performSelect(this.page, path5, options);
10716
- if (!actionResult.ok) {
10717
- const failure = actionResult.failure || classifyActionFailure({
10718
- action: "select",
10719
- error: actionResult.error || defaultActionFailureMessage("select"),
10720
- fallbackMessage: defaultActionFailureMessage("select")
10721
- });
10722
- throw this.buildActionError(
10723
- "select",
10724
- options.description,
10725
- failure,
10726
- actionResult.usedSelector || null
10727
- );
10728
- }
10729
- return actionResult;
10920
+ return await this.runWithPostActionWait(
10921
+ "select",
10922
+ options.wait,
10923
+ async () => {
10924
+ const actionResult = await performSelect(this.page, path3, options);
10925
+ if (!actionResult.ok) {
10926
+ const failure = actionResult.failure || classifyActionFailure({
10927
+ action: "select",
10928
+ error: actionResult.error || defaultActionFailureMessage("select"),
10929
+ fallbackMessage: defaultActionFailureMessage("select")
10930
+ });
10931
+ throw this.buildActionError(
10932
+ "select",
10933
+ options.description,
10934
+ failure,
10935
+ actionResult.usedSelector || null
10936
+ );
10937
+ }
10938
+ return actionResult;
10939
+ }
10940
+ );
10730
10941
  }
10731
10942
  );
10732
10943
  this.snapshotCache = null;
@@ -10768,13 +10979,23 @@ var Opensteer = class _Opensteer {
10768
10979
  );
10769
10980
  }
10770
10981
  const delta = getScrollDelta2(options);
10771
- await this.runWithPostActionWait("scroll", options.wait, async () => {
10772
- await handle.evaluate((el, value) => {
10773
- if (el instanceof HTMLElement) {
10774
- el.scrollBy(value.x, value.y);
10775
- }
10776
- }, delta);
10777
- });
10982
+ await this.runWithCursorPreview(
10983
+ () => this.resolveHandleTargetPoint(handle),
10984
+ "scroll",
10985
+ async () => {
10986
+ await this.runWithPostActionWait(
10987
+ "scroll",
10988
+ options.wait,
10989
+ async () => {
10990
+ await handle.evaluate((el, value) => {
10991
+ if (el instanceof HTMLElement) {
10992
+ el.scrollBy(value.x, value.y);
10993
+ }
10994
+ }, delta);
10995
+ }
10996
+ );
10997
+ }
10998
+ );
10778
10999
  } catch (err) {
10779
11000
  const failure = classifyActionFailure({
10780
11001
  action: "scroll",
@@ -10805,29 +11026,35 @@ var Opensteer = class _Opensteer {
10805
11026
  `[c="${resolution.counter}"]`
10806
11027
  );
10807
11028
  }
10808
- const result = await this.runWithPostActionWait(
11029
+ const result = await this.runWithCursorPreview(
11030
+ () => resolution.path ? this.resolvePathTargetPoint(resolution.path) : this.resolveViewportAnchorPoint(),
10809
11031
  "scroll",
10810
- options.wait,
10811
11032
  async () => {
10812
- const actionResult = await performScroll(
10813
- this.page,
10814
- resolution.path,
10815
- options
11033
+ return await this.runWithPostActionWait(
11034
+ "scroll",
11035
+ options.wait,
11036
+ async () => {
11037
+ const actionResult = await performScroll(
11038
+ this.page,
11039
+ resolution.path,
11040
+ options
11041
+ );
11042
+ if (!actionResult.ok) {
11043
+ const failure = actionResult.failure || classifyActionFailure({
11044
+ action: "scroll",
11045
+ error: actionResult.error || defaultActionFailureMessage("scroll"),
11046
+ fallbackMessage: defaultActionFailureMessage("scroll")
11047
+ });
11048
+ throw this.buildActionError(
11049
+ "scroll",
11050
+ options.description,
11051
+ failure,
11052
+ actionResult.usedSelector || null
11053
+ );
11054
+ }
11055
+ return actionResult;
11056
+ }
10816
11057
  );
10817
- if (!actionResult.ok) {
10818
- const failure = actionResult.failure || classifyActionFailure({
10819
- action: "scroll",
10820
- error: actionResult.error || defaultActionFailureMessage("scroll"),
10821
- fallbackMessage: defaultActionFailureMessage("scroll")
10822
- });
10823
- throw this.buildActionError(
10824
- "scroll",
10825
- options.description,
10826
- failure,
10827
- actionResult.usedSelector || null
10828
- );
10829
- }
10830
- return actionResult;
10831
11058
  }
10832
11059
  );
10833
11060
  this.snapshotCache = null;
@@ -10981,7 +11208,7 @@ var Opensteer = class _Opensteer {
10981
11208
  const text = await handle.textContent();
10982
11209
  return text ?? "";
10983
11210
  },
10984
- (path5) => getElementText(this.page, path5)
11211
+ (path3) => getElementText(this.page, path3)
10985
11212
  );
10986
11213
  }
10987
11214
  async getElementValue(options) {
@@ -10997,7 +11224,7 @@ var Opensteer = class _Opensteer {
10997
11224
  async (handle) => {
10998
11225
  return await handle.inputValue();
10999
11226
  },
11000
- (path5) => getElementValue(this.page, path5)
11227
+ (path3) => getElementValue(this.page, path3)
11001
11228
  );
11002
11229
  }
11003
11230
  async getElementAttributes(options) {
@@ -11019,7 +11246,7 @@ var Opensteer = class _Opensteer {
11019
11246
  return attrs;
11020
11247
  });
11021
11248
  },
11022
- (path5) => getElementAttributes(this.page, path5)
11249
+ (path3) => getElementAttributes(this.page, path3)
11023
11250
  );
11024
11251
  }
11025
11252
  async getElementBoundingBox(options) {
@@ -11035,7 +11262,7 @@ var Opensteer = class _Opensteer {
11035
11262
  async (handle) => {
11036
11263
  return await handle.boundingBox();
11037
11264
  },
11038
- (path5) => getElementBoundingBox(this.page, path5)
11265
+ (path3) => getElementBoundingBox(this.page, path3)
11039
11266
  );
11040
11267
  }
11041
11268
  async getHtml(selector) {
@@ -11113,11 +11340,17 @@ var Opensteer = class _Opensteer {
11113
11340
  resolution.counter
11114
11341
  );
11115
11342
  }
11116
- await this.runWithPostActionWait(
11343
+ await this.runWithCursorPreview(
11344
+ () => this.resolveHandleTargetPoint(handle),
11117
11345
  "uploadFile",
11118
- options.wait,
11119
11346
  async () => {
11120
- await handle.setInputFiles(options.paths);
11347
+ await this.runWithPostActionWait(
11348
+ "uploadFile",
11349
+ options.wait,
11350
+ async () => {
11351
+ await handle.setInputFiles(options.paths);
11352
+ }
11353
+ );
11121
11354
  }
11122
11355
  );
11123
11356
  } catch (err) {
@@ -11153,30 +11386,36 @@ var Opensteer = class _Opensteer {
11153
11386
  if (!resolution.path) {
11154
11387
  throw new Error("Unable to resolve element path for file upload.");
11155
11388
  }
11156
- const path5 = resolution.path;
11157
- const result = await this.runWithPostActionWait(
11389
+ const path3 = resolution.path;
11390
+ const result = await this.runWithCursorPreview(
11391
+ () => this.resolvePathTargetPoint(path3),
11158
11392
  "uploadFile",
11159
- options.wait,
11160
11393
  async () => {
11161
- const actionResult = await performFileUpload(
11162
- this.page,
11163
- path5,
11164
- options.paths
11394
+ return await this.runWithPostActionWait(
11395
+ "uploadFile",
11396
+ options.wait,
11397
+ async () => {
11398
+ const actionResult = await performFileUpload(
11399
+ this.page,
11400
+ path3,
11401
+ options.paths
11402
+ );
11403
+ if (!actionResult.ok) {
11404
+ const failure = actionResult.failure || classifyActionFailure({
11405
+ action: "uploadFile",
11406
+ error: actionResult.error || defaultActionFailureMessage("uploadFile"),
11407
+ fallbackMessage: defaultActionFailureMessage("uploadFile")
11408
+ });
11409
+ throw this.buildActionError(
11410
+ "uploadFile",
11411
+ options.description,
11412
+ failure,
11413
+ actionResult.usedSelector || null
11414
+ );
11415
+ }
11416
+ return actionResult;
11417
+ }
11165
11418
  );
11166
- if (!actionResult.ok) {
11167
- const failure = actionResult.failure || classifyActionFailure({
11168
- action: "uploadFile",
11169
- error: actionResult.error || defaultActionFailureMessage("uploadFile"),
11170
- fallbackMessage: defaultActionFailureMessage("uploadFile")
11171
- });
11172
- throw this.buildActionError(
11173
- "uploadFile",
11174
- options.description,
11175
- failure,
11176
- actionResult.usedSelector || null
11177
- );
11178
- }
11179
- return actionResult;
11180
11419
  }
11181
11420
  );
11182
11421
  this.snapshotCache = null;
@@ -11277,9 +11516,9 @@ var Opensteer = class _Opensteer {
11277
11516
  const schemaHash = computeSchemaHash(options.schema);
11278
11517
  let fields = await this.buildFieldTargetsFromPlan(options.plan);
11279
11518
  if (!fields.length && options.plan.paths) {
11280
- fields = Object.entries(options.plan.paths).map(([key, path5]) => ({
11519
+ fields = Object.entries(options.plan.paths).map(([key, path3]) => ({
11281
11520
  key,
11282
- path: this.normalizePath(path5)
11521
+ path: this.normalizePath(path3)
11283
11522
  }));
11284
11523
  }
11285
11524
  if (!fields.length) {
@@ -11312,6 +11551,25 @@ var Opensteer = class _Opensteer {
11312
11551
  getConfig() {
11313
11552
  return this.config;
11314
11553
  }
11554
+ setCursorEnabled(enabled) {
11555
+ this.getCursorController().setEnabled(enabled);
11556
+ }
11557
+ getCursorState() {
11558
+ const controller = this.cursorController;
11559
+ if (!controller) {
11560
+ return {
11561
+ enabled: this.config.cursor?.enabled === true,
11562
+ active: false,
11563
+ reason: this.config.cursor?.enabled === true ? "not_initialized" : "disabled"
11564
+ };
11565
+ }
11566
+ const status = controller.getStatus();
11567
+ return {
11568
+ enabled: status.enabled,
11569
+ active: status.active,
11570
+ reason: status.reason
11571
+ };
11572
+ }
11315
11573
  getStorage() {
11316
11574
  return this.storage;
11317
11575
  }
@@ -11339,24 +11597,107 @@ var Opensteer = class _Opensteer {
11339
11597
  this.agentExecutionInFlight = true;
11340
11598
  try {
11341
11599
  const options = normalizeExecuteOptions(instructionOrOptions);
11600
+ const cursorController = this.getCursorController();
11601
+ const previousCursorEnabled = cursorController.isEnabled();
11602
+ if (options.highlightCursor !== void 0) {
11603
+ cursorController.setEnabled(options.highlightCursor);
11604
+ }
11342
11605
  const handler = new OpensteerCuaAgentHandler({
11343
11606
  page: this.page,
11344
11607
  config: resolvedAgentConfig,
11345
11608
  client: createCuaClient(resolvedAgentConfig),
11346
- debug: Boolean(this.config.debug),
11609
+ cursorController,
11347
11610
  onMutatingAction: () => {
11348
11611
  this.snapshotCache = null;
11349
11612
  }
11350
11613
  });
11351
- const result = await handler.execute(options);
11352
- this.snapshotCache = null;
11353
- return result;
11614
+ try {
11615
+ const result = await handler.execute(options);
11616
+ this.snapshotCache = null;
11617
+ return result;
11618
+ } finally {
11619
+ if (options.highlightCursor !== void 0) {
11620
+ cursorController.setEnabled(previousCursorEnabled);
11621
+ }
11622
+ }
11354
11623
  } finally {
11355
11624
  this.agentExecutionInFlight = false;
11356
11625
  }
11357
11626
  }
11358
11627
  };
11359
11628
  }
11629
+ getCursorController() {
11630
+ if (!this.cursorController) {
11631
+ this.cursorController = new CursorController({
11632
+ config: this.config.cursor,
11633
+ debug: Boolean(this.config.debug)
11634
+ });
11635
+ if (this.pageRef) {
11636
+ void this.cursorController.attachPage(this.pageRef);
11637
+ }
11638
+ }
11639
+ return this.cursorController;
11640
+ }
11641
+ async runWithCursorPreview(pointResolver, intent, execute) {
11642
+ if (this.isCursorPreviewEnabled()) {
11643
+ const point = await pointResolver();
11644
+ await this.previewCursorPoint(point, intent);
11645
+ }
11646
+ return await execute();
11647
+ }
11648
+ isCursorPreviewEnabled() {
11649
+ return this.cursorController ? this.cursorController.isEnabled() : this.config.cursor?.enabled === true;
11650
+ }
11651
+ async previewCursorPoint(point, intent) {
11652
+ const cursor = this.getCursorController();
11653
+ await cursor.attachPage(this.page);
11654
+ await cursor.preview(point, intent);
11655
+ }
11656
+ resolveCursorPointFromBoundingBox(box, position) {
11657
+ if (position) {
11658
+ return {
11659
+ x: box.x + position.x,
11660
+ y: box.y + position.y
11661
+ };
11662
+ }
11663
+ return {
11664
+ x: box.x + box.width / 2,
11665
+ y: box.y + box.height / 2
11666
+ };
11667
+ }
11668
+ async resolveHandleTargetPoint(handle, position) {
11669
+ try {
11670
+ const box = await handle.boundingBox();
11671
+ if (!box) return null;
11672
+ return this.resolveCursorPointFromBoundingBox(box, position);
11673
+ } catch {
11674
+ return null;
11675
+ }
11676
+ }
11677
+ async resolvePathTargetPoint(path3, position) {
11678
+ if (!path3) {
11679
+ return null;
11680
+ }
11681
+ let resolved = null;
11682
+ try {
11683
+ resolved = await resolveElementPath(this.page, path3);
11684
+ return await this.resolveHandleTargetPoint(resolved.element, position);
11685
+ } catch {
11686
+ return null;
11687
+ } finally {
11688
+ await resolved?.element.dispose().catch(() => void 0);
11689
+ }
11690
+ }
11691
+ async resolveViewportAnchorPoint() {
11692
+ const viewport = this.page.viewportSize();
11693
+ if (viewport?.width && viewport?.height) {
11694
+ return {
11695
+ x: viewport.width / 2,
11696
+ y: viewport.height / 2
11697
+ };
11698
+ }
11699
+ return null;
11700
+ }
11360
11701
  async runWithPostActionWait(action, waitOverride, execute) {
11361
11702
  const waitSession = createPostActionWaitSession(
11362
11703
  this.page,
@@ -11389,13 +11730,19 @@ var Opensteer = class _Opensteer {
11389
11730
  resolution.counter
11390
11731
  );
11391
11732
  }
11392
- await this.runWithPostActionWait(method, options.wait, async () => {
11393
- await handle.click({
11394
- button: options.button,
11395
- clickCount: options.clickCount,
11396
- modifiers: options.modifiers
11397
- });
11398
- });
11733
+ await this.runWithCursorPreview(
11734
+ () => this.resolveHandleTargetPoint(handle),
11735
+ method,
11736
+ async () => {
11737
+ await this.runWithPostActionWait(method, options.wait, async () => {
11738
+ await handle.click({
11739
+ button: options.button,
11740
+ clickCount: options.clickCount,
11741
+ modifiers: options.modifiers
11742
+ });
11743
+ });
11744
+ }
11745
+ );
11399
11746
  } catch (err) {
11400
11747
  const failure = classifyActionFailure({
11401
11748
  action: method,
@@ -11429,26 +11776,32 @@ var Opensteer = class _Opensteer {
11429
11776
  if (!resolution.path) {
11430
11777
  throw new Error("Unable to resolve element path for click action.");
11431
11778
  }
11432
- const path5 = resolution.path;
11433
- const result = await this.runWithPostActionWait(
11779
+ const path3 = resolution.path;
11780
+ const result = await this.runWithCursorPreview(
11781
+ () => this.resolvePathTargetPoint(path3),
11434
11782
  method,
11435
- options.wait,
11436
11783
  async () => {
11437
- const actionResult = await performClick(this.page, path5, options);
11438
- if (!actionResult.ok) {
11439
- const failure = actionResult.failure || classifyActionFailure({
11440
- action: method,
11441
- error: actionResult.error || defaultActionFailureMessage(method),
11442
- fallbackMessage: defaultActionFailureMessage(method)
11443
- });
11444
- throw this.buildActionError(
11445
- method,
11446
- options.description,
11447
- failure,
11448
- actionResult.usedSelector || null
11449
- );
11450
- }
11451
- return actionResult;
11784
+ return await this.runWithPostActionWait(
11785
+ method,
11786
+ options.wait,
11787
+ async () => {
11788
+ const actionResult = await performClick(this.page, path3, options);
11789
+ if (!actionResult.ok) {
11790
+ const failure = actionResult.failure || classifyActionFailure({
11791
+ action: method,
11792
+ error: actionResult.error || defaultActionFailureMessage(method),
11793
+ fallbackMessage: defaultActionFailureMessage(method)
11794
+ });
11795
+ throw this.buildActionError(
11796
+ method,
11797
+ options.description,
11798
+ failure,
11799
+ actionResult.usedSelector || null
11800
+ );
11801
+ }
11802
+ return actionResult;
11803
+ }
11804
+ );
11452
11805
  }
11453
11806
  );
11454
11807
  this.snapshotCache = null;
@@ -11487,14 +11840,14 @@ var Opensteer = class _Opensteer {
11487
11840
  };
11488
11841
  }
11489
11842
  if (options.selector) {
11490
- const path5 = await this.buildPathFromSelector(options.selector);
11491
- if (!path5) {
11843
+ const path3 = await this.buildPathFromSelector(options.selector);
11844
+ if (!path3) {
11492
11845
  throw new Error(
11493
11846
  `Unable to build element path from selector: ${options.selector}`
11494
11847
  );
11495
11848
  }
11496
11849
  return {
11497
- path: path5,
11850
+ path: path3,
11498
11851
  counter: null,
11499
11852
  shouldPersist: Boolean(storageKey),
11500
11853
  source: "selector"
@@ -11554,9 +11907,9 @@ var Opensteer = class _Opensteer {
11554
11907
  counter: parsedCounter
11555
11908
  };
11556
11909
  }
11557
- const path5 = await this.buildPathFromSelector(response);
11558
- return path5 ? {
11559
- path: path5
11910
+ const path3 = await this.buildPathFromSelector(response);
11911
+ return path3 ? {
11912
+ path: path3
11560
11913
  } : null;
11561
11914
  }
11562
11915
  if (!response || typeof response !== "object") {
@@ -11574,9 +11927,9 @@ var Opensteer = class _Opensteer {
11574
11927
  };
11575
11928
  }
11576
11929
  if (record.selector) {
11577
- const path5 = await this.buildPathFromSelector(record.selector);
11578
- return path5 ? {
11579
- path: path5
11930
+ const path3 = await this.buildPathFromSelector(record.selector);
11931
+ return path3 ? {
11932
+ path: path3
11580
11933
  } : null;
11581
11934
  }
11582
11935
  return null;
@@ -11678,10 +12031,10 @@ var Opensteer = class _Opensteer {
11678
12031
  if (fallback.nodes.length) return fallback;
11679
12032
  return normalizedBuilt;
11680
12033
  }
11681
- async withHandleIframeContext(handle, path5) {
12034
+ async withHandleIframeContext(handle, path3) {
11682
12035
  const ownFrame = await handle.ownerFrame();
11683
12036
  if (!ownFrame) {
11684
- return this.normalizePath(path5);
12037
+ return this.normalizePath(path3);
11685
12038
  }
11686
12039
  let frame = ownFrame;
11687
12040
  let prefix = [];
@@ -11708,11 +12061,11 @@ var Opensteer = class _Opensteer {
11708
12061
  frame = parent;
11709
12062
  }
11710
12063
  if (!prefix.length) {
11711
- return this.normalizePath(path5);
12064
+ return this.normalizePath(path3);
11712
12065
  }
11713
12066
  return this.normalizePath({
11714
- context: [...prefix, ...cloneContextHops(path5.context)],
11715
- nodes: cloneElementPath(path5).nodes
12067
+ context: [...prefix, ...cloneContextHops(path3.context)],
12068
+ nodes: cloneElementPath(path3).nodes
11716
12069
  });
11717
12070
  }
11718
12071
  async readPathFromCounterIndex(counter) {
@@ -11726,11 +12079,11 @@ var Opensteer = class _Opensteer {
11726
12079
  return normalized;
11727
12080
  }
11728
12081
  async buildPathFromSelector(selector) {
11729
- const path5 = await buildElementPathFromSelector(this.page, selector);
11730
- if (!path5) return null;
11731
- return this.normalizePath(path5);
12082
+ const path3 = await buildElementPathFromSelector(this.page, selector);
12083
+ if (!path3) return null;
12084
+ return this.normalizePath(path3);
11732
12085
  }
11733
- persistPath(id, method, description, path5) {
12086
+ persistPath(id, method, description, path3) {
11734
12087
  const now = Date.now();
11735
12088
  const safeFile = this.storage.getSelectorFileName(id);
11736
12089
  const existing = this.storage.readSelector(id);
@@ -11739,7 +12092,7 @@ var Opensteer = class _Opensteer {
11739
12092
  id,
11740
12093
  method,
11741
12094
  description: description || `${method} path`,
11742
- path: this.normalizePath(path5),
12095
+ path: this.normalizePath(path3),
11743
12096
  metadata: {
11744
12097
  createdAt,
11745
12098
  updatedAt: now,
@@ -11929,9 +12282,9 @@ var Opensteer = class _Opensteer {
11929
12282
  }
11930
12283
  let fields = await this.buildFieldTargetsFromPlan(normalized);
11931
12284
  if (!fields.length && normalized.paths) {
11932
- fields = Object.entries(normalized.paths).map(([key, path5]) => ({
12285
+ fields = Object.entries(normalized.paths).map(([key, path3]) => ({
11933
12286
  key,
11934
- path: this.normalizePath(path5)
12287
+ path: this.normalizePath(path3)
11935
12288
  }));
11936
12289
  }
11937
12290
  if (!fields.length && normalized.data !== void 0) {
@@ -11995,13 +12348,13 @@ var Opensteer = class _Opensteer {
11995
12348
  return;
11996
12349
  }
11997
12350
  if (normalized.selector) {
11998
- const path5 = await this.buildPathFromSelector(
12351
+ const path3 = await this.buildPathFromSelector(
11999
12352
  normalized.selector
12000
12353
  );
12001
- if (path5) {
12354
+ if (path3) {
12002
12355
  fields.push({
12003
12356
  key: fieldKey,
12004
- path: path5,
12357
+ path: path3,
12005
12358
  attribute: normalized.attribute
12006
12359
  });
12007
12360
  } else {
@@ -12039,11 +12392,11 @@ var Opensteer = class _Opensteer {
12039
12392
  continue;
12040
12393
  }
12041
12394
  if (!fieldPlan.selector) continue;
12042
- const path5 = await this.buildPathFromSelector(fieldPlan.selector);
12043
- if (!path5) continue;
12395
+ const path3 = await this.buildPathFromSelector(fieldPlan.selector);
12396
+ if (!path3) continue;
12044
12397
  fields.push({
12045
12398
  key,
12046
- path: path5,
12399
+ path: path3,
12047
12400
  attribute: fieldPlan.attribute
12048
12401
  });
12049
12402
  }
@@ -12112,15 +12465,15 @@ var Opensteer = class _Opensteer {
12112
12465
  });
12113
12466
  continue;
12114
12467
  }
12115
- const path5 = await this.buildPathFromElement(field.counter);
12116
- if (!path5) {
12468
+ const path3 = await this.buildPathFromElement(field.counter);
12469
+ if (!path3) {
12117
12470
  throw new Error(
12118
12471
  `Unable to persist extraction schema field "${field.key}": counter ${field.counter} could not be converted into a stable element path.`
12119
12472
  );
12120
12473
  }
12121
12474
  resolved.push({
12122
12475
  key: field.key,
12123
- path: path5,
12476
+ path: path3,
12124
12477
  attribute: field.attribute
12125
12478
  });
12126
12479
  }
@@ -12137,10 +12490,10 @@ var Opensteer = class _Opensteer {
12137
12490
  }
12138
12491
  resolveStorageKey(description) {
12139
12492
  if (!description) return null;
12140
- return createHash("sha256").update(description).digest("hex").slice(0, 16);
12493
+ return createHash2("sha256").update(description).digest("hex").slice(0, 16);
12141
12494
  }
12142
- normalizePath(path5) {
12143
- return sanitizeElementPath(path5);
12495
+ normalizePath(path3) {
12496
+ return sanitizeElementPath(path3);
12144
12497
  }
12145
12498
  };
12146
12499
  function formatActionFailureMessage(action, description, cause) {
@@ -12150,8 +12503,8 @@ function formatActionFailureMessage(action, description, cause) {
12150
12503
  function cloneContextHops(context) {
12151
12504
  return JSON.parse(JSON.stringify(context || []));
12152
12505
  }
12153
- function collectIframeContextPrefix(path5) {
12154
- const context = path5.context || [];
12506
+ function collectIframeContextPrefix(path3) {
12507
+ const context = path3.context || [];
12155
12508
  let lastIframeIndex = -1;
12156
12509
  for (let index = 0; index < context.length; index += 1) {
12157
12510
  if (context[index]?.kind === "iframe") {
@@ -12209,7 +12562,7 @@ function normalizeExtractSource(source) {
12209
12562
  }
12210
12563
  function computeSchemaHash(schema) {
12211
12564
  const stable = stableStringify(schema);
12212
- return createHash("sha256").update(stable).digest("hex");
12565
+ return createHash2("sha256").update(stable).digest("hex");
12213
12566
  }
12214
12567
  function buildPathMap(fields) {
12215
12568
  const out = {};
@@ -12443,15 +12796,34 @@ function isInternalOrBlankPageUrl(url) {
12443
12796
  if (url === "about:blank") return true;
12444
12797
  return url.startsWith("chrome://") || url.startsWith("devtools://") || url.startsWith("edge://");
12445
12798
  }
12799
+ function normalizeCloudBrowserProfilePreference(value, source) {
12800
+ if (!value) {
12801
+ return void 0;
12802
+ }
12803
+ const profileId = typeof value.profileId === "string" ? value.profileId.trim() : "";
12804
+ if (!profileId) {
12805
+ throw new Error(
12806
+ `Invalid cloud browser profile in ${source}: profileId must be a non-empty string.`
12807
+ );
12808
+ }
12809
+ if (value.reuseIfActive !== void 0 && typeof value.reuseIfActive !== "boolean") {
12810
+ throw new Error(
12811
+ `Invalid cloud browser profile in ${source}: reuseIfActive must be a boolean.`
12812
+ );
12813
+ }
12814
+ return {
12815
+ profileId,
12816
+ reuseIfActive: value.reuseIfActive
12817
+ };
12818
+ }
12446
12819
  function buildLocalRunId(namespace) {
12447
12820
  const normalized = namespace.trim() || "default";
12448
12821
  return `${normalized}-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
12449
12822
  }
12450
12823
 
12451
12824
  export {
12452
- normalizeError,
12453
- normalizeNamespace,
12454
- resolveNamespaceDir,
12825
+ expandHome,
12826
+ loadCookiesFromLocalProfileDir,
12455
12827
  waitForVisualStability,
12456
12828
  createEmptyRegistry,
12457
12829
  LocalSelectorStorage,
@@ -12512,13 +12884,8 @@ export {
12512
12884
  performFileUpload,
12513
12885
  OpensteerActionError,
12514
12886
  cloudSessionContractVersion,
12515
- OpensteerCloudError,
12516
- cloudUnsupportedMethodError,
12517
- cloudNotLaunchedError,
12518
12887
  ActionWsClient,
12519
12888
  collectLocalSelectorCacheEntries,
12520
- CloudCdpClient,
12521
- CloudSessionClient,
12522
12889
  OpensteerAgentError,
12523
12890
  OpensteerAgentConfigError,
12524
12891
  OpensteerAgentProviderError,
@@ -12529,5 +12896,8 @@ export {
12529
12896
  resolveAgentConfig,
12530
12897
  createCuaClient,
12531
12898
  OpensteerCuaAgentHandler,
12899
+ planSnappyCursorMotion,
12900
+ SvgCursorRenderer,
12901
+ CursorController,
12532
12902
  Opensteer
12533
12903
  };