browser-pilot 0.0.15 → 0.0.16

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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-LCNFBXB5.mjs";
4
4
  import {
5
5
  Page
6
- } from "./chunk-USYSHCI3.mjs";
6
+ } from "./chunk-6GBYX7C2.mjs";
7
7
 
8
8
  // src/providers/browserbase.ts
9
9
  var BrowserBaseProvider = class {
@@ -190,6 +190,330 @@ async function getBrowserWebSocketUrl(host = "localhost:9222") {
190
190
  return info.webSocketDebuggerUrl;
191
191
  }
192
192
 
193
+ // src/providers/local-discovery.ts
194
+ var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
195
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
196
+ var DevToolsActivePortParseError = class extends Error {
197
+ constructor(message, reason) {
198
+ super(message);
199
+ this.reason = reason;
200
+ this.name = "DevToolsActivePortParseError";
201
+ }
202
+ };
203
+ function getRuntimeEnv() {
204
+ if (typeof process === "undefined") {
205
+ return {};
206
+ }
207
+ return process.env;
208
+ }
209
+ function getRuntimePlatform() {
210
+ if (typeof process === "undefined") {
211
+ return void 0;
212
+ }
213
+ return process.platform;
214
+ }
215
+ function normalizePlatform(platform) {
216
+ if (platform === "darwin" || platform === "linux" || platform === "win32") {
217
+ return platform;
218
+ }
219
+ throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
220
+ }
221
+ function trimTrailingSeparator(path) {
222
+ return path.replace(/[\\/]+$/, "");
223
+ }
224
+ function joinPath(platform, ...parts) {
225
+ const separator = platform === "win32" ? "\\" : "/";
226
+ const cleaned = parts.map((part, index) => {
227
+ if (index === 0) return trimTrailingSeparator(part);
228
+ return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
229
+ }).filter((part) => part.length > 0);
230
+ return cleaned.join(separator);
231
+ }
232
+ function resolveHomeDir(platform, env, explicitHomeDir) {
233
+ if (explicitHomeDir) {
234
+ return explicitHomeDir;
235
+ }
236
+ if (platform === "win32") {
237
+ return env["USERPROFILE"] ?? env["HOME"] ?? "";
238
+ }
239
+ return env["HOME"] ?? env["USERPROFILE"] ?? "";
240
+ }
241
+ function toFileFailure(target, error) {
242
+ const errno = error?.code;
243
+ if (errno === "ENOENT") {
244
+ return {
245
+ ...target,
246
+ reason: "missing-file",
247
+ message: `DevToolsActivePort not found at ${target.portFile}`
248
+ };
249
+ }
250
+ return {
251
+ ...target,
252
+ reason: "unreadable-file",
253
+ message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
254
+ };
255
+ }
256
+ function toProbeFailure(target, wsUrl, error) {
257
+ const message = error instanceof Error ? error.message : String(error);
258
+ const lowerMessage = message.toLowerCase();
259
+ let reason = "connection-error";
260
+ if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
261
+ reason = "connection-refused";
262
+ } else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
263
+ reason = "connection-timeout";
264
+ } else if (lowerMessage.includes("closed")) {
265
+ reason = "unexpected-close";
266
+ } else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
267
+ reason = "cdp-error";
268
+ }
269
+ return {
270
+ ...target,
271
+ wsUrl,
272
+ reason,
273
+ message
274
+ };
275
+ }
276
+ async function readTextFile(path) {
277
+ const fs = await import("fs/promises");
278
+ return fs.readFile(path, "utf-8");
279
+ }
280
+ async function probeBrowserWebSocket(wsUrl, timeoutMs) {
281
+ let client;
282
+ try {
283
+ client = await createCDPClient(wsUrl, { timeout: timeoutMs });
284
+ const version = await client.send("Browser.getVersion", void 0, null);
285
+ return { browserVersion: version.product };
286
+ } finally {
287
+ await client?.close().catch(() => {
288
+ });
289
+ }
290
+ }
291
+ var defaultDependencies = {
292
+ readTextFile,
293
+ probeBrowserWebSocket,
294
+ getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
295
+ };
296
+ function resolveChromeUserDataDirs(options = {}) {
297
+ const env = options.env ?? getRuntimeEnv();
298
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
299
+ const homeDir = resolveHomeDir(platform, env, options.homeDir);
300
+ if (!homeDir) {
301
+ throw new Error("Could not determine home directory for local Chrome discovery");
302
+ }
303
+ switch (platform) {
304
+ case "darwin": {
305
+ const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
306
+ return {
307
+ stable: joinPath(platform, base, "Chrome"),
308
+ beta: joinPath(platform, base, "Chrome Beta"),
309
+ dev: joinPath(platform, base, "Chrome Dev"),
310
+ canary: joinPath(platform, base, "Chrome Canary")
311
+ };
312
+ }
313
+ case "linux": {
314
+ const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
315
+ return {
316
+ stable: joinPath(platform, configHome, "google-chrome"),
317
+ beta: joinPath(platform, configHome, "google-chrome-beta"),
318
+ dev: joinPath(platform, configHome, "google-chrome-dev"),
319
+ canary: joinPath(platform, configHome, "google-chrome-canary")
320
+ };
321
+ }
322
+ case "win32": {
323
+ const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
324
+ const base = joinPath(platform, localAppData, "Google");
325
+ return {
326
+ stable: joinPath(platform, base, "Chrome", "User Data"),
327
+ beta: joinPath(platform, base, "Chrome Beta", "User Data"),
328
+ dev: joinPath(platform, base, "Chrome Dev", "User Data"),
329
+ canary: joinPath(platform, base, "Chrome SxS", "User Data")
330
+ };
331
+ }
332
+ }
333
+ throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
334
+ }
335
+ function buildLocalBrowserScanTargets(options = {}) {
336
+ const env = options.env ?? getRuntimeEnv();
337
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
338
+ if (options.userDataDir) {
339
+ return [
340
+ {
341
+ channel: options.channel ?? "custom",
342
+ userDataDir: options.userDataDir,
343
+ portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
344
+ }
345
+ ];
346
+ }
347
+ const dirs = resolveChromeUserDataDirs({
348
+ platform,
349
+ env,
350
+ homeDir: options.homeDir
351
+ });
352
+ const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
353
+ return channels.map((channel) => ({
354
+ channel,
355
+ userDataDir: dirs[channel],
356
+ portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
357
+ }));
358
+ }
359
+ function parseDevToolsActivePortFile(content) {
360
+ const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
361
+ if (lines.length !== 2) {
362
+ throw new DevToolsActivePortParseError(
363
+ `Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
364
+ "malformed-file"
365
+ );
366
+ }
367
+ const portText = lines[0];
368
+ const browserPath = lines[1];
369
+ const port = Number.parseInt(portText, 10);
370
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
371
+ throw new DevToolsActivePortParseError(
372
+ `Invalid DevToolsActivePort port: ${portText}`,
373
+ "invalid-port"
374
+ );
375
+ }
376
+ if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
377
+ throw new DevToolsActivePortParseError(
378
+ `Invalid DevToolsActivePort browser path: ${browserPath}`,
379
+ "invalid-path"
380
+ );
381
+ }
382
+ return {
383
+ port,
384
+ browserPath,
385
+ wsUrl: `ws://127.0.0.1:${port}${browserPath}`
386
+ };
387
+ }
388
+ async function inspectScanTarget(target, options, deps) {
389
+ let content;
390
+ try {
391
+ content = await deps.readTextFile(target.portFile);
392
+ } catch (error) {
393
+ return { kind: "failure", failure: toFileFailure(target, error) };
394
+ }
395
+ let parsed;
396
+ try {
397
+ parsed = parseDevToolsActivePortFile(content);
398
+ } catch (error) {
399
+ if (error instanceof DevToolsActivePortParseError) {
400
+ return {
401
+ kind: "failure",
402
+ failure: {
403
+ ...target,
404
+ reason: error.reason,
405
+ message: error.message
406
+ }
407
+ };
408
+ }
409
+ throw error;
410
+ }
411
+ try {
412
+ const probe = await deps.probeBrowserWebSocket(
413
+ parsed.wsUrl,
414
+ options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
415
+ );
416
+ return {
417
+ kind: "candidate",
418
+ candidate: {
419
+ ...target,
420
+ port: parsed.port,
421
+ browserPath: parsed.browserPath,
422
+ wsUrl: parsed.wsUrl,
423
+ browserVersion: probe.browserVersion
424
+ }
425
+ };
426
+ } catch (error) {
427
+ return {
428
+ kind: "failure",
429
+ failure: toProbeFailure(target, parsed.wsUrl, error)
430
+ };
431
+ }
432
+ }
433
+ async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
434
+ const scanTargets = buildLocalBrowserScanTargets(options);
435
+ const outcomes = await Promise.all(
436
+ scanTargets.map((target) => inspectScanTarget(target, options, deps))
437
+ );
438
+ const candidates = [];
439
+ const failures = [];
440
+ for (const outcome of outcomes) {
441
+ if (outcome.kind === "candidate") {
442
+ candidates.push(outcome.candidate);
443
+ } else {
444
+ failures.push(outcome.failure);
445
+ }
446
+ }
447
+ return { candidates, failures };
448
+ }
449
+ var BrowserEndpointResolutionError = class extends Error {
450
+ constructor(code, message, details = {}) {
451
+ super(message);
452
+ this.code = code;
453
+ this.details = details;
454
+ }
455
+ name = "BrowserEndpointResolutionError";
456
+ };
457
+ async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
458
+ if (options.explicitWsUrl) {
459
+ return {
460
+ wsUrl: options.explicitWsUrl,
461
+ source: "explicit-ws"
462
+ };
463
+ }
464
+ let localDiscovery;
465
+ if (options.allowLocalDiscovery ?? true) {
466
+ localDiscovery = await discoverLocalBrowsers(options, deps);
467
+ if (localDiscovery.candidates.length === 1) {
468
+ const candidate = localDiscovery.candidates[0];
469
+ return {
470
+ wsUrl: candidate.wsUrl,
471
+ source: "devtools-active-port",
472
+ channel: candidate.channel,
473
+ userDataDir: candidate.userDataDir
474
+ };
475
+ }
476
+ if (localDiscovery.candidates.length > 1) {
477
+ throw new BrowserEndpointResolutionError(
478
+ "multiple-local-browsers",
479
+ "Multiple local Chrome profiles are available for auto-discovery",
480
+ {
481
+ candidates: localDiscovery.candidates,
482
+ failures: localDiscovery.failures
483
+ }
484
+ );
485
+ }
486
+ }
487
+ if (options.allowLegacyHostFallback ?? true) {
488
+ const legacyHost = options.legacyHost ?? "localhost:9222";
489
+ try {
490
+ return {
491
+ wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
492
+ source: "json-version"
493
+ };
494
+ } catch (error) {
495
+ throw new BrowserEndpointResolutionError(
496
+ "browser-not-found",
497
+ "Could not resolve a browser endpoint",
498
+ {
499
+ candidates: localDiscovery?.candidates,
500
+ failures: localDiscovery?.failures,
501
+ legacyError: error instanceof Error ? error : new Error(String(error)),
502
+ legacyHost
503
+ }
504
+ );
505
+ }
506
+ }
507
+ throw new BrowserEndpointResolutionError(
508
+ "browser-not-found",
509
+ "Could not resolve a browser endpoint",
510
+ {
511
+ candidates: localDiscovery?.candidates,
512
+ failures: localDiscovery?.failures
513
+ }
514
+ );
515
+ }
516
+
193
517
  // src/providers/index.ts
