libretto 0.5.0 → 0.5.1

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 (116) hide show
  1. package/README.md +106 -36
  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 +30 -8
  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 +9 -2
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +132 -29
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +5 -2
  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 +10 -2
  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 +6 -13
  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/instrumentation/instrument.js +101 -5
  38. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  39. package/dist/shared/llm/client.js +3 -1
  40. package/dist/shared/logger/index.js +4 -1
  41. package/dist/shared/run/api.js +3 -1
  42. package/dist/shared/run/browser.js +7 -2
  43. package/dist/shared/state/session-state.d.ts +2 -1
  44. package/dist/shared/state/session-state.js +5 -2
  45. package/dist/shared/visualization/ghost-cursor.js +19 -10
  46. package/dist/shared/visualization/highlight.js +9 -6
  47. package/dist/shared/workflow/workflow.d.ts +4 -5
  48. package/dist/shared/workflow/workflow.js +3 -5
  49. package/package.json +6 -2
  50. package/scripts/check-skills-sync.mjs +25 -0
  51. package/scripts/compare-eval-summary.mjs +47 -0
  52. package/scripts/postinstall.mjs +15 -15
  53. package/scripts/prepare-release.sh +97 -0
  54. package/scripts/skills-libretto.mjs +103 -0
  55. package/scripts/summarize-evals.mjs +135 -0
  56. package/scripts/sync-skills.mjs +12 -0
  57. package/skills/libretto/SKILL.md +113 -49
  58. package/skills/libretto/references/code-generation-rules.md +208 -0
  59. package/skills/libretto/references/configuration-file-reference.md +53 -0
  60. package/skills/libretto/references/site-security-review.md +143 -0
  61. package/src/cli/cli.ts +23 -110
  62. package/src/cli/commands/browser.ts +94 -70
  63. package/src/cli/commands/execution.ts +233 -102
  64. package/src/cli/commands/init.ts +32 -9
  65. package/src/cli/commands/logs.ts +7 -7
  66. package/src/cli/commands/shared.ts +36 -37
  67. package/src/cli/commands/snapshot.ts +44 -59
  68. package/src/cli/core/ai-config.ts +12 -3
  69. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  70. package/src/cli/core/browser.ts +178 -41
  71. package/src/cli/core/context.ts +7 -2
  72. package/src/cli/core/session-telemetry.ts +19 -8
  73. package/src/cli/core/session.ts +21 -7
  74. package/src/cli/core/snapshot-analyzer.ts +26 -46
  75. package/src/cli/core/snapshot-api-config.ts +170 -175
  76. package/src/cli/core/telemetry.ts +16 -3
  77. package/src/cli/framework/simple-cli.ts +144 -77
  78. package/src/cli/router.ts +13 -21
  79. package/src/cli/workers/run-integration-runtime.ts +36 -9
  80. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  81. package/src/cli/workers/run-integration-worker.ts +1 -4
  82. package/src/index.ts +73 -66
  83. package/src/runtime/download/download.ts +62 -58
  84. package/src/runtime/download/index.ts +5 -5
  85. package/src/runtime/extract/extract.ts +71 -61
  86. package/src/runtime/network/index.ts +3 -3
  87. package/src/runtime/network/network.ts +99 -93
  88. package/src/runtime/recovery/agent.ts +217 -212
  89. package/src/runtime/recovery/errors.ts +107 -104
  90. package/src/runtime/recovery/index.ts +3 -3
  91. package/src/runtime/recovery/recovery.ts +38 -35
  92. package/src/shared/condense-dom/condense-dom.ts +15 -18
  93. package/src/shared/config/config.ts +0 -19
  94. package/src/shared/config/index.ts +0 -5
  95. package/src/shared/debug/pause.ts +57 -51
  96. package/src/shared/instrumentation/errors.ts +64 -62
  97. package/src/shared/instrumentation/index.ts +5 -5
  98. package/src/shared/instrumentation/instrument.ts +339 -209
  99. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  100. package/src/shared/llm/client.ts +181 -174
  101. package/src/shared/llm/types.ts +39 -39
  102. package/src/shared/logger/index.ts +11 -4
  103. package/src/shared/logger/logger.ts +312 -306
  104. package/src/shared/logger/sinks.ts +118 -114
  105. package/src/shared/paths/paths.ts +50 -49
  106. package/src/shared/paths/repo-root.ts +17 -17
  107. package/src/shared/run/api.ts +5 -1
  108. package/src/shared/run/browser.ts +12 -3
  109. package/src/shared/state/index.ts +9 -9
  110. package/src/shared/state/session-state.ts +46 -43
  111. package/src/shared/visualization/ghost-cursor.ts +161 -148
  112. package/src/shared/visualization/highlight.ts +89 -86
  113. package/src/shared/visualization/index.ts +13 -13
  114. package/src/shared/workflow/workflow.ts +19 -25
  115. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  116. package/skills/libretto/references/user-action-log.md +0 -31
