@btraut/browser-bridge 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -51,14 +51,11 @@ var import_node_path2 = require("node:path");
51
51
  var import_node_fs = require("node:fs");
52
52
  var import_node_path = require("node:path");
53
53
  var DEFAULT_HOST = "127.0.0.1";
54
- var LEGACY_DEFAULT_PORT = 3210;
55
- var DETERMINISTIC_PORT_WINDOW = 2e3;
54
+ var DEFAULT_PORT = 3210;
56
55
  var ENV_CORE_HOST = "BROWSER_BRIDGE_CORE_HOST";
57
56
  var ENV_VISION_HOST = "BROWSER_VISION_CORE_HOST";
58
57
  var ENV_CORE_PORT = "BROWSER_BRIDGE_CORE_PORT";
59
58
  var ENV_VISION_PORT = "BROWSER_VISION_CORE_PORT";
60
- var ENV_ISOLATED_MODE = "BROWSER_BRIDGE_ISOLATED_MODE";
61
- var ENV_VISION_ISOLATED_MODE = "BROWSER_VISION_ISOLATED_MODE";
62
59
  var ENV_BRIDGE_CWD = "BROWSER_BRIDGE_CWD";
63
60
  var ENV_PROCESS_PWD = "PWD";
64
61
  var ENV_PROCESS_INIT_CWD = "INIT_CWD";
@@ -115,22 +112,6 @@ var normalizeHost = (value) => {
115
112
  const trimmed = value.trim();
116
113
  return trimmed.length > 0 ? trimmed : void 0;
117
114
  };
