libretto 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/cli/cli.js +2 -0
  2. package/dist/cli/commands/auth.js +535 -0
  3. package/dist/cli/commands/billing.js +74 -0
  4. package/dist/cli/commands/browser.js +8 -3
  5. package/dist/cli/commands/deploy.js +2 -7
  6. package/dist/cli/commands/execution.js +112 -137
  7. package/dist/cli/commands/snapshot.js +38 -126
  8. package/dist/cli/core/ai-model.js +0 -3
  9. package/dist/cli/core/auth-fetch.js +195 -0
  10. package/dist/cli/core/auth-storage.js +52 -0
  11. package/dist/cli/core/browser.js +151 -206
  12. package/dist/cli/core/daemon/config.js +6 -0
  13. package/dist/cli/core/daemon/daemon.js +298 -0
  14. package/dist/cli/core/daemon/exec.js +86 -0
  15. package/dist/cli/core/daemon/index.js +16 -0
  16. package/dist/cli/core/daemon/ipc.js +171 -0
  17. package/dist/cli/core/daemon/pages.js +15 -0
  18. package/dist/cli/core/daemon/snapshot.js +86 -0
  19. package/dist/cli/core/daemon/spawn.js +90 -0
  20. package/dist/cli/core/exec-compiler.js +111 -0
  21. package/dist/cli/core/prompt.js +72 -0
  22. package/dist/cli/core/providers/browserbase.js +1 -0
  23. package/dist/cli/core/providers/kernel.js +1 -0
  24. package/dist/cli/core/providers/libretto-cloud.js +6 -7
  25. package/dist/cli/core/readonly-exec.js +1 -1
  26. package/dist/cli/router.js +4 -0
  27. package/dist/cli/workers/run-integration-runtime.js +0 -5
  28. package/dist/shared/state/session-state.d.ts +1 -0
  29. package/dist/shared/state/session-state.js +2 -1
  30. package/docs/browser-automation-approaches.md +435 -0
  31. package/docs/releasing.md +117 -0
  32. package/package.json +4 -3
  33. package/skills/libretto/SKILL.md +14 -1
  34. package/skills/libretto-readonly/SKILL.md +1 -1
  35. package/src/cli/cli.ts +2 -0
  36. package/src/cli/commands/auth.ts +787 -0
  37. package/src/cli/commands/billing.ts +133 -0
  38. package/src/cli/commands/browser.ts +8 -2
  39. package/src/cli/commands/deploy.ts +2 -7
  40. package/src/cli/commands/execution.ts +139 -187
  41. package/src/cli/commands/snapshot.ts +46 -143
  42. package/src/cli/core/ai-model.ts +4 -5
  43. package/src/cli/core/auth-fetch.ts +283 -0
  44. package/src/cli/core/auth-storage.ts +102 -0
  45. package/src/cli/core/browser.ts +182 -245
  46. package/src/cli/core/daemon/config.ts +46 -0
  47. package/src/cli/core/daemon/daemon.ts +429 -0
  48. package/src/cli/core/daemon/exec.ts +128 -0
  49. package/src/cli/core/daemon/index.ts +24 -0
  50. package/src/cli/core/daemon/ipc.ts +294 -0
  51. package/src/cli/core/daemon/pages.ts +21 -0
  52. package/src/cli/core/daemon/snapshot.ts +114 -0
  53. package/src/cli/core/daemon/spawn.ts +171 -0
  54. package/src/cli/core/exec-compiler.ts +169 -0
  55. package/src/cli/core/prompt.ts +94 -0
  56. package/src/cli/core/providers/browserbase.ts +1 -0
  57. package/src/cli/core/providers/kernel.ts +1 -0
  58. package/src/cli/core/providers/libretto-cloud.ts +13 -7
  59. package/src/cli/core/providers/types.ts +12 -1
  60. package/src/cli/core/readonly-exec.ts +2 -1
  61. package/src/cli/router.ts +4 -0
  62. package/src/cli/workers/run-integration-runtime.ts +0 -6
  63. package/src/shared/state/session-state.ts +1 -0
  64. package/dist/cli/core/browser-daemon.js +0 -122
  65. package/src/cli/core/browser-daemon.ts +0 -198