@@ -5,16 +5,10 @@ import { getSessionSnapshotRunDir } from "../core/context.js";
5
5
  import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
6
6
  import { readSessionState } from "../core/session.js";
7
7
  import { SimpleCLI } from "../framework/simple-cli.js";
8
- import {
9
- loadSessionStateMiddleware,
10
- pageOption,
11
- resolveSessionMiddleware,
12
- sessionOption
13
- } from "./shared.js";
8
+ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
14
9
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
15
10
  import { readAiConfig } from "../core/ai-config.js";
16
11
  import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
17
- const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
18
12
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
19
13
  function generateSnapshotRunId() {
20
14
  return `snapshot-${Date.now()}`;
@@ -106,6 +100,12 @@ async function captureScreenshot(session, logger, pageId) {
106
100
  const pngPath = `${snapshotRunDir}/page.png`;
107
101
  const htmlPath = `${snapshotRunDir}/page.html`;
108
102
  const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
103
+ const RENDER_SETTLE_TIMEOUT_MS = 1e4;
104
+ await Promise.race([
105
+ page.waitForLoadState("networkidle").catch(() => {
106
+ }),
107
+ new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS))
108
+ ]);
109
109
  const restoreViewport = resolveSnapshotViewport(session, logger);
110
110
  const viewportMetrics = await readSnapshotViewportMetrics(page);