194
518
  function createProvider(options) {
195
519
  switch (options.provider) {
@@ -272,13 +596,26 @@ var Browser = class _Browser {
272
596
  * Connect to a browser instance
273
597
  */
274
598
  static async connect(options) {
275
- const provider = createProvider(options);
276
- const session = await provider.createSession(options.session);
599
+ let connectOptions = options;
600
+ if (options.provider === "generic" && !options.wsUrl) {
601
+ const endpoint = await resolveBrowserEndpoint({
602
+ channel: options.channel,
603
+ userDataDir: options.userDataDir,
604
+ allowLocalDiscovery: true,
605
+ allowLegacyHostFallback: true
606
+ });
607
+ connectOptions = {
608
+ ...options,
609
+ wsUrl: endpoint.wsUrl
610
+ };
611
+ }
612
+ const provider = createProvider(connectOptions);
613
+ const session = await provider.createSession(connectOptions.session);
277
614
  const cdp = await createCDPClient(session.wsUrl, {
278
- debug: options.debug,
279
- timeout: options.timeout
615
+ debug: connectOptions.debug,
616
+ timeout: connectOptions.timeout
280
617
  });
281
- return new _Browser(cdp, provider, session, options);
618
+ return new _Browser(cdp, provider, session, connectOptions);
282
619
  }
283
620
  /**
284
621
  * Get or create a page by name.
@@ -460,7 +797,8 @@ function connect(options) {
460
797
  }
461
798
 
462
799
  export {
463
- getBrowserWebSocketUrl,
800
+ BrowserEndpointResolutionError,
801
+ resolveBrowserEndpoint,
464
802
  Browser,
465
803
  connect
466
804
  };
@@ -860,7 +860,9 @@ function buildTraceSummaries(events) {
860
860
  };
861
861
  }
862
862
  function summarizeWs(events) {
863
- const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
863
+ const relevant = events.filter(
864
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
865
+ );
864
866
  const connections = /* @__PURE__ */ new Map();
865
867
  for (const event of relevant) {
866
868
  const id = event.connectionId ?? event.requestId ?? event.traceId;
@@ -890,7 +892,7 @@ function summarizeWs(events) {
890
892
  }
891
893
  const values = [...connections.values()];
892
894
  const reconnects = values.reduce((count, connection) => {
893
- return connection.closedAt && !connection.createdAt ? count : count;
895
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
894
896
  }, 0);
895
897
  return {
896
898
  view: "ws",
@@ -1143,6 +1145,31 @@ function frameToStep(frame) {
1143
1145
  }
1144
1146
  }
1145
1147
 
1148
+ // src/trace/model.ts
1149
+ function createTraceId(prefix = "evt") {
1150
+ const random = Math.random().toString(36).slice(2, 10);
1151
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1152
+ }
1153
+ function normalizeTraceEvent(event) {
1154
+ return {
1155
+ traceId: event.traceId ?? createTraceId(event.channel),
1156
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1157
+ elapsedMs: event.elapsedMs ?? 0,
1158
+ severity: event.severity ?? inferSeverity(event.event),
1159
+ data: event.data ?? {},
1160
+ ...event
1161
+ };
1162
+ }
1163
+ function inferSeverity(eventName) {
1164
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1165
+ return "error";
1166
+ }
1167
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1168
+ return "warn";
1169
+ }
1170
+ return "info";
1171
+ }
1172
+
1146
1173
  // src/trace/script.ts
1147
1174
  var TRACE_BINDING_NAME = "__bpTraceBinding";
1148
1175
  var TRACE_SCRIPT = `
@@ -1422,31 +1449,6 @@ var TRACE_SCRIPT = `
1422
1449
  })();
