@btraut/browser-bridge 0.14.0 → 0.15.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
@@ -687,9 +687,10 @@ var createCoreReadinessController = (options = {}) => {
687
687
  return;
688
688
  }
689
689
  if (!ensurePromise) {
690
- ensurePromise = ensureCoreRunning().catch((error) => {
690
+ ensurePromise = (async () => {
691
+ await ensureCoreRunning();
692
+ })().finally(() => {
691
693
  ensurePromise = null;
692
- throw error;
693
694
  });
694
695
  }
695
696
  await ensurePromise;
@@ -1096,6 +1097,63 @@ var SessionCloseInputSchema = SessionIdSchema;
1096
1097
  var SessionCloseOutputSchema = import_zod3.z.object({
1097
1098
  ok: import_zod3.z.boolean()
1098
1099
  });
1100
+ var PermissionsModeSchema = import_zod3.z.enum(["granular", "bypass"]);
1101
+ var PermissionsRequestSourceSchema = import_zod3.z.enum(["cli", "mcp", "api"]);
1102
+ var PermissionsPendingRequestKindSchema = import_zod3.z.enum([
1103
+ "allow_site",
1104
+ "revoke_site",
1105
+ "set_mode"
1106
+ ]);
1107
+ var PermissionsPendingRequestStatusSchema = import_zod3.z.enum([
1108
+ "pending",
1109
+ "approved",
1110
+ "denied",
1111
+ "timed_out"
1112
+ ]);
1113
+ var PermissionsSiteEntrySchema = import_zod3.z.object({
1114
+ site: import_zod3.z.string().min(1),
1115
+ created_at: import_zod3.z.string().datetime(),
1116
+ last_used_at: import_zod3.z.string().datetime()
1117
+ });
1118
+ var PermissionsListInputSchema = import_zod3.z.object({}).strict().default({});
1119
+ var PermissionsListOutputSchema = import_zod3.z.object({
1120
+ sites: import_zod3.z.array(PermissionsSiteEntrySchema)
1121
+ });
1122
+ var PermissionsGetModeInputSchema = import_zod3.z.object({}).strict().default({});
1123
+ var PermissionsGetModeOutputSchema = import_zod3.z.object({
1124
+ mode: PermissionsModeSchema
1125
+ });
1126
+ var PermissionsPendingRequestSchema = import_zod3.z.object({
1127
+ request_id: import_zod3.z.string().min(1),
1128
+ kind: PermissionsPendingRequestKindSchema,
1129
+ status: PermissionsPendingRequestStatusSchema,
1130
+ requested_at: import_zod3.z.string().datetime(),
1131
+ site: import_zod3.z.string().min(1).optional(),
1132
+ mode: PermissionsModeSchema.optional(),
1133
+ source: PermissionsRequestSourceSchema.optional(),
1134
+ warning: import_zod3.z.string().optional(),
1135
+ message: import_zod3.z.string().optional()
1136
+ });
1137
+ var PermissionsListPendingRequestsInputSchema = import_zod3.z.object({}).strict().default({});
1138
+ var PermissionsListPendingRequestsOutputSchema = import_zod3.z.object({
1139
+ requests: import_zod3.z.array(PermissionsPendingRequestSchema)
1140
+ });
1141
+ var PermissionsRequestAllowSiteInputSchema = import_zod3.z.object({
1142
+ site: import_zod3.z.string().min(1),
1143
+ timeout_ms: import_zod3.z.number().int().positive().max(3e5).optional(),
1144
+ source: PermissionsRequestSourceSchema.optional()
1145
+ });
1146
+ var PermissionsRequestRevokeSiteInputSchema = import_zod3.z.object({
1147
+ site: import_zod3.z.string().min(1),
1148
+ timeout_ms: import_zod3.z.number().int().positive().max(3e5).optional(),
1149
+ source: PermissionsRequestSourceSchema.optional()
1150
+ });
1151
+ var PermissionsRequestSetModeInputSchema = import_zod3.z.object({
1152
+ mode: PermissionsModeSchema,
1153
+ timeout_ms: import_zod3.z.number().int().positive().max(3e5).optional(),
1154
+ source: PermissionsRequestSourceSchema.optional()
1155
+ });
1156
+ var PermissionsRequestOutputSchema = PermissionsPendingRequestSchema;
1099
1157
  var DriveWaitConditionSchema = import_zod3.z.object({
1100
1158
  kind: import_zod3.z.enum(["locator_visible", "text_present", "url_matches"]),
1101
1159
  value: import_zod3.z.string().min(1)
@@ -1282,6 +1340,12 @@ var FormFieldInfoSchema = import_zod3.z.object({
1282
1340
  name: import_zod3.z.string(),
1283
1341
  type: import_zod3.z.string(),
1284
1342
  value: import_zod3.z.string(),
1343
+ selector: import_zod3.z.string().optional(),
1344
+ label: import_zod3.z.string().optional(),
1345
+ placeholder: import_zod3.z.string().optional(),
1346
+ checked: import_zod3.z.boolean().optional(),
1347
+ disabled: import_zod3.z.boolean().optional(),
1348
+ visible: import_zod3.z.boolean().optional(),
1285
1349
  options: import_zod3.z.array(import_zod3.z.string()).optional()
1286
1350
  });
1287
1351
  var FormInfoSchema = import_zod3.z.object({
@@ -1294,7 +1358,31 @@ var StorageEntrySchema = import_zod3.z.object({
1294
1358
  key: import_zod3.z.string(),
1295
1359
  value: import_zod3.z.string()
1296
1360
  });
1361
+ var FocusedElementSchema = import_zod3.z.object({
1362
+ selector: import_zod3.z.string().optional(),
1363
+ name: import_zod3.z.string().optional(),
1364
+ label: import_zod3.z.string().optional(),
1365
+ role: import_zod3.z.string().optional(),
1366
+ type: import_zod3.z.string().optional(),
1367
+ text: import_zod3.z.string().optional()
1368
+ });
1369
+ var PageActionSchema = import_zod3.z.object({
1370
+ selector: import_zod3.z.string(),
1371
+ role: import_zod3.z.string(),
1372
+ name: import_zod3.z.string()
1373
+ });
1374
+ var StorageSummarySchema = import_zod3.z.object({
1375
+ localStorageCount: import_zod3.z.number().int().nonnegative(),
1376
+ sessionStorageCount: import_zod3.z.number().int().nonnegative(),
1377
+ cookieCount: import_zod3.z.number().int().nonnegative()
1378
+ });
1297
1379
  var PageStateSchema = import_zod3.z.object({
1380
+ url: import_zod3.z.string().optional(),
1381
+ title: import_zod3.z.string().optional(),
1382
+ readyState: import_zod3.z.string().optional(),
1383
+ focused: FocusedElementSchema.optional(),
1384
+ primaryActions: import_zod3.z.array(PageActionSchema).optional(),
1385
+ storageSummary: StorageSummarySchema.optional(),
1298
1386
  forms: import_zod3.z.array(FormInfoSchema),
1299
1387
  localStorage: import_zod3.z.array(StorageEntrySchema),
1300
1388
  sessionStorage: import_zod3.z.array(StorageEntrySchema),
@@ -1357,6 +1445,7 @@ var InspectFindOutputSchema = import_zod3.z.object({
1357
1445
  warnings: import_zod3.z.array(import_zod3.z.string()).optional()
1358
1446
  });
1359
1447
  var InspectPageStateInputSchema = SessionIdSchema.extend({
1448
+ include_values: import_zod3.z.boolean().default(false),
1360
1449
  target: TargetHintSchema.optional()
1361
1450
  });
1362
1451
  var InspectPageStateOutputSchema = PageStateSchema;
@@ -1367,6 +1456,7 @@ var InspectExtractContentFormatSchema = import_zod3.z.enum([
1367
1456
  ]);
1368
1457
  var InspectExtractContentInputSchema = SessionIdSchema.extend({
1369
1458
  format: InspectExtractContentFormatSchema.default("markdown"),
1459
+ consistency: InspectConsistencySchema.default("quiesce"),
1370
1460
  include_metadata: import_zod3.z.boolean().default(true),
1371
1461
  target: TargetHintSchema.optional()
1372
1462
  });
@@ -1379,6 +1469,7 @@ var InspectExtractContentOutputSchema = import_zod3.z.object({
1379
1469
  warnings: import_zod3.z.array(import_zod3.z.string()).optional()
1380
1470
  });
1381
1471
  var InspectConsoleListInputSchema = SessionIdSchema.extend({
1472
+ since: import_zod3.z.string().optional(),
1382
1473
  target: TargetHintSchema.optional()
1383
1474
  });
1384
1475
  var ConsoleSourceLocationSchema = import_zod3.z.object({
@@ -1983,6 +2074,7 @@ var ExtensionBridge = class {
1983
2074
  constructor(options = {}) {
1984
2075
  this.socket = null;
1985
2076
  this.pending = /* @__PURE__ */ new Map();
2077
+ this.readyWaiters = /* @__PURE__ */ new Set();
1986
2078
  this.connected = false;
1987
2079
  this.capabilityNegotiated = false;
1988
2080
  this.capabilities = {};
@@ -2031,6 +2123,36 @@ var ExtensionBridge = class {
2031
2123
  tabs: this.tabs
2032
2124
  };
2033
2125
  }
2126
+ async waitForReady(timeoutMs = 1500) {
2127
+ if (this.isReadyForRequests()) {
2128
+ return true;
2129
+ }
2130
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
2131
+ return this.isReadyForRequests();
2132
+ }
2133
+ return await new Promise((resolve3) => {
2134
+ let settled = false;
2135
+ const finish = (value) => {
2136
+ if (settled) {
2137
+ return;
2138
+ }
2139
+ settled = true;
2140
+ clearTimeout(timeout);
2141
+ this.readyWaiters.delete(waiter);
2142
+ resolve3(value);
2143
+ };
2144
+ const waiter = () => {
2145
+ if (this.isReadyForRequests()) {
2146
+ finish(true);
2147
+ }
2148
+ };
2149
+ const timeout = setTimeout(() => {
2150
+ finish(this.isReadyForRequests());
2151
+ }, timeoutMs);
2152
+ this.readyWaiters.add(waiter);
2153
+ waiter();
2154
+ });
2155
+ }
2034
2156
  async request(action, params, timeoutMs = 3e4) {
2035
2157
  const response = await this.requestInternal(action, params, timeoutMs);
2036
2158
  return response;
@@ -2102,6 +2224,7 @@ var ExtensionBridge = class {
2102
2224
  status: "request",
2103
2225
  params
2104
2226
  };
2227
+ const envelope2 = request;
2105
2228
  const response = await new Promise((resolve3, reject) => {
2106
2229
  const timeout = setTimeout(() => {
2107
2230
  this.pending.delete(id);
@@ -2119,7 +2242,7 @@ var ExtensionBridge = class {
2119
2242
  reject,
2120
2243
  timeout
2121
2244
  });
2122
- this.socket?.send(JSON.stringify(request));
2245
+ this.socket?.send(JSON.stringify(envelope2));
2123
2246
  });
2124
2247
  return response;
2125
2248
  }
@@ -2310,6 +2433,7 @@ var ExtensionBridge = class {
2310
2433
  expected: "drive.hello.capabilities"
2311
2434
  };
2312
2435
  }
2436
+ this.flushReadyWaiters();
2313
2437
  }
2314
2438
  }
2315
2439
  if (typeof message.action === "string" && message.action.startsWith("debugger.")) {
@@ -2325,6 +2449,23 @@ var ExtensionBridge = class {
2325
2449
  }
2326
2450
  }
2327
2451
  }
2452
+ isReadyForRequests() {
2453
+ return this.connected && this.capabilityNegotiated;
2454
+ }
2455
+ flushReadyWaiters() {
2456
+ if (!this.isReadyForRequests() || this.readyWaiters.size === 0) {
2457
+ return;
2458
+ }
2459
+ const waiters = Array.from(this.readyWaiters);
2460
+ this.readyWaiters.clear();
2461
+ for (const waiter of waiters) {
2462
+ try {
2463
+ waiter();
2464
+ } catch (error) {
2465
+ console.debug("Extension ready waiter failed.", error);
2466
+ }
2467
+ }
2468
+ }
2328
2469
  applyDriveConnected() {
2329
2470
  if (!this.registry) {
2330
2471
  return;
@@ -2379,7 +2520,8 @@ var toDriveError = (error) => ({
2379
2520
  // packages/core/src/drive.ts
2380
2521
  var LOOPBACK_NAVIGATION_PREFLIGHT_TIMEOUT_MS = 1200;
2381
2522
  var POST_CLICK_SETTLE_MS = 75;
2382
- var TRANSIENT_LOCATOR_RETRY_DELAY_MS = 150;
2523
+ var TRANSIENT_LOCATOR_RETRY_DELAYS_MS = [150, 300, 600];
2524
+ var EXTENSION_READY_WAIT_MS = 1500;
2383
2525
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2384
2526
  var ACTIONS_WITH_OPTIONAL_TAB_ID = /* @__PURE__ */ new Set([
2385
2527
  "drive.navigate",
@@ -2497,6 +2639,16 @@ var DriveController = class {
2497
2639
  async sleep(ms) {
2498
2640
  await new Promise((resolve3) => setTimeout(resolve3, ms));
2499
2641
  }
2642
+ async waitForBridgeReady() {
2643
+ if (this.bridge.isConnected()) {
2644
+ return true;
2645
+ }
2646
+ const waitForReady = this.bridge.waitForReady;
2647
+ if (typeof waitForReady === "function") {
2648
+ return await waitForReady.call(this.bridge, EXTENSION_READY_WAIT_MS);
2649
+ }
2650
+ return this.bridge.isConnected();
2651
+ }
2500
2652
  isTransientLocatorError(action, error) {
2501
2653
  if (action !== "drive.click") {
2502
2654
  return false;
@@ -2538,7 +2690,7 @@ var DriveController = class {
2538
2690
  error: errorInfo
2539
2691
  };
2540
2692
  }
2541
- if (!this.bridge.isConnected()) {
2693
+ if (!await this.waitForBridgeReady()) {
2542
2694
  const errorInfo = {
2543
2695
  code: "EXTENSION_DISCONNECTED",
2544
2696
  message: "Extension is not connected. Open Chrome with the Browser Bridge extension enabled, then retry.",
@@ -2604,9 +2756,10 @@ var DriveController = class {
2604
2756
  max_attempts: 1
2605
2757
  }
2606
2758
  };
2607
- if (attempt === 0 && this.isTransientLocatorError(action, errorInfo)) {
2759
+ if (attempt < TRANSIENT_LOCATOR_RETRY_DELAYS_MS.length && this.isTransientLocatorError(action, errorInfo)) {
2760
+ const delayMs = TRANSIENT_LOCATOR_RETRY_DELAYS_MS[attempt] ?? 0;
2608
2761
  attempt += 1;
2609
- await this.sleep(TRANSIENT_LOCATOR_RETRY_DELAY_MS);
2762
+ await this.sleep(delayMs);
2610
2763
  continue;
2611
2764
  }
2612
2765
  if (shouldRetryDriveOp({
@@ -2884,6 +3037,10 @@ var filterAxSnapshot = (snapshot, predicate) => {
2884
3037
  }
2885
3038
  return replaceAxNodes(snapshot, filtered);
2886
3039
  };
3040
+ var filterAxSnapshotByRefs = (snapshot, allowedRefs) => filterAxSnapshot(snapshot, (node) => {
3041
+ const ref = node.ref;
3042
+ return typeof ref === "string" && allowedRefs.has(ref);
3043
+ });
2887
3044
  var collectKeptDescendants = (nodeId, nodeById, keepIds, visited = /* @__PURE__ */ new Set()) => {
2888
3045
  if (visited.has(nodeId)) {
2889
3046
  return [];
@@ -3514,7 +3671,8 @@ var captureHtml = async (tabId, options) => {
3514
3671
  const result = await options.debuggerCommand(tabId, "Runtime.evaluate", {
3515
3672
  expression,
3516
3673
  returnByValue: true,
3517
- awaitPromise: true
3674
+ awaitPromise: true,
3675
+ ...typeof options.executionContextId === "number" ? { contextId: options.executionContextId } : {}
3518
3676
  });
3519
3677
  if (result && typeof result === "object" && "exceptionDetails" in result) {
3520
3678
  return options.onEvaluationFailed();
@@ -4033,111 +4191,209 @@ var selectInspectTab = (options) => {
4033
4191
  };
4034
4192
 
4035
4193
  // 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");
4194
+ var buildPageStateScript = (options) => {
4195
+ const includeValues = options?.includeValues === true;
4196
+ return [
4197
+ "(() => {",
4198
+ ` const includeValues = ${includeValues ? "true" : "false"};`,
4199
+ " const escape = (value) => {",
4200
+ " if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {",
4201
+ " return CSS.escape(value);",
4202
+ " }",
4203
+ ` return String(value).replace(/["'\\\\]/g, '\\\\$&');`,
4204
+ " };",
4205
+ " const truncate = (value, max) => {",
4206
+ " const text = String(value ?? '');",
4207
+ " return text.length > max ? text.slice(0, max) : text;",
4208
+ " };",
4209
+ ' const redact = (value) => (includeValues ? truncate(value, 500) : "[redacted]");',
4210
+ ' const textFor = (element) => truncate((element?.textContent || "").replace(/\\s+/g, " ").trim(), 120);',
4211
+ " const isVisible = (element) => {",
4212
+ ' if (!element || typeof element.getClientRects !== "function") {',
4213
+ " return false;",
4214
+ " }",
4215
+ " const style = window.getComputedStyle(element);",
4216
+ ' if (style.display === "none" || style.visibility === "hidden") {',
4217
+ " return false;",
4218
+ " }",
4219
+ ' if (element.hidden || element.getAttribute("aria-hidden") === "true") {',
4220
+ " return false;",
4221
+ " }",
4222
+ " return element.getClientRects().length > 0;",
4223
+ " };",
4224
+ " const selectorFor = (element) => {",
4225
+ " if (!element || element.nodeType !== 1) {",
4226
+ ' return "";',
4227
+ " }",
4228
+ " if (element.id) {",
4229
+ " return `#${escape(element.id)}`;",
4230
+ " }",
4231
+ ' const testId = element.getAttribute("data-testid");',
4232
+ " if (testId) {",
4233
+ ' return `[data-testid="${escape(testId)}"]`;',
4234
+ " }",
4235
+ ' const name = element.getAttribute("name");',
4236
+ " if (name) {",
4237
+ ' return `${element.tagName.toLowerCase()}[name="${escape(name)}"]`;',
4238
+ " }",
4239
+ ' const ariaLabel = element.getAttribute("aria-label");',
4240
+ " if (ariaLabel) {",
4241
+ ' return `${element.tagName.toLowerCase()}[aria-label="${escape(ariaLabel)}"]`;',
4242
+ " }",
4243
+ " const parts = [];",
4244
+ " let node = element;",
4245
+ " while (node && node.nodeType === 1 && parts.length < 4) {",
4246
+ " let part = node.tagName.toLowerCase();",
4247
+ " const parent = node.parentElement;",
4248
+ " if (parent) {",
4249
+ " const siblings = Array.from(parent.children).filter(",
4250
+ " (child) => child.tagName === node.tagName",
4251
+ " );",
4252
+ " if (siblings.length > 1) {",
4253
+ " part += `:nth-of-type(${siblings.indexOf(node) + 1})`;",
4254
+ " }",
4255
+ " }",
4256
+ " parts.unshift(part);",
4257
+ " node = parent;",
4258
+ " }",
4259
+ " return parts.join('>');",
4260
+ " };",
4261
+ " const labelFor = (element) => {",
4262
+ " if (!element) {",
4263
+ " return undefined;",
4264
+ " }",
4265
+ ' const ariaLabel = element.getAttribute("aria-label");',
4266
+ " if (ariaLabel) {",
4267
+ " return truncate(ariaLabel, 120);",
4268
+ " }",
4269
+ " if (element.id) {",
4270
+ ' const explicitLabel = document.querySelector(`label[for="${escape(element.id)}"]`);',
4271
+ " if (explicitLabel) {",
4272
+ " return textFor(explicitLabel);",
4273
+ " }",
4274
+ " }",
4275
+ ' const parentLabel = element.closest("label");',
4276
+ " if (parentLabel) {",
4277
+ " return textFor(parentLabel);",
4278
+ " }",
4279
+ " return undefined;",
4280
+ " };",
4281
+ " const readStorage = (storage, limit) => {",
4282
+ " try {",
4283
+ " const keys = Object.keys(storage);",
4284
+ " return {",
4285
+ " count: keys.length,",
4286
+ " entries: keys.slice(0, limit).map((key) => ({",
4287
+ " key,",
4288
+ " value: redact(storage.getItem(key)),",
4289
+ " })),",
4290
+ " };",
4291
+ " } catch {",
4292
+ " return { count: 0, entries: [] };",
4293
+ " }",
4294
+ " };",
4295
+ ' const forms = Array.from(document.querySelectorAll("form"))',
4296
+ " .filter((form) => isVisible(form))",
4297
+ " .map((form) => {",
4298
+ " const fields = Array.from(form.elements)",
4299
+ " .filter((element) => element && element.tagName)",
4300
+ " .map((element) => {",
4301
+ " const tag = element.tagName.toLowerCase();",
4302
+ ' const type = "type" in element && element.type ? element.type : tag;',
4303
+ ' const name = element.name || element.getAttribute("name") || element.id || "";',
4304
+ ' let value = "";',
4305
+ " let options;",
4306
+ ' if (tag === "select") {',
4307
+ " const select = element;",
4308
+ ' value = redact(select.value ?? "");',
4309
+ " options = Array.from(select.options).map((option) => truncate(option.text, 120));",
4310
+ ' } else if (tag === "input" && element.type === "password") {',
4311
+ ' value = "[redacted]";',
4312
+ ' } else if (tag === "input" || tag === "textarea") {',
4313
+ ' value = redact(element.value ?? "");',
4314
+ " } else if (element.isContentEditable) {",
4315
+ ' value = redact(element.textContent ?? "");',
4316
+ ' } else if ("value" in element) {',
4317
+ ' value = redact(element.value ?? "");',
4318
+ " }",
4319
+ " return {",
4320
+ " name,",
4321
+ " type,",
4322
+ " value,",
4323
+ " selector: selectorFor(element),",
4324
+ " label: labelFor(element),",
4325
+ ' placeholder: truncate(element.getAttribute?.("placeholder") ?? "", 120) || undefined,',
4326
+ ' checked: typeof element.checked === "boolean" ? element.checked : undefined,',
4327
+ " disabled: !!element.disabled,",
4328
+ " visible: isVisible(element),",
4329
+ " ...(options ? { options } : {}),",
4330
+ " };",
4331
+ " });",
4332
+ " return {",
4333
+ " selector: selectorFor(form),",
4334
+ ' action: form.getAttribute("action") || undefined,',
4335
+ ' method: form.getAttribute("method") || undefined,',
4336
+ " fields,",
4337
+ " };",
4338
+ " });",
4339
+ " const localStorageData = readStorage(window.localStorage, 25);",
4340
+ " const sessionStorageData = readStorage(window.sessionStorage, 25);",
4341
+ ' const cookies = (document.cookie ? document.cookie.split(";") : [])',
4342
+ " .map((entry) => entry.trim())",
4343
+ " .filter((entry) => entry.length > 0)",
4344
+ " .slice(0, 25)",
4345
+ " .map((entry) => {",
4346
+ ' const [key, ...rest] = entry.split("=");',
4347
+ " return {",
4348
+ " key,",
4349
+ ' value: includeValues ? truncate(rest.join("="), 500) : "[redacted]",',
4350
+ " };",
4351
+ " });",
4352
+ " const focused = document.activeElement && document.activeElement !== document.body",
4353
+ " ? {",
4354
+ " selector: selectorFor(document.activeElement),",
4355
+ ' name: document.activeElement.getAttribute?.("name") || document.activeElement.id || undefined,',
4356
+ " label: labelFor(document.activeElement),",
4357
+ ' role: document.activeElement.getAttribute?.("role") || document.activeElement.tagName?.toLowerCase(),',
4358
+ ' type: document.activeElement.getAttribute?.("type") || undefined,',
4359
+ " text: textFor(document.activeElement) || undefined,",
4360
+ " }",
4361
+ " : undefined;",
4362
+ ' const primaryActions = Array.from(document.querySelectorAll("button, a[href], input[type=button], input[type=submit]"))',
4363
+ " .filter((element) => isVisible(element))",
4364
+ " .map((element) => ({",
4365
+ " selector: selectorFor(element),",
4366
+ ' role: element.getAttribute("role") || element.tagName.toLowerCase(),',
4367
+ ' name: truncate(element.getAttribute("aria-label") || element.innerText || element.value || "", 120),',
4368
+ " }))",
4369
+ " .filter((entry) => entry.name.length > 0)",
4370
+ " .slice(0, 12);",
4371
+ " return {",
4372
+ " url: location.href,",
4373
+ " title: document.title || undefined,",
4374
+ " readyState: document.readyState || undefined,",
4375
+ " focused,",
4376
+ " primaryActions,",
4377
+ " storageSummary: {",
4378
+ " localStorageCount: localStorageData.count,",
4379
+ " sessionStorageCount: sessionStorageData.count,",
4380
+ " cookieCount: cookies.length,",
4381
+ " },",
4382
+ " forms,",
4383
+ " localStorage: localStorageData.entries,",
4384
+ " sessionStorage: sessionStorageData.entries,",
4385
+ " cookies,",
4386
+ " };",
4387
+ "})()"
4388
+ ].join("\n");
4389
+ };
4135
4390
 
4136
4391
  // packages/core/src/inspect/service.ts
4137
4392
  var DEFAULT_MAX_SNAPSHOTS_PER_SESSION = 20;
4138
4393
  var DEFAULT_MAX_SNAPSHOT_HISTORY = 100;
4139
4394
  var InspectService = class {
4140
4395
  constructor(options) {
4396
+ this.consoleSinceBySessionTab = /* @__PURE__ */ new Map();
4141
4397
  this.registry = options.registry;
4142
4398
  this.debugger = options.debuggerBridge;
4143
4399
  this.extensionBridge = options.extensionBridge;
@@ -4198,11 +4454,15 @@ var InspectService = class {
4198
4454
  this.requireSession(input.sessionId);
4199
4455
  const selection = await this.resolveTab(input.sessionId, input.targetHint);
4200
4456
  const debuggerCommand = this.debuggerCommand.bind(this);
4457
+ const executionContextId = await this.resolveMainFrameExecutionContextId(
4458
+ selection.tabId
4459
+ );
4201
4460
  const work = async () => {
4202
4461
  if (input.format === "html") {
4203
4462
  const html = await captureHtml(selection.tabId, {
4204
4463
  selector: input.selector,
4205
4464
  debuggerCommand,
4465
+ executionContextId,
4206
4466
  onEvaluationFailed: () => {
4207
4467
  const error = new InspectError(
4208
4468
  "EVALUATION_FAILED",
@@ -4296,18 +4556,36 @@ var InspectService = class {
4296
4556
  refMap,
4297
4557
  debuggerCommand
4298
4558
  );
4559
+ let actionableRefs = refResult.appliedRefs;
4560
+ let actionableBindings = refResult.appliedBindings;
4561
+ const actionabilityWarnings = [];
4562
+ if (input.interactive && refResult.appliedBindings.length > 0) {
4563
+ const actionableResult = await this.collectActionableSnapshotRefs(
4564
+ selection.tabId,
4565
+ refResult.appliedBindings
4566
+ );
4567
+ actionabilityWarnings.push(...actionableResult.warnings);
4568
+ if (actionableResult.actionableRefs) {
4569
+ actionableRefs = actionableResult.actionableRefs;
4570
+ actionableBindings = refResult.appliedBindings.filter(
4571
+ (binding) => actionableRefs.has(binding.ref)
4572
+ );
4573
+ snapshot = filterAxSnapshotByRefs(snapshot, actionableRefs);
4574
+ }
4575
+ }
4299
4576
  const persistResult = refMap.size === 0 ? { warnings: [] } : await persistSnapshotRefRegistry(
4300
4577
  selection.tabId,
4301
- refResult.appliedBindings,
4578
+ actionableBindings,
4302
4579
  debuggerCommand
4303
4580
  );
4304
- pruneUnappliedRefsFromSnapshot(snapshot, refResult.appliedRefs);
4581
+ pruneUnappliedRefsFromSnapshot(snapshot, actionableRefs);
4305
4582
  const warnings = [
4306
4583
  ...selection.warnings ?? [],
4307
4584
  ...selectorWarnings,
4308
4585
  ...truncationWarnings,
4309
4586
  ...clearResult.warnings,
4310
4587
  ...refResult.warnings,
4588
+ ...actionabilityWarnings,
4311
4589
  ...persistResult.warnings
4312
4590
  ];
4313
4591
  return {
@@ -4329,6 +4607,7 @@ var InspectService = class {
4329
4607
  const html = await captureHtml(selection.tabId, {
4330
4608
  selector: input.selector,
4331
4609
  debuggerCommand,
4610
+ executionContextId,
4332
4611
  onEvaluationFailed: () => {
4333
4612
  const error2 = new InspectError(
4334
4613
  "EVALUATION_FAILED",
@@ -4433,11 +4712,27 @@ var InspectService = class {
4433
4712
  };
4434
4713
  }
4435
4714
  async consoleList(input) {
4436
- this.requireSession(input.sessionId);
4715
+ const session = this.requireSession(input.sessionId);
4716
+ const tabCount = this.extensionBridge?.getStatus().tabs.length ?? 0;
4717
+ if (!input.targetHint && typeof session.selectedTabId !== "number" && tabCount > 1) {
4718
+ const error = new InspectError(
4719
+ "TAB_NOT_FOUND",
4720
+ "Console inspection requires an explicit target or a session-selected tab.",
4721
+ { retryable: false }
4722
+ );
4723
+ this.recordError(error);
4724
+ throw error;
4725
+ }
4437
4726
  const selection = await this.resolveTab(input.sessionId, input.targetHint);
4438
4727
  await this.enableConsole(selection.tabId);
4728
+ const since = this.resolveConsoleSince({
4729
+ session,
4730
+ tabId: selection.tabId,
4731
+ requestedSince: input.since,
4732
+ tabLastActiveAt: selection.tab.last_active_at
4733
+ });
4439
4734
  const events = this.ensureDebugger().getConsoleEvents(selection.tabId);
4440
- const entries = events.map((event) => toConsoleEntry(event)).filter((entry) => entry !== null);
4735
+ const entries = events.filter((event) => this.isEventOnOrAfter(event.timestamp, since)).map((event) => toConsoleEntry(event)).filter((entry) => entry !== null);
4441
4736
  const result = {
4442
4737
  entries,
4443
4738
  warnings: selection.warnings
@@ -4476,16 +4771,7 @@ var InspectService = class {
4476
4771
  this.requireSession(input.sessionId);
4477
4772
  const selection = await this.resolveTab(input.sessionId, input.targetHint);
4478
4773
  const expression = input.expression ?? "undefined";
4479
- await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
4480
- const result = await this.debuggerCommand(
4481
- selection.tabId,
4482
- "Runtime.evaluate",
4483
- {
4484
- expression,
4485
- returnByValue: true,
4486
- awaitPromise: true
4487
- }
4488
- );
4774
+ const result = await this.evaluateInMainFrame(selection.tabId, expression);
4489
4775
  if (result && typeof result === "object" && "exceptionDetails" in result) {
4490
4776
  const output2 = {
4491
4777
  exception: result.exceptionDetails,
@@ -4504,54 +4790,58 @@ var InspectService = class {
4504
4790
  async extractContent(input) {
4505
4791
  this.requireSession(input.sessionId);
4506
4792
  const selection = await this.resolveTab(input.sessionId, input.targetHint);
4793
+ const consistency = input.consistency ?? "quiesce";
4507
4794
  const debuggerCommand = this.debuggerCommand.bind(this);
4508
- const html = await captureHtml(selection.tabId, {
4509
- debuggerCommand,
4510
- onEvaluationFailed: () => {
4511
- const error = new InspectError(
4512
- "EVALUATION_FAILED",
4513
- "Failed to evaluate HTML snapshot.",
4514
- { retryable: false }
4515
- );
4516
- this.recordError(error);
4517
- throw error;
4518
- }
4519
- });
4795
+ const executionContextId = await this.resolveMainFrameExecutionContextId(
4796
+ selection.tabId
4797
+ );
4520
4798
  const url = selection.tab.url ?? "about:blank";
4521
- try {
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
4799
+ const work = async () => {
4800
+ if (consistency === "quiesce") {
4801
+ await this.waitForDomSettled(selection.tabId, executionContextId);
4802
+ }
4803
+ const html = await captureHtml(selection.tabId, {
4804
+ debuggerCommand,
4805
+ executionContextId,
4806
+ onEvaluationFailed: () => {
4807
+ const error = new InspectError(
4808
+ "EVALUATION_FAILED",
4809
+ "Failed to evaluate HTML snapshot.",
4810
+ { retryable: false }
4811
+ );
4812
+ this.recordError(error);
4813
+ throw error;
4814
+ }
4532
4815
  });
4533
- this.markInspectConnected(input.sessionId);
4534
- return output;
4535
- } catch (error) {
4536
- if (error instanceof InspectError) {
4537
- this.recordError(error);
4816
+ try {
4817
+ const { article, semanticMainCandidate } = parseExtractContentSource({
4818
+ html,
4819
+ url
4820
+ });
4821
+ return renderExtractContent({
4822
+ format: input.format,
4823
+ article,
4824
+ semanticMainCandidate,
4825
+ includeMetadata: input.includeMetadata,
4826
+ warnings: selection.warnings
4827
+ });
4828
+ } catch (error) {
4829
+ if (error instanceof InspectError) {
4830
+ this.recordError(error);
4831
+ }
4832
+ throw error;
4538
4833
  }
4539
- throw error;
4540
- }
4834
+ };
4835
+ const output = consistency === "quiesce" ? await driveMutex.runExclusive(work) : await work();
4836
+ this.markInspectConnected(input.sessionId);
4837
+ return output;
4541
4838
  }
4542
4839
  async pageState(input) {
4543
4840
  this.requireSession(input.sessionId);
4544
4841
  const selection = await this.resolveTab(input.sessionId, input.targetHint);
4545
- await this.debuggerCommand(selection.tabId, "Runtime.enable", {});
4546
- const expression = PAGE_STATE_SCRIPT;
4547
- const result = await this.debuggerCommand(
4842
+ const result = await this.evaluateInMainFrame(
4548
4843
  selection.tabId,
4549
- "Runtime.evaluate",
4550
- {
4551
- expression,
4552
- returnByValue: true,
4553
- awaitPromise: true
4554
- }
4844
+ buildPageStateScript({ includeValues: input.includeValues })
4555
4845
  );
4556
4846
  if (result && typeof result === "object" && "exceptionDetails" in result) {
4557
4847
  const error = new InspectError(
@@ -4563,16 +4853,22 @@ var InspectService = class {
4563
4853
  throw error;
4564
4854
  }
4565
4855
  const value = result?.result?.value;
4566
- const raw = value && typeof value === "object" ? value : {};
4856
+ const parsed = PageStateSchema.safeParse(value);
4857
+ if (!parsed.success) {
4858
+ const error = new InspectError(
4859
+ "EVALUATION_FAILED",
4860
+ "Captured page state did not match the expected schema.",
4861
+ { retryable: false }
4862
+ );
4863
+ this.recordError(error);
4864
+ throw error;
4865
+ }
4567
4866
  const warnings = [
4568
- ...Array.isArray(raw.warnings) ? raw.warnings : [],
4867
+ ...parsed.data.warnings ?? [],
4569
4868
  ...selection.warnings ?? []
4570
4869
  ];
4571
4870
  const output = {
4572
- forms: Array.isArray(raw.forms) ? raw.forms : [],
4573
- localStorage: Array.isArray(raw.localStorage) ? raw.localStorage : [],
4574
- sessionStorage: Array.isArray(raw.sessionStorage) ? raw.sessionStorage : [],
4575
- cookies: Array.isArray(raw.cookies) ? raw.cookies : [],
4871
+ ...parsed.data,
4576
4872
  ...warnings.length > 0 ? { warnings } : {}
4577
4873
  };
4578
4874
  this.markInspectConnected(input.sessionId);
@@ -4796,6 +5092,186 @@ var InspectService = class {
4796
5092
  async enableAccessibility(tabId) {
4797
5093
  await this.debuggerCommand(tabId, "Accessibility.enable", {});
4798
5094
  }
5095
+ async collectActionableSnapshotRefs(tabId, bindings) {
5096
+ try {
5097
+ const result = await this.debuggerCommand(tabId, "Runtime.evaluate", {
5098
+ expression: `(() => {
5099
+ const refs = new Set(${JSON.stringify(
5100
+ bindings.map((binding) => binding.ref)
5101
+ )});
5102
+ const isVisible = (element) => {
5103
+ if (!(element instanceof HTMLElement)) {
5104
+ return false;
5105
+ }
5106
+ const style = window.getComputedStyle(element);
5107
+ if (style.visibility === 'hidden' || style.display === 'none') {
5108
+ return false;
5109
+ }
5110
+ const rect = element.getBoundingClientRect();
5111
+ if (rect.width === 0 && rect.height === 0) {
5112
+ return false;
5113
+ }
5114
+ if (
5115
+ element.offsetWidth === 0 &&
5116
+ element.offsetHeight === 0 &&
5117
+ element.getClientRects().length === 0
5118
+ ) {
5119
+ return false;
5120
+ }
5121
+ let current = element;
5122
+ while (current) {
5123
+ const currentStyle = window.getComputedStyle(current);
5124
+ if (currentStyle.display === 'none') {
5125
+ return false;
5126
+ }
5127
+ if (
5128
+ currentStyle.visibility === 'hidden' ||
5129
+ currentStyle.visibility === 'collapse'
5130
+ ) {
5131
+ return false;
5132
+ }
5133
+ const opacity = Number.parseFloat(currentStyle.opacity ?? '1');
5134
+ if (Number.isFinite(opacity) && opacity <= 0) {
5135
+ return false;
5136
+ }
5137
+ if (currentStyle.pointerEvents === 'none') {
5138
+ return false;
5139
+ }
5140
+ current = current.parentElement;
5141
+ }
5142
+ return true;
5143
+ };
5144
+
5145
+ return Array.from(document.querySelectorAll('[data-bv-ref]'))
5146
+ .filter((element) => {
5147
+ const ref = element.getAttribute('data-bv-ref');
5148
+ return typeof ref === 'string' && refs.has(ref) && isVisible(element);
5149
+ })
5150
+ .map((element) => element.getAttribute('data-bv-ref'));
5151
+ })() /* browser-bridge:collect-actionable-snapshot-refs */`,
5152
+ returnByValue: true,
5153
+ awaitPromise: true
5154
+ });
5155
+ const rawRefs = result.value;
5156
+ if (!Array.isArray(rawRefs)) {
5157
+ return {
5158
+ warnings: [
5159
+ "Interactive AX snapshot could not verify live actionability."
5160
+ ]
5161
+ };
5162
+ }
5163
+ const refs = rawRefs.filter(
5164
+ (ref) => typeof ref === "string"
5165
+ );
5166
+ return {
5167
+ actionableRefs: new Set(refs),
5168
+ warnings: []
5169
+ };
5170
+ } catch {
5171
+ return {
5172
+ warnings: ["Interactive AX snapshot could not prune hidden controls."]
5173
+ };
5174
+ }
5175
+ }
5176
+ async waitForDomSettled(tabId, executionContextId) {
5177
+ const result = await this.debuggerCommand(tabId, "Runtime.evaluate", {
5178
+ expression: `(() => {
5179
+ const quietMs = 100;
5180
+ const timeoutMs = 2000;
5181
+ return new Promise((resolve) => {
5182
+ const doc = document;
5183
+ const root = doc.documentElement || doc.body || doc;
5184
+ let finished = false;
5185
+ let quietTimer;
5186
+ let timeoutTimer;
5187
+ let raf1 = 0;
5188
+ let raf2 = 0;
5189
+ const finish = () => {
5190
+ if (finished) {
5191
+ return;
5192
+ }
5193
+ finished = true;
5194
+ if (observer) {
5195
+ observer.disconnect();
5196
+ }
5197
+ clearTimeout(quietTimer);
5198
+ clearTimeout(timeoutTimer);
5199
+ if (raf1) cancelAnimationFrame(raf1);
5200
+ if (raf2) cancelAnimationFrame(raf2);
5201
+ resolve(true);
5202
+ };
5203
+ const scheduleQuiet = () => {
5204
+ clearTimeout(quietTimer);
5205
+ quietTimer = setTimeout(() => {
5206
+ raf1 = requestAnimationFrame(() => {
5207
+ raf2 = requestAnimationFrame(finish);
5208
+ });
5209
+ }, quietMs);
5210
+ };
5211
+ const observer =
5212
+ typeof MutationObserver === 'function'
5213
+ ? new MutationObserver(() => {
5214
+ scheduleQuiet();
5215
+ })
5216
+ : null;
5217
+ if (observer && root) {
5218
+ observer.observe(root, {
5219
+ subtree: true,
5220
+ childList: true,
5221
+ attributes: true,
5222
+ characterData: true,
5223
+ });
5224
+ }
5225
+ scheduleQuiet();
5226
+ timeoutTimer = setTimeout(finish, timeoutMs);
5227
+ });
5228
+ })()`,
5229
+ returnByValue: true,
5230
+ awaitPromise: true,
5231
+ ...typeof executionContextId === "number" ? { contextId: executionContextId } : {}
5232
+ });
5233
+ if (result && typeof result === "object" && "exceptionDetails" in result) {
5234
+ const error = new InspectError(
5235
+ "EVALUATION_FAILED",
5236
+ "Failed while waiting for the page to settle.",
5237
+ { retryable: false }
5238
+ );
5239
+ this.recordError(error);
5240
+ throw error;
5241
+ }
5242
+ }
5243
+ async evaluateInMainFrame(tabId, expression) {
5244
+ await this.debuggerCommand(tabId, "Runtime.enable", {});
5245
+ const executionContextId = await this.resolveMainFrameExecutionContextId(tabId);
5246
+ return await this.debuggerCommand(tabId, "Runtime.evaluate", {
5247
+ expression,
5248
+ returnByValue: true,
5249
+ awaitPromise: true,
5250
+ ...typeof executionContextId === "number" ? { contextId: executionContextId } : {}
5251
+ });
5252
+ }
5253
+ async resolveMainFrameExecutionContextId(tabId) {
5254
+ const debuggerBridge = this.ensureDebugger();
5255
+ const runOptional = async (method, params) => {
5256
+ const result = await debuggerBridge.command(tabId, method, params);
5257
+ if (!result.ok) {
5258
+ return void 0;
5259
+ }
5260
+ return result.result;
5261
+ };
5262
+ await runOptional("Page.enable", {});
5263
+ const frameTree = await runOptional("Page.getFrameTree", {});
5264
+ const frameId = frameTree?.frameTree?.frame?.id;
5265
+ if (typeof frameId !== "string" || frameId.length === 0) {
5266
+ return void 0;
5267
+ }
5268
+ const isolatedWorld = await runOptional("Page.createIsolatedWorld", {
5269
+ frameId,
5270
+ worldName: "browser_bridge_inspect"
5271
+ });
5272
+ const contextId = isolatedWorld?.executionContextId;
5273
+ return typeof contextId === "number" ? contextId : void 0;
5274
+ }
4799
5275
  async debuggerCommand(tabId, method, params, timeoutMs) {
4800
5276
  const debuggerBridge = this.ensureDebugger();
4801
5277
  const result = await debuggerBridge.command(
@@ -4874,6 +5350,32 @@ var InspectService = class {
4874
5350
  throw wrapped;
4875
5351
  }
4876
5352
  }
5353
+ resolveConsoleSince(options) {
5354
+ if (typeof options.requestedSince === "string") {
5355
+ return options.requestedSince;
5356
+ }
5357
+ const key = `${options.session.id}:${options.tabId}`;
5358
+ const existing = this.consoleSinceBySessionTab.get(key);
5359
+ if (existing) {
5360
+ return existing;
5361
+ }
5362
+ const candidates = [options.session.createdAt.toISOString()];
5363
+ if (typeof options.tabLastActiveAt === "string") {
5364
+ candidates.push(options.tabLastActiveAt);
5365
+ }
5366
+ const baseline = candidates.map((value) => ({ value, time: Date.parse(value) })).filter((entry) => Number.isFinite(entry.time)).sort((a, b) => b.time - a.time)[0]?.value;
5367
+ const resolved = baseline ?? options.session.createdAt.toISOString();
5368
+ this.consoleSinceBySessionTab.set(key, resolved);
5369
+ return resolved;
5370
+ }
5371
+ isEventOnOrAfter(timestamp, since) {
5372
+ const eventTime = Date.parse(timestamp);
5373
+ const sinceTime = Date.parse(since);
5374
+ if (!Number.isFinite(eventTime) || !Number.isFinite(sinceTime)) {
5375
+ return true;
5376
+ }
5377
+ return eventTime >= sinceTime;
5378
+ }
4877
5379
  };
4878
5380
  var createInspectService = (options) => new InspectService(options);
4879
5381
 
@@ -5299,6 +5801,7 @@ var registerDiagnosticsRoutes = (router, options = {}) => {
5299
5801
  const extensionStatus = options.extensionBridge?.getStatus();
5300
5802
  sendResult(res, {
5301
5803
  started_at: PROCESS_STARTED_AT,
5804
+ pid: process.pid,
5302
5805
  uptime_ms: Math.floor(process.uptime() * 1e3),
5303
5806
  memory: process.memoryUsage(),
5304
5807
  sessions: { active: sessionsActive },
@@ -5948,6 +6451,7 @@ var registerInspectRoutes = (router, options) => {
5948
6451
  return await inspect.extractContent({
5949
6452
  sessionId: body.session_id,
5950
6453
  format: body.format,
6454
+ consistency: body.consistency,
5951
6455
  includeMetadata: body.include_metadata,
5952
6456
  targetHint: resolveInspectTargetHint({
5953
6457
  sessionId: body.session_id,
@@ -5963,6 +6467,7 @@ var registerInspectRoutes = (router, options) => {
5963
6467
  makeHandler2(InspectPageStateInputSchema, async (body) => {
5964
6468
  return await inspect.pageState({
5965
6469
  sessionId: body.session_id,
6470
+ includeValues: body.include_values,
5966
6471
  targetHint: resolveInspectTargetHint({
5967
6472
  sessionId: body.session_id,
5968
6473
  target: body.target,
@@ -5977,6 +6482,7 @@ var registerInspectRoutes = (router, options) => {
5977
6482
  makeHandler2(InspectConsoleListInputSchema, async (body) => {
5978
6483
  return await inspect.consoleList({
5979
6484
  sessionId: body.session_id,
6485
+ since: body.since,
5980
6486
  targetHint: resolveInspectTargetHint({
5981
6487
  sessionId: body.session_id,
5982
6488
  target: body.target,
@@ -6031,6 +6537,157 @@ var registerInspectRoutes = (router, options) => {
6031
6537
  );
6032
6538
  };
6033
6539
 
6540
+ // packages/core/src/routes/permissions.ts
6541
+ var EXTENSION_READY_WAIT_MS2 = 1500;
6542
+ var parseBody3 = (schema, body) => {
6543
+ const result = schema.safeParse(body);
6544
+ if (result.success) {
6545
+ return { data: result.data };
6546
+ }
6547
+ const issue = result.error.issues[0];
6548
+ const details = issue && issue.path.length > 0 ? { field: issue.path.map((part) => part.toString()).join(".") } : void 0;
6549
+ return {
6550
+ error: {
6551
+ message: issue?.message ?? "Request body is invalid.",
6552
+ ...details ? { details } : {}
6553
+ }
6554
+ };
6555
+ };
6556
+ var sendBridgeUnavailable = (res) => {
6557
+ sendError(res, errorStatus("EXTENSION_DISCONNECTED"), {
6558
+ code: "EXTENSION_DISCONNECTED",
6559
+ message: "Extension bridge is unavailable.",
6560
+ retryable: true
6561
+ });
6562
+ };
6563
+ var waitForExtensionReady = async (extensionBridge) => {
6564
+ if (extensionBridge.isConnected()) {
6565
+ return true;
6566
+ }
6567
+ const waitForReady = extensionBridge.waitForReady;
6568
+ if (typeof waitForReady !== "function") {
6569
+ return extensionBridge.isConnected();
6570
+ }
6571
+ return await waitForReady.call(extensionBridge, EXTENSION_READY_WAIT_MS2);
6572
+ };
6573
+ var makePermissionsHandler = (schema, action, options) => {
6574
+ return (req, res) => {
6575
+ const parsed = parseBody3(schema, req.body ?? {});
6576
+ if (parsed.error) {
6577
+ sendError(res, errorStatus("INVALID_ARGUMENT"), {
6578
+ code: "INVALID_ARGUMENT",
6579
+ message: parsed.error.message,
6580
+ retryable: false,
6581
+ ...parsed.error.details ? { details: parsed.error.details } : {}
6582
+ });
6583
+ return;
6584
+ }
6585
+ if (!options.extensionBridge) {
6586
+ sendBridgeUnavailable(res);
6587
+ return;
6588
+ }
6589
+ const extensionBridge = options.extensionBridge;
6590
+ void waitForExtensionReady(extensionBridge).then(async (ready) => {
6591
+ if (!ready) {
6592
+ throw new ExtensionBridgeError(
6593
+ "EXTENSION_DISCONNECTED",
6594
+ "Extension is not connected.",
6595
+ true
6596
+ );
6597
+ }
6598
+ return await extensionBridge.request(
6599
+ action,
6600
+ parsed.data
6601
+ );
6602
+ }).then((envelope2) => {
6603
+ if (envelope2.status === "error") {
6604
+ const error = envelope2.error ?? {
6605
+ code: "INTERNAL",
6606
+ message: "Extension request failed.",
6607
+ retryable: false
6608
+ };
6609
+ sendError(res, errorStatus(error.code), {
6610
+ code: error.code,
6611
+ message: error.message,
6612
+ retryable: error.retryable,
6613
+ details: error.details
6614
+ });
6615
+ return;
6616
+ }
6617
+ sendResult(res, envelope2.result ?? {});
6618
+ }).catch((error) => {
6619
+ if (error instanceof ExtensionBridgeError) {
6620
+ sendError(res, errorStatus(error.code), {
6621
+ code: error.code,
6622
+ message: error.message,
6623
+ retryable: error.retryable,
6624
+ details: error.details
6625
+ });
6626
+ return;
6627
+ }
6628
+ sendError(res, errorStatus("INTERNAL"), {
6629
+ code: "INTERNAL",
6630
+ message: "Unexpected permissions route error.",
6631
+ retryable: false,
6632
+ details: {
6633
+ hint: error instanceof Error ? error.message : "Unknown error.",
6634
+ action
6635
+ }
6636
+ });
6637
+ });
6638
+ };
6639
+ };
6640
+ var registerPermissionsRoutes = (router, options) => {
6641
+ router.post(
6642
+ "/permissions/list",
6643
+ makePermissionsHandler(
6644
+ PermissionsListInputSchema,
6645
+ "permissions.list",
6646
+ options
6647
+ )
6648
+ );
6649
+ router.post(
6650
+ "/permissions/get_mode",
6651
+ makePermissionsHandler(
6652
+ PermissionsGetModeInputSchema,
6653
+ "permissions.get_mode",
6654
+ options
6655
+ )
6656
+ );
6657
+ router.post(
6658
+ "/permissions/list_pending_requests",
6659
+ makePermissionsHandler(
6660
+ PermissionsListPendingRequestsInputSchema,
6661
+ "permissions.list_pending_requests",
6662
+ options
6663
+ )
6664
+ );
6665
+ router.post(
6666
+ "/permissions/request_allow_site",
6667
+ makePermissionsHandler(
6668
+ PermissionsRequestAllowSiteInputSchema,
6669
+ "permissions.request_allow_site",
6670
+ options
6671
+ )
6672
+ );
6673
+ router.post(
6674
+ "/permissions/request_revoke_site",
6675
+ makePermissionsHandler(
6676
+ PermissionsRequestRevokeSiteInputSchema,
6677
+ "permissions.request_revoke_site",
6678
+ options
6679
+ )
6680
+ );
6681
+ router.post(
6682
+ "/permissions/request_set_mode",
6683
+ makePermissionsHandler(
6684
+ PermissionsRequestSetModeInputSchema,
6685
+ "permissions.request_set_mode",
6686
+ options
6687
+ )
6688
+ );
6689
+ };
6690
+
6034
6691
  // packages/core/src/recovery.ts
6035
6692
  var RecoveryTracker = class {
6036
6693
  constructor() {
@@ -6446,6 +7103,9 @@ var createCoreServer = (options = {}) => {
6446
7103
  })
6447
7104
  );
6448
7105
  registerDriveRoutes(app, { drive, registry });
7106
+ registerPermissionsRoutes(app, {
7107
+ extensionBridge
7108
+ });
6449
7109
  registerInspectRoutes(app, {
6450
7110
  registry,
6451
7111
  extensionBridge,
@@ -6857,6 +7517,10 @@ var readSessionId2 = (args) => {
6857
7517
  const sessionId = args.session_id;
6858
7518
  return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : void 0;
6859
7519
  };
7520
+ var withPermissionsSource = (source) => (args) => isRecord3(args) ? {
7521
+ ...args,
7522
+ source
7523
+ } : args;
6860
7524
  var supportsSessionMigration = (corePath) => corePath.startsWith("/drive/") || corePath.startsWith("/inspect/") || corePath.startsWith("/artifacts/") || corePath === "/diagnostics/doctor";
6861
7525
  var isSessionNotFoundEnvelope = (envelopeResult) => {
6862
7526
  if (envelopeResult.ok) {
@@ -6970,6 +7634,69 @@ var TOOL_DEFINITIONS = [
6970
7634
  corePath: "/session/close"
6971
7635
  }
6972
7636
  },
7637
+ {
7638
+ name: "permissions.list",
7639
+ config: {
7640
+ title: "Permissions List",
7641
+ description: "List allowlisted Browser Bridge sites.",
7642
+ inputSchema: PermissionsListInputSchema,
7643
+ outputSchema: envelope(PermissionsListOutputSchema),
7644
+ corePath: "/permissions/list"
7645
+ }
7646
+ },
7647
+ {
7648
+ name: "permissions.get_mode",
7649
+ config: {
7650
+ title: "Permissions Get Mode",
7651
+ description: "Read the current Browser Bridge permissions mode.",
7652
+ inputSchema: PermissionsGetModeInputSchema,
7653
+ outputSchema: envelope(PermissionsGetModeOutputSchema),
7654
+ corePath: "/permissions/get_mode"
7655
+ }
7656
+ },
7657
+ {
7658
+ name: "permissions.list_pending_requests",
7659
+ config: {
7660
+ title: "Permissions List Pending Requests",
7661
+ description: "List pending external Browser Bridge permission-change requests.",
7662
+ inputSchema: PermissionsListPendingRequestsInputSchema,
7663
+ outputSchema: envelope(PermissionsListPendingRequestsOutputSchema),
7664
+ corePath: "/permissions/list_pending_requests"
7665
+ }
7666
+ },
7667
+ {
7668
+ name: "permissions.request_allow_site",
7669
+ config: {
7670
+ title: "Permissions Request Allow Site",
7671
+ description: "Request allowlisting a site. A human must approve the change in Chrome before it applies.",
7672
+ inputSchema: PermissionsRequestAllowSiteInputSchema,
7673
+ outputSchema: envelope(PermissionsRequestOutputSchema),
7674
+ corePath: "/permissions/request_allow_site",
7675
+ transformInput: withPermissionsSource("mcp")
7676
+ }
7677
+ },
7678
+ {
7679
+ name: "permissions.request_revoke_site",
7680
+ config: {
7681
+ title: "Permissions Request Revoke Site",
7682
+ description: "Request revoking a site from the allowlist. A human must approve the change in Chrome before it applies.",
7683
+ inputSchema: PermissionsRequestRevokeSiteInputSchema,
7684
+ outputSchema: envelope(PermissionsRequestOutputSchema),
7685
+ corePath: "/permissions/request_revoke_site",
7686
+ transformInput: withPermissionsSource("mcp")
7687
+ }
7688
+ },
7689
+ {
7690
+ name: "permissions.request_set_mode",
7691
+ config: {
7692
+ title: "Permissions Request Set Mode",
7693
+ description: "Request changing Browser Bridge permission mode. A human must approve the change in Chrome before it applies.",
7694
+ inputSchema: PermissionsRequestSetModeInputSchema,
7695
+ outputSchema: envelope(PermissionsRequestOutputSchema),
7696
+ corePath: "/permissions/request_set_mode",
7697
+ transformInput: withPermissionsSource("mcp")
7698
+ }
7699
+ },
6973
7700
  {
6974
7701
  name: "drive.navigate",
6975
7702
  config: {