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
@@ -1,4 +1,10 @@
1
- import { chromium, type Browser, type BrowserContext, type CDPSession, type Page } from "playwright";
1
+ import {
2
+ chromium,
3
+ type Browser,
4
+ type BrowserContext,
5
+ type CDPSession,
6
+ type Page,
7
+ } from "playwright";
2
8
  import { openSync, existsSync, writeFileSync } from "node:fs";
3
9
  import { basename, dirname, join, resolve } from "node:path";
4
10
  import { fileURLToPath } from "node:url";
@@ -66,13 +72,12 @@ export function hasProfile(domain: string): boolean {
66
72
  return existsSync(getProfilePath(domain));
67
73
  }
68
74
 
69
- async function tryConnectToPort(
70
- port: number,
75
+ async function tryConnectToCDP(
76
+ endpoint: string,
71
77
  logger: LoggerApi,
72
78
  timeoutMs: number = 5000,
73
79
  ): Promise<Browser | null> {
74
- const endpoint = `http://localhost:${port}`;
75
- logger.info("cdp-connect-attempt", { port, endpoint, timeoutMs });
80
+ logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
76
81
  try {
77
82
  const connectPromise = chromium.connectOverCDP(endpoint);
78
83
  const timeoutPromise = new Promise<null>((resolve) =>
@@ -81,16 +86,15 @@ async function tryConnectToPort(
81
86
  const browser = await Promise.race([connectPromise, timeoutPromise]);
82
87
  if (browser) {
83
88
  logger.info("cdp-connect-success", {
84
- port,
85
89
  endpoint,
86
90
  contexts: browser.contexts().length,
87
91
  });
88
92
  } else {
89
- logger.warn("cdp-connect-timeout", { port, endpoint, timeoutMs });
93
+ logger.warn("cdp-connect-timeout", { endpoint, timeoutMs });
90
94
  }
91
95
  return browser;
92
96
  } catch (err) {
93
- logger.error("cdp-connect-error", { error: err, port, endpoint });
97
+ logger.error("cdp-connect-error", { error: err, endpoint });
94
98
  return null;
95
99
  }
96
100
  }
@@ -135,10 +139,12 @@ async function resolvePageId(page: Page): Promise<string> {
135
139
  const cdpSession: CDPSession = await page.context().newCDPSession(page);
136
140
  try {
137
141
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
138
- const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })?.targetInfo
139
- ?.targetId;
142
+ const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
143
+ ?.targetInfo?.targetId;
140
144
  if (typeof targetId !== "string" || targetId.length === 0) {
141
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
145
+ throw new Error(
146
+ `Could not resolve target id for page at URL "${page.url()}".`,
147
+ );
142
148
  }
143
149
  return targetId;
144
150
  } finally {
@@ -162,7 +168,10 @@ export async function listOpenPages(
162
168
  ): Promise<OpenPageSummary[]> {
163
169
  const { browser, page: activePage } = await connect(session, logger);
164
170
  try {
165
- const pages = browser.contexts().flatMap((ctx) => ctx.pages()).filter(isOperationalPage);
171
+ const pages = browser
172
+ .contexts()
173
+ .flatMap((ctx) => ctx.pages())
174
+ .filter(isOperationalPage);
166
175
  const pageRefs = await resolvePageReferences(pages);
167
176
  return pageRefs.map(({ id, page }) => ({
168
177
  id,
@@ -190,14 +199,15 @@ export async function connect(
190
199
  }> {
191
200
  logger.info("connect", { session, timeoutMs });
192
201
  const state = readSessionStateOrThrow(session);
193
- const browser = await tryConnectToPort(state.port, logger, timeoutMs);
202
+ const endpoint = state.cdpEndpoint ?? `http://localhost:${state.port}`;
203
+ const browser = await tryConnectToCDP(endpoint, logger, timeoutMs);
194
204
  if (!browser) {
195
205
  logger.error("connect-no-browser", {
196
206
  session,
197
- port: state.port,
207
+ endpoint,
198
208
  pid: state.pid,
199
209
  });
200
- if (!isPidRunning(state.pid)) {
210
+ if (state.pid == null || !isPidRunning(state.pid)) {
201
211
  clearSessionState(session, logger);
202
212
  throw new Error(
203
213
  `No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`,
@@ -205,7 +215,7 @@ export async function connect(
205
215
  }
206
216
 
207
217
  throw new Error(
208
- `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.`,
218
+ `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.`,
209
219
  );
210
220
  }
211
221
 
@@ -276,7 +286,10 @@ export async function connect(
276
286
  return { browser, context, page, pageId: pageRef.id };
277
287
  }
278
288
 
279
- export async function runPages(session: string, logger: LoggerApi): Promise<void> {
289
+ export async function runPages(
290
+ session: string,
291
+ logger: LoggerApi,
292
+ ): Promise<void> {
280
293
  logger.info("pages-start", { session });
281
294
  const pageSummaries = await listOpenPages(session, logger);
282
295
 
@@ -294,7 +307,7 @@ export async function runPages(session: string, logger: LoggerApi): Promise<void
294
307
 
295
308
  const DEFAULT_VIEWPORT = { width: 1366, height: 768 } as const;
296
309
 
297
- function resolveViewport(
310
+ export function resolveViewport(
298
311
  cliViewport: { width: number; height: number } | undefined,
299
312
  logger: LoggerApi,
300
313
  ): { width: number; height: number } {
@@ -304,10 +317,16 @@ function resolveViewport(
304
317
  }
305
318
  const config = readLibrettoConfig();
306
319
  if (config.viewport) {
307
- logger.info("viewport-source", { source: "config", viewport: config.viewport });
320
+ logger.info("viewport-source", {
321
+ source: "config",
322
+ viewport: config.viewport,
323
+ });
308
324
  return config.viewport;
309
325
  }
310
- logger.info("viewport-source", { source: "default", viewport: DEFAULT_VIEWPORT });
326
+ logger.info("viewport-source", {
327
+ source: "default",
328
+ viewport: DEFAULT_VIEWPORT,
329
+ });
311
330
  return DEFAULT_VIEWPORT;
312
331
  }
313
332
 
@@ -474,8 +493,10 @@ await new Promise(() => {});
474
493
  logger.info("open-child-spawned", { pid: child.pid, port, session });
475
494
 
476
495
  let childSpawnError: Error | null = null;
477
- let childEarlyExit: { code: number | null; signal: NodeJS.Signals | null } | null =
478
- null;
496
+ let childEarlyExit: {
497
+ code: number | null;
498
+ signal: NodeJS.Signals | null;
499
+ } | null = null;
479
500
 
480
501
  child.on("error", (err) => {
481
502
  childSpawnError = err;
@@ -529,14 +550,17 @@ await new Promise(() => {});
529
550
  logger.info("open-waiting-for-cdp", { attempt: i, port, session });
530
551
  }
531
552
  if (ready) {
532
- writeSessionState({
533
- port,
534
- pid: child.pid!,
535
- session,
536
- startedAt: new Date().toISOString(),
537
- status: "active",
538
- viewport,
539
- }, logger);
553
+ writeSessionState(
554
+ {
555
+ port,
556
+ pid: child.pid!,
557
+ session,
558
+ startedAt: new Date().toISOString(),
559
+ status: "active",
560
+ viewport,
561
+ },
562
+ logger,
563
+ );
540
564
  logger.info("open-success", {
541
565
  url,
542
566
  mode: browserMode,
@@ -644,7 +668,10 @@ export async function runSave(
644
668
  }
645
669
  }
646
670
 
647
- export async function runClose(session: string, logger: LoggerApi): Promise<void> {
671
+ export async function runClose(
672
+ session: string,
673
+ logger: LoggerApi,
674
+ ): Promise<void> {
648
675
  logger.info("close-start", { session });
649
676
  const state = readSessionState(session, logger);
650
677
  if (!state) {
@@ -655,9 +682,10 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
655
682
 
656
683
  logger.info("close-killing", { session, pid: state.pid, port: state.port });
657
684
 
658
- sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
659
-
660
- await waitForCloseSignalWindow(CLOSE_WAIT_MS);
685
+ if (state.pid != null) {
686
+ sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
687
+ await waitForCloseSignalWindow(CLOSE_WAIT_MS);
688
+ }
661
689
 
662
690
  clearSessionState(session, logger);
663
691
  logger.info("close-success", { session });
@@ -666,7 +694,7 @@ export async function runClose(session: string, logger: LoggerApi): Promise<void
666
694
 
667
695
  type ClosableSession = {
668
696
  session: string;
669
- pid: number;
697
+ pid?: number;
670
698
  port: number;
671
699
  };
672
700
 
@@ -705,7 +733,9 @@ function sendSignalToProcessGroupOrPid(
705
733
  }
706
734
  }
707
735
 
708
- function formatSessionList(targets: ReadonlyArray<{ session: string }>): string {
736
+ function formatSessionList(
737
+ targets: ReadonlyArray<{ session: string }>,
738
+ ): string {
709
739
  return targets.map((target) => `"${target.session}"`).join(", ");
710
740
  }
711
741
 
@@ -739,7 +769,7 @@ function clearStoppedSessionStates(
739
769
  ): number {
740
770
  let cleared = 0;
741
771
  for (const session of sessions) {
742
- if (!isPidRunning(session.pid)) {
772
+ if (session.pid == null || !isPidRunning(session.pid)) {
743
773
  clearSessionState(session.session, logger);
744
774
  cleared += 1;
745
775
  }
@@ -770,12 +800,21 @@ export async function runCloseAll(
770
800
  pid: target.pid,
771
801
  port: target.port,
772
802
  });
773
- sendSignalToProcessGroupOrPid(target.pid, "SIGTERM", logger, target.session);
803
+ if (target.pid != null) {
804
+ sendSignalToProcessGroupOrPid(
805
+ target.pid,
806
+ "SIGTERM",
807
+ logger,
808
+ target.session,
809
+ );
810
+ }
774
811
  }
775
812
 
776
813
  await waitForCloseSignalWindow(CLOSE_WAIT_MS);
777
814
 
778
- let survivors = closable.filter((target) => isPidRunning(target.pid));
815
+ let survivors = closable.filter(
816
+ (target) => target.pid != null && isPidRunning(target.pid),
817
+ );
779
818
  if (survivors.length > 0 && !force) {
780
819
  const closed = clearStoppedSessionStates(closable, logger);
781
820
 
@@ -795,11 +834,20 @@ export async function runCloseAll(
795
834
  session: survivor.session,
796
835
  pid: survivor.pid,
797
836
  });
798
- sendSignalToProcessGroupOrPid(survivor.pid, "SIGKILL", logger, survivor.session);
837
+ if (survivor.pid != null) {
838
+ sendSignalToProcessGroupOrPid(
839
+ survivor.pid,
840
+ "SIGKILL",
841
+ logger,
842
+ survivor.session,
843
+ );
844
+ }
799
845
  forceKilled += 1;
800
846
  }
801
847
  await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
802
- survivors = survivors.filter((target) => isPidRunning(target.pid));
848
+ survivors = survivors.filter(
849
+ (target) => target.pid != null && isPidRunning(target.pid),
850
+ );
803
851
  if (survivors.length > 0) {
804
852
  const closed = clearStoppedSessionStates(closable, logger);
805
853
  throw new Error(
@@ -824,6 +872,95 @@ export async function runCloseAll(
824
872
  }
825
873
  }
826
874
 
875
+ export async function runConnect(
876
+ cdpUrl: string,
877
+ session: string,
878
+ logger: LoggerApi,
879
+ ): Promise<void> {
880
+ logger.info("connect-start", { cdpUrl, session });
881
+ assertSessionAvailableForStart(session, logger);
882
+
883
+ let parsedUrl: URL;
884
+ try {
885
+ parsedUrl = new URL(cdpUrl);
886
+ } catch {
887
+ throw new Error(
888
+ [
889
+ `Invalid CDP URL: ${cdpUrl}`,
890
+ ``,
891
+ `Expected an HTTP URL pointing to a Chrome DevTools Protocol endpoint, for example:`,
892
+ ` libretto connect http://127.0.0.1:9222`,
893
+ ` libretto connect http://remote-host:9222`,
894
+ ` libretto connect http://remote-host:9222/devtools/browser/<id>`,
895
+ ].join("\n"),
896
+ );
897
+ }
898
+
899
+ const endpoint = parsedUrl.href;
900
+ const port = parsedUrl.port
901
+ ? Number(parsedUrl.port)
902
+ : parsedUrl.protocol === "https:"
903
+ ? 443
904
+ : 80;
905
+
906
+ console.log(
907
+ `Connecting to CDP endpoint at ${endpoint} (session: ${session})...`,
908
+ );
909
+
910
+ // Verify the CDP endpoint is reachable
911
+ const versionUrl = `${parsedUrl.protocol}//${parsedUrl.host}/json/version`;
912
+ try {
913
+ const resp = await fetch(versionUrl);
914
+ const versionInfo = await resp.json();
915
+ logger.info("connect-version-ok", { versionUrl, versionInfo });
916
+ } catch (err) {
917
+ logger.error("connect-version-failed", { versionUrl, error: err });
918
+ throw new Error(
919
+ `Cannot reach CDP endpoint at ${versionUrl}. Make sure the target is running and accessible at ${parsedUrl.host}.`,
920
+ );
921
+ }
922
+
923
+ // Connect via CDP using the full endpoint URL
924
+ const browser = await tryConnectToCDP(endpoint, logger, 10_000);
925
+ if (!browser) {
926
+ throw new Error(
927
+ `CDP endpoint at ${endpoint} is reachable but Playwright could not connect. Check that the URL is a Chrome DevTools Protocol endpoint.`,
928
+ );
929
+ }
930
+
931
+ const pages = resolveOperationalPages(browser);
932
+ logger.info("connect-pages", {
933
+ session,
934
+ pageCount: pages.length,
935
+ urls: pages.map((p) => p.url()),
936
+ });
937
+
938
+ disconnectBrowser(browser, logger, session);
939
+
940
+ writeSessionState(
941
+ {
942
+ port,
943
+ cdpEndpoint: endpoint,
944
+ session,
945
+ startedAt: new Date().toISOString(),
946
+ status: "active",
947
+ },
948
+ logger,
949
+ );
950
+
951
+ logger.info("connect-success", { cdpUrl: endpoint, session, port });
952
+ console.log(`Connected to ${endpoint} (session: ${session})`);
953
+ console.log(` Pages found: ${pages.length}`);
954
+ if (pages.length > 0) {
955
+ for (const p of pages.slice(0, 5)) {
956
+ console.log(` ${p.url()}`);
957
+ }
958
+ if (pages.length > 5) {
959
+ console.log(` ... and ${pages.length - 5} more`);
960
+ }
961
+ }
962
+ }
963
+
827
964
  export function resolvePath(filePath: string): string {
828
965
  return join(process.cwd(), filePath);
829
966
  }
@@ -66,10 +66,15 @@ export function createLoggerForSession(session: string): Logger {
66
66
  const sessionDir = getSessionDir(session);
67
67
  mkdirSync(sessionDir, { recursive: true });
68
68
  const logFilePath = getSessionLogsPath(session);
69
- return new Logger(["libretto"], [createFileLogSink({ filePath: logFilePath })]);
69
+ return new Logger(
70
+ ["libretto"],
71
+ [createFileLogSink({ filePath: logFilePath })],
72
+ );
70
73
  }
71
74
 
72
- export async function closeLogger(logger: Logger | null | undefined): Promise<void> {
75
+ export async function closeLogger(
76
+ logger: Logger | null | undefined,
77
+ ): Promise<void> {
73
78
  if (!logger) return;
74
79
  await logger.close();
75
80
  }
@@ -13,7 +13,8 @@ type InstallSessionTelemetryOptions = {
13
13
  export async function installSessionTelemetry(
14
14
  options: InstallSessionTelemetryOptions,
15
15
  ): Promise<void> {
16
- const STATIC_EXT_RE = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
16
+ const STATIC_EXT_RE =
17
+ /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
17
18
  const { context, initialPage, logAction, logNetwork } = options;
18
19
  const includeUserDomActions = options.includeUserDomActions ?? false;
19
20
  const pageIdCache = new WeakMap<Page, string>();
@@ -25,10 +26,12 @@ export async function installSessionTelemetry(
25
26
  const cdpSession = await context.newCDPSession(page);
26
27
  try {
27
28
  const targetInfo = await cdpSession.send("Target.getTargetInfo");
28
- const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })?.targetInfo
29
- ?.targetId;
29
+ const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
30
+ ?.targetInfo?.targetId;
30
31
  if (typeof targetId !== "string" || targetId.length === 0) {
31
- throw new Error(`Could not resolve target id for page at URL "${page.url()}".`);
32
+ throw new Error(
33
+ `Could not resolve target id for page at URL "${page.url()}".`,
34
+ );
32
35
  }
33
36
  pageIdCache.set(page, targetId);
34
37
  return targetId;
@@ -51,7 +54,10 @@ export async function installSessionTelemetry(
51
54
  });
52
55
  };
53
56
 
54
- const markApiActionInProgress = async (page: Page, inProgress: boolean): Promise<void> => {
57
+ const markApiActionInProgress = async (
58
+ page: Page,
59
+ inProgress: boolean,
60
+ ): Promise<void> => {
55
61
  await page.evaluate((flag) => {
56
62
  (window as any).__btApiActionInProgress = flag;
57
63
  }, inProgress);
@@ -180,7 +186,10 @@ export async function installSessionTelemetry(
180
186
  return locator;
181
187
  };
182
188
 
183
- const installUserDomTracking = async (page: Page, pageId: string): Promise<void> => {
189
+ const installUserDomTracking = async (
190
+ page: Page,
191
+ pageId: string,
192
+ ): Promise<void> => {
184
193
  if (exposedPages.has(page)) return;
185
194
  exposedPages.add(page);
186
195
 
@@ -425,7 +434,8 @@ export async function installSessionTelemetry(
425
434
  action: method,
426
435
  source: "agent",
427
436
  selector: typeof args[0] === "string" ? args[0] : undefined,
428
- value: args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
437
+ value:
438
+ args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
429
439
  duration: Date.now() - start,
430
440
  success: true,
431
441
  });
@@ -497,7 +507,8 @@ export async function installSessionTelemetry(
497
507
  page.on("response", async (response) => {
498
508
  const request = response.request();
499
509
  const url = request.url();
500
- if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://")) return;
510
+ if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
511
+ return;
501
512
  emitNetwork({
502
513
  pageId,
503
514
  method: request.method(),
@@ -23,9 +23,17 @@ import {
23
23
 
24
24
  const SESSION_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
25
25
 
26
- export const SESSION_DEFAULT = "default";
27
26
  export const SESSION_DEV_SERVER = "dev-server";
28
27
  export const SESSION_BROWSER_AGENT = "browser-agent";
28
+
29
+ export function generateSessionName(): string {
30
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
31
+ let id = "";
32
+ for (let i = 0; i < 4; i++) {
33
+ id += chars[Math.floor(Math.random() * chars.length)];
34
+ }
35
+ return `ses-${id}`;
36
+ }
29
37
  export { SESSION_STATE_VERSION };
30
38
  export type { SessionStatus, SessionState };
31
39
 
@@ -136,7 +144,10 @@ export function readSessionStateOrThrow(session: string): SessionState {
136
144
  }
137
145
 
138
146
  try {
139
- return parseSessionStateContent(readFileSync(stateFile, "utf-8"), stateFile);
147
+ return parseSessionStateContent(
148
+ readFileSync(stateFile, "utf-8"),
149
+ stateFile,
150
+ );
140
151
  } catch (err) {
141
152
  throw new Error(
142
153
  `Could not read session state for "${session}": ${err instanceof Error ? err.message : String(err)}`,
@@ -186,10 +197,13 @@ export function setSessionStatus(
186
197
  const state = readSessionState(session, logger);
187
198
  if (!state) return;
188
199
  if (state.status === status) return;
189
- writeSessionState({
190
- ...state,
191
- status,
192
- }, logger);
200
+ writeSessionState(
201
+ {
202
+ ...state,
203
+ status,
204
+ },
205
+ logger,
206
+ );
193
207
  }
194
208
 
195
209
  export function assertSessionAvailableForStart(
@@ -198,7 +212,7 @@ export function assertSessionAvailableForStart(
198
212
  ): void {
199
213
  const existingState = readSessionState(session, logger);
200
214
  if (!existingState) return;
201
- if (!isPidRunning(existingState.pid)) {
215
+ if (existingState.pid == null || !isPidRunning(existingState.pid)) {
202
216
  setSessionStatus(session, "exited", logger);
203
217
  return;
204
218
  }
@@ -8,15 +8,10 @@
8
8
  * to the CLI-agent approach if needed.
9
9
  *
10
10
  * Shared types and utilities (InterpretResultSchema, buildInlinePromptSelection,
11
- * formatInterpretationOutput, etc.) are still actively used by the API analyzer.
11
+ * etc.) are still actively used by the API analyzer.
12
12
  */
13
13
 
14
- import {
15
- existsSync,
16
- mkdtempSync,
17
- readFileSync,
18
- rmSync,
19
- } from "node:fs";
14
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
20
15
  import { extname, isAbsolute, join, resolve } from "node:path";
21
16
  import { spawn } from "node:child_process";
22
17
  import { tmpdir } from "node:os";
@@ -147,7 +142,12 @@ abstract class UserCodingAgent {
147
142
  logger: LoggerApi,
148
143
  stdinText?: string,
149
144
  ): Promise<ExternalCommandResult> {
150
- const result = await runExternalCommand(this.command, args, logger, stdinText);
145
+ const result = await runExternalCommand(
146
+ this.command,
147
+ args,
148
+ logger,
149
+ stdinText,
150
+ );
151
151
  if (result.exitCode !== 0) {
152
152
  throw new Error(
153
153
  `Analyzer command failed (${[this.command, ...args].join(" ")}).\n${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No error output."}`,
@@ -586,17 +586,18 @@ function estimateTokensFromChars(chars: number): number {
586
586
  return Math.ceil(chars / 4);
587
587
  }
588
588
 
589
- function inferContextWindowTokens(
590
- model: string,
591
- ): { contextWindowTokens: number; source: string } {
589
+ function inferContextWindowTokens(model: string): {
590
+ contextWindowTokens: number;
591
+ source: string;
592
+ } {
592
593
  const normalized = model.trim().toLowerCase();
593
594
  if (normalized.includes("claude")) {
594
595
  return { contextWindowTokens: 200_000, source: "model:claude" };
595
596
  }
596
597
  if (
597
- normalized.includes("gpt-5")
598
- || normalized.includes("o3")
599
- || normalized.includes("o4")
598
+ normalized.includes("gpt-5") ||
599
+ normalized.includes("o3") ||
600
+ normalized.includes("o4")
600
601
  ) {
601
602
  return { contextWindowTokens: 200_000, source: "model:openai" };
602
603
  }
@@ -699,7 +700,9 @@ export function buildInlinePromptSelection(
699
700
  fullDomChars: fullHtmlContent.length,
700
701
  fullDomEstimatedTokens: estimateTokensFromChars(fullHtmlContent.length),
701
702
  condensedDomChars: condensedHtmlContent.length,
702
- condensedDomEstimatedTokens: estimateTokensFromChars(condensedHtmlContent.length),
703
+ condensedDomEstimatedTokens: estimateTokensFromChars(
704
+ condensedHtmlContent.length,
705
+ ),
703
706
  configuredModel: model,
704
707
  };
705
708
 
@@ -740,8 +743,7 @@ export function buildInlinePromptSelection(
740
743
  false,
741
744
  );
742
745
  if (fullCandidate.promptEstimatedTokens <= budget.promptBudgetTokens) {
743
- const selectionReason =
744
- `Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
746
+ const selectionReason = `Full DOM fits within the estimated prompt budget (~${fullCandidate.promptEstimatedTokens.toLocaleString()} <= ${budget.promptBudgetTokens.toLocaleString()} tokens), so the analyzer receives the uncondensed page HTML.`;
745
747
  const prompt = buildInlineHtmlPrompt(args, {
746
748
  htmlContent: fullHtmlContent,
747
749
  domLabel: "full DOM",
@@ -784,7 +786,10 @@ export function buildInlinePromptSelection(
784
786
  2_000,
785
787
  budget.promptBudgetTokens - estimateTokensFromChars(basePrompt.length),
786
788
  );
787
- const truncatedHtml = truncateText(condensedHtmlContent, availableHtmlTokens * 4);
789
+ const truncatedHtml = truncateText(
790
+ condensedHtmlContent,
791
+ availableHtmlTokens * 4,
792
+ );
788
793
 
789
794
  return buildCandidate(
790
795
  "condensed",
@@ -794,31 +799,6 @@ export function buildInlinePromptSelection(
794
799
  );
795
800
  }
796
801
 
797
- export function formatInterpretationOutput(
798
- parsed: InterpretResult,
799
- header: string = "Interpretation:",
800
- ): string {
801
- const outputLines: string[] = [];
802
- outputLines.push(header);
803
- outputLines.push(`Answer: ${parsed.answer}`);
804
- outputLines.push("");
805
- if (parsed.selectors.length === 0) {
806
- outputLines.push("Selectors: none found.");
807
- } else {
808
- outputLines.push("Selectors:");
809
- parsed.selectors.forEach((selector, index) => {
810
- outputLines.push(` ${index + 1}. ${selector.label}`);
811
- outputLines.push(` selector: ${selector.selector}`);
812
- outputLines.push(` rationale: ${selector.rationale}`);
813
- });
814
- }
815
- if (parsed.notes && parsed.notes.trim()) {
816
- outputLines.push("");
817
- outputLines.push(`Notes: ${parsed.notes.trim()}`);
818
- }
819
- return outputLines.join("\n");
820
- }
821
-
822
802
  export async function runInterpret(
823
803
  args: InterpretArgs,
824
804
  logger: LoggerApi,
@@ -860,14 +840,14 @@ export async function runInterpret(
860
840
  // re-enabled, the caller must supply a valid provider/model-id string.
861
841
  throw new Error(
862
842
  "The CLI-agent snapshot analysis path is not active. " +
863
- "Update your config to the current format with `npx libretto ai configure <provider>`, " +
864
- "or set API credentials in .env for direct API analysis.",
843
+ "Update your config to the current format with `npx libretto ai configure <provider>`, " +
844
+ "or set API credentials in .env for direct API analysis.",
865
845
  );
866
846
 
867
847
  // Preserved for reference — to re-enable, remove the throw above and:
868
848
  // const selection = buildInlinePromptSelection(args, fullHtmlContent, condensedHtmlContent, model);
869
849
  // const parsed = await configuredAgent.analyzeSnapshot(selection.prompt, pngPath, logger);
870
- // console.log(formatInterpretationOutput(parsed));
850
+ // console.log(parsed.answer);
871
851
  }
872
852
 
873
853
  export function canAnalyzeSnapshots(): boolean {