1423
1450
  `;
1424
1451
 
1425
- // src/trace/model.ts
1426
- function createTraceId(prefix = "evt") {
1427
- const random = Math.random().toString(36).slice(2, 10);
1428
- return `${prefix}-${Date.now().toString(36)}-${random}`;
1429
- }
1430
- function normalizeTraceEvent(event) {
1431
- return {
1432
- traceId: event.traceId ?? createTraceId(event.channel),
1433
- ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1434
- elapsedMs: event.elapsedMs ?? 0,
1435
- severity: event.severity ?? inferSeverity(event.event),
1436
- data: event.data ?? {},
1437
- ...event
1438
- };
1439
- }
1440
- function inferSeverity(eventName) {
1441
- if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1442
- return "error";
1443
- }
1444
- if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1445
- return "warn";
1446
- }
1447
- return "info";
1448
- }
1449
-
1450
1452
  // src/trace/live.ts
1451
1453
  function globToRegex(pattern) {
1452
1454
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
@@ -1463,6 +1465,15 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1463
1465
  "text",
1464
1466
  "screenshot"
1465
1467
  ];
1468
+ function readString(value) {
1469
+ return typeof value === "string" ? value : void 0;
1470
+ }
1471
+ function readStringOr(value, fallback = "") {
1472
+ return readString(value) ?? fallback;
1473
+ }
1474
+ function formatConsoleArg(entry) {
1475
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
1476
+ }
1466
1477
  function loadExistingRecording(manifestPath) {
1467
1478
  try {
1468
1479
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
@@ -2263,8 +2274,13 @@ Valid actions: ${valid}`);
2263
2274
  await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2264
