libretto 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +210 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. package/skills/libretto/references/user-action-log.md +0 -31
@@ -1,9 +1,19 @@
1
- import { chromium } from "playwright";
1
+ import {
2
+ chromium
3
+ } from "playwright";
2
4
  import { openSync, existsSync } from "node:fs";
3
5
  import { dirname, join, resolve } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { createServer } from "node:net";
6
8
  import { spawn } from "node:child_process";
9
+ import {
10
+ filterSemanticClasses,
11
+ INTERACTIVE_ROLE_NAMES,
12
+ INTERACTIVE_TAG_NAMES,
13
+ isObfuscatedClass,
14
+ TEST_ATTRIBUTE_NAMES,
15
+ TRUSTED_ATTRIBUTE_NAMES
16
+ } from "../../shared/dom-semantics.js";
7
17
  import {
8
18
  getSessionActionsLogPath,
9
19
  getSessionNetworkLogPath,
@@ -57,9 +67,8 @@ function getProfilePath(domain) {
57
67
  function hasProfile(domain) {
58
68
  return existsSync(getProfilePath(domain));
59
69
  }
60
- async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
61
- const endpoint = `http://localhost:${port}`;
62
- logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
70
+ async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
71
+ logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
63
72
  try {
64
73
  const connectPromise = chromium.connectOverCDP(endpoint);
65
74
  const timeoutPromise = new Promise(
@@ -68,16 +77,15 @@ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
68
77
  const browser = await Promise.race([connectPromise, timeoutPromise]);
69
78
  if (browser) {
70
79
  logger.info("cdp-connect-success", {
71
- port,
72
80
  endpoint,
73
81
  contexts: browser.contexts().length
74
82
  });
75
83
  } else {
76
- logger.warn("cdp-connect-timeout", { port, endpoint, timeoutMs });
84
+ logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
77
85
  }
78
86
  return browser;
79
87
  } catch (err) {
80
- logger.error("cdp-connect-error", { error: err, port, endpoint });
88
+ logger.error("cdp-connect-error", { error: err, endpoint });
81
89
  return null;
82
90
  }
83
91
  }
@@ -102,7 +110,9 @@ async function resolvePageId(page) {
102
110
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
103
111
  const targetId = targetInfo?.targetInfo?.targetId;
104
112
  if (typeof targetId !== "string" || targetId.length === 0) {
105
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
113
+ throw new Error(
114
+ `Could not resolve target id for page at URL "${page.url()}".`
115
+ );
106
116
  }
107
117
  return targetId;
108
118
  } finally {
@@ -135,21 +145,22 @@ async function listOpenPages(session, logger) {
135
145
  async function connect(session, logger, timeoutMs = 1e4, options) {
136
146
  logger.info("connect", { session, timeoutMs });
137
147
  const state = readSessionStateOrThrow(session);
138
- const browser = await tryConnectToPort(state.port, logger, timeoutMs);
148
+ const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
149
+ const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
139
150
  if (!browser) {
140
151
  logger.error("connect-no-browser", {
141
152
  session,
142
- port: state.port,
153
+ endpoint,
143
154
  pid: state.pid
144
155
  });
145
- if (!isPidRunning(state.pid)) {
156
+ if (state.pid == null || !isPidRunning(state.pid)) {
146
157
  clearSessionState(session, logger);
147
158
  throw new Error(
148
159
  `No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
149
160
  );
150
161
  }
151
162
  throw new Error(
152
- `Could not connect to the browser for session "${session}" at http://127.0.0.1:${state.port}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
163
+ `Could not connect to the browser for session "${session}" at ${endpoint}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
153
164
  );
154
165
  }
155
166
  const contexts = browser.contexts();
@@ -230,16 +241,34 @@ function resolveViewport(cliViewport, logger) {
230
241
  }
231
242
  const config = readLibrettoConfig();
232
243
  if (config.viewport) {
233
- logger.info("viewport-source", { source: "config", viewport: config.viewport });
244
+ logger.info("viewport-source", {
245
+ source: "config",
246
+ viewport: config.viewport
247
+ });
234
248
  return config.viewport;
235
249
  }
236
- logger.info("viewport-source", { source: "default", viewport: DEFAULT_VIEWPORT });
250
+ logger.info("viewport-source", {
251
+ source: "default",
252
+ viewport: DEFAULT_VIEWPORT
253
+ });
237
254
  return DEFAULT_VIEWPORT;
238
255
  }
256
+ function resolveWindowPosition(logger) {
257
+ const config = readLibrettoConfig();
258
+ if (config.windowPosition) {
259
+ logger.info("window-position-source", {
260
+ source: "config",
261
+ windowPosition: config.windowPosition
262
+ });
263
+ return config.windowPosition;
264
+ }
265
+ return void 0;
266
+ }
239
267
  async function runOpen(rawUrl, headed, session, logger, options) {
240
268
  const url = normalizeUrl(rawUrl);
241
269
  const viewport = resolveViewport(options?.viewport, logger);
242
- logger.info("open-start", { url, headed, session, viewport });
270
+ const windowPosition = headed ? resolveWindowPosition(logger) : void 0;
271
+ logger.info("open-start", { url, headed, session, viewport, windowPosition });
243
272
  assertSessionAvailableForStart(session, logger);
244
273
  const port = await pickFreePort();
245
274
  const runLogPath = logFileForSession(session);
@@ -268,6 +297,45 @@ async function runOpen(rawUrl, headed, session, logger, options) {
268
297
  const escapedLogPath = runLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
269
298
  const escapedNetworkLogPath = networkLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
270
299
  const escapedActionsLogPath = actionsLogPath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
300
+ const windowPositionArg = windowPosition ? `, '--window-position=${windowPosition.x},${windowPosition.y}'` : "";
301
+ const windowBoundsSetupCode = windowPosition ? `
302
+ const requestedWindowBounds = { left: ${windowPosition.x}, top: ${windowPosition.y}, windowState: 'normal' };
303
+ const pageCdp = await context.newCDPSession(page);
304
+ let browserCdp;
305
+ try {
306
+ const targetInfo = await pageCdp.send('Target.getTargetInfo');
307
+ const targetId = targetInfo?.targetInfo?.targetId;
308
+ browserCdp = await browser.newBrowserCDPSession();
309
+ const windowResult = await browserCdp.send(
310
+ 'Browser.getWindowForTarget',
311
+ targetId ? { targetId } : {},
312
+ );
313
+ await browserCdp.send('Browser.setWindowBounds', {
314
+ windowId: windowResult.windowId,
315
+ bounds: requestedWindowBounds,
316
+ });
317
+ await new Promise((resolve) => setTimeout(resolve, 250));
318
+ const actualWindow = await browserCdp.send('Browser.getWindowBounds', {
319
+ windowId: windowResult.windowId,
320
+ });
321
+ childLog('info', 'window-bounds-set', {
322
+ windowId: windowResult.windowId,
323
+ requestedBounds: requestedWindowBounds,
324
+ actualBounds: actualWindow.bounds,
325
+ });
326
+ } catch (error) {
327
+ childLog('warn', 'window-bounds-set-failed', {
328
+ requestedBounds: requestedWindowBounds,
329
+ message: error instanceof Error ? error.message : String(error),
330
+ stack: error instanceof Error ? error.stack : undefined,
331
+ });
332
+ } finally {
333
+ await pageCdp.detach().catch(() => {});
334
+ if (browserCdp) {
335
+ await browserCdp.detach().catch(() => {});
336
+ }
337
+ }
338
+ ` : "";
271
339
  const launcherCode = `
272
340
  import { chromium } from 'playwright';
273
341
  import { appendFileSync, mkdirSync } from 'node:fs';
@@ -279,14 +347,21 @@ const ACTIONS_LOG = '${escapedActionsLogPath}';
279
347
  mkdirSync(dirname(NETWORK_LOG), { recursive: true });
280
348
 
281
349
  // tsx/esbuild may emit __name() wrappers in Function#toString output.
282
- const __name = (target, value) =>
283
- Object.defineProperty(target, 'name', { value, configurable: true });
350
+ const __name = (target, value) =>
351
+ Object.defineProperty(target, 'name', { value, configurable: true });
284
352
 
285
- ${installSessionTelemetry.toString()}
353
+ const TEST_ATTRIBUTE_NAMES = ${JSON.stringify([...TEST_ATTRIBUTE_NAMES])};
354
+ const TRUSTED_ATTRIBUTE_NAMES = ${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])};
355
+ const INTERACTIVE_TAG_NAMES = ${JSON.stringify([...INTERACTIVE_TAG_NAMES])};
356
+ const INTERACTIVE_ROLE_NAMES = ${JSON.stringify([...INTERACTIVE_ROLE_NAMES])};
357
+ const filterSemanticClasses = ${filterSemanticClasses.toString()};
358
+ const isObfuscatedClass = ${isObfuscatedClass.toString()};
286
359
 
287
- function logAction(entry) {
288
- appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
289
- }
360
+ ${installSessionTelemetry.toString()}
361
+
362
+ function logAction(entry) {
363
+ appendFileSync(ACTIONS_LOG, JSON.stringify(entry) + '\\n');
364
+ }
290
365
 
291
366
  function logNetwork(entry) {
292
367
  appendFileSync(NETWORK_LOG, JSON.stringify(entry) + '\\n');
@@ -308,7 +383,7 @@ function childLog(level, event, data = {}) {
308
383
 
309
384
  const browser = await chromium.launch({
310
385
  headless: ${!headed},
311
- args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'],
386
+ args: ['--disable-blink-features=AutomationControlled', '--remote-debugging-port=${port}', '--remote-debugging-address=127.0.0.1', '--no-focus-on-check'${windowPositionArg}],
312
387
  });
313
388
 
314
389
  browser.on('disconnected', () => {
@@ -322,6 +397,7 @@ const context = await browser.newContext({
322
397
  });
323
398
 
324
399
  const page = await context.newPage();
400
+ ${windowBoundsSetupCode}
325
401
  page.setDefaultTimeout(30000);
326
402
  page.setDefaultNavigationTimeout(45000);
327
403
 
@@ -414,14 +490,17 @@ await new Promise(() => {});
414
490
  logger.info("open-waiting-for-cdp", { attempt: i, port, session });
415
491
  }
416
492
  if (ready) {
417
- writeSessionState({
418
- port,
419
- pid: child.pid,
420
- session,
421
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
422
- status: "active",
423
- viewport
424
- }, logger);
493
+ writeSessionState(
494
+ {
495
+ port,
496
+ pid: child.pid,
497
+ session,
498
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
499
+ status: "active",
500
+ viewport
501
+ },
502
+ logger
503
+ );
425
504
  logger.info("open-success", {
426
505
  url,
427
506
  mode: browserMode,
@@ -517,8 +596,10 @@ async function runClose(session, logger) {
517
596
  return;
518
597
  }
519
598
  logger.info("close-killing", { session, pid: state.pid, port: state.port });
520
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
521
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
599
+ if (state.pid != null) {
600
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
601
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
602
+ }
522
603
  clearSessionState(session, logger);
523
604
  logger.info("close-success", { session });
524
605
  console.log(`Browser closed (session: ${session}).`);
@@ -575,7 +656,7 @@ function resolveClosableSessions(logger) {
575
656
  function clearStoppedSessionStates(sessions, logger) {
576
657
  let cleared = 0;
577
658
  for (const session of sessions) {
578
- if (!isPidRunning(session.pid)) {
659
+ if (session.pid == null || !isPidRunning(session.pid)) {
579
660
  clearSessionState(session.session, logger);
580
661
  cleared += 1;
581
662
  }
@@ -601,10 +682,19 @@ async function runCloseAll(logger, options) {
601
682
  pid: target.pid,
602
683
  port: target.port
603
684
  });
604
- sendSignalToProcessGroupOrPid(target.pid, "SIGTERM", logger, target.session);
685
+ if (target.pid != null) {
686
+ sendSignalToProcessGroupOrPid(
687
+ target.pid,
688
+ "SIGTERM",
689
+ logger,
690
+ target.session
691
+ );
692
+ }
605
693
  }
606
694
  await waitForCloseSignalWindow(CLOSE_WAIT_MS);
607
- let survivors = closable.filter((target) => isPidRunning(target.pid));
695
+ let survivors = closable.filter(
696
+ (target) => target.pid != null && isPidRunning(target.pid)
697
+ );
608
698
  if (survivors.length > 0 && !force) {
609
699
  const closed = clearStoppedSessionStates(closable, logger);
610
700
  throw new Error(
@@ -622,11 +712,20 @@ async function runCloseAll(logger, options) {
622
712
  session: survivor.session,
623
713
  pid: survivor.pid
624
714
  });
625
- sendSignalToProcessGroupOrPid(survivor.pid, "SIGKILL", logger, survivor.session);
715
+ if (survivor.pid != null) {
716
+ sendSignalToProcessGroupOrPid(
717
+ survivor.pid,
718
+ "SIGKILL",
719
+ logger,
720
+ survivor.session
721
+ );
722
+ }
626
723
  forceKilled += 1;
627
724
  }
628
725
  await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
629
- survivors = survivors.filter((target) => isPidRunning(target.pid));
726
+ survivors = survivors.filter(
727
+ (target) => target.pid != null && isPidRunning(target.pid)
728
+ );
630
729
  if (survivors.length > 0) {
631
730
  const closed = clearStoppedSessionStates(closable, logger);
632
731
  throw new Error(
@@ -648,6 +747,75 @@ async function runCloseAll(logger, options) {
648
747
  console.log(`Force-killed ${forceKilled} session(s).`);
649
748
  }
650
749
  }
750
+ async function runConnect(cdpUrl, session, logger) {
751
+ logger.info("connect-start", { cdpUrl, session });
752
+ assertSessionAvailableForStart(session, logger);
753
+ let parsedUrl;
754
+ try {
755
+ parsedUrl = new URL(cdpUrl);
756
+ } catch {
757
+ throw new Error(
758
+ [
759
+ `Invalid CDP URL: ${cdpUrl}`,
760
+ ``,
761
+ `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
762
+ ` libretto connect http://127.0.0.1:9222`,
763
+ ` libretto connect http://remote-host:9222`,
764
+ ` libretto connect http://remote-host:9222/devtools/browser/<id>`
765
+ ].join("\n")
766
+ );
767
+ }
768
+ const endpoint = parsedUrl.href;
769
+ const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" ? 443 : 80;
770
+ console.log(
771
+ `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`
772
+ );
773
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
774
+ try {
775
+ const resp = await fetch(versionUrl);
776
+ const versionInfo = await resp.json();
777
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
778
+ } catch (err) {
779
+ logger.error("connect-version-failed", { versionUrl, error: err });
780
+ throw new Error(
781
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
782
+ );
783
+ }
784
+ const browser = await tryConnectToCDP(endpoint, logger, 1e4);
785
+ if (!browser) {
786
+ throw new Error(
787
+ `CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
788
+ );
789
+ }
790
+ const pages = resolveOperationalPages(browser);
791
+ logger.info("connect-pages", {
792
+ session,
793
+ pageCount: pages.length,
794
+ urls: pages.map((p) => p.url())
795
+ });
796
+ disconnectBrowser(browser, logger, session);
797
+ writeSessionState(
798
+ {
799
+ port,
800
+ cdpEndpoint: endpoint,
801
+ session,
802
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
803
+ status: "active"
804
+ },
805
+ logger
806
+ );
807
+ logger.info("connect-success", { cdpUrl: endpoint, session, port });
808
+ console.log(`Connected to ${endpoint} (session: ${session})`);
809
+ console.log(` Pages found: ${pages.length}`);
810
+ if (pages.length > 0) {
811
+ for (const p of pages.slice(0, 5)) {
812
+ console.log(` ${p.url()}`);
813
+ }
814
+ if (pages.length > 5) {
815
+ console.log(` ... and ${pages.length - 5} more`);
816
+ }
817
+ }
818
+ }
651
819
  function resolvePath(filePath) {
652
820
  return join(process.cwd(), filePath);
653
821
  }
@@ -666,8 +834,10 @@ export {
666
834
  normalizeDomain,
667
835
  normalizeUrl,
668
836
  resolvePath,
837
+ resolveViewport,
669
838
  runClose,
670
839
  runCloseAll,
840
+ runConnect,
671
841
  runOpen,
672
842
  runPages,
673
843
  runSave
@@ -49,7 +49,10 @@ function createLoggerForSession(session) {
49
49
  const sessionDir = getSessionDir(session);
50
50
  mkdirSync(sessionDir, { recursive: true });
51
51
  const logFilePath = getSessionLogsPath(session);
52
- return new Logger(["libretto"], [createFileLogSink({ filePath: logFilePath })]);
52
+ return new Logger(
53
+ ["libretto"],
54
+ [createFileLogSink({ filePath: logFilePath })]
55
+ );
53
56
  }
54
57
  async function closeLogger(logger) {
55
58
  if (!logger) return;