@@ -1,12 +1,9 @@
1
1
  import {
2
2
  chromium
3
3
  } from "playwright";
4
- import { openSync, closeSync, existsSync } from "node:fs";
4
+ import { existsSync, unlinkSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { createRequire } from "node:module";
8
6
  import { createServer } from "node:net";
9
- import { spawn } from "node:child_process";
10
7
  import { PROFILES_DIR } from "./context.js";
11
8
  import { readLibrettoConfig } from "./config.js";
12
9
  import {
@@ -20,6 +17,7 @@ import {
20
17
  writeSessionState
21
18
  } from "./session.js";
22
19
  import { getCloudProviderApi } from "./providers/index.js";
20
+ import { DaemonClient, spawnSessionDaemon } from "./daemon/index.js";
23
21
  const CLOSE_WAIT_MS = 1500;
24
22
  const FORCE_CLOSE_WAIT_MS = 300;
25
23
  async function pickFreePort() {
@@ -143,20 +141,6 @@ async function resolvePageReferences(pages) {
143
141
  );
144
142
  return refs;
145
143
  }
146
- async function listOpenPages(session, logger) {
147
- const { browser, page: activePage } = await connect(session, logger);
148
- try {
149
- const pages = browser.contexts().flatMap((ctx) => ctx.pages()).filter(isOperationalPage);
150
- const pageRefs = await resolvePageReferences(pages);
151
- return pageRefs.map(({ id, page }) => ({
152
- id,
153
- url: page.url(),
154
- active: page === activePage
155
- }));
156
- } finally {
157
- disconnectBrowser(browser, logger, session);
158
- }
159
- }
160
144
  async function connect(session, logger, timeoutMs = 1e4, options) {
161
145
  logger.info("connect", { session, timeoutMs });
162
146
  const state = readSessionStateOrThrow(session);
@@ -242,7 +226,15 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
242
226
  }
243
227
  async function runPages(session, logger) {
244
228
  logger.info("pages-start", { session });
245
- const pageSummaries = await listOpenPages(session, logger);
229
+ const state = readSessionStateOrThrow(session);
230
+ let pageSummaries;
231
+ if (!state.daemonSocketPath) {
232
+ throw new Error(
233
+ `Session "${session}" has no daemon socket. The browser daemon may have crashed. Close and reopen the session: libretto close --session ${session}`
234
+ );
235
+ }
236
+ const client = new DaemonClient(state.daemonSocketPath);
237
+ pageSummaries = await client.pages();
246
238
  if (pageSummaries.length === 0) {
247
239
  console.log("No pages found.");
248
240
  return;
@@ -302,8 +294,17 @@ async function runOpen(rawUrl, headed, session, logger, options) {
302
294
  const port = await pickFreePort();
303
295
  const runLogPath = logFileForSession(session);
304
296
  const browserMode = headed ? "headed" : "headless";
297
+ const authDomain = options?.authProfileDomain ? normalizeDomain(normalizeUrl(options.authProfileDomain)) : void 0;
298
+ if (authDomain) {
299
+ const authProfilePath = getProfilePath(authDomain);
300
+ if (!existsSync(authProfilePath)) {
301
+ throw new Error(
302
+ `No saved auth profile for "${authDomain}". Save one first: libretto open https://${authDomain} --headed --session <name>, log in, then run: libretto save ${authDomain} --session <name>`
303
+ );
304
+ }
305
+ }
305
306
  const supportsSavedProfile = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
306
- const domain = supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0;
307
+ const domain = authDomain ?? (supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0);
307
308
  const profilePath = domain ? getProfilePath(domain) : void 0;
308
309
  const useProfile = domain ? hasProfile(domain) : false;
309
310
  logger.info("open-launching", {
@@ -319,106 +320,46 @@ async function runOpen(rawUrl, headed, session, logger, options) {
319
320
  console.log(`Loading saved profile for ${domain}`);
320
321
  }
321
322
  console.log(`Launching ${browserMode} browser (session: ${session})...`);
322
- const daemonEntryPath = fileURLToPath(
323
- new URL("./browser-daemon.js", import.meta.url)
324
- );
325
- const require2 = createRequire(import.meta.url);
326
- const tsxImportPath = pathToFileURL(require2.resolve("tsx/esm")).href;
327
- const daemonConfig = {
328
- port,
329
- url,
323
+ const { pid, socketPath: daemonSocketPath } = await spawnSessionDaemon({
324
+ config: {
325
+ port,
326
+ url,
327
+ session,
328
+ headed,
329
+ viewport,
330
+ storageStatePath: useProfile ? profilePath : void 0,
331
+ windowPosition
332
+ },
330
333
  session,
331
- headed,
332
- viewport,
333
- storageStatePath: useProfile ? profilePath : void 0,
334
- windowPosition
335
- };
336
- const childStderrFd = openSync(runLogPath, "a");
337
- const child = spawn(
338
- process.execPath,
339
- ["--import", tsxImportPath, daemonEntryPath, JSON.stringify(daemonConfig)],
340
- {
341
- detached: true,
342
- stdio: ["ignore", "ignore", childStderrFd]
343
- }
344
- );
345
- child.unref();
346
- closeSync(childStderrFd);
347
- logger.info("open-child-spawned", { pid: child.pid, port, session });
348
- let childSpawnError = null;
349
- let childEarlyExit = null;
350
- child.on("error", (err) => {
351
- childSpawnError = err;
352
- logger.error("open-child-spawn-error", { error: err, session, port });
334
+ logger,
335
+ logPath: runLogPath,
336
+ // The daemon launches Chromium, installs telemetry, navigates to
337
+ // the URL, and only then starts IPC. Navigation alone can take up
338
+ // to 45s (page.setDefaultNavigationTimeout), so the IPC timeout
339
+ // must cover launch + navigation.
340
+ ipcTimeoutMs: 6e4
353
341
  });
354
- child.on("exit", (code, signal) => {
355
- childEarlyExit = { code, signal };
356
- logger.warn("open-child-exited", {
357
- code,
358
- signal,
359
- session,
342
+ writeSessionState(
343
+ {
360
344
  port,
361
- pid: child.pid
362
- });
363
- });
364
- const cdpPollIntervalMs = 500;
365
- const cdpMaxAttempts = 30;
366
- const cdpStartupTimeoutMs = cdpPollIntervalMs * cdpMaxAttempts;
367
- for (let i = 0; i < cdpMaxAttempts; i++) {
368
- const spawnError = childSpawnError;
369
- if (spawnError !== null) {
370
- const errWithCode = spawnError;
371
- const hint = errWithCode.code === "ENOENT" ? " Ensure Node.js is available in PATH for child processes." : "";
372
- throw new Error(
373
- `Failed to launch browser child process: ${spawnError.message}.${hint} Check logs: ${runLogPath}`
374
- );
375
- }
376
- const earlyExit = childEarlyExit;
377
- if (earlyExit !== null) {
378
- const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
379
- throw new Error(
380
- `Browser child process exited before startup (status: ${status}). Check logs: ${runLogPath}`
381
- );
382
- }
383
- await new Promise((r) => setTimeout(r, cdpPollIntervalMs));
384
- const ready = await fetch(`http://127.0.0.1:${port}/json/version`).then(() => true).catch(() => false);
385
- if (i > 0 && i % 5 === 0) {
386
- logger.info("open-waiting-for-cdp", { attempt: i, port, session });
387
- }
388
- if (ready) {
389
- writeSessionState(
390
- {
391
- port,
392
- pid: child.pid,
393
- session,
394
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
395
- status: "active",
396
- mode: accessMode,
397
- viewport
398
- },
399
- logger
400
- );
401
- logger.info("open-success", {
402
- url,
403
- mode: browserMode,
404
- session,
405
- port,
406
- pid: child.pid
407
- });
408
- console.log(`Browser open (${browserMode}): ${url}`);
409
- await new Promise((r) => setTimeout(r, 2e3));
410
- return;
411
- }
412
- }
413
- logger.error("open-timeout", {
345
+ pid,
346
+ session,
347
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
348
+ status: "active",
349
+ mode: accessMode,
350
+ viewport,
351
+ daemonSocketPath
352
+ },
353
+ logger
354
+ );
355
+ logger.info("open-success", {
356
+ url,
357
+ mode: browserMode,
414
358
  session,
415
359
  port,
416
- pid: child.pid,
417
- attempts: cdpMaxAttempts
360
+ pid
418
361
  });
419
- throw new Error(
420
- `Failed to connect to browser after ${Math.ceil(cdpStartupTimeoutMs / 1e3)}s. Check startup logs: ${runLogPath}`
421
- );
362
+ console.log(`Browser open (${browserMode}): ${url}`);
422
363
  }
423
364
  async function runOpenWithProvider(rawUrl, providerName, provider, session, logger, accessMode = "write-access") {
424
365
  const parsedUrl = normalizeUrl(rawUrl);
@@ -431,67 +372,45 @@ async function runOpenWithProvider(rawUrl, providerName, provider, session, logg
431
372
  logger.info("open-provider-session-created", {
432
373
  provider: providerName,
433
374
  sessionId: providerSession.sessionId,
434
- cdpEndpoint: providerSession.cdpEndpoint
375
+ cdpEndpoint: providerSession.cdpEndpoint,
376
+ liveViewUrl: providerSession.liveViewUrl
435
377
  });
436
- console.log(`Connecting to ${providerName} browser...`);
437
- let browser = null;
438
- try {
439
- browser = await tryConnectToCDP(
440
- providerSession.cdpEndpoint,
441
- logger,
442
- 3e4
443
- );
444
- if (!browser) {
445
- throw new Error(
446
- `Could not connect to ${providerName} browser at ${providerSession.cdpEndpoint}. The remote session was created but CDP connection failed.`
447
- );
448
- }
449
- const contexts = browser.contexts();
450
- let page;
451
- if (contexts.length > 0 && contexts[0].pages().length > 0) {
452
- page = contexts[0].pages()[0];
453
- } else {
454
- const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
455
- page = await context.newPage();
456
- }
457
- await page.goto(url);
458
- logger.info("open-provider-navigated", { url, session });
459
- writeSessionState(
460
- {
461
- port: 0,
462
- cdpEndpoint: providerSession.cdpEndpoint,
463
- session,
464
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
465
- status: "active",
466
- mode: accessMode,
467
- provider: {
468
- name: providerName,
469
- sessionId: providerSession.sessionId
470
- }
471
- },
472
- logger
473
- );
474
- disconnectBrowser(browser, logger, session);
475
- } catch (err) {
476
- if (browser) {
477
- disconnectBrowser(browser, logger, session);
478
- }
479
- logger.warn("open-provider-cleanup-after-error", {
480
- provider: providerName,
481
- sessionId: providerSession.sessionId,
482
- error: err
483
- });
484
- try {
485
- await provider.closeSession(providerSession.sessionId);
486
- } catch (cleanupErr) {
487
- logger.warn("open-provider-cleanup-failed", {
488
- provider: providerName,
489
- sessionId: providerSession.sessionId,
490
- error: cleanupErr
491
- });
492
- }
493
- throw err;
378
+ if (providerSession.liveViewUrl) {
379
+ console.log(`View live session: ${providerSession.liveViewUrl}`);
494
380
  }
381
+ console.log(`Connecting to ${providerName} browser...`);
382
+ const runLogPath = logFileForSession(session);
383
+ const { pid, socketPath: daemonSocketPath } = await spawnSessionDaemon({
384
+ config: {
385
+ mode: "connect",
386
+ session,
387
+ cdpEndpoint: providerSession.cdpEndpoint,
388
+ url
389
+ },
390
+ session,
391
+ logger,
392
+ logPath: runLogPath,
393
+ // Remote CDP connection + navigation; must cover both.
394
+ ipcTimeoutMs: 6e4,
395
+ onFailure: () => provider.closeSession(providerSession.sessionId)
396
+ });
397
+ writeSessionState(
398
+ {
399
+ port: 0,
400
+ pid,
401
+ cdpEndpoint: providerSession.cdpEndpoint,
402
+ session,
403
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
404
+ status: "active",
405
+ mode: accessMode,
406
+ daemonSocketPath,
407
+ provider: {
408
+ name: providerName,
409
+ sessionId: providerSession.sessionId
410
+ }
411
+ },
412
+ logger
413
+ );
495
414
  logger.info("open-provider-success", {
496
415
  url,
497
416
  provider: providerName,
@@ -572,6 +491,12 @@ async function runClose(session, logger) {
572
491
  console.log(`No browser running for session "${session}".`);
573
492
  return;
574
493
  }
494
+ if (state.pid != null) {
495
+ logger.info("close-killing", { session, pid: state.pid, port: state.port });
496
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
497
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
498
+ }
499
+ let replayUrl;
575
500
  if (state.provider) {
576
501
  logger.info("close-provider", {
577
502
  session,
@@ -580,7 +505,8 @@ async function runClose(session, logger) {
580
505
  });
581
506
  try {
582
507
  const provider = getCloudProviderApi(state.provider.name);
583
- await provider.closeSession(state.provider.sessionId);
508
+ const result = await provider.closeSession(state.provider.sessionId);
509
+ replayUrl = result.replayUrl;
584
510
  } catch (err) {
585
511
  logger.warn("close-provider-error", {
586
512
  session,
@@ -593,16 +519,14 @@ async function runClose(session, logger) {
593
519
  `Failed to close remote ${state.provider.name} session "${state.provider.sessionId}" for session "${session}". State preserved with status "cleanup-failed". Retry with: libretto close --session ${session}`
594
520
  );
595
521
  }
596
- } else {
597
- logger.info("close-killing", { session, pid: state.pid, port: state.port });
598
- if (state.pid != null) {
599
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
600
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
601
- }
602
522
  }
523
+ unlinkDaemonSocket(state.daemonSocketPath, logger, session);
603
524
  clearSessionState(session, logger);
604
- logger.info("close-success", { session });
525
+ logger.info("close-success", { session, replayUrl });
605
526
  console.log(`Browser closed (session: ${session}).`);
527
+ if (replayUrl) {
528
+ console.log(`View recording: ${replayUrl}`);
529
+ }
606
530
  }
607
531
  function waitForCloseSignalWindow(ms) {
608
532
  return new Promise((r) => setTimeout(r, ms));
@@ -641,16 +565,32 @@ function resolveClosableSessions(logger) {
641
565
  session,
642
566
  pid: state.pid,
643
567
  port: state.port,
644
- provider: state.provider
568
+ provider: state.provider,
569
+ daemonSocketPath: state.daemonSocketPath
645
570
  });
646
571
  }
647
572
  return { closable, clearedUnreadableStates };
648
573
  }
574
+ function unlinkDaemonSocket(socketPath, logger, session) {
575
+ if (!socketPath) return;
576
+ try {
577
+ unlinkSync(socketPath);
578
+ } catch (err) {
579
+ if (err.code !== "ENOENT") {
580
+ logger.warn("close-socket-unlink-failed", {
581
+ session,
582
+ socketPath,
583
+ error: err
584
+ });
585
+ }
586
+ }
587
+ }
649
588
  function clearStoppedSessionStates(sessions, logger, skip) {
650
589
  let cleared = 0;
651
590
  for (const session of sessions) {
652
591
  if (skip?.has(session.session)) continue;
653
592
  if (session.pid == null || !isPidRunning(session.pid)) {
593
+ unlinkDaemonSocket(session.daemonSocketPath, logger, session.session);
654
594
  clearSessionState(session.session, logger);
655
595
  cleared += 1;
656
596
  }
@@ -671,6 +611,7 @@ async function runCloseAll(logger, options) {
671
611
  return;
672
612
  }
673
613
  const failedProviderSessions = /* @__PURE__ */ new Set();
614
+ const replayUrls = [];
674
615
  for (const target of closable) {
675
616
  if (target.provider) {
676
617
  logger.info("close-all-provider", {
@@ -680,7 +621,13 @@ async function runCloseAll(logger, options) {
680
621
  });
681
622
  try {
682
623
  const provider = getCloudProviderApi(target.provider.name);
683
- await provider.closeSession(target.provider.sessionId);
624
+ const result = await provider.closeSession(target.provider.sessionId);
625
+ if (result.replayUrl) {
626
+ replayUrls.push({
627
+ session: target.session,
628
+ replayUrl: result.replayUrl
629
+ });
630
+ }
684
631
  } catch (err) {
685
632
  logger.warn("close-all-provider-error", {
686
633
  session: target.session,
@@ -697,20 +644,18 @@ async function runCloseAll(logger, options) {
697
644
  }
698
645
  }
699
646
  for (const target of closable) {
700
- if (target.provider) continue;
647
+ if (target.pid == null) continue;
701
648
  logger.info("close-all-sigterm", {
702
649
  session: target.session,
703
650
  pid: target.pid,
704
651
  port: target.port
705
652
  });
706
- if (target.pid != null) {
707
- sendSignalToProcessGroupOrPid(
708
- target.pid,
709
- "SIGTERM",
710
- logger,
711
- target.session
712
- );
713
- }
653
+ sendSignalToProcessGroupOrPid(
654
+ target.pid,
655
+ "SIGTERM",
656
+ logger,
657
+ target.session
658
+ );
714
659
  }
715
660
  await waitForCloseSignalWindow(CLOSE_WAIT_MS);
716
661
  let survivors = closable.filter(
@@ -781,6 +726,9 @@ async function runCloseAll(logger, options) {
781
726
  if (forceKilled > 0) {
782
727
  console.log(`Force-killed ${forceKilled} session(s).`);
783
728
  }
729
+ for (const { session, replayUrl } of replayUrls) {
730
+ console.log(`View recording (${session}): ${replayUrl}`);
731
+ }
784
732
  }
785
733
  async function runConnect(cdpUrl, session, logger, accessMode = "write-access") {
786
734
  logger.info("connect-start", { cdpUrl, session, accessMode });
@@ -826,36 +774,34 @@ async function runConnect(cdpUrl, session, logger, accessMode = "write-access")
826
774
  endpoint
827
775
  });
828
776
  }
829
- const browser = await tryConnectToCDP(endpoint, logger, 1e4);
830
- if (!browser) {
831
- throw new Error(
832
- `CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
833
- );
834
- }
835
- const pages = resolveOperationalPages(browser);
836
- logger.info("connect-pages", {
777
+ const runLogPath = logFileForSession(session);
778
+ const { pid, socketPath: daemonSocketPath, client } = await spawnSessionDaemon({
779
+ config: { mode: "connect", session, cdpEndpoint: endpoint },
837
780
  session,
838
- pageCount: pages.length,
839
- urls: pages.map((p) => p.url())
781
+ logger,
782
+ logPath: runLogPath,
783
+ ipcTimeoutMs: 1e4
840
784
  });
841
- disconnectBrowser(browser, logger, session);
842
785
  writeSessionState(
843
786
  {
844
787
  port,
788
+ pid,
845
789
  cdpEndpoint: endpoint,
846
790
  session,
847
791
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
848
792
  status: "active",
849
- mode: accessMode
793
+ mode: accessMode,
794
+ daemonSocketPath
850
795
  },
851
796
  logger
852
797
  );
798
+ const pages = await client.pages();
853
799
  logger.info("connect-success", { cdpUrl: endpoint, session, port });
854
800
  console.log(`Connected to ${endpoint} (session: ${session})`);
855
801
  console.log(` Pages found: ${pages.length}`);
856
802
  if (pages.length > 0) {
857
803
  for (const p of pages.slice(0, 5)) {
858
- console.log(` ${p.url()}`);
804
+ console.log(` ${p.url}`);
859
805
  }
860
806
  if (pages.length > 5) {
861
807
  console.log(` ... and ${pages.length - 5} more`);
@@ -876,7 +822,6 @@ export {
876
822
  getProfilePath,
877
823
  getScreenshotBaseName,
878
824
  hasProfile,
879
- listOpenPages,
880
825
  normalizeDomain,
881
826
  normalizeUrl,
882
827
  resolvePath,
@@ -0,0 +1,6 @@
1
+ function isConnectConfig(config) {
2
+ return "mode" in config && config.mode === "connect";
3
+ }
4
+ export {
5
+ isConnectConfig
6
+ };