2275
  } catch {
2265
2276
  }
2266
- await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2267
- await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
2277
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
2278
+ source: TRACE_SCRIPT
2279
+ });
2280
+ await this.page.cdpClient.send("Runtime.evaluate", {
2281
+ expression: TRACE_SCRIPT,
2282
+ awaitPromise: false
2283
+ });
2268
2284
  }
2269
2285
  async waitForWsMessage(match, where, timeout) {
2270
2286
  await this.ensureTraceHooks();
@@ -2282,12 +2298,12 @@ Valid actions: ${valid}`);
2282
2298
  clearTimeout(timer);
2283
2299
  };
2284
2300
  const onCreated = (params) => {
2285
- wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
2301
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2286
2302
  };
2287
2303
  const onFrame = (params) => {
2288
- const requestId = String(params["requestId"] ?? "");
2304
+ const requestId = readStringOr(params["requestId"]);
2289
2305
  const response = params["response"] ?? {};
2290
- const payload = String(response.payloadData ?? "");
2306
+ const payload = response.payloadData ?? "";
2291
2307
  const url = wsUrls.get(requestId) ?? "";
2292
2308
  if (!regex.test(url) && !regex.test(payload)) {
2293
2309
  return;
@@ -2303,13 +2319,13 @@ Valid actions: ${valid}`);
2303
2319
  return;