111
111
  logger.info("screenshot-viewport-metrics", {
@@ -185,17 +185,10 @@ async function captureScreenshot(session, logger, pageId) {
185
185
  }
186
186
  }
187
187
  async function runSnapshot(session, logger, pageId, objective, context) {
188
- const normalizedObjective = objective?.trim();
189
- const normalizedContext = context?.trim();
190
- if (!normalizedObjective && normalizedContext) {
191
- throw new Error(
192
- "Couldn't run analysis: --objective is required when providing --context."
193
- );
194
- }
195
- const configuredAi = normalizedObjective ? readAiConfig() : null;
196
- if (normalizedObjective) {
197
- resolveSnapshotApiModelOrThrow(configuredAi);
198
- }
188
+ const normalizedObjective = objective.trim();
189
+ const normalizedContext = context.trim();
190
+ const configuredAi = readAiConfig();
191
+ resolveSnapshotApiModelOrThrow(configuredAi);
199
192
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
200
193
  session,
201
194
  logger,
@@ -205,14 +198,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
205
198
  console.log(` PNG: ${pngPath}`);
206
199
  console.log(` HTML: ${htmlPath}`);
207
200
  console.log(` Condensed HTML: ${condensedHtmlPath}`);
208
- if (!normalizedObjective) {
209
- console.log("Use --objective flag to analyze snapshots.");
210
- return;
211
- }
212
201
  const interpretArgs = {
213
202
  objective: normalizedObjective,
214
203
  session,
215
- context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
204
+ context: normalizedContext,
216
205
  pngPath,
217
206
  htmlPath,
218
207
  condensedHtmlPath
@@ -224,24 +213,22 @@ const snapshotInput = SimpleCLI.input({
224
213
  named: {
225
214
  session: sessionOption(),
226
215
  page: pageOption(),
227
- objective: SimpleCLI.option(z.string().optional()),
228
- context: SimpleCLI.option(z.string().optional())
216
+ objective: SimpleCLI.option(z.string()),
217
+ context: SimpleCLI.option(z.string())
229
218
  }
230
219
  });
231
- function createSnapshotCommand(logger) {
232
- return SimpleCLI.command({
233
- description: "Capture PNG + HTML; analyze when --objective is provided (--context optional)"
234
- }).input(snapshotInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
235
- await runSnapshot(
236
- ctx.session,
237
- logger,
238
- input.page,
239
- input.objective,
240
- input.context
241
- );
242
- });
243
- }
220
+ const snapshotCommand = SimpleCLI.command({
221
+ description: "Capture PNG + HTML and analyze with --objective and --context"
222
+ }).input(snapshotInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
223
+ await runSnapshot(
224
+ ctx.session,
225
+ ctx.logger,
226
+ input.page,
227
+ input.objective,
228
+ input.context
229
+ );
230
+ });
244
231
  export {
245
- createSnapshotCommand,
232
+ snapshotCommand,
246
233
  snapshotInput
247
234
  };
@@ -26,7 +26,12 @@ const PROVIDER_ALIASES = {
26
26
  claude: DEFAULT_MODELS.anthropic,
27
27
  google: DEFAULT_MODELS.gemini
28
28
  };
29
- const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"];
29
+ const CONFIGURE_PROVIDERS = [
30
+ "openai",
31
+ "anthropic",
32
+ "gemini",
33
+ "vertex"
34
+ ];
30
35
  function formatConfigureProviders(separator = " | ") {
31
36
  return CONFIGURE_PROVIDERS.join(separator);
32
37
  }
@@ -147,7 +152,9 @@ function runAiConfigure(input, options = {}) {
147
152
  console.log(
148
153
  `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
149
154
  );
150
- console.log("Provider credentials still come from your shell or .env file.");
155
+ console.log(
156
+ "Provider credentials still come from your shell or .env file."
157
+ );
151
158
  return;
152
159
  }
153
160
  printAiConfig(config2, configPath);
@@ -1,16 +1,13 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { createLLMClient } from "../../shared/llm/client.js";
3
3
  import {
4
- formatInterpretationOutput,
5
4
  InterpretResultSchema,
6
5
  buildInlinePromptSelection,
7
6
  getMimeType,
8
7
  readFileAsBase64
9
8
  } from "./snapshot-analyzer.js";
10
9
  import { readAiConfig } from "./ai-config.js";
11
- import {
12
- resolveSnapshotApiModelOrThrow
13
- } from "./snapshot-api-config.js";
10
+ import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
14
11
  async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
15
12
  const selection = resolveSnapshotApiModelOrThrow(configuredAi);
16
13
  logger.info("api-interpret-start", {
@@ -67,7 +64,20 @@ async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
67
64
  selectorCount: parsed.selectors.length,
68
65
  answer: parsed.answer.slice(0, 200)
69
66
  });
70
- console.log(formatInterpretationOutput(parsed, "Interpretation (via API):"));
67
+ console.log("");
68
+ console.log("Analysis:");
69
+ console.log(parsed.answer);
70
+ if (parsed.selectors.length > 0) {
71
+ console.log("");
72
+ console.log("Selectors:");
73
+ parsed.selectors.forEach((selector, index) => {
74
+ console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
75
+ });
76
+ }
77
+ if (parsed.notes?.trim()) {
78
+ console.log("");
79
+ console.log(`Notes: ${parsed.notes.trim()}`);
80
+ }
71
81
  }
72
82
  export {
73
83
  runApiInterpret
@@ -1,4 +1,6 @@
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";
@@ -57,9 +59,8 @@ function getProfilePath(domain) {
57
59
  function hasProfile(domain) {
58
60
  return existsSync(getProfilePath(domain));
59
61
  }
60
- async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
61
- const endpoint = `http://localhost:${port}`;
62
- logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
62
+ async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
63
+ logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
63
64
  try {
64
65
  const connectPromise = chromium.connectOverCDP(endpoint);
65
66
  const timeoutPromise = new Promise(
@@ -68,16 +69,15 @@ async function tryConnectToPort(port, logger, timeoutMs = 5e3) {
68
69
  const browser = await Promise.race([connectPromise, timeoutPromise]);
69
70
  if (browser) {
70
71
  logger.info("cdp-connect-success", {
71
- port,
72
72
  endpoint,
73
73
  contexts: browser.contexts().length
74
74
  });
75
75
  } else {
76
- logger.warn("cdp-connect-timeout", { port, endpoint, timeoutMs });
76
+ logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
77
77
  }
78
78
  return browser;
79
79
  } catch (err) {
80
- logger.error("cdp-connect-error", { error: err, port, endpoint });
80
+ logger.error("cdp-connect-error", { error: err, endpoint });
81
81
  return null;
82
82
  }
83
83
  }
@@ -102,7 +102,9 @@ async function resolvePageId(page) {
102
102
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
103
103
  const targetId = targetInfo?.targetInfo?.targetId;
104
104
  if (typeof targetId !== "string" || targetId.length === 0) {
105
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
105
+ throw new Error(
106
+ `Could not resolve target id for page at URL "${page.url()}".`
107
+ );
106
108
  }
107
109
  return targetId;
108
110
  } finally {
@@ -135,21 +137,22 @@ async function listOpenPages(session, logger) {
135
137
  async function connect(session, logger, timeoutMs = 1e4, options) {
136
138
  logger.info("connect", { session, timeoutMs });
137
139
  const state = readSessionStateOrThrow(session);
138
- const browser = await tryConnectToPort(state.port, logger, timeoutMs);
140
+ const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
141
+ const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
139
142
  if (!browser) {
140
143
  logger.error("connect-no-browser", {
141
144
  session,
142
- port: state.port,
145
+ endpoint,
143
146
  pid: state.pid
144
147
  });
145
- if (!isPidRunning(state.pid)) {
148
+ if (state.pid == null || !isPidRunning(state.pid)) {
146
149
  clearSessionState(session, logger);
147
150
  throw new Error(
148
151
  `No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
149
152
  );
150
153
  }
151
154
  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.`
155
+ `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
156
  );
154
157
  }
155
158
  const contexts = browser.contexts();
@@ -230,10 +233,16 @@ function resolveViewport(cliViewport, logger) {
230
233
  }
231
234
  const config = readLibrettoConfig();
232
235
  if (config.viewport) {
233
- logger.info("viewport-source", { source: "config", viewport: config.viewport });
236
+ logger.info("viewport-source", {
237
+ source: "config",
238
+ viewport: config.viewport
239
+ });
234
240
  return config.viewport;
235
241
  }
236
- logger.info("viewport-source", { source: "default", viewport: DEFAULT_VIEWPORT });
242
+ logger.info("viewport-source", {
243
+ source: "default",
244
+ viewport: DEFAULT_VIEWPORT
245
+ });
237
246
  return DEFAULT_VIEWPORT;
238
247
  }
239
248
  async function runOpen(rawUrl, headed, session, logger, options) {
@@ -414,14 +423,17 @@ await new Promise(() => {});
414
423
  logger.info("open-waiting-for-cdp", { attempt: i, port, session });
415
424
  }
416
425
  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);
426
+ writeSessionState(
427
+ {
428
+ port,
429
+ pid: child.pid,
430
+ session,
431
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
432
+ status: "active",
433
+ viewport
434
+ },
435
+ logger
436
+ );
425
437
  logger.info("open-success", {
426
438
  url,
427
439
  mode: browserMode,
@@ -517,8 +529,10 @@ async function runClose(session, logger) {
517
529
  return;
518
530
  }
519
531
  logger.info("close-killing", { session, pid: state.pid, port: state.port });
520
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
521
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
532
+ if (state.pid != null) {
533
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
534
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
535
+ }
522
536
  clearSessionState(session, logger);
523
537
  logger.info("close-success", { session });
524
538
  console.log(`Browser closed (session: ${session}).`);
@@ -575,7 +589,7 @@ function resolveClosableSessions(logger) {
575
589
  function clearStoppedSessionStates(sessions, logger) {
576
590
  let cleared = 0;
577
591
  for (const session of sessions) {
578
- if (!isPidRunning(session.pid)) {
592
+ if (session.pid == null || !isPidRunning(session.pid)) {
579
593
  clearSessionState(session.session, logger);
580
594
  cleared += 1;
581
595
  }
@@ -601,10 +615,19 @@ async function runCloseAll(logger, options) {
601
615
  pid: target.pid,
602
616
  port: target.port
603
617
  });
604
- sendSignalToProcessGroupOrPid(target.pid, "SIGTERM", logger, target.session);
618
+ if (target.pid != null) {
619
+ sendSignalToProcessGroupOrPid(
620
+ target.pid,
621
+ "SIGTERM",
622
+ logger,
623
+ target.session
624
+ );
625
+ }
605
626
  }
606
627
  await waitForCloseSignalWindow(CLOSE_WAIT_MS);
607
- let survivors = closable.filter((target) => isPidRunning(target.pid));
628
+ let survivors = closable.filter(
629
+ (target) => target.pid != null && isPidRunning(target.pid)
630
+ );
608
631
  if (survivors.length > 0 && !force) {
609
632
  const closed = clearStoppedSessionStates(closable, logger);
610
633
  throw new Error(
@@ -622,11 +645,20 @@ async function runCloseAll(logger, options) {
622
645
  session: survivor.session,
623
646
  pid: survivor.pid
624
647
  });
625
- sendSignalToProcessGroupOrPid(survivor.pid, "SIGKILL", logger, survivor.session);
648
+ if (survivor.pid != null) {
649
+ sendSignalToProcessGroupOrPid(
650
+ survivor.pid,
651
+ "SIGKILL",
652
+ logger,
653
+ survivor.session
654
+ );
655
+ }
626
656
  forceKilled += 1;
627
657
  }
628
658
  await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
629
- survivors = survivors.filter((target) => isPidRunning(target.pid));
659
+ survivors = survivors.filter(
660
+ (target) => target.pid != null && isPidRunning(target.pid)
661
+ );
630
662
  if (survivors.length > 0) {
631
663
  const closed = clearStoppedSessionStates(closable, logger);
632
664
  throw new Error(
@@ -648,6 +680,75 @@ async function runCloseAll(logger, options) {
648
680
  console.log(`Force-killed ${forceKilled} session(s).`);
649
681
  }
650
682
  }
683
+ async function runConnect(cdpUrl, session, logger) {
684
+ logger.info("connect-start", { cdpUrl, session });
685
+ assertSessionAvailableForStart(session, logger);
686
+ let parsedUrl;
687
+ try {
688
+ parsedUrl = new URL(cdpUrl);
689
+ } catch {
690
+ throw new Error(
691
+ [
692
+ `Invalid CDP URL: ${cdpUrl}`,
693
+ ``,
694
+ `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
695
+ ` libretto connect http://127.0.0.1:9222`,
696
+ ` libretto connect http://remote-host:9222`,
697
+ ` libretto connect http://remote-host:9222/devtools/browser/<id>`
698
+ ].join("\n")
699
+ );
700
+ }
701
+ const endpoint = parsedUrl.href;
702
+ const port = parsedUrl.port ? Number(parsedUrl.port) : parsedUrl.protocol === "https:" ? 443 : 80;
703
+ console.log(
704
+ `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`
705
+ );
706
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
707
+ try {
708
+ const resp = await fetch(versionUrl);
709
+ const versionInfo = await resp.json();
710
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
711
+ } catch (err) {
712
+ logger.error("connect-version-failed", { versionUrl, error: err });
713
+ throw new Error(
714
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`
715
+ );
716
+ }
717
+ const browser = await tryConnectToCDP(endpoint, logger, 1e4);
718
+ if (!browser) {
719
+ throw new Error(
720
+ `CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`
721
+ );
722
+ }
723
+ const pages = resolveOperationalPages(browser);
724
+ logger.info("connect-pages", {
725
+ session,
726
+ pageCount: pages.length,
727
+ urls: pages.map((p) => p.url())
728
+ });
729
+ disconnectBrowser(browser, logger, session);
730
+ writeSessionState(
731
+ {
732
+ port,
733
+ cdpEndpoint: endpoint,
734
+ session,
735
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
736
+ status: "active"
737
+ },
738
+ logger
739
+ );
740
+ logger.info("connect-success", { cdpUrl: endpoint, session, port });
741
+ console.log(`Connected to ${endpoint} (session: ${session})`);
742
+ console.log(` Pages found: ${pages.length}`);
743
+ if (pages.length > 0) {
744
+ for (const p of pages.slice(0, 5)) {
745
+ console.log(` ${p.url()}`);
746
+ }
747
+ if (pages.length > 5) {
748
+ console.log(` ... and ${pages.length - 5} more`);
749
+ }
750
+ }
751
+ }
651
752
  function resolvePath(filePath) {
652
753
  return join(process.cwd(), filePath);
653
754
  }
@@ -666,8 +767,10 @@ export {
666
767
  normalizeDomain,
667
768
  normalizeUrl,
668
769
  resolvePath,
770
+ resolveViewport,
669
771
  runClose,
670
772
  runCloseAll,
773
+ runConnect,
671
774
  runOpen,
672
775
  runPages,
673
776
  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;
@@ -12,7 +12,9 @@ async function installSessionTelemetry(options) {
12
12
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
13
13
  const targetId = targetInfo?.targetInfo?.targetId;
14
14
  if (typeof targetId !== "string" || targetId.length === 0) {
15
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
15
+ throw new Error(
16
+ `Could not resolve target id for page at URL "${page.url()}".`
17
+ );
16
18
  }
17
19
  pageIdCache.set(page, targetId);
18
20
  return targetId;
@@ -439,7 +441,8 @@ async function installSessionTelemetry(options) {
439
441
  page.on("response", async (response) => {
440
442
  const request = response.request();
441
443
  const url = request.url();
442
- if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://")) return;
444
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
445
+ return;
443
446
  emitNetwork({
444
447
  pageId,
445
448
  method: request.method(),
@@ -18,9 +18,16 @@ import {
18
18
  serializeSessionState
19
19
  } from "../../shared/state/index.js";
20
20
  const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
21
- const SESSION_DEFAULT = "default";
22
21
  const SESSION_DEV_SERVER = "dev-server";
23
22
  const SESSION_BROWSER_AGENT = "browser-agent";
23
+ function generateSessionName() {
24
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
25
+ let id = "";
26
+ for (let i = 0; i < 4; i++) {
27
+ id += chars[Math.floor(Math.random() * chars.length)];
28
+ }
29
+ return `ses-${id}`;
30
+ }
24
31
  function logFileForSession(session) {
25
32
  validateSessionName(session);
26
33
  const dir = getSessionDir(session);
@@ -108,7 +115,10 @@ function readSessionStateOrThrow(session) {
108
115
  throwSessionNotFoundError(session);
109
116
  }
110
117
  try {
111
- return parseSessionStateContent(readFileSync(stateFile, "utf-8"), stateFile);
118
+ return parseSessionStateContent(
119
+ readFileSync(stateFile, "utf-8"),
120
+ stateFile
121
+ );
112
122
  } catch (err) {
113
123
  throw new Error(
114
124
  `Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`
@@ -147,15 +157,18 @@ function setSessionStatus(session, status, logger) {
147
157
  const state = readSessionState(session, logger);
148
158
  if (!state) return;
149
159
  if (state.status === status) return;
150
- writeSessionState({
151
- ...state,
152
- status
153
- }, logger);
160
+ writeSessionState(
161
+ {
162
+ ...state,
163
+ status
164
+ },
165
+ logger
166
+ );
154
167
  }
155
168
  function assertSessionAvailableForStart(session, logger) {
156
169
  const existingState = readSessionState(session, logger);
157
170
  if (!existingState) return;
158
- if (!isPidRunning(existingState.pid)) {
171
+ if (existingState.pid == null || !isPidRunning(existingState.pid)) {
159
172
  setSessionStatus(session, "exited", logger);
160
173
  return;
161
174
  }
@@ -166,12 +179,12 @@ function assertSessionAvailableForStart(session, logger) {
166
179
  }
167
180
  export {
168
181
  SESSION_BROWSER_AGENT,
169
- SESSION_DEFAULT,
170
182
  SESSION_DEV_SERVER,
171
183
  SESSION_STATE_VERSION,
172
184
  assertSessionAvailableForStart,
173
185
  assertSessionStateExistsOrThrow,
174
186
  clearSessionState,
187
+ generateSessionName,
175
188
  getStateFilePath,
176
189
  listSessionsWithStateFile,
177
190
  logFileForSession,
@@ -1,9 +1,4 @@
1
- import {
2
- existsSync,
3
- mkdtempSync,
4
- readFileSync,
5
- rmSync
6
- } from "node:fs";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
7
2
  import { extname, isAbsolute, join, resolve } from "node:path";
8
3
  import { spawn } from "node:child_process";
9
4
  import { tmpdir } from "node:os";
@@ -60,7 +55,12 @@ Screenshot file path: ${pngPath}
60
55
  Use the screenshot alongside the HTML snapshot context above.`;
61
56
  }
62
57
  async runAnalyzer(args, logger, stdinText) {
63
- const result = await runExternalCommand(this.command, args, logger, stdinText);
58
+ const result = await runExternalCommand(
59
+ this.command,
60
+ args,
61
+ logger,
62
+ stdinText
63
+ );
64
64
  if (result.exitCode !== 0) {
65
65
  throw new Error(
66
66
  `Analyzer command failed (${[this.command, ...args].join(" ")}).
@@ -535,7 +535,9 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
535
535
  fullDomChars: fullHtmlContent.length,
536
536
  fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
537
537
  condensedDomChars: condensedHtmlContent.length,
538
- condensedDomEstimatedTokens: estimateTokensFromChars(condensedHtmlContent.length),
538
+ condensedDomEstimatedTokens: estimateTokensFromChars(
539
+ condensedHtmlContent.length
540
+ ),
539
541
  configuredModel: model
540
542
  };
541
543
  const buildCandidate = (domSource, htmlContent, selectionReason, truncated) => {
@@ -607,7 +609,10 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
607
609
  2e3,
608
610
  budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length)
609
611
  );
610
- const truncatedHtml = truncateText(condensedHtmlContent, availableHtmlTokens * 4);
612
+ const truncatedHtml = truncateText(
613
+ condensedHtmlContent,
614
+ availableHtmlTokens * 4
615
+ );
611
616
  return buildCandidate(
612
617
  "condensed",
613
618
  truncatedHtml.text,
@@ -615,27 +620,6 @@ function buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent,
615
620
  truncatedHtml.truncated
616
621
  );
617
622
  }
618
- function formatInterpretationOutput(parsed, header = "Interpretation:") {
619
- const outputLines = [];
620
- outputLines.push(header);
621
- outputLines.push(`Answer: ${parsed.answer}`);
622
- outputLines.push("");
623
- if (parsed.selectors.length === 0) {
624
- outputLines.push("Selectors: none found.");
625
- } else {
626
- outputLines.push("Selectors:");
627
- parsed.selectors.forEach((selector, index) => {
628
- outputLines.push(` ${index + 1}. ${selector.label}`);
629
- outputLines.push(` selector: ${selector.selector}`);
630
- outputLines.push(` rationale: ${selector.rationale}`);
631
- });
632
- }
633
- if (parsed.notes && parsed.notes.trim()) {
634
- outputLines.push("");
635
- outputLines.push(`Notes: ${parsed.notes.trim()}`);
636
- }
637
- return outputLines.join("\n");
638
- }
639
623
  async function runInterpret(args, logger) {
640
624
  logger.info("interpret-start", {
641
625
  objective: args.objective,
@@ -676,7 +660,6 @@ export {
676
660
  InterpretResultSchema,
677
661
  buildInlinePromptSelection,
678
662
  canAnalyzeSnapshots,
679
- formatInterpretationOutput,
680
663
  getMimeType,
681
664
  readFileAsBase64,
682
665
  runInterpret
@@ -1,8 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import {
4
- readAiConfig
5
- } from "./ai-config.js";
3
+ import { readAiConfig } from "./ai-config.js";
6
4
  import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
7
5
  import {
8
6
  hasProviderCredentials,
@@ -154,9 +152,7 @@ function resolveSnapshotApiModel(config = readAiConfig()) {
154
152
  function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
155
153
  const selection = resolveSnapshotApiModel(config);
156
154
  if (!selection) {
157
- throw new SnapshotApiUnavailableError(
158
- noSnapshotApiConfiguredMessage()
159
- );
155
+ throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
160
156
  }
161
157
  if (!hasProviderCredentials(selection.provider)) {
162
158
  throw new SnapshotApiUnavailableError(
@@ -1,4 +1,9 @@
1
- import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ readFileSync,
5
+ writeFileSync
6
+ } from "node:fs";
2
7
  import {
3
8
  getSessionActionsLogPath,
4
9
  getSessionNetworkLogPath
@@ -56,7 +61,10 @@ function clearNetworkLog(session) {
56
61
  function parentLogAction(session, entry) {
57
62
  try {
58
63
  const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
59
- appendFileSync(getSessionActionsLogPath(session), JSON.stringify(record) + "\n");
64
+ appendFileSync(
65
+ getSessionActionsLogPath(session),
66
+ JSON.stringify(record) + "\n"
67
+ );
60
68
  } catch {
61
69
  }
62
70
  }