118
- var parseBoolean = (value) => {
119
- if (typeof value === "boolean") {
120
- return value;
121
- }
122
- if (typeof value !== "string") {
123
- return void 0;
124
- }
125
- const normalized = value.trim().toLowerCase();
126
- if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
127
- return true;
128
- }
129
- if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
130
- return false;
131
- }
132
- return void 0;
133
- };
134
115
  var parsePort = (value, label, invalidPolicy) => {
135
116
  if (value === void 0 || value === null) {
136
117
  return void 0;
@@ -147,60 +128,6 @@ var parsePort = (value, label, invalidPolicy) => {
147
128
  }
148
129
  return void 0;
149
130
  };
150
- var hashString = (value) => {
151
- let hash = 2166136261;
152
- for (let index = 0; index < value.length; index += 1) {
153
- hash ^= value.charCodeAt(index);
154
- hash = Math.imul(hash, 16777619);
155
- }
156
- return hash >>> 0;
157
- };
158
- var normalizePathForHash = (value) => value.replace(/\\/g, "/").toLowerCase();
159
- var sanitizeToken = (value) => value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
160
- var fallbackWorktreeId = (gitRoot) => {
161
- const hash = hashString(normalizePathForHash(gitRoot)).toString(16).padStart(8, "0");
162
- return `wt-${hash}`;
163
- };
164
- var extractWorktreeIdFromGitDir = (gitDir) => {
165
- const normalized = gitDir.replace(/\\/g, "/");
166
- const marker = "/.git/worktrees/";
167
- const markerIndex = normalized.lastIndexOf(marker);
168
- if (markerIndex < 0) {
169
- return null;
170
- }
171
- const remainder = normalized.slice(markerIndex + marker.length);
172
- const rawId = remainder.split("/")[0];
173
- if (!rawId) {
174
- return null;
175
- }
176
- const sanitized = sanitizeToken(rawId);
177
- return sanitized.length > 0 ? sanitized : null;
178
- };
179
- var readWorktreeGitDir = (gitRoot) => {
180
- const gitPath = (0, import_node_path.join)(gitRoot, ".git");
181
- try {
182
- const stats = (0, import_node_fs.statSync)(gitPath);
183
- if (stats.isDirectory()) {
184
- return gitPath;
185
- }
186
- if (!stats.isFile()) {
187
- return null;
188
- }
189
- } catch {
190
- return null;
191
- }
192
- try {
193
- const raw = (0, import_node_fs.readFileSync)(gitPath, "utf8");
194
- const match = raw.match(/^gitdir:\s*(.+)$/m);
195
- if (!match?.[1]) {
196
- return null;
197
- }
198
- const candidate = match[1].trim();
199
- return (0, import_node_path.isAbsolute)(candidate) ? candidate : (0, import_node_path.resolve)(gitRoot, candidate);
200
- } catch {
201
- return null;
202
- }
203
- };
204
131
  var resolveEnvHost = (env) => {
205
132
  const bridgeHost = normalizeHost(env[ENV_CORE_HOST]);
206
133
  if (bridgeHost) {
@@ -214,35 +141,18 @@ var resolveEnvPortRaw = (env) => {
214
141
  }
215
142
  return env[ENV_VISION_PORT];
216
143
  };
217
- var resolveEnvIsolatedMode = (env) => {
218
- const bridge = parseBoolean(env[ENV_ISOLATED_MODE]);
219
- if (bridge !== void 0) {
220
- return bridge;
221
- }
222
- return parseBoolean(env[ENV_VISION_ISOLATED_MODE]);
223
- };
224
144
  var sanitizeMetadata = (raw) => {
225
145
  if (!raw || typeof raw !== "object") {
226
146
  return null;
227
147
  }
228
148
  const candidate = raw;
229
- const host = normalizeHost(candidate.host);
230
- const port = parsePort(candidate.port, "port", "ignore");
231
- const gitRoot = normalizeHost(candidate.git_root);
232
- const worktreeId = normalizeHost(candidate.worktree_id);
233
149
  const extensionId = normalizeHost(candidate.extension_id);
234
- const isolatedMode = parseBoolean(candidate.isolated_mode);
235
150
  const updatedAt = normalizeHost(candidate.updated_at);
236
- if (!host && port === void 0 && !gitRoot && !worktreeId && !extensionId && isolatedMode === void 0 && !updatedAt) {
151
+ if (!extensionId && !updatedAt) {
237
152
  return null;
238
153
  }
239
154
  return {
240
- host,
241
- port,
242
- git_root: gitRoot,
243
- worktree_id: worktreeId,
244
155
  extension_id: extensionId,
245
- isolated_mode: isolatedMode,
246
156
  updated_at: updatedAt
247
157
  };
248
158
  };
@@ -259,32 +169,6 @@ var findGitRoot = (cwd) => {
259
169
  current = parent;
260
170
  }
261
171
  };
262
- var resolveWorktreeId = ({
263
- cwd,
264
- gitRoot
265
- } = {}) => {
266
- const resolvedGitRoot = gitRoot ?? findGitRoot(resolveCwd(cwd));
267
- if (!resolvedGitRoot) {
268
- return null;
269
- }
270
- const gitDir = readWorktreeGitDir(resolvedGitRoot);
271
- const parsedId = gitDir ? extractWorktreeIdFromGitDir(gitDir) : null;
272
- if (parsedId) {
273
- return parsedId;
274
- }
275
- return fallbackWorktreeId(resolvedGitRoot);
276
- };
277
- var resolveDeterministicCorePort = ({
278
- cwd,
279
- gitRoot
280
- } = {}) => {
281
- const resolvedGitRoot = gitRoot ?? findGitRoot(resolveCwd(cwd));
282
- if (!resolvedGitRoot) {
283
- return LEGACY_DEFAULT_PORT;
284
- }
285
- const seed = normalizePathForHash(resolvedGitRoot);
286
- return LEGACY_DEFAULT_PORT + hashString(seed) % DETERMINISTIC_PORT_WINDOW;
287
- };
288
172
  var resolveRuntimeMetadataPath = ({
289
173
  cwd,
290
174
  gitRoot,
@@ -342,26 +226,6 @@ var writeRuntimeMetadata = (metadata, {
342
226
  `, "utf8");
343
227
  return path3;
344
228
  };
345
- var createBoundedPortProbeSequence = (startPort, maxAttempts = 20, maxPort = 65535) => {
346
- const normalizedStart = parsePort(startPort, "startPort", "throw");
347
- if (normalizedStart === void 0) {
348
- throw new Error(`Invalid startPort: ${String(startPort)}`);
349
- }
350
- const attempts = Math.max(1, Math.floor(maxAttempts));
351
- const ceiling = Math.min(65535, Math.max(1, Math.floor(maxPort)));
352
- if (normalizedStart > ceiling) {
353
- throw new Error(`startPort ${normalizedStart} exceeds maxPort ${ceiling}.`);
354
- }
355
- const sequence = [];
356
- for (let offset = 0; offset < attempts; offset += 1) {
357
- const port = normalizedStart + offset;
358
- if (port > ceiling) {
359
- break;
360
- }
361
- sequence.push(port);
362
- }
363
- return sequence;
364
- };
365
229
  var resolveCoreRuntime = (options = {}) => {
366
230
  const env = options.env ?? process.env;
367
231
  const resolvedCwd = resolveCwd(options.cwd);
@@ -372,45 +236,18 @@ var resolveCoreRuntime = (options = {}) => {
372
236
  metadataPath: options.metadataPath
373
237
  });
374
238
  const metadata = options.metadata === void 0 ? readRuntimeMetadata({ metadataPath }) : sanitizeMetadata(options.metadata);
375
- const deterministicPort = resolveDeterministicCorePort({
376
- cwd: resolvedCwd,
377
- gitRoot
378
- });
379
239
  const optionHost = normalizeHost(options.host);
380
240
  const envHost = resolveEnvHost(env);
381
- const metadataHost = normalizeHost(metadata?.host);
382
- const host = optionHost ?? envHost ?? metadataHost ?? DEFAULT_HOST;
383
- const hostSource = optionHost ? "option" : envHost ? "env" : metadataHost ? "metadata" : "default";
241
+ const host = optionHost ?? envHost ?? DEFAULT_HOST;
242
+ const hostSource = optionHost ? "option" : envHost ? "env" : "default";
384
243
  const optionPort = parsePort(options.port, "port", "throw");
385
244
  const envPort = parsePort(
386
245
  resolveEnvPortRaw(env),
387
246
  "port",
388
247
  options.strictEnvPort ? "throw" : "ignore"
389
248
  );
390
- const metadataPort = parsePort(metadata?.port, "port", "ignore");
391
- const optionIsolatedMode = options.isolatedMode;
392
- const envIsolatedMode = resolveEnvIsolatedMode(env);
393
- const metadataIsolatedMode = metadata?.isolated_mode;
394
- const isolatedMode = optionIsolatedMode ?? envIsolatedMode ?? metadataIsolatedMode ?? false;
395
- const isolatedModeSource = optionIsolatedMode !== void 0 ? "option" : envIsolatedMode !== void 0 ? "env" : metadataIsolatedMode !== void 0 ? "metadata" : "default";
396
- let port;
397
- let portSource;
398
- if (optionPort !== void 0) {
399
- port = optionPort;
400
- portSource = "option";
401
- } else if (envPort !== void 0) {
402
- port = envPort;
403
- portSource = "env";
404
- } else if (metadataPort !== void 0 && isolatedMode) {
405
- port = metadataPort;
406
- portSource = "metadata";
407
- } else if (isolatedMode) {
408
- port = deterministicPort;
409
- portSource = "deterministic";
410
- } else {
411
- port = LEGACY_DEFAULT_PORT;
412
- portSource = "default";
413
- }
249
+ const port = optionPort ?? envPort ?? DEFAULT_PORT;
250
+ const portSource = optionPort !== void 0 ? "option" : envPort !== void 0 ? "env" : "default";
414
251
  return {
415
252
  host,
416
253
  port,
@@ -418,11 +255,7 @@ var resolveCoreRuntime = (options = {}) => {
418
255
  portSource,
419
256
  metadataPath,
420
257
  metadata,
421
- gitRoot,
422
- worktreeId: resolveWorktreeId({ cwd: resolvedCwd, gitRoot }),
423
- deterministicPort,
424
- isolatedMode,
425
- isolatedModeSource
258
+ gitRoot
426
259
  };
427
260
  };
428
261
 
@@ -841,7 +674,7 @@ var createCoreReadinessController = (options = {}) => {
841
674
  }
842
675
  if (portOccupied) {
843
676
  throw new Error(
844
- `Core daemon failed to start on ${runtime.host}:${runtime.port}. A process is already listening on this port but did not pass Browser Bridge health checks. Retry with --no-daemon to reuse it, or enable isolated mode (BROWSER_BRIDGE_ISOLATED_MODE=1) for per-worktree ports.`
677
+ `Core daemon failed to start on ${runtime.host}:${runtime.port}. A process is already listening on this port but did not pass Browser Bridge health checks. Retry with --no-daemon to reuse the existing process or stop whatever is already bound to that port.`
845
678
  );
846
679
  }
847
680
  throw new Error(
@@ -1169,9 +1002,7 @@ var DiagnosticsRuntimeEndpointSchema = import_zod3.z.object({
1169
1002
  port: import_zod3.z.number().finite().optional(),
1170
1003
  base_url: import_zod3.z.string().optional(),
1171
1004
  host_source: import_zod3.z.string().optional(),
1172
- port_source: import_zod3.z.string().optional(),
1173
- metadata_path: import_zod3.z.string().optional(),
1174
- isolated_mode: import_zod3.z.boolean().optional()
1005
+ port_source: import_zod3.z.string().optional()
1175
1006
  });
1176
1007
  var DiagnosticsRuntimeProcessSchema = import_zod3.z.object({
1177
1008
  component: import_zod3.z.enum(["cli", "mcp", "core"]).optional(),
@@ -1192,12 +1023,13 @@ var DiagnosticsRuntimeContextSchema = import_zod3.z.object({
1192
1023
  process: DiagnosticsRuntimeProcessSchema.optional()
1193
1024
  }).optional(),
1194
1025
  extension: import_zod3.z.object({
1026
+ extension_id: import_zod3.z.string().optional(),
1195
1027
  version: import_zod3.z.string().optional(),
1196
1028
  protocol_version: import_zod3.z.string().optional(),
1197
1029
  capability_negotiated: import_zod3.z.boolean().optional(),
1198
1030
  capabilities: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.boolean()).optional(),
1199
1031
  endpoint: DiagnosticsRuntimeEndpointSchema.optional(),
1200
- port_source: import_zod3.z.enum(["default", "storage"]).optional()
1032
+ port_source: import_zod3.z.enum(["default"]).optional()
1201
1033
  }).optional()
1202
1034
  });
1203
1035
  var DiagnosticReportSchema = import_zod3.z.object({
@@ -1272,7 +1104,7 @@ var DriveNavigateInputSchema = import_zod3.z.object({
1272
1104
  session_id: import_zod3.z.string().min(1).optional(),
1273
1105
  url: import_zod3.z.string().min(1),
1274
1106
  tab_id: import_zod3.z.number().finite().optional(),
1275
- wait: import_zod3.z.enum(["none", "domcontentloaded"]).default("domcontentloaded")
1107
+ wait: import_zod3.z.enum(["none", "domcontentloaded", "networkidle"]).default("domcontentloaded")
1276
1108
  });
1277
1109
  var DriveNavigateOutputSchema = OpResultSchema.extend({
1278
1110
  session_id: import_zod3.z.string().min(1)
@@ -1441,6 +1273,8 @@ var InspectConsistencySchema = import_zod3.z.enum(["best_effort", "quiesce"]);
1441
1273
  var TargetHintSchema = import_zod3.z.object({
1442
1274
  url: import_zod3.z.string().min(1).optional(),
1443
1275
  title: import_zod3.z.string().min(1).optional(),
1276
+ tab_id: import_zod3.z.number().finite().optional(),
1277
+ tabId: import_zod3.z.number().finite().optional(),
1444
1278
  last_active_at: import_zod3.z.string().optional(),
1445
1279
  lastActiveAt: import_zod3.z.string().optional()
1446
1280
  });
@@ -1635,6 +1469,7 @@ var HealthCheckOutputSchema = import_zod3.z.object({
1635
1469
  }).passthrough(),
1636
1470
  extension: import_zod3.z.object({
1637
1471
  connected: import_zod3.z.boolean(),
1472
+ extension_id: import_zod3.z.string().optional(),
1638
1473
  last_seen_at: import_zod3.z.string().min(1).optional()
1639
1474
  }).passthrough()
1640
1475
  }).passthrough();
@@ -1744,7 +1579,8 @@ var SessionRegistry = class {
1744
1579
  state: "INIT" /* INIT */,
1745
1580
  createdAt: now,
1746
1581
  updatedAt: now,
1747
- lastAccessedAt: now
1582
+ lastAccessedAt: now,
1583
+ selectedTabId: void 0
1748
1584
  };
1749
1585
  this.sessions.set(id, session);
1750
1586
  return session;
@@ -1843,6 +1679,18 @@ var SessionRegistry = class {
1843
1679
  session.updatedAt = /* @__PURE__ */ new Date();
1844
1680
  return session;
1845
1681
  }
1682
+ setSelectedTab(sessionId, tabId) {
1683
+ const session = this.require(sessionId);
1684
+ session.selectedTabId = tabId;
1685
+ session.updatedAt = /* @__PURE__ */ new Date();
1686
+ return session;
1687
+ }
1688
+ clearSelectedTab(sessionId) {
1689
+ const session = this.require(sessionId);
1690
+ session.selectedTabId = void 0;
1691
+ session.updatedAt = /* @__PURE__ */ new Date();
1692
+ return session;
1693
+ }
1846
1694
  };
1847
1695
 
1848
1696
  // packages/core/src/routes/shared.ts
@@ -1883,13 +1731,14 @@ var deriveHintFromTabs = (tabs) => {
1883
1731
  if (!Array.isArray(tabs) || tabs.length === 0) {
1884
1732
  return void 0;
1885
1733
  }
1734
+ const candidatePool = tabs.some((tab) => tab.active === true) ? tabs.filter((tab) => tab.active === true) : tabs;
1886
1735
  let best;
1887
1736
  let bestTime = -Infinity;
1888
- for (const tab of tabs) {
1737
+ for (const tab of candidatePool) {
1889
1738
  const raw = tab.last_active_at;
1890
1739
  const time = raw ? Date.parse(raw) : NaN;
1891
1740
  const score = Number.isFinite(time) ? time : -Infinity;
1892
- if (!best || score > bestTime) {
1741
+ if (!best || score > bestTime || score === bestTime && tab.tab_id < best.tab_id) {
1893
1742
  best = tab;
1894
1743
  bestTime = score;
1895
1744
  }
@@ -1965,7 +1814,8 @@ var createSessionRouter = (registry, options = {}) => {
1965
1814
  return sendResult(res, {
1966
1815
  session_id: session.id,
1967
1816
  state: session.state,
1968
- updated_at: session.updatedAt.toISOString()
1817
+ updated_at: session.updatedAt.toISOString(),
1818
+ ...typeof session.selectedTabId === "number" ? { selected_tab_id: session.selectedTabId } : {}
1969
1819
  });
1970
1820
  } catch (error) {
1971
1821
  if (error instanceof SessionError) {
@@ -2103,9 +1953,6 @@ var InspectError = class extends Error {
2103
1953
  var import_crypto3 = require("crypto");
2104
1954
  var import_promises3 = require("node:fs/promises");
2105
1955
  var import_node_path4 = __toESM(require("node:path"));
2106
- var import_readability = require("@mozilla/readability");
2107
- var import_jsdom = require("jsdom");
2108
- var import_turndown = __toESM(require("turndown"));
2109
1956
 
2110
1957
  // packages/core/src/artifacts.ts
2111
1958
  var import_promises2 = require("node:fs/promises");
@@ -2172,6 +2019,7 @@ var ExtensionBridge = class {
2172
2019
  return {
2173
2020
  connected: this.connected,
2174
2021
  lastSeenAt: this.lastSeenAt,
2022
+ extensionId: this.extensionId,
2175
2023
  version: this.version,
2176
2024
  protocolVersion: this.protocolVersion,
2177
2025
  protocolMismatch: this.protocolMismatch,
@@ -2352,6 +2200,7 @@ var ExtensionBridge = class {
2352
2200
  this.stopHeartbeat();
2353
2201
  this.connected = false;
2354
2202
  this.socket = null;
2203
+ this.extensionId = void 0;
2355
2204
  this.version = void 0;
2356
2205
  this.protocolVersion = void 0;
2357
2206
  this.protocolMismatch = void 0;
@@ -2414,6 +2263,11 @@ var ExtensionBridge = class {
2414
2263
  this.tabs = tabs;
2415
2264
  }
2416
2265
  if (message.action === "drive.hello") {
2266
+ if (typeof params?.extension_id === "string") {
2267
+ this.extensionId = params.extension_id;
2268
+ } else {
2269
+ this.extensionId = void 0;
2270
+ }
2417
2271
  if (typeof params?.version === "string") {
2418
2272
  this.version = params.version;
2419
2273
  }
@@ -2436,7 +2290,7 @@ var ExtensionBridge = class {
2436
2290
  if (typeof params?.core_port === "number" && Number.isFinite(params.core_port)) {
2437
2291
  this.corePort = params.core_port;
2438
2292
  }
2439
- if (params?.core_port_source === "default" || params?.core_port_source === "storage") {
2293
+ if (params?.core_port_source === "default") {
2440
2294
  this.corePortSource = params.core_port_source;
2441
2295
  }
2442
2296
  const capabilities = params?.capabilities;
@@ -2524,7 +2378,26 @@ var toDriveError = (error) => ({
2524
2378
 
2525
2379
  // packages/core/src/drive.ts
2526
2380
  var LOOPBACK_NAVIGATION_PREFLIGHT_TIMEOUT_MS = 1200;
2381
+ var POST_CLICK_SETTLE_MS = 75;
2382
+ var TRANSIENT_LOCATOR_RETRY_DELAY_MS = 150;
2527
2383
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2384
+ var ACTIONS_WITH_OPTIONAL_TAB_ID = /* @__PURE__ */ new Set([
2385
+ "drive.navigate",
2386
+ "drive.go_back",
2387
+ "drive.go_forward",
2388
+ "drive.click",
2389
+ "drive.hover",
2390
+ "drive.select",
2391
+ "drive.type",
2392
+ "drive.fill_form",
2393
+ "drive.drag",
2394
+ "drive.handle_dialog",
2395
+ "drive.key",
2396
+ "drive.key_press",
2397
+ "drive.scroll",
2398
+ "drive.screenshot",
2399
+ "drive.wait_for"
2400
+ ]);
2528
2401
  var isLikelyUnreachableLoopbackError = (message) => {
2529
2402
  const normalized = message.toLowerCase();
2530
2403
  return normalized.includes("connection refused") || normalized.includes("econnrefused") || normalized.includes("err_connection_refused") || normalized.includes("enotfound") || normalized.includes("err_name_not_resolved") || normalized.includes("eai_again") || normalized.includes("ehostunreach") || normalized.includes("enetunreach");
@@ -2621,10 +2494,26 @@ var DriveController = class {
2621
2494
  this.lastError = void 0;
2622
2495
  this.lastErrorAt = void 0;
2623
2496
  }
2497
+ async sleep(ms) {
2498
+ await new Promise((resolve3) => setTimeout(resolve3, ms));
2499
+ }
2500
+ isTransientLocatorError(action, error) {
2501
+ if (action !== "drive.click") {
2502
+ return false;
2503
+ }
2504
+ if (error.code.toUpperCase() !== "NOT_FOUND") {
2505
+ return false;
2506
+ }
2507
+ const reason = error.details?.reason;
2508
+ const legacyCode = error.details?.legacy_code;
2509
+ const resource = error.details?.resource;
2510
+ return reason === "locator_not_found" || legacyCode === "LOCATOR_NOT_FOUND" || resource === "locator";
2511
+ }
2624
2512
  async execute(sessionId, action, params, timeoutMs) {
2625
2513
  return await driveMutex.runExclusive(async () => {
2514
+ let selectedTabId;
2626
2515
  try {
2627
- this.registry.require(sessionId);
2516
+ selectedTabId = this.registry.require(sessionId).selectedTabId;
2628
2517
  } catch (error) {
2629
2518
  if (error instanceof SessionError) {
2630
2519
  const errorInfo2 = {
@@ -2668,7 +2557,12 @@ var DriveController = class {
2668
2557
  return { ok: false, error: errorInfo };
2669
2558
  }
2670
2559
  this.ensureDriveReady(sessionId);
2671
- const preflightError = await preflightLoopbackNavigation(action, params);
2560
+ const prepared = this.prepareRequestParams(action, params, selectedTabId);
2561
+ const requestParams = prepared.params;
2562
+ const preflightError = await preflightLoopbackNavigation(
2563
+ action,
2564
+ requestParams
2565
+ );
2672
2566
  if (preflightError) {
2673
2567
  this.recordError(preflightError);
2674
2568
  return {
@@ -2681,10 +2575,19 @@ var DriveController = class {
2681
2575
  try {
2682
2576
  const response = await this.bridge.request(
2683
2577
  action,
2684
- params,
2578
+ requestParams,
2685
2579
  timeoutMs
2686
2580
  );
2687
2581
  if (response.status === "ok") {
2582
+ if (action === "drive.click") {
2583
+ await this.sleep(POST_CLICK_SETTLE_MS);
2584
+ }
2585
+ this.applySessionTargetOnSuccess(
2586
+ sessionId,
2587
+ action,
2588
+ requestParams,
2589
+ response.result
2590
+ );
2688
2591
  this.clearLastError();
2689
2592
  return {
2690
2593
  ok: true,
@@ -2701,6 +2604,11 @@ var DriveController = class {
2701
2604
  max_attempts: 1
2702
2605
  }
2703
2606
  };
2607
+ if (attempt === 0 && this.isTransientLocatorError(action, errorInfo)) {
2608
+ attempt += 1;
2609
+ await this.sleep(TRANSIENT_LOCATOR_RETRY_DELAY_MS);
2610
+ continue;
2611
+ }
2704
2612
  if (shouldRetryDriveOp({
2705
2613
  attempt,
2706
2614
  retryable: errorInfo.retryable,
@@ -2709,6 +2617,9 @@ var DriveController = class {
2709
2617
  attempt += 1;
2710
2618
  continue;
2711
2619
  }
2620
+ if (prepared.injectedSessionTabId && this.isMissingTabError(errorInfo)) {
2621
+ this.clearSessionTarget(sessionId);
2622
+ }
2712
2623
  this.recordError(errorInfo);
2713
2624
  return { ok: false, error: errorInfo };
2714
2625
  } catch (error) {
@@ -2725,6 +2636,9 @@ var DriveController = class {
2725
2636
  attempt += 1;
2726
2637
  continue;
2727
2638
  }
2639
+ if (prepared.injectedSessionTabId && this.isMissingTabError(errorInfo2)) {
2640
+ this.clearSessionTarget(sessionId);
2641
+ }
2728
2642
  this.recordError(errorInfo2);
2729
2643
  return { ok: false, error: errorInfo2 };
2730
2644
  }
@@ -2742,6 +2656,74 @@ var DriveController = class {
2742
2656
  }
2743
2657
  });
2744
2658
  }
2659
+ readTabId(params) {
2660
+ const value = params?.tab_id;
2661
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2662
+ }
2663
+ readTabIdFromResult(result) {
2664
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
2665
+ return void 0;
2666
+ }
2667
+ const value = result.tab_id;
2668
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2669
+ }
2670
+ prepareRequestParams(action, params, selectedTabId) {
2671
+ if (!ACTIONS_WITH_OPTIONAL_TAB_ID.has(action)) {
2672
+ return { params, injectedSessionTabId: false };
2673
+ }
2674
+ if (!selectedTabId || selectedTabId <= 0) {
2675
+ return { params, injectedSessionTabId: false };
2676
+ }
2677
+ const explicitTabId = this.readTabId(params);
2678
+ if (explicitTabId !== void 0) {
2679
+ return { params, injectedSessionTabId: false };
2680
+ }
2681
+ return {
2682
+ params: { ...params ?? {}, tab_id: selectedTabId },
2683
+ injectedSessionTabId: true
2684
+ };
2685
+ }
2686
+ applySessionTargetOnSuccess(sessionId, action, params, result) {
2687
+ const tabId = this.readTabIdFromResult(result) ?? this.readTabId(params);
2688
+ if (tabId === void 0) {
2689
+ return;
2690
+ }
2691
+ try {
2692
+ if (action === "drive.tab_close") {
2693
+ const session = this.registry.require(sessionId);
2694
+ if (session.selectedTabId === tabId) {
2695
+ this.registry.clearSelectedTab(sessionId);
2696
+ }
2697
+ return;
2698
+ }
2699
+ if (action === "drive.tab_activate" || ACTIONS_WITH_OPTIONAL_TAB_ID.has(action)) {
2700
+ this.registry.setSelectedTab(sessionId, tabId);
2701
+ }
2702
+ } catch (error) {
2703
+ console.debug(
2704
+ `Drive target update ignored for session ${sessionId}.`,
2705
+ error
2706
+ );
2707
+ }
2708
+ }
2709
+ isMissingTabError(error) {
2710
+ const code = error.code.toUpperCase();
2711
+ if (code === "TAB_NOT_FOUND") {
2712
+ return true;
2713
+ }
2714
+ const resource = error.details?.resource;
2715
+ return code === "NOT_FOUND" && (resource === void 0 || resource === "tab");
2716
+ }
2717
+ clearSessionTarget(sessionId) {
2718
+ try {
2719
+ this.registry.clearSelectedTab(sessionId);
2720
+ } catch (error) {
2721
+ console.debug(
2722
+ `Drive target clear ignored for session ${sessionId}.`,
2723
+ error
2724
+ );
2725
+ }
2726
+ }
2745
2727
  ensureDriveReady(sessionId) {
2746
2728
  try {
2747
2729
  const session = this.registry.require(sessionId);
@@ -2781,6 +2763,10 @@ var INTERACTIVE_AX_ROLES = /* @__PURE__ */ new Set([
2781
2763
  "textbox",
2782
2764
  "combobox",
2783
2765
  "listbox",
2766
+ "menu",
2767
+ "menuitem",
2768
+ "menuitemcheckbox",
2769
+ "menuitemradio",
2784
2770
  "checkbox",
2785
2771
  "radio",
2786
2772
  "switch",
@@ -3238,6 +3224,136 @@ var toConsoleEntry = (event) => {
3238
3224
  }
3239
3225
  };
3240
3226
 
3227
+ // packages/core/src/inspect/extract-content-policy.ts
3228
+ var import_readability = require("@mozilla/readability");
3229
+ var import_jsdom = require("jsdom");
3230
+ var import_turndown = __toESM(require("turndown"));
3231
+ var normalizeExtractedText = (value) => value.replace(/\s+/g, " ").trim();
3232
+ var collapseRepeatedMarkdownBlocks = (markdown) => {
3233
+ const blocks = markdown.split(/\n{2,}/).map((block) => block.trim()).filter((block) => block.length > 0);
3234
+ for (let sequenceLength = Math.floor(blocks.length / 2); sequenceLength >= 1; sequenceLength -= 1) {
3235
+ for (let start = 0; start + sequenceLength * 2 <= blocks.length; start += 1) {
3236
+ const first = blocks.slice(start, start + sequenceLength).map((block) => normalizeExtractedText(block)).join("\n");
3237
+ const second = blocks.slice(start + sequenceLength, start + sequenceLength * 2).map((block) => normalizeExtractedText(block)).join("\n");
3238
+ if (first.length === 0 || first !== second) {
3239
+ continue;
3240
+ }
3241
+ blocks.splice(start + sequenceLength, sequenceLength);
3242
+ sequenceLength = Math.min(
3243
+ sequenceLength + 1,
3244
+ Math.floor(blocks.length / 2)
3245
+ );
3246
+ start = Math.max(start - 1, -1);
3247
+ }
3248
+ }
3249
+ return blocks.join("\n\n").trim();
3250
+ };
3251
+ var extractSemanticMainCandidate = (document) => {
3252
+ const candidates = Array.from(
3253
+ document.querySelectorAll('main, [role="main"], article')
3254
+ );
3255
+ let best = candidates[0] ?? null;
3256
+ let bestLength = 0;
3257
+ for (const candidate of candidates) {
3258
+ const text = normalizeExtractedText(candidate.textContent ?? "");
3259
+ if (text.length <= bestLength) {
3260
+ continue;
3261
+ }
3262
+ best = candidate;
3263
+ bestLength = text.length;
3264
+ }
3265
+ if (!best || bestLength === 0) {
3266
+ return null;
3267
+ }
3268
+ return {
3269
+ html: best.innerHTML,
3270
+ text: normalizeExtractedText(best.textContent ?? ""),
3271
+ tagName: best.tagName.toLowerCase()
3272
+ };
3273
+ };
3274
+ var shouldPreferSemanticMainCandidate = (options) => {
3275
+ const articleLength = normalizeExtractedText(options.articleText).length;
3276
+ const mainLength = normalizeExtractedText(options.mainText).length;
3277
+ if (mainLength === 0) {
3278
+ return false;
3279
+ }
3280
+ if (articleLength === 0) {
3281
+ return true;
3282
+ }
3283
+ if (options.mainTagName === "article") {
3284
+ return false;
3285
+ }
3286
+ return articleLength < 160 && mainLength > articleLength + 20;
3287
+ };
3288
+ var parseExtractContentSource = (options) => {
3289
+ let article = null;
3290
+ let semanticMainCandidate = null;
3291
+ try {
3292
+ const dom = new import_jsdom.JSDOM(options.html, { url: options.url });
3293
+ const reader = new import_readability.Readability(dom.window.document);
3294
+ article = reader.parse();
3295
+ semanticMainCandidate = extractSemanticMainCandidate(dom.window.document);
3296
+ } catch {
3297
+ throw new InspectError(
3298
+ "EVALUATION_FAILED",
3299
+ "Failed to parse page content.",
3300
+ {
3301
+ retryable: false
3302
+ }
3303
+ );
3304
+ }
3305
+ if (!article) {
3306
+ throw new InspectError(
3307
+ "NOT_SUPPORTED",
3308
+ "Readability could not extract content.",
3309
+ {
3310
+ retryable: false
3311
+ }
3312
+ );
3313
+ }
3314
+ return {
3315
+ article,
3316
+ semanticMainCandidate
3317
+ };
3318
+ };
3319
+ var renderExtractContent = (options) => {
3320
+ let content = "";
3321
+ if (options.format === "article_json") {
3322
+ content = JSON.stringify(options.article, null, 2);
3323
+ } else if (options.format === "text") {
3324
+ const articleText = options.article.textContent ?? "";
3325
+ if (options.semanticMainCandidate && shouldPreferSemanticMainCandidate({
3326
+ articleText,
3327
+ mainText: options.semanticMainCandidate.text,
3328
+ mainTagName: options.semanticMainCandidate.tagName
3329
+ })) {
3330
+ content = options.semanticMainCandidate.text;
3331
+ } else {
3332
+ content = articleText;
3333
+ }
3334
+ } else {
3335
+ const turndown = new import_turndown.default();
3336
+ const articleText = options.article.textContent ?? "";
3337
+ const sourceHtml = options.semanticMainCandidate && shouldPreferSemanticMainCandidate({
3338
+ articleText,
3339
+ mainText: options.semanticMainCandidate.text,
3340
+ mainTagName: options.semanticMainCandidate.tagName
3341
+ }) ? options.semanticMainCandidate.html : options.article.content ?? "";
3342
+ content = collapseRepeatedMarkdownBlocks(turndown.turndown(sourceHtml));
3343
+ }
3344
+ const includeMetadata = options.includeMetadata ?? true;
3345
+ return {
3346
+ content,
3347
+ ...includeMetadata ? {
3348
+ title: options.article.title ?? void 0,
3349
+ byline: options.article.byline ?? void 0,
3350
+ excerpt: options.article.excerpt ?? void 0,
3351
+ siteName: options.article.siteName ?? void 0
3352
+ } : {},
3353
+ ...options.warnings && options.warnings.length > 0 ? { warnings: options.warnings } : {}
3354
+ };
3355
+ };
3356
+
3241
3357
  // packages/core/src/inspect/har.ts
3242
3358
  var buildHar = (events, title) => {
3243
3359
  const requests = /* @__PURE__ */ new Map();
@@ -3539,11 +3655,19 @@ var SnapshotHistory = class {
3539
3655
 
3540
3656
  // packages/core/src/inspect/snapshot-refs.ts
3541
3657
  var SNAPSHOT_REF_ATTRIBUTE = "data-bv-ref";
3658
+ var SNAPSHOT_REF_REGISTRY_ID = "__bb_snapshot_ref_registry__";
3542
3659
  var MAX_REF_ASSIGNMENTS = 500;
3543
3660
  var MAX_REF_WARNINGS = 5;
3544
3661
  var isInspectError = (error) => Boolean(
3545
3662
  error && typeof error === "object" && "name" in error && error.name === "InspectError" && "message" in error && typeof error.message === "string"
3546
3663
  );
3664
+ var isExpectedSnapshotRefMiss = (error) => {
3665
+ if (!isInspectError(error)) {
3666
+ return false;
3667
+ }
3668
+ const message = error.message.toLowerCase();
3669
+ return message.includes("could not find node") || message.includes("no node with given id") || message.includes("does not belong to the document");
3670
+ };
3547
3671
  var assignRefsToAxSnapshot = (snapshot) => {
3548
3672
  const nodes = getAxNodes(snapshot);
3549
3673
  const refs = /* @__PURE__ */ new Map();
@@ -3562,31 +3686,61 @@ var assignRefsToAxSnapshot = (snapshot) => {
3562
3686
  const ref = `@e${index}`;
3563
3687
  index += 1;
3564
3688
  node.ref = ref;
3565
- refs.set(backendId, ref);
3689
+ refs.set(backendId, {
3690
+ ref,
3691
+ ...getAxRole(node) ? { role: getAxRole(node) } : {},
3692
+ ...getAxName(node) ? { name: getAxName(node) } : {},
3693
+ ...extractUrl(node) ? { url: extractUrl(node) } : {}
3694
+ });
3566
3695
  }
3567
3696
  return refs;
3568
3697
  };
3569
- var clearSnapshotRefs = async (tabId, debuggerCommand) => {
3570
- await debuggerCommand(tabId, "Runtime.evaluate", {
3571
- expression: `document.querySelectorAll('[${SNAPSHOT_REF_ATTRIBUTE}]').forEach((el) => el.removeAttribute('${SNAPSHOT_REF_ATTRIBUTE}'))`,
3572
- returnByValue: true,
3573
- awaitPromise: true
3574
- });
3698
+ var extractUrl = (node) => {
3699
+ if (!Array.isArray(node.properties)) {
3700
+ return void 0;
3701
+ }
3702
+ for (const prop of node.properties) {
3703
+ if (!prop || typeof prop !== "object") {
3704
+ continue;
3705
+ }
3706
+ const name = prop.name;
3707
+ if (name !== "url") {
3708
+ continue;
3709
+ }
3710
+ const value = prop.value?.value;
3711
+ if (typeof value === "string" && value.length > 0) {
3712
+ return value;
3713
+ }
3714
+ }
3715
+ return void 0;
3575
3716
  };
3576
- var applySnapshotRefs = async (tabId, refs, debuggerCommand) => {
3577
- const warnings = [];
3578
- await debuggerCommand(tabId, "DOM.enable", {});
3579
- await debuggerCommand(tabId, "Runtime.enable", {});
3717
+ var clearSnapshotRefArtifacts = async (tabId, debuggerCommand) => {
3580
3718
  try {
3581
- await clearSnapshotRefs(tabId, debuggerCommand);
3719
+ await debuggerCommand(tabId, "Runtime.evaluate", {
3720
+ expression: `(() => {
3721
+ document.querySelectorAll('[${SNAPSHOT_REF_ATTRIBUTE}]').forEach((el) => el.removeAttribute('${SNAPSHOT_REF_ATTRIBUTE}'));
3722
+ document.getElementById('${SNAPSHOT_REF_REGISTRY_ID}')?.remove();
3723
+ })()`,
3724
+ returnByValue: true,
3725
+ awaitPromise: true
3726
+ });
3727
+ return { warnings: [] };
3582
3728
  } catch {
3583
- warnings.push("Failed to clear prior snapshot refs.");
3729
+ return { warnings: ["Failed to clear prior snapshot refs."] };
3584
3730
  }
3731
+ };
3732
+ var applySnapshotRefAttributes = async (tabId, refs, debuggerCommand) => {
3733
+ const warnings = [];
3734
+ const appliedRefs = /* @__PURE__ */ new Set();
3735
+ const appliedBindings = [];
3736
+ await debuggerCommand(tabId, "DOM.enable", {});
3737
+ await debuggerCommand(tabId, "Runtime.enable", {});
3585
3738
  if (refs.size === 0) {
3586
- return warnings;
3739
+ return { warnings, appliedRefs, appliedBindings };
3587
3740
  }
3588
3741
  let applied = 0;
3589
- for (const [backendNodeId, ref] of refs) {
3742
+ for (const [backendNodeId, binding] of refs) {
3743
+ const ref = binding.ref;
3590
3744
  if (applied >= MAX_REF_ASSIGNMENTS) {
3591
3745
  warnings.push(
3592
3746
  `Snapshot refs truncated at ${MAX_REF_ASSIGNMENTS} elements.`
@@ -3599,9 +3753,6 @@ var applySnapshotRefs = async (tabId, refs, debuggerCommand) => {
3599
3753
  });
3600
3754
  const node = described.node;
3601
3755
  if (!node || node.nodeType !== 1 || typeof node.nodeId !== "number") {
3602
- if (warnings.length < MAX_REF_WARNINGS) {
3603
- warnings.push(`Ref ${ref} could not be applied to a DOM element.`);
3604
- }
3605
3756
  continue;
3606
3757
  }
3607
3758
  await debuggerCommand(tabId, "DOM.setAttributeValue", {
@@ -3610,13 +3761,52 @@ var applySnapshotRefs = async (tabId, refs, debuggerCommand) => {
3610
3761
  value: ref
3611
3762
  });
3612
3763
  applied += 1;
3613
- } catch {
3764
+ appliedRefs.add(ref);
3765
+ appliedBindings.push(binding);
3766
+ } catch (error) {
3767
+ if (isExpectedSnapshotRefMiss(error)) {
3768
+ continue;
3769
+ }
3614
3770
  if (warnings.length < MAX_REF_WARNINGS) {
3615
3771
  warnings.push(`Ref ${ref} could not be applied.`);
3616
3772
  }
3617
3773
  }
3618
3774
  }
3619
- return warnings;
3775
+ return { warnings, appliedRefs, appliedBindings };
3776
+ };
3777
+ var persistSnapshotRefRegistry = async (tabId, bindings, debuggerCommand) => {
3778
+ try {
3779
+ const registry = JSON.stringify(bindings);
3780
+ await debuggerCommand(tabId, "Runtime.evaluate", {
3781
+ expression: `(() => {
3782
+ const id = ${JSON.stringify(SNAPSHOT_REF_REGISTRY_ID)};
3783
+ let el = document.getElementById(id);
3784
+ if (!el) {
3785
+ el = document.createElement('script');
3786
+ el.id = id;
3787
+ el.type = 'application/json';
3788
+ document.documentElement.appendChild(el);
3789
+ }
3790
+ el.textContent = ${JSON.stringify(registry)};
3791
+ })()`,
3792
+ returnByValue: true,
3793
+ awaitPromise: true
3794
+ });
3795
+ return { warnings: [] };
3796
+ } catch {
3797
+ return { warnings: ["Snapshot ref registry could not be applied."] };
3798
+ }
3799
+ };
3800
+ var pruneUnappliedRefsFromSnapshot = (snapshot, appliedRefs) => {
3801
+ for (const node of getAxNodes(snapshot)) {
3802
+ if (!node || typeof node !== "object") {
3803
+ continue;
3804
+ }
3805
+ const ref = node.ref;
3806
+ if (typeof ref === "string" && !appliedRefs.has(ref)) {
3807
+ delete node.ref;
3808
+ }
3809
+ }
3620
3810
  };
3621
3811
  var resolveNodeIdForSelector = async (tabId, selector, debuggerCommand) => {
3622
3812
  await debuggerCommand(tabId, "DOM.enable", {});
@@ -3645,107 +3835,6 @@ var resolveNodeIdForSelector = async (tabId, selector, debuggerCommand) => {
3645
3835
  }
3646
3836
  };
3647
3837
 
3648
- // packages/core/src/page-state-script.ts
3649
- var PAGE_STATE_SCRIPT = [
3650
- "(() => {",
3651
- " const escape = (value) => {",
3652
- " if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {",
3653
- " return CSS.escape(value);",
3654
- " }",
3655
- ` return String(value).replace(/["'\\\\]/g, '\\\\$&');`,
3656
- " };",
3657
- " const truncate = (value, max) => {",
3658
- " const text = String(value ?? '');",
3659
- " return text.length > max ? text.slice(0, max) : text;",
3660
- " };",
3661
- " const selectorFor = (element) => {",
3662
- " if (element.id) {",
3663
- " return `#${escape(element.id)}`;",
3664
- " }",
3665
- " const name = element.getAttribute('name');",
3666
- " if (name) {",
3667
- ' return `${element.tagName.toLowerCase()}[name="${escape(name)}"]`;',
3668
- " }",
3669
- " const parts = [];",
3670
- " let node = element;",
3671
- " while (node && node.nodeType === 1 && parts.length < 4) {",
3672
- " let part = node.tagName.toLowerCase();",
3673
- " const parent = node.parentElement;",
3674
- " if (parent) {",
3675
- " const siblings = Array.from(parent.children).filter(",
3676
- " (child) => child.tagName === node.tagName",
3677
- " );",
3678
- " if (siblings.length > 1) {",
3679
- " part += `:nth-of-type(${siblings.indexOf(node) + 1})`;",
3680
- " }",
3681
- " }",
3682
- " parts.unshift(part);",
3683
- " node = parent;",
3684
- " }",
3685
- " return parts.join('>');",
3686
- " };",
3687
- " const readStorage = (storage, limit) => {",
3688
- " try {",
3689
- " return Object.keys(storage)",
3690
- " .slice(0, limit)",
3691
- " .map((key) => ({",
3692
- " key,",
3693
- " value: truncate(storage.getItem(key), 500),",
3694
- " }));",
3695
- " } catch {",
3696
- " return [];",
3697
- " }",
3698
- " };",
3699
- " const forms = Array.from(document.querySelectorAll('form')).map((form) => {",
3700
- " const fields = Array.from(form.elements)",
3701
- " .filter((element) => element && element.tagName)",
3702
- " .map((element) => {",
3703
- " const tag = element.tagName.toLowerCase();",
3704
- " const type = 'type' in element && element.type ? element.type : tag;",
3705
- " const name = element.name || element.getAttribute('name') || element.id || '';",
3706
- " let value = '';",
3707
- " let options;",
3708
- " if (tag === 'select') {",
3709
- " const select = element;",
3710
- " value = select.value ?? '';",
3711
- " options = Array.from(select.options).map((option) => option.text);",
3712
- " } else if (tag === 'input' && element.type === 'password') {",
3713
- " value = '[redacted]';",
3714
- " } else if (tag === 'input' || tag === 'textarea') {",
3715
- " value = element.value ?? '';",
3716
- " } else if (element.isContentEditable) {",
3717
- " value = element.textContent ?? '';",
3718
- " } else if ('value' in element) {",
3719
- " value = element.value ?? '';",
3720
- " }",
3721
- " return {",
3722
- " name,",
3723
- " type,",
3724
- " value: truncate(value, 500),",
3725
- " ...(options ? { options } : {}),",
3726
- " };",
3727
- " });",
3728
- " return {",
3729
- " selector: selectorFor(form),",
3730
- " action: form.getAttribute('action') || undefined,",
3731
- " method: form.getAttribute('method') || undefined,",
3732
- " fields,",
3733
- " };",
3734
- " });",
3735
- " const localStorage = readStorage(window.localStorage, 100);",
3736
- " const sessionStorage = readStorage(window.sessionStorage, 100);",
3737
- " const cookies = (document.cookie ? document.cookie.split(';') : [])",
3738
- " .map((entry) => entry.trim())",
3739
- " .filter((entry) => entry.length > 0)",
3740
- " .slice(0, 50)",
3741
- " .map((entry) => {",
3742
- " const [key, ...rest] = entry.split('=');",
3743
- " return { key, value: truncate(rest.join('='), 500) };",
3744
- " });",
3745
- " return { forms, localStorage, sessionStorage, cookies };",
3746
- "})()"
3747
- ].join("\n");
3748
-
3749
3838
  // packages/core/src/target-matching.ts
3750
3839
  var normalizeText = (value) => (value ?? "").trim().toLowerCase();
3751
3840
  var normalizeUrl = (value) => {
@@ -3851,6 +3940,199 @@ var pickBestTarget = (candidates, hint, now = Date.now()) => {
3851
3940
  return ranked.length > 0 ? ranked[0] : null;
3852
3941
  };
3853
3942
 
3943
+ // packages/core/src/inspect/target-selection.ts
3944
+ var readTargetHintInput = (target) => {
3945
+ if (!target) {
3946
+ return void 0;
3947
+ }
3948
+ const url = typeof target.url === "string" ? target.url : void 0;
3949
+ const title = typeof target.title === "string" ? target.title : void 0;
3950
+ const tabIdRaw = target.tab_id ?? target.tabId;
3951
+ const tabId = typeof tabIdRaw === "number" ? tabIdRaw : void 0;
3952
+ const lastActiveAtRaw = target.last_active_at ?? target.lastActiveAt;
3953
+ const lastActiveAt = typeof lastActiveAtRaw === "string" ? lastActiveAtRaw : void 0;
3954
+ if (!url && !title && tabId === void 0 && !lastActiveAt) {
3955
+ return void 0;
3956
+ }
3957
+ return { url, title, tabId, lastActiveAt };
3958
+ };
3959
+ var readSessionTargetHint = (registry, sessionId) => {
3960
+ if (typeof sessionId !== "string") {
3961
+ return void 0;
3962
+ }
3963
+ const selectedTabId = registry.get(sessionId)?.selectedTabId;
3964
+ return typeof selectedTabId === "number" && Number.isFinite(selectedTabId) ? { tabId: selectedTabId } : void 0;
3965
+ };
3966
+ var resolveInspectTargetHint = (options) => {
3967
+ const explicit = readTargetHintInput(options.target);
3968
+ if (explicit) {
3969
+ return explicit;
3970
+ }
3971
+ const sessionHint = readSessionTargetHint(
3972
+ options.registry,
3973
+ options.sessionId
3974
+ );
3975
+ if (sessionHint) {
3976
+ return sessionHint;
3977
+ }
3978
+ const tabs = options.extensionBridge?.getStatus().tabs ?? [];
3979
+ return deriveHintFromTabs(tabs);
3980
+ };
3981
+ var selectInspectTab = (options) => {
3982
+ if (!options.extensionBridge || !options.extensionBridge.isConnected()) {
3983
+ throw new InspectError(
3984
+ "EXTENSION_DISCONNECTED",
3985
+ "Extension is not connected.",
3986
+ {
3987
+ retryable: true
3988
+ }
3989
+ );
3990
+ }
3991
+ const tabs = options.extensionBridge.getStatus().tabs ?? [];
3992
+ if (!Array.isArray(tabs) || tabs.length === 0) {
3993
+ throw new InspectError("TAB_NOT_FOUND", "No tabs available to inspect.");
3994
+ }
3995
+ const effectiveHint = options.targetHint ?? readSessionTargetHint(options.registry, options.sessionId);
3996
+ if (typeof effectiveHint?.tabId === "number" && Number.isFinite(effectiveHint.tabId)) {
3997
+ const tab2 = tabs.find((entry) => entry.tab_id === effectiveHint.tabId);
3998
+ if (!tab2) {
3999
+ throw new InspectError(
4000
+ "TAB_NOT_FOUND",
4001
+ `No matching tab found for tab_id ${effectiveHint.tabId}.`,
4002
+ { details: { tab_id: effectiveHint.tabId } }
4003
+ );
4004
+ }
4005
+ return { tabId: effectiveHint.tabId, tab: tab2 };
4006
+ }
4007
+ const candidates = tabs.map((tab2) => ({
4008
+ id: String(tab2.tab_id),
4009
+ url: tab2.url ?? "",
4010
+ title: tab2.title,
4011
+ lastSeenAt: tab2.last_active_at ? Date.parse(tab2.last_active_at) : void 0
4012
+ }));
4013
+ const ranked = pickBestTarget(candidates, effectiveHint);
4014
+ if (!ranked) {
4015
+ throw new InspectError("TAB_NOT_FOUND", "No matching tab found.");
4016
+ }
4017
+ const tabId = Number(ranked.candidate.id);
4018
+ if (!Number.isFinite(tabId)) {
4019
+ throw new InspectError("TAB_NOT_FOUND", "Resolved tab id is invalid.");
4020
+ }
4021
+ const tab = tabs.find((entry) => entry.tab_id === tabId) ?? tabs[0];
4022
+ const warnings = [];
4023
+ if (!effectiveHint) {
4024
+ warnings.push("No target hint provided; using the most recent tab.");
4025
+ } else if (ranked.score < 20) {
4026
+ warnings.push("Weak target match; using best available tab.");
4027
+ }
4028
+ return {
4029
+ tabId,
4030
+ tab,
4031
+ warnings: warnings.length > 0 ? warnings : void 0
4032
+ };
4033
+ };
4034
+
4035
+ // packages/core/src/page-state-script.ts
4036
+ var PAGE_STATE_SCRIPT = [
4037
+ "(() => {",
4038
+ " const escape = (value) => {",
4039
+ " if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {",
4040
+ " return CSS.escape(value);",
4041
+ " }",
4042
+ ` return String(value).replace(/["'\\\\]/g, '\\\\$&');`,
4043
+ " };",
4044
+ " const truncate = (value, max) => {",
4045
+ " const text = String(value ?? '');",
4046
+ " return text.length > max ? text.slice(0, max) : text;",
4047
+ " };",
4048
+ " const selectorFor = (element) => {",
4049
+ " if (element.id) {",
4050
+ " return `#${escape(element.id)}`;",
4051
+ " }",
4052
+ " const name = element.getAttribute('name');",
4053
+ " if (name) {",
4054
+ ' return `${element.tagName.toLowerCase()}[name="${escape(name)}"]`;',
4055
+ " }",
4056
+ " const parts = [];",
4057
+ " let node = element;",
4058
+ " while (node && node.nodeType === 1 && parts.length < 4) {",
4059
+ " let part = node.tagName.toLowerCase();",
4060
+ " const parent = node.parentElement;",
4061
+ " if (parent) {",
4062
+ " const siblings = Array.from(parent.children).filter(",
4063
+ " (child) => child.tagName === node.tagName",
4064
+ " );",
4065
+ " if (siblings.length > 1) {",
4066
+ " part += `:nth-of-type(${siblings.indexOf(node) + 1})`;",
4067
+ " }",
4068
+ " }",
4069
+ " parts.unshift(part);",
4070
+ " node = parent;",
4071
+ " }",
4072
+ " return parts.join('>');",
4073
+ " };",
4074
+ " const readStorage = (storage, limit) => {",
4075
+ " try {",
4076
+ " return Object.keys(storage)",
4077
+ " .slice(0, limit)",
4078
+ " .map((key) => ({",
4079
+ " key,",
4080
+ " value: truncate(storage.getItem(key), 500),",
4081
+ " }));",
4082
+ " } catch {",
4083
+ " return [];",
4084
+ " }",
4085
+ " };",
4086
+ " const forms = Array.from(document.querySelectorAll('form')).map((form) => {",
4087
+ " const fields = Array.from(form.elements)",
4088
+ " .filter((element) => element && element.tagName)",
4089
+ " .map((element) => {",
4090
+ " const tag = element.tagName.toLowerCase();",
4091
+ " const type = 'type' in element && element.type ? element.type : tag;",
4092
+ " const name = element.name || element.getAttribute('name') || element.id || '';",
4093
+ " let value = '';",
4094
+ " let options;",
4095
+ " if (tag === 'select') {",
4096
+ " const select = element;",
4097
+ " value = select.value ?? '';",
4098
+ " options = Array.from(select.options).map((option) => option.text);",
4099
+ " } else if (tag === 'input' && element.type === 'password') {",
4100
+ " value = '[redacted]';",
4101
+ " } else if (tag === 'input' || tag === 'textarea') {",
4102
+ " value = element.value ?? '';",
4103
+ " } else if (element.isContentEditable) {",
4104
+ " value = element.textContent ?? '';",
4105
+ " } else if ('value' in element) {",
4106
+ " value = element.value ?? '';",
4107
+ " }",
4108
+ " return {",
4109
+ " name,",
4110
+ " type,",
4111
+ " value: truncate(value, 500),",
4112
+ " ...(options ? { options } : {}),",
4113
+ " };",
4114
+ " });",
4115
+ " return {",
4116
+ " selector: selectorFor(form),",
4117
+ " action: form.getAttribute('action') || undefined,",
4118
+ " method: form.getAttribute('method') || undefined,",
4119
+ " fields,",
4120
+ " };",
4121
+ " });",
4122
+ " const localStorage = readStorage(window.localStorage, 100);",
4123
+ " const sessionStorage = readStorage(window.sessionStorage, 100);",
4124
+ " const cookies = (document.cookie ? document.cookie.split(';') : [])",
4125
+ " .map((entry) => entry.trim())",
4126
+ " .filter((entry) => entry.length > 0)",
4127
+ " .slice(0, 50)",
4128
+ " .map((entry) => {",
4129
+ " const [key, ...rest] = entry.split('=');",
4130
+ " return { key, value: truncate(rest.join('='), 500) };",
4131
+ " });",
4132
+ " return { forms, localStorage, sessionStorage, cookies };",
4133
+ "})()"
4134
+ ].join("\n");
4135
+
3854
4136
  // packages/core/src/inspect/service.ts
3855
4137
  var DEFAULT_MAX_SNAPSHOTS_PER_SESSION = 20;
3856
4138
  var DEFAULT_MAX_SNAPSHOT_HISTORY = 100;
@@ -3895,7 +4177,7 @@ var InspectService = class {
3895
4177
  async reconnect(sessionId) {
3896
4178
  try {
3897
4179
  this.requireSession(sessionId);
3898
- const selection = await this.resolveTab();
4180
+ const selection = await this.resolveTab(sessionId);
3899
4181
  const debuggerBridge = this.ensureDebugger();
3900
4182
  const result = await debuggerBridge.attach(selection.tabId);
3901
4183
  if (result.ok) {
@@ -3914,7 +4196,7 @@ var InspectService = class {
3914
4196
  }
3915
4197
  async domSnapshot(input) {
3916
4198
  this.requireSession(input.sessionId);
3917
- const selection = await this.resolveTab(input.targetHint);
4199
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
3918
4200
  const debuggerCommand = this.debuggerCommand.bind(this);
3919
4201
  const work = async () => {
3920
4202
  if (input.format === "html") {
@@ -3964,20 +4246,11 @@ var InspectService = class {
3964
4246
  );
3965
4247
  selectorWarnings.push(...resolved.warnings ?? []);
3966
4248
  if (!resolved.nodeId) {
3967
- let refWarnings2 = [];
3968
- try {
3969
- refWarnings2 = await applySnapshotRefs(
3970
- selection.tabId,
3971
- /* @__PURE__ */ new Map(),
3972
- debuggerCommand
3973
- );
3974
- } catch {
3975
- refWarnings2 = ["Failed to clear prior snapshot refs."];
3976
- }
4249
+ const refWarnings = (await clearSnapshotRefArtifacts(selection.tabId, debuggerCommand)).warnings;
3977
4250
  const warnings2 = [
3978
4251
  ...selection.warnings ?? [],
3979
4252
  ...selectorWarnings,
3980
- ...refWarnings2
4253
+ ...refWarnings
3981
4254
  ];
3982
4255
  return {
3983
4256
  format: "ax",
@@ -4014,16 +4287,28 @@ var InspectService = class {
4014
4287
  }
4015
4288
  }
4016
4289
  const refMap = assignRefsToAxSnapshot(snapshot);
4017
- const refWarnings = await applySnapshotRefs(
4290
+ const clearResult = await clearSnapshotRefArtifacts(
4291
+ selection.tabId,
4292
+ debuggerCommand
4293
+ );
4294
+ const refResult = await applySnapshotRefAttributes(
4018
4295
  selection.tabId,
4019
4296
  refMap,
4020
4297
  debuggerCommand
4021
4298
  );
4299
+ const persistResult = refMap.size === 0 ? { warnings: [] } : await persistSnapshotRefRegistry(
4300
+ selection.tabId,
4301
+ refResult.appliedBindings,
4302
+ debuggerCommand
4303
+ );
4304
+ pruneUnappliedRefsFromSnapshot(snapshot, refResult.appliedRefs);
4022
4305
  const warnings = [
4023
4306
  ...selection.warnings ?? [],
4024
4307
  ...selectorWarnings,
4025
4308
  ...truncationWarnings,
4026
- ...refWarnings ?? []
4309
+ ...clearResult.warnings,
4310
+ ...refResult.warnings,
4311
+ ...persistResult.warnings
4027
4312
  ];
4028
4313
  return {
4029
4314
  format: "ax",
@@ -4149,7 +4434,7 @@ var InspectService = class {
4149
4434
  }
4150
4435
  async consoleList(input) {
4151
4436
  this.requireSession(input.sessionId);
4152
- const selection = await this.resolveTab(input.targetHint);
4437
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4153
4438
  await this.enableConsole(selection.tabId);
4154
4439
  const events = this.ensureDebugger().getConsoleEvents(selection.tabId);
4155
4440
  const entries = events.map((event) => toConsoleEntry(event)).filter((entry) => entry !== null);
@@ -4162,7 +4447,7 @@ var InspectService = class {
4162
4447
  }
4163
4448
  async networkHar(input) {
4164
4449
  this.requireSession(input.sessionId);
4165
- const selection = await this.resolveTab(input.targetHint);
4450
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4166
4451
  await this.enableNetwork(selection.tabId);
4167
4452
  const events = this.ensureDebugger().getNetworkEvents(selection.tabId);
4168
4453
  const har = buildHar(events, selection.tab.title);
@@ -4189,7 +4474,7 @@ var InspectService = class {
4189
4474
  }
4190
4475
  async evaluate(input) {
4191
4476
  this.requireSession(input.sessionId);
4192
- const selection = await this.resolveTab(input.targetHint);
4477
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4193
4478
  const expression = input.expression ?? "undefined";
4194
4479
  await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
4195
4480
  const result = await this.debuggerCommand(
@@ -4218,7 +4503,7 @@ var InspectService = class {
4218
4503
  }
4219
4504
  async extractContent(input) {
4220
4505
  this.requireSession(input.sessionId);
4221
- const selection = await this.resolveTab(input.targetHint);
4506
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4222
4507
  const debuggerCommand = this.debuggerCommand.bind(this);
4223
4508
  const html = await captureHtml(selection.tabId, {
4224
4509
  debuggerCommand,
@@ -4233,56 +4518,30 @@ var InspectService = class {
4233
4518
  }
4234
4519
  });
4235
4520
  const url = selection.tab.url ?? "about:blank";
4236
- let article = null;
4237
4521
  try {
4238
- const dom = new import_jsdom.JSDOM(html, { url });
4239
- const reader = new import_readability.Readability(dom.window.document);
4240
- article = reader.parse();
4241
- } catch {
4242
- const err = new InspectError(
4243
- "EVALUATION_FAILED",
4244
- "Failed to parse page content.",
4245
- { retryable: false }
4246
- );
4247
- this.recordError(err);
4248
- throw err;
4249
- }
4250
- if (!article) {
4251
- const err = new InspectError(
4252
- "NOT_SUPPORTED",
4253
- "Readability could not extract content.",
4254
- { retryable: false }
4255
- );
4256
- this.recordError(err);
4257
- throw err;
4258
- }
4259
- let content = "";
4260
- if (input.format === "article_json") {
4261
- content = JSON.stringify(article, null, 2);
4262
- } else if (input.format === "text") {
4263
- content = article.textContent ?? "";
4264
- } else {
4265
- const turndown = new import_turndown.default();
4266
- content = turndown.turndown(article.content ?? "");
4522
+ const { article, semanticMainCandidate } = parseExtractContentSource({
4523
+ html,
4524
+ url
4525
+ });
4526
+ const output = renderExtractContent({
4527
+ format: input.format,
4528
+ article,
4529
+ semanticMainCandidate,
4530
+ includeMetadata: input.includeMetadata,
4531
+ warnings: selection.warnings
4532
+ });
4533
+ this.markInspectConnected(input.sessionId);
4534
+ return output;
4535
+ } catch (error) {
4536
+ if (error instanceof InspectError) {
4537
+ this.recordError(error);
4538
+ }
4539
+ throw error;
4267
4540
  }
4268
- const warnings = selection.warnings ?? [];
4269
- const includeMetadata = input.includeMetadata ?? true;
4270
- const output = {
4271
- content,
4272
- ...includeMetadata ? {
4273
- title: article.title ?? void 0,
4274
- byline: article.byline ?? void 0,
4275
- excerpt: article.excerpt ?? void 0,
4276
- siteName: article.siteName ?? void 0
4277
- } : {},
4278
- ...warnings.length > 0 ? { warnings } : {}
4279
- };
4280
- this.markInspectConnected(input.sessionId);
4281
- return output;
4282
4541
  }
4283
4542
  async pageState(input) {
4284
4543
  this.requireSession(input.sessionId);
4285
- const selection = await this.resolveTab(input.targetHint);
4544
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4286
4545
  await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
4287
4546
  const expression = PAGE_STATE_SCRIPT;
4288
4547
  const result = await this.debuggerCommand(
@@ -4321,7 +4580,7 @@ var InspectService = class {
4321
4580
  }
4322
4581
  async performanceMetrics(input) {
4323
4582
  this.requireSession(input.sessionId);
4324
- const selection = await this.resolveTab(input.targetHint);
4583
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4325
4584
  await this.debuggerCommand(selection.tabId, "Performance.enable", {});
4326
4585
  const result = await this.debuggerCommand(
4327
4586
  selection.tabId,
@@ -4338,9 +4597,13 @@ var InspectService = class {
4338
4597
  }
4339
4598
  async screenshot(input) {
4340
4599
  this.requireSession(input.sessionId);
4341
- const selection = await this.resolveTab(input.targetHint);
4600
+ const selection = await this.resolveTab(input.sessionId, input.targetHint);
4342
4601
  const format = input.format ?? "png";
4343
- const writeArtifact = async (data2) => {
4602
+ const createScreenshotError = (code, message, retryable = false, details) => new InspectError(code, message, {
4603
+ retryable,
4604
+ ...details ? { details } : {}
4605
+ });
4606
+ const writeArtifact = async (data) => {
4344
4607
  try {
4345
4608
  const rootDir = await ensureArtifactRootDir(input.sessionId);
4346
4609
  const artifactId = (0, import_crypto3.randomUUID)();
@@ -4349,7 +4612,7 @@ var InspectService = class {
4349
4612
  rootDir,
4350
4613
  `screenshot-${artifactId}.${extension}`
4351
4614
  );
4352
- await (0, import_promises3.writeFile)(filePath, Buffer.from(data2, "base64"));
4615
+ await (0, import_promises3.writeFile)(filePath, Buffer.from(data, "base64"));
4353
4616
  const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
4354
4617
  const output = {
4355
4618
  artifact_id: artifactId,
@@ -4367,143 +4630,138 @@ var InspectService = class {
4367
4630
  throw error;
4368
4631
  }
4369
4632
  };
4370
- if (input.selector) {
4633
+ const captureViaExtension = async (mode, failureMessage) => {
4371
4634
  if (!this.extensionBridge?.request) {
4372
- const error = new InspectError(
4635
+ throw createScreenshotError(
4373
4636
  "NOT_SUPPORTED",
4374
- "Element screenshots require an extension that supports drive.screenshot."
4637
+ "Screenshots require an extension that supports drive.screenshot."
4375
4638
  );
4376
- this.recordError(error);
4377
- throw error;
4378
4639
  }
4379
4640
  const response = await this.extensionBridge.request(
4380
4641
  "drive.screenshot",
4381
4642
  {
4382
4643
  tab_id: selection.tabId,
4383
- mode: "element",
4384
- selector: input.selector,
4644
+ mode,
4645
+ ...input.selector ? { selector: input.selector } : {},
4385
4646
  format,
4386
4647
  ...typeof input.quality === "number" ? { quality: input.quality } : {}
4387
4648
  },
4388
4649
  12e4
4389
4650
  );
4390
4651
  if (response.status === "error") {
4391
- const error = new InspectError(
4652
+ const error = createScreenshotError(
4392
4653
  response.error?.code ?? "INSPECT_UNAVAILABLE",
4393
- response.error?.message ?? "Failed to capture element screenshot.",
4394
- {
4395
- retryable: response.error?.retryable ?? false,
4396
- ...response.error?.details ? { details: response.error.details } : {}
4397
- }
4654
+ response.error?.message ?? failureMessage,
4655
+ response.error?.retryable ?? false,
4656
+ response.error?.details
4657
+ );
4658
+ this.recordError(error);
4659
+ throw error;
4660
+ }
4661
+ const result = response.result;
4662
+ if (!result?.data_base64 || typeof result.data_base64 !== "string") {
4663
+ const error = createScreenshotError(
4664
+ "INSPECT_UNAVAILABLE",
4665
+ failureMessage
4398
4666
  );
4399
4667
  this.recordError(error);
4400
4668
  throw error;
4401
4669
  }
4402
- const result2 = response.result;
4403
- if (!result2?.data_base64 || typeof result2.data_base64 !== "string") {
4670
+ return await writeArtifact(result.data_base64);
4671
+ };
4672
+ const shouldFallbackFromExtensionScreenshot = (error) => [
4673
+ "NOT_SUPPORTED",
4674
+ "NOT_IMPLEMENTED",
4675
+ "INSPECT_UNAVAILABLE",
4676
+ "PERMISSION_REQUIRED",
4677
+ "RATE_LIMITED"
4678
+ ].includes(error.code);
4679
+ const shouldPreserveExtensionScreenshotError = (fallbackError) => [
4680
+ "INSPECT_UNAVAILABLE",
4681
+ "ATTACH_DENIED",
4682
+ "NOT_SUPPORTED",
4683
+ "NOT_IMPLEMENTED"
4684
+ ].includes(fallbackError.code);
4685
+ const captureViaDebugger = async () => {
4686
+ await this.debuggerCommand(selection.tabId, "Page.enable", {});
4687
+ let captureParams = {
4688
+ format,
4689
+ fromSurface: true
4690
+ };
4691
+ if (format !== "png" && typeof input.quality === "number") {
4692
+ captureParams = { ...captureParams, quality: input.quality };
4693
+ }
4694
+ if (input.target === "full") {
4695
+ const layout = await this.debuggerCommand(
4696
+ selection.tabId,
4697
+ "Page.getLayoutMetrics",
4698
+ {}
4699
+ );
4700
+ const contentSize = layout?.contentSize;
4701
+ if (contentSize) {
4702
+ captureParams = {
4703
+ ...captureParams,
4704
+ clip: {
4705
+ x: 0,
4706
+ y: 0,
4707
+ width: contentSize.width,
4708
+ height: contentSize.height,
4709
+ scale: 1
4710
+ }
4711
+ };
4712
+ } else {
4713
+ captureParams = { ...captureParams, captureBeyondViewport: true };
4714
+ }
4715
+ }
4716
+ const result = await this.debuggerCommand(
4717
+ selection.tabId,
4718
+ "Page.captureScreenshot",
4719
+ captureParams
4720
+ );
4721
+ const data = result.data;
4722
+ if (!data) {
4404
4723
  const error = new InspectError(
4405
4724
  "INSPECT_UNAVAILABLE",
4406
- "Failed to capture element screenshot."
4725
+ "Failed to capture screenshot.",
4726
+ { retryable: false }
4407
4727
  );
4408
4728
  this.recordError(error);
4409
4729
  throw error;
4410
4730
  }
4411
- return await writeArtifact(result2.data_base64);
4731
+ return await writeArtifact(data);
4732
+ };
4733
+ if (input.selector) {
4734
+ return await captureViaExtension(
4735
+ "element",
4736
+ "Failed to capture element screenshot."
4737
+ );
4412
4738
  }
4413
- if (input.target === "full" && this.extensionBridge?.request) {
4739
+ let extensionScreenshotError;
4740
+ if (this.extensionBridge?.request) {
4414
4741
  try {
4415
- const response = await this.extensionBridge.request(
4416
- "drive.screenshot",
4417
- {
4418
- tab_id: selection.tabId,
4419
- mode: "full_page",
4420
- format,
4421
- ...typeof input.quality === "number" ? { quality: input.quality } : {}
4422
- },
4423
- 12e4
4742
+ return await captureViaExtension(
4743
+ input.target === "full" ? "full_page" : "viewport",
4744
+ input.target === "full" ? "Failed to capture full page screenshot." : "Failed to capture viewport screenshot."
4424
4745
  );
4425
- if (response.status === "error") {
4426
- const error = new InspectError(
4427
- response.error?.code ?? "INSPECT_UNAVAILABLE",
4428
- response.error?.message ?? "Failed to capture full page screenshot.",
4429
- {
4430
- retryable: response.error?.retryable ?? false,
4431
- ...response.error?.details ? { details: response.error.details } : {}
4432
- }
4433
- );
4434
- this.recordError(error);
4746
+ } catch (error) {
4747
+ if (!(error instanceof InspectError)) {
4435
4748
  throw error;
4436
4749
  }
4437
- const result2 = response.result;
4438
- if (!result2?.data_base64 || typeof result2.data_base64 !== "string") {
4439
- const error = new InspectError(
4440
- "INSPECT_UNAVAILABLE",
4441
- "Failed to capture full page screenshot."
4442
- );
4443
- this.recordError(error);
4750
+ if (!shouldFallbackFromExtensionScreenshot(error)) {
4444
4751
  throw error;
4445
4752
  }
4446
- return await writeArtifact(result2.data_base64);
4447
- } catch (error) {
4448
- if (error instanceof InspectError) {
4449
- const code = String(error.code);
4450
- if (!error.retryable && ![
4451
- "NOT_SUPPORTED",
4452
- "NOT_IMPLEMENTED",
4453
- "INSPECT_UNAVAILABLE",
4454
- "RATE_LIMITED"
4455
- ].includes(code)) {
4456
- throw error;
4457
- }
4458
- }
4753
+ extensionScreenshotError = error;
4459
4754
  }
4460
4755
  }
4461
- await this.debuggerCommand(selection.tabId, "Page.enable", {});
4462
- let captureParams = {
4463
- format,
4464
- fromSurface: true
4465
- };
4466
- if (format !== "png" && typeof input.quality === "number") {
4467
- captureParams = { ...captureParams, quality: input.quality };
4468
- }
4469
- if (input.target === "full") {
4470
- const layout = await this.debuggerCommand(
4471
- selection.tabId,
4472
- "Page.getLayoutMetrics",
4473
- {}
4474
- );
4475
- const contentSize = layout?.contentSize;
4476
- if (contentSize) {
4477
- captureParams = {
4478
- ...captureParams,
4479
- clip: {
4480
- x: 0,
4481
- y: 0,
4482
- width: contentSize.width,
4483
- height: contentSize.height,
4484
- scale: 1
4485
- }
4486
- };
4487
- } else {
4488
- captureParams = { ...captureParams, captureBeyondViewport: true };
4756
+ try {
4757
+ return await captureViaDebugger();
4758
+ } catch (error) {
4759
+ if (extensionScreenshotError && error instanceof InspectError && shouldPreserveExtensionScreenshotError(error)) {
4760
+ this.recordError(extensionScreenshotError);
4761
+ throw extensionScreenshotError;
4489
4762
  }
4490
- }
4491
- const result = await this.debuggerCommand(
4492
- selection.tabId,
4493
- "Page.captureScreenshot",
4494
- captureParams
4495
- );
4496
- const data = result.data;
4497
- if (!data) {
4498
- const error = new InspectError(
4499
- "INSPECT_UNAVAILABLE",
4500
- "Failed to capture screenshot.",
4501
- { retryable: false }
4502
- );
4503
- this.recordError(error);
4504
4763
  throw error;
4505
4764
  }
4506
- return await writeArtifact(data);
4507
4765
  }
4508
4766
  ensureDebugger() {
4509
4767
  if (!this.debugger) {
@@ -4513,58 +4771,20 @@ var InspectService = class {
4513
4771
  }
4514
4772
  return this.debugger;
4515
4773
  }
4516
- async resolveTab(hint) {
4517
- if (!this.extensionBridge || !this.extensionBridge.isConnected()) {
4518
- const error = new InspectError(
4519
- "EXTENSION_DISCONNECTED",
4520
- "Extension is not connected.",
4521
- { retryable: true }
4522
- );
4523
- this.recordError(error);
4524
- throw error;
4525
- }
4526
- const tabs = this.extensionBridge.getStatus().tabs ?? [];
4527
- if (!Array.isArray(tabs) || tabs.length === 0) {
4528
- const error = new InspectError(
4529
- "TAB_NOT_FOUND",
4530
- "No tabs available to inspect."
4531
- );
4532
- this.recordError(error);
4533
- throw error;
4534
- }
4535
- const candidates = tabs.map((tab2) => ({
4536
- id: String(tab2.tab_id),
4537
- url: tab2.url ?? "",
4538
- title: tab2.title,
4539
- lastSeenAt: tab2.last_active_at ? Date.parse(tab2.last_active_at) : void 0
4540
- }));
4541
- const ranked = pickBestTarget(candidates, hint);
4542
- if (!ranked) {
4543
- const error = new InspectError("TAB_NOT_FOUND", "No matching tab found.");
4544
- this.recordError(error);
4545
- throw error;
4546
- }
4547
- const tabId = Number(ranked.candidate.id);
4548
- if (!Number.isFinite(tabId)) {
4549
- const error = new InspectError(
4550
- "TAB_NOT_FOUND",
4551
- "Resolved tab id is invalid."
4552
- );
4553
- this.recordError(error);
4774
+ async resolveTab(sessionId, hint) {
4775
+ try {
4776
+ return selectInspectTab({
4777
+ sessionId,
4778
+ targetHint: hint,
4779
+ registry: this.registry,
4780
+ extensionBridge: this.extensionBridge
4781
+ });
4782
+ } catch (error) {
4783
+ if (error instanceof InspectError) {
4784
+ this.recordError(error);
4785
+ }
4554
4786
  throw error;
4555
4787
  }
4556
- const tab = tabs.find((entry) => entry.tab_id === tabId) ?? tabs[0];
4557
- const warnings = [];
4558
- if (!hint) {
4559
- warnings.push("No target hint provided; using the most recent tab.");
4560
- } else if (ranked.score < 20) {
4561
- warnings.push("Weak target match; using best available tab.");
4562
- }
4563
- return {
4564
- tabId,
4565
- tab,
4566
- warnings: warnings.length > 0 ? warnings : void 0
4567
- };
4568
4788
  }
4569
4789
  async enableConsole(tabId) {
4570
4790
  await this.debuggerCommand(tabId, "Runtime.enable", {});
@@ -4667,7 +4887,11 @@ var sendArtifactsError = (res, code, message, details, retryable = false) => {
4667
4887
  });
4668
4888
  };
4669
4889
  var registerArtifactsRoutes = (router, options = {}) => {
4670
- const inspect = options.inspectService ?? (options.registry ? createInspectService({ registry: options.registry }) : void 0);
4890
+ const inspect = options.inspectService ?? (options.registry ? createInspectService({
4891
+ registry: options.registry,
4892
+ extensionBridge: options.extensionBridge,
4893
+ debuggerBridge: options.debuggerBridge
4894
+ }) : void 0);
4671
4895
  router.post("/artifacts/screenshot", async (req, res) => {
4672
4896
  if (!isRecord2(req.body)) {
4673
4897
  sendArtifactsError(
@@ -4761,6 +4985,7 @@ var readCapability = (capabilities, name) => {
4761
4985
  const candidate = capabilities[name];
4762
4986
  return typeof candidate === "boolean" ? candidate : void 0;
4763
4987
  };
4988
+ var hasCapturePermissionErrorReason = (details) => typeof details?.reason === "string" && details.reason === "capture_visible_tab_permission_required";
4764
4989
  var hasEndpoint = (endpoint) => Boolean(
4765
4990
  endpoint && typeof endpoint.host === "string" && endpoint.host.length > 0 && typeof endpoint.port === "number" && Number.isFinite(endpoint.port)
4766
4991
  );
@@ -4773,9 +4998,7 @@ var toRuntimeEndpoint = (endpoint) => {
4773
4998
  port: endpoint.port,
4774
4999
  base_url: endpoint.baseUrl,
4775
5000
  host_source: endpoint.hostSource,
4776
- port_source: endpoint.portSource,
4777
- metadata_path: endpoint.metadataPath,
4778
- isolated_mode: endpoint.isolatedMode
5001
+ port_source: endpoint.portSource
4779
5002
  };
4780
5003
  };
4781
5004
  var toRuntimeProcess = (process2) => {
@@ -4839,23 +5062,6 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4839
5062
  }
4840
5063
  });
4841
5064
  }
4842
- if (callerEndpoint?.metadataPath && coreEndpoint?.metadataPath) {
4843
- const matches = callerEndpoint.metadataPath === coreEndpoint.metadataPath;
4844
- checks.push({
4845
- name: "runtime.caller.metadata_path_match",
4846
- ok: matches,
4847
- message: matches ? "Caller metadata path matches the active core runtime metadata path." : "Caller metadata path differs from core runtime metadata path (shared core across worktrees).",
4848
- details: {
4849
- caller_metadata_path: callerEndpoint.metadataPath,
4850
- core_metadata_path: coreEndpoint.metadataPath
4851
- }
4852
- });
4853
- if (!matches) {
4854
- warnings.push(
4855
- "CLI is talking to a core process started from a different worktree metadata path. If daemon auto-start fails, retry with --no-daemon or enable isolated mode."
4856
- );
4857
- }
4858
- }
4859
5065
  if (extensionConnected && hasEndpoint(coreEndpoint) && hasEndpoint(extensionEndpoint)) {
4860
5066
  const matches = coreEndpoint.host === extensionEndpoint.host && coreEndpoint.port === extensionEndpoint.port;
4861
5067
  checks.push({
@@ -4903,7 +5109,7 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4903
5109
  checks.push({
4904
5110
  name: "inspect.capability",
4905
5111
  ok: capabilityNegotiated && inspectEnabled,
4906
- message: capabilityNegotiated && inspectEnabled ? "Inspect debugger capability is enabled." : capabilityNegotiated ? "Inspect debugger capability is disabled in extension options." : "Inspect capability is unknown until extension capability negotiation completes.",
5112
+ message: capabilityNegotiated && inspectEnabled ? "Inspect capability is available." : capabilityNegotiated ? "Inspect capability is unavailable from the connected extension/runtime." : "Inspect capability is unknown until extension capability negotiation completes.",
4907
5113
  details: {
4908
5114
  required_capabilities: ["debugger.attach", "debugger.command"],
4909
5115
  debugger_attach: inspectAttachCapability,
@@ -4912,7 +5118,7 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4912
5118
  });
4913
5119
  if (capabilityNegotiated && !inspectEnabled) {
4914
5120
  warnings.push(
4915
- "Inspect commands require debugger capability. Enable debugger-based inspect in extension options to use inspect.* routes."
5121
+ "Inspect commands are unavailable from the connected extension/runtime. Reload or update the extension, then verify inspect.capability before using inspect.* routes."
4916
5122
  );
4917
5123
  }
4918
5124
  }
@@ -4945,9 +5151,15 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4945
5151
  code: context.driveLastError.code,
4946
5152
  retryable: context.driveLastError.retryable,
4947
5153
  at: context.driveLastError.at,
5154
+ ...context.driveLastError.details ?? {},
4948
5155
  ...ageMs !== void 0 ? { age_ms: ageMs } : {}
4949
5156
  }
4950
5157
  });
5158
+ if (hasCapturePermissionErrorReason(context.driveLastError.details)) {
5159
+ warnings.push(
5160
+ "Screenshot capture permission is missing. Reload the Browser Bridge extension in chrome://extensions and retry screenshot capture."
5161
+ );
5162
+ }
4951
5163
  }
4952
5164
  if (context.inspectLastError) {
4953
5165
  const ageMs = getErrorAgeMs(context.inspectLastError.at);
@@ -4965,9 +5177,15 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
4965
5177
  code: context.inspectLastError.code,
4966
5178
  retryable: context.inspectLastError.retryable,
4967
5179
  at: context.inspectLastError.at,
5180
+ ...context.inspectLastError.details ?? {},
4968
5181
  ...ageMs !== void 0 ? { age_ms: ageMs } : {}
4969
5182
  }
4970
5183
  });
5184
+ if (hasCapturePermissionErrorReason(context.inspectLastError.details)) {
5185
+ warnings.push(
5186
+ "Screenshot capture permission is missing. Reload the Browser Bridge extension in chrome://extensions and retry screenshot capture."
5187
+ );
5188
+ }
4971
5189
  }
4972
5190
  if (context.recoveryAttempt) {
4973
5191
  checks.push({
@@ -5001,6 +5219,7 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
5001
5219
  } : void 0,
5002
5220
  extension: {
5003
5221
  connected: extensionConnected,
5222
+ extension_id: context.extension?.extensionId,
5004
5223
  version: context.extension?.version,
5005
5224
  last_seen_at: context.extension?.lastSeenAt
5006
5225
  },
@@ -5037,6 +5256,7 @@ var buildDiagnosticReport = (sessionId, context = {}) => {
5037
5256
  process: toRuntimeProcess(context.runtime.core.process)
5038
5257
  } : void 0,
5039
5258
  extension: context.runtime.extension ? {
5259
+ extension_id: context.runtime.extension.extensionId,
5040
5260
  version: context.runtime.extension.version,
5041
5261
  protocol_version: context.runtime.extension.protocolVersion,
5042
5262
  capability_negotiated: context.runtime.extension.capabilityNegotiated,
@@ -5084,12 +5304,78 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5084
5304
  sessions: { active: sessionsActive },
5085
5305
  extension: {
5086
5306
  connected: extensionStatus?.connected ?? false,
5307
+ ...extensionStatus?.extensionId ? { extension_id: extensionStatus.extensionId } : {},
5087
5308
  ...extensionStatus?.lastSeenAt ? { last_seen_at: extensionStatus.lastSeenAt } : {}
5088
5309
  }
5089
5310
  });
5090
5311
  };
5091
5312
  router.post("/health/check", handleHealthCheck);
5092
5313
  router.post("/health_check", handleHealthCheck);
5314
+ router.post("/diagnostics/enable_inspect", async (req, res) => {
5315
+ const body = req.body ?? {};
5316
+ if (!isRecord2(body)) {
5317
+ sendError(res, 400, {
5318
+ code: "INVALID_ARGUMENT",
5319
+ message: "Request body must be an object.",
5320
+ retryable: false
5321
+ });
5322
+ return;
5323
+ }
5324
+ const extensionIdRaw = body.extension_id;
5325
+ if (extensionIdRaw !== void 0 && typeof extensionIdRaw !== "string") {
5326
+ sendError(res, 400, {
5327
+ code: "INVALID_ARGUMENT",
5328
+ message: "extension_id must be a string when provided.",
5329
+ retryable: false,
5330
+ details: { field: "extension_id" }
5331
+ });
5332
+ return;
5333
+ }
5334
+ if (!options.extensionBridge) {
5335
+ sendError(res, 503, {
5336
+ code: "EXTENSION_DISCONNECTED",
5337
+ message: "Extension bridge is unavailable.",
5338
+ retryable: true
5339
+ });
5340
+ return;
5341
+ }
5342
+ try {
5343
+ const envelope2 = await options.extensionBridge.request("drive.set_debugger_capability", {
5344
+ enabled: true,
5345
+ ...extensionIdRaw ? { extension_id: extensionIdRaw } : {}
5346
+ });
5347
+ if (envelope2.status === "error") {
5348
+ const error = envelope2.error ?? {
5349
+ code: "INTERNAL",
5350
+ message: "Extension request failed.",
5351
+ retryable: false
5352
+ };
5353
+ sendError(res, 409, {
5354
+ code: error.code,
5355
+ message: error.message,
5356
+ retryable: error.retryable,
5357
+ details: error.details
5358
+ });
5359
+ return;
5360
+ }
5361
+ sendResult(res, envelope2.result ?? { ok: true, enabled: true });
5362
+ } catch (error) {
5363
+ if (error instanceof ExtensionBridgeError) {
5364
+ sendError(res, 409, {
5365
+ code: error.code,
5366
+ message: error.message,
5367
+ retryable: error.retryable,
5368
+ details: error.details
5369
+ });
5370
+ return;
5371
+ }
5372
+ sendError(res, 500, {
5373
+ code: "INTERNAL",
5374
+ message: "Unexpected inspect enablement error.",
5375
+ retryable: false
5376
+ });
5377
+ }
5378
+ });
5093
5379
  router.post("/diagnostics/doctor", (req, res) => {
5094
5380
  const body = req.body ?? {};
5095
5381
  if (!isRecord2(body)) {
@@ -5121,9 +5407,7 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5121
5407
  port: parsedDoctorInput.data.caller.endpoint.port,
5122
5408
  baseUrl: parsedDoctorInput.data.caller.endpoint.base_url,
5123
5409
  hostSource: parsedDoctorInput.data.caller.endpoint.host_source,
5124
- portSource: parsedDoctorInput.data.caller.endpoint.port_source,
5125
- metadataPath: parsedDoctorInput.data.caller.endpoint.metadata_path,
5126
- isolatedMode: parsedDoctorInput.data.caller.endpoint.isolated_mode
5410
+ portSource: parsedDoctorInput.data.caller.endpoint.port_source
5127
5411
  } : void 0,
5128
5412
  process: parsedDoctorInput.data.caller.process ? {
5129
5413
  component: parsedDoctorInput.data.caller.process.component,
@@ -5140,9 +5424,7 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5140
5424
  port: options.coreRuntime.port,
5141
5425
  baseUrl: `http://${options.coreRuntime.host}:${options.coreRuntime.port}`,
5142
5426
  hostSource: options.coreRuntime.hostSource,
5143
- portSource: options.coreRuntime.portSource,
5144
- metadataPath: options.coreRuntime.metadataPath,
5145
- isolatedMode: options.coreRuntime.isolatedMode
5427
+ portSource: options.coreRuntime.portSource
5146
5428
  } : void 0,
5147
5429
  process: {
5148
5430
  component: "core",
@@ -5185,11 +5467,13 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5185
5467
  const status = options.extensionBridge.getStatus();
5186
5468
  context.extension = {
5187
5469
  connected: status.connected,
5470
+ extensionId: status.extensionId,
5188
5471
  version: status.version,
5189
5472
  lastSeenAt: status.lastSeenAt
5190
5473
  };
5191
5474
  if (status.connected) {
5192
5475
  context.runtime.extension = {
5476
+ extensionId: status.extensionId,
5193
5477
  version: status.version,
5194
5478
  protocolVersion: status.protocolVersion,
5195
5479
  capabilityNegotiated: status.capabilityNegotiated,
@@ -5226,7 +5510,8 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5226
5510
  code: lastError.error.code,
5227
5511
  message: lastError.error.message,
5228
5512
  retryable: lastError.error.retryable,
5229
- at: lastError.at
5513
+ at: lastError.at,
5514
+ details: lastError.error.details
5230
5515
  };
5231
5516
  }
5232
5517
  }
@@ -5237,7 +5522,8 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5237
5522
  code: lastError.error.code,
5238
5523
  message: lastError.error.message,
5239
5524
  retryable: lastError.error.retryable,
5240
- at: lastError.at
5525
+ at: lastError.at,
5526
+ details: lastError.error.details
5241
5527
  };
5242
5528
  }
5243
5529
  }
@@ -5267,6 +5553,11 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5267
5553
  };
5268
5554
 
5269
5555
  // packages/core/src/routes/drive.ts
5556
+ var SUPPORTED_NAVIGATE_WAIT_MODES = [
5557
+ "none",
5558
+ "domcontentloaded",
5559
+ "networkidle"
5560
+ ];
5270
5561
  var parseBody = (schema, body) => {
5271
5562
  const result = schema.safeParse(body);
5272
5563
  if (result.success) {
@@ -5283,6 +5574,24 @@ var parseBody = (schema, body) => {
5283
5574
  };
5284
5575
  var makeNavigateHandler = (schema, drive, registry) => {
5285
5576
  return (req, res) => {
5577
+ if (isRecord2(req.body) && req.body.wait !== void 0) {
5578
+ const waitMode = req.body.wait;
5579
+ if (typeof waitMode !== "string" || !SUPPORTED_NAVIGATE_WAIT_MODES.includes(
5580
+ waitMode
5581
+ )) {
5582
+ sendError(res, errorStatus("INVALID_ARGUMENT"), {
5583
+ code: "INVALID_ARGUMENT",
5584
+ message: `Unsupported wait mode: ${String(waitMode)}.`,
5585
+ retryable: false,
5586
+ details: {
5587
+ field: "wait",
5588
+ supported_wait_modes: SUPPORTED_NAVIGATE_WAIT_MODES,
5589
+ mapped_wait_mode: "domcontentloaded"
5590
+ }
5591
+ });
5592
+ return;
5593
+ }
5594
+ }
5286
5595
  const parsed = parseBody(schema, req.body ?? {});
5287
5596
  if (parsed.error) {
5288
5597
  sendError(res, errorStatus("INVALID_ARGUMENT"), {
@@ -5539,27 +5848,6 @@ var parseBody2 = (schema, body) => {
5539
5848
  }
5540
5849
  };
5541
5850
  };
5542
- var readTargetHint = (target) => {
5543
- if (!target) {
5544
- return void 0;
5545
- }
5546
- const url = typeof target.url === "string" ? target.url : void 0;
5547
- const title = typeof target.title === "string" ? target.title : void 0;
5548
- const lastActiveAtRaw = target.last_active_at ?? target.lastActiveAt;
5549
- const lastActiveAt = typeof lastActiveAtRaw === "string" ? lastActiveAtRaw : void 0;
5550
- if (!url && !title && !lastActiveAt) {
5551
- return void 0;
5552
- }
5553
- return { url, title, lastActiveAt };
5554
- };
5555
- var resolveTargetHint = (target, options) => {
5556
- const explicit = readTargetHint(target);
5557
- if (explicit) {
5558
- return explicit;
5559
- }
5560
- const tabs = options.extensionBridge?.getStatus().tabs ?? [];
5561
- return deriveHintFromTabs(tabs);
5562
- };
5563
5851
  var makeHandler2 = (schema, handler) => async (req, res) => {
5564
5852
  const parsed = parseBody2(schema, req.body ?? {});
5565
5853
  if (parsed.error) {
@@ -5605,7 +5893,12 @@ var registerInspectRoutes = (router, options) => {
5605
5893
  compact: body.compact,
5606
5894
  maxNodes: body.max_nodes,
5607
5895
  selector: body.selector,
5608
- targetHint: resolveTargetHint(body.target, options)
5896
+ targetHint: resolveInspectTargetHint({
5897
+ sessionId: body.session_id,
5898
+ target: body.target,
5899
+ registry: options.registry,
5900
+ extensionBridge: options.extensionBridge
5901
+ })
5609
5902
  });
5610
5903
  })
5611
5904
  );
@@ -5618,7 +5911,12 @@ var registerInspectRoutes = (router, options) => {
5618
5911
  router.post(
5619
5912
  "/inspect/find",
5620
5913
  makeHandler2(InspectFindInputSchema, async (body) => {
5621
- const targetHint = resolveTargetHint(body.target, options);
5914
+ const targetHint = resolveInspectTargetHint({
5915
+ sessionId: body.session_id,
5916
+ target: body.target,
5917
+ registry: options.registry,
5918
+ extensionBridge: options.extensionBridge
5919
+ });
5622
5920
  if (body.kind === "role") {
5623
5921
  return await inspect.find({
5624
5922
  sessionId: body.session_id,
@@ -5651,7 +5949,12 @@ var registerInspectRoutes = (router, options) => {
5651
5949
  sessionId: body.session_id,
5652
5950
  format: body.format,
5653
5951
  includeMetadata: body.include_metadata,
5654
- targetHint: resolveTargetHint(body.target, options)
5952
+ targetHint: resolveInspectTargetHint({
5953
+ sessionId: body.session_id,
5954
+ target: body.target,
5955
+ registry: options.registry,
5956
+ extensionBridge: options.extensionBridge
5957
+ })
5655
5958
  });
5656
5959
  })
5657
5960
  );
@@ -5660,7 +5963,12 @@ var registerInspectRoutes = (router, options) => {
5660
5963
  makeHandler2(InspectPageStateInputSchema, async (body) => {
5661
5964
  return await inspect.pageState({
5662
5965
  sessionId: body.session_id,
5663
- targetHint: resolveTargetHint(body.target, options)
5966
+ targetHint: resolveInspectTargetHint({
5967
+ sessionId: body.session_id,
5968
+ target: body.target,
5969
+ registry: options.registry,
5970
+ extensionBridge: options.extensionBridge
5971
+ })
5664
5972
  });
5665
5973
  })
5666
5974
  );
@@ -5669,7 +5977,12 @@ var registerInspectRoutes = (router, options) => {
5669
5977
  makeHandler2(InspectConsoleListInputSchema, async (body) => {
5670
5978
  return await inspect.consoleList({
5671
5979
  sessionId: body.session_id,
5672
- targetHint: resolveTargetHint(body.target, options)
5980
+ targetHint: resolveInspectTargetHint({
5981
+ sessionId: body.session_id,
5982
+ target: body.target,
5983
+ registry: options.registry,
5984
+ extensionBridge: options.extensionBridge
5985
+ })
5673
5986
  });
5674
5987
  })
5675
5988
  );
@@ -5678,7 +5991,12 @@ var registerInspectRoutes = (router, options) => {
5678
5991
  makeHandler2(InspectNetworkHarInputSchema, async (body) => {
5679
5992
  return await inspect.networkHar({
5680
5993
  sessionId: body.session_id,
5681
- targetHint: resolveTargetHint(body.target, options)
5994
+ targetHint: resolveInspectTargetHint({
5995
+ sessionId: body.session_id,
5996
+ target: body.target,
5997
+ registry: options.registry,
5998
+ extensionBridge: options.extensionBridge
5999
+ })
5682
6000
  });
5683
6001
  })
5684
6002
  );
@@ -5688,7 +6006,12 @@ var registerInspectRoutes = (router, options) => {
5688
6006
  return await inspect.evaluate({
5689
6007
  sessionId: body.session_id,
5690
6008
  expression: body.expression,
5691
- targetHint: resolveTargetHint(body.target, options)
6009
+ targetHint: resolveInspectTargetHint({
6010
+ sessionId: body.session_id,
6011
+ target: body.target,
6012
+ registry: options.registry,
6013
+ extensionBridge: options.extensionBridge
6014
+ })
5692
6015
  });
5693
6016
  })
5694
6017
  );
@@ -5697,7 +6020,12 @@ var registerInspectRoutes = (router, options) => {
5697
6020
  makeHandler2(InspectPerformanceMetricsInputSchema, async (body) => {
5698
6021
  return await inspect.performanceMetrics({
5699
6022
  sessionId: body.session_id,
5700
- targetHint: resolveTargetHint(body.target, options)
6023
+ targetHint: resolveInspectTargetHint({
6024
+ sessionId: body.session_id,
6025
+ target: body.target,
6026
+ registry: options.registry,
6027
+ extensionBridge: options.extensionBridge
6028
+ })
5701
6029
  });
5702
6030
  })
5703
6031
  );
@@ -6126,6 +6454,7 @@ var createCoreServer = (options = {}) => {
6126
6454
  registerArtifactsRoutes(app, {
6127
6455
  registry,
6128
6456
  extensionBridge,
6457
+ debuggerBridge,
6129
6458
  inspectService: inspect
6130
6459
  });
6131
6460
  registerDiagnosticsRoutes(app, {
@@ -6148,27 +6477,12 @@ var createCoreServer = (options = {}) => {
6148
6477
  recoveryTracker
6149
6478
  };
6150
6479
  };
6151
- var CORE_PORT_PROBE_ATTEMPTS = 20;
6152
- var buildRuntimeMetadataForPersist = (runtime, resolvedPort) => ({
6480
+ var buildRuntimeMetadataForPersist = (runtime) => ({
6153
6481
  ...runtime.metadata ?? {},
6154
- host: runtime.host,
6155
- port: resolvedPort,
6156
- git_root: runtime.gitRoot ?? runtime.metadata?.git_root,
6157
- worktree_id: runtime.worktreeId ?? runtime.metadata?.worktree_id,
6482
+ extension_id: runtime.metadata?.extension_id,
6158
6483
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
6159
6484
  });
6160
- var resolveProbePortsForRuntime = (runtime) => {
6161
- if (!runtime.isolatedMode) {
6162
- return [runtime.port];
6163
- }
6164
- if (runtime.portSource === "metadata" || runtime.portSource === "deterministic") {
6165
- return createBoundedPortProbeSequence(
6166
- runtime.port,
6167
- CORE_PORT_PROBE_ATTEMPTS
6168
- );
6169
- }
6170
- return [runtime.port];
6171
- };
6485
+ var resolveProbePortsForRuntime = (runtime) => [runtime.port];
6172
6486
  var resolveSessionTtlMs = () => {
6173
6487
  const env = process.env.BROWSER_BRIDGE_SESSION_TTL_MS || process.env.BROWSER_VISION_SESSION_TTL_MS;
6174
6488
  if (env) {
@@ -6249,11 +6563,7 @@ var startCoreServer = async (options = {}) => {
6249
6563
  host: runtime.host,
6250
6564
  port: runtime.port,
6251
6565
  host_source: runtime.hostSource,
6252
- port_source: runtime.portSource,
6253
- metadata_path: runtime.metadataPath,
6254
- git_root: runtime.gitRoot,
6255
- worktree_id: runtime.worktreeId,
6256
- deterministic_port: runtime.deterministicPort
6566
+ port_source: runtime.portSource
6257
6567
  });
6258
6568
  const { app, registry, extensionBridge } = createCoreServer({
6259
6569
  registry: options.registry,
@@ -6283,10 +6593,9 @@ var startCoreServer = async (options = {}) => {
6283
6593
  duration_ms: Number(startupDurationMs.toFixed(3))
6284
6594
  });
6285
6595
  try {
6286
- writeRuntimeMetadata(
6287
- buildRuntimeMetadataForPersist(runtime, resolvedPort),
6288
- { metadataPath: runtime.metadataPath }
6289
- );
6596
+ writeRuntimeMetadata(buildRuntimeMetadataForPersist(runtime), {
6597
+ metadataPath: runtime.metadataPath
6598
+ });
6290
6599
  logger.info("core.runtime_metadata.persisted", {
6291
6600
  metadata_path: runtime.metadataPath,
6292
6601
  host: runtime.host,
@@ -6495,9 +6804,7 @@ startCoreServer({ ${startOptions.join(
6495
6804
  port: readiness.runtime.port,
6496
6805
  base_url: readiness.baseUrl,
6497
6806
  host_source: readiness.runtime.hostSource,
6498
- port_source: readiness.runtime.portSource,
6499
- metadata_path: readiness.runtime.metadataPath,
6500
- isolated_mode: readiness.runtime.isolatedMode
6807
+ port_source: readiness.runtime.portSource
6501
6808
  },
6502
6809
  process: {
6503
6810
  component: "mcp",
@@ -6543,6 +6850,70 @@ var toInternalErrorEnvelope = (error) => ({
6543
6850
  });
6544
6851
  var envelope = (schema) => successEnvelopeSchema(schema);
6545
6852
  var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6853
+ var readSessionId2 = (args) => {
6854
+ if (!isRecord3(args)) {
6855
+ return void 0;
6856
+ }
6857
+ const sessionId = args.session_id;
6858
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : void 0;
6859
+ };
6860
+ var supportsSessionMigration = (corePath) => corePath.startsWith("/drive/") || corePath.startsWith("/inspect/") || corePath.startsWith("/artifacts/") || corePath === "/diagnostics/doctor";
6861
+ var isSessionNotFoundEnvelope = (envelopeResult) => {
6862
+ if (envelopeResult.ok) {
6863
+ return false;
6864
+ }
6865
+ const details = envelopeResult.error.details;
6866
+ return envelopeResult.error.code === "NOT_FOUND" && isRecord3(details) && details.reason === "session_not_found";
6867
+ };
6868
+ var addSessionRecoveryHint = (envelopeResult) => {
6869
+ if (envelopeResult.ok) {
6870
+ return envelopeResult;
6871
+ }
6872
+ return {
6873
+ ok: false,
6874
+ error: {
6875
+ ...envelopeResult.error,
6876
+ details: {
6877
+ ...isRecord3(envelopeResult.error.details) ? envelopeResult.error.details : {},
6878
+ recover_action: "session.create"
6879
+ }
6880
+ }
6881
+ };
6882
+ };
6883
+ var addSessionMigrationNotice = (envelopeResult, staleSessionId, replacementSessionId) => {
6884
+ if (envelopeResult.ok) {
6885
+ if (!isRecord3(envelopeResult.result)) {
6886
+ return envelopeResult;
6887
+ }
6888
+ const warning = `Session ${staleSessionId} became stale after runtime switch; retried with ${replacementSessionId}.`;
6889
+ const existingWarnings = Array.isArray(envelopeResult.result.warnings) ? envelopeResult.result.warnings.filter(
6890
+ (item) => typeof item === "string"
6891
+ ) : [];
6892
+ return {
6893
+ ok: true,
6894
+ result: {
6895
+ ...envelopeResult.result,
6896
+ warnings: existingWarnings.includes(warning) ? existingWarnings : [...existingWarnings, warning],
6897
+ session_migration: {
6898
+ stale_session_id: staleSessionId,
6899
+ replacement_session_id: replacementSessionId
6900
+ }
6901
+ }
6902
+ };
6903
+ }
6904
+ return {
6905
+ ok: false,
6906
+ error: {
6907
+ ...envelopeResult.error,
6908
+ details: {
6909
+ ...isRecord3(envelopeResult.error.details) ? envelopeResult.error.details : {},
6910
+ stale_session_id: staleSessionId,
6911
+ replacement_session_id: replacementSessionId,
6912
+ recover_action: "session.recover"
6913
+ }
6914
+ }
6915
+ };
6916
+ };
6546
6917
  var addDeprecatedAliasWarning = (envelopeResult, deprecationAlias) => {
6547
6918
  if (!deprecationAlias || !envelopeResult.ok || typeof envelopeResult.result !== "object" || !envelopeResult.result) {
6548
6919
  return envelopeResult;
@@ -6953,10 +7324,26 @@ var createToolHandler = (clientProvider, corePath, deprecationAlias, transformIn
6953
7324
  void _extra;
6954
7325
  try {
6955
7326
  const client = typeof clientProvider === "function" ? await clientProvider() : clientProvider;
6956
- const envelopeResult = await client.post(
6957
- corePath,
6958
- transformInput ? transformInput(args) : args
6959
- );
7327
+ const transformedArgs = transformInput ? transformInput(args) : args;
7328
+ const staleSessionId = readSessionId2(transformedArgs);
7329
+ let envelopeResult = await client.post(corePath, transformedArgs);
7330
+ if (staleSessionId && supportsSessionMigration(corePath) && isSessionNotFoundEnvelope(envelopeResult) && isRecord3(transformedArgs)) {
7331
+ const createdSession = await client.post("/session/create", {});
7332
+ if (createdSession.ok && isRecord3(createdSession.result) && typeof createdSession.result.session_id === "string" && createdSession.result.session_id.length > 0) {
7333
+ const replacementSessionId = createdSession.result.session_id;
7334
+ const retryArgs = {
7335
+ ...transformedArgs,
7336
+ session_id: replacementSessionId
7337
+ };
7338
+ envelopeResult = addSessionMigrationNotice(
7339
+ await client.post(corePath, retryArgs),
7340
+ staleSessionId,
7341
+ replacementSessionId
7342
+ );
7343
+ } else {
7344
+ envelopeResult = addSessionRecoveryHint(envelopeResult);
7345
+ }
7346
+ }
6960
7347
  return toToolResult(
6961
7348
  addDeprecatedAliasWarning(envelopeResult, deprecationAlias)
6962
7349
  );
@@ -7097,7 +7484,7 @@ var DEFAULT_SERVER_NAME = "browser-bridge";
7097
7484
  var DEFAULT_SERVER_VERSION = "0.0.0";
7098
7485
  var ENV_MCP_EAGER = "BROWSER_BRIDGE_MCP_EAGER";
7099
7486
  var ENV_LEGACY_MCP_EAGER = "BROWSER_VISION_MCP_EAGER";
7100
- var parseBoolean2 = (value) => {
7487
+ var parseBoolean = (value) => {
7101
7488
  if (value === void 0) {
7102
7489
  return void 0;
7103
7490
  }
@@ -7114,7 +7501,7 @@ var resolveEagerMode = (explicit) => {
7114
7501
  if (typeof explicit === "boolean") {
7115
7502
  return explicit;
7116
7503
  }
7117
- const envValue = parseBoolean2(process.env[ENV_MCP_EAGER]) ?? parseBoolean2(process.env[ENV_LEGACY_MCP_EAGER]);
7504
+ const envValue = parseBoolean(process.env[ENV_MCP_EAGER]) ?? parseBoolean(process.env[ENV_LEGACY_MCP_EAGER]);
7118
7505
  return envValue ?? false;
7119
7506
  };
7120
7507
  var toCoreClientOptions = (options, logger) => ({