2304
2320
  }
2305
2321
  try {
2306
- const parsed = JSON.parse(String(params["payload"] ?? ""));
2322
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2307
2323
  if (parsed.event !== "ws.frame.received") {
2308
2324
  return;
2309
2325
  }
2310
2326
  const data = parsed.data ?? {};
2311
- const payload = String(data["payload"] ?? "");
2312
- const url = String(data["url"] ?? "");
2327
+ const payload = readStringOr(data["payload"]);
2328
+ const url = readStringOr(data["url"]);
2313
2329
  if (!regex.test(url) && !regex.test(payload)) {
2314
2330
  return;
2315
2331
  }
@@ -2318,7 +2334,7 @@ Valid actions: ${valid}`);
2318
2334
  }
2319
2335
  cleanup();
2320
2336
  resolve({
2321
- requestId: String(data["connectionId"] ?? ""),
2337
+ requestId: readStringOr(data["connectionId"]),
2322
2338
  url,
2323
2339
  payload
2324
2340
  });
@@ -2362,13 +2378,14 @@ Valid actions: ${valid}`);
2362
2378
  if (!entry || typeof entry !== "object") {
2363
2379
  continue;
2364
2380
  }
2365
- const event = String(entry["event"] ?? "");
2381
+ const record = entry;
2382
+ const event = readStringOr(record["event"]);
2366
2383
  if (event !== "ws.frame.received") {
2367
2384
  continue;
2368
2385
  }
2369
- const data = entry["data"] ?? {};
2370
- const payload = String(data["payload"] ?? "");
2371
- const url = String(data["url"] ?? "");
2386
+ const data = record["data"] ?? {};
2387
+ const payload = readStringOr(data["payload"]);
2388
+ const url = readStringOr(data["url"]);
2372
2389
  if (!regex.test(url) && !regex.test(payload)) {
2373
2390
  continue;
2374
2391
  }
@@ -2376,7 +2393,7 @@ Valid actions: ${valid}`);
2376
2393
  continue;
2377
2394
  }
2378
2395
  return {
2379
- requestId: String(data["connectionId"] ?? ""),
2396
+ requestId: readStringOr(data["connectionId"]),
2380
2397
  url,
2381
2398
  payload
2382
2399
  };
@@ -2397,13 +2414,11 @@ Valid actions: ${valid}`);
2397
2414
  return;
2398
2415
  }
2399
2416
  const args = Array.isArray(params["args"]) ? params["args"] : [];
2400
- errors.push(
2401
- args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2402
- );
2417
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2403
2418
  };
2404
2419
  const onException = (params) => {
2405
2420
  const details = params["exceptionDetails"] ?? {};
2406
- errors.push(String(details["text"] ?? "Runtime exception"));
2421
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2407
2422
  };
2408
2423
  const timer = setTimeout(() => {
2409
2424
  cleanup();