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,11 @@
1
+ import type { BrowserContext } from "playwright";
1
2
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
2
3
  import { mkdir, writeFile } from "node:fs/promises";
3
4
  import { cwd } from "node:process";
4
5
  import { isAbsolute, resolve } from "node:path";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import {
8
+ instrumentContext,
7
9
  launchBrowser,
8
10
  type LibrettoWorkflowContext,
9
11
  } from "../../index.js";
@@ -16,14 +18,16 @@ import {
16
18
  getSessionNetworkLogPath,
17
19
  getSessionStatePath,
18
20
  } from "../core/context.js";
19
- import { getPauseSignalPaths, removeSignalIfExists } from "../core/pause-signals.js";
21
+ import {
22
+ getPauseSignalPaths,
23
+ removeSignalIfExists,
24
+ } from "../core/pause-signals.js";
20
25
  import { installSessionTelemetry } from "../core/session-telemetry.js";
21
26
  import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
22
27
 
23
28
  const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
24
29
 
25
30
  type LoadedLibrettoWorkflow = {
26
- metadata: {};
27
31
  run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
28
32
  };
29
33
 
@@ -71,7 +75,10 @@ function readSessionStatePid(session: string): number | null {
71
75
  if (!existsSync(statePath)) return null;
72
76
 
73
77
  try {
74
- return parseSessionStateContent(readFileSync(statePath, "utf8"), statePath).pid;
78
+ return (
79
+ parseSessionStateContent(readFileSync(statePath, "utf8"), statePath)
80
+ .pid ?? null
81
+ );
75
82
  } catch {
76
83
  return null;
77
84
  }
@@ -101,14 +108,14 @@ async function waitForFailureSessionRelease(args: {
101
108
  }
102
109
  }
103
110
 
104
- function isLoadedLibrettoWorkflow(value: unknown): value is LoadedLibrettoWorkflow {
111
+ function isLoadedLibrettoWorkflow(
112
+ value: unknown,
113
+ ): value is LoadedLibrettoWorkflow {
105
114
  if (!value || typeof value !== "object") return false;
106
115
  const candidate = value as Record<PropertyKey, unknown>;
107
116
  return (
108
117
  candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
109
- typeof candidate.run === "function" &&
110
- !!candidate.metadata &&
111
- typeof candidate.metadata === "object"
118
+ typeof candidate.run === "function"
112
119
  );
113
120
  }
114
121
 
@@ -182,8 +189,8 @@ async function loadWorkflowExport(
182
189
  ' import { workflow } from "libretto";',
183
190
  "",
184
191
  ` export const ${exportName} = workflow<InputType, OutputType>(`,
185
- " {},",
186
192
  " async (ctx, input) => {",
193
+ " // ctx.session — libretto session name",
187
194
  " // ctx.page — Playwright Page instance",
188
195
  " // ctx.logger — MinimalLogger",
189
196
  " // ctx.services — injected dependencies (generic, default {})",
@@ -198,6 +205,17 @@ async function loadWorkflowExport(
198
205
  return targetExport;
199
206
  }
200
207
 
208
+ export async function installHeadedWorkflowVisualization(args: {
209
+ context: BrowserContext;
210
+ logger: LoggerApi;
211
+ instrument?: typeof instrumentContext;
212
+ }): Promise<void> {
213
+ await (args.instrument ?? instrumentContext)(args.context, {
214
+ visualize: true,
215
+ logger: args.logger,
216
+ });
217
+ }
218
+
201
219
  async function runIntegrationInternal(
202
220
  args: RunIntegrationWorkerRequest,
203
221
  options: {
@@ -242,7 +260,14 @@ async function runIntegrationInternal(
242
260
  sessionName: args.session,
243
261
  headless: args.headless,
244
262
  storageStatePath,
263
+ viewport: args.viewport,
245
264
  });
265
+ if (!args.headless && args.visualize !== false) {
266
+ await installHeadedWorkflowVisualization({
267
+ context: browserSession.context,
268
+ logger: integrationLogger,
269
+ });
270
+ }
246
271
  const actionsLogPath = getSessionActionsLogPath(args.session);
247
272
  const networkLogPath = getSessionNetworkLogPath(args.session);
248
273
  await installSessionTelemetry({
@@ -258,6 +283,7 @@ async function runIntegrationInternal(
258
283
  });
259
284
 
260
285
  const workflowContext: LibrettoWorkflowContext = {
286
+ session: args.session,
261
287
  logger: integrationLogger,
262
288
  page: browserSession.page,
263
289
  services: {},
@@ -267,7 +293,8 @@ async function runIntegrationInternal(
267
293
  try {
268
294
  await workflow.run(workflowContext, args.params ?? {});
269
295
  } catch (error) {
270
- const errorMessage = error instanceof Error ? error.message : String(error);
296
+ const errorMessage =
297
+ error instanceof Error ? error.message : String(error);
271
298
  await writeFile(
272
299
  signalPaths.failedSignalPath,
273
300
  JSON.stringify(
@@ -6,7 +6,9 @@ export const RunIntegrationWorkerRequestSchema = z.object({
6
6
  session: z.string().min(1),
7
7
  params: z.unknown(),
8
8
  headless: z.boolean(),
9
+ visualize: z.boolean().default(true),
9
10
  authProfileDomain: z.string().optional(),
11
+ viewport: z.object({ width: z.number(), height: z.number() }).optional(),
10
12
  });
11
13
 
12
14
  export type RunIntegrationWorkerRequest = z.infer<
@@ -5,10 +5,7 @@ import {
5
5
  type RunIntegrationWorkerRequest,
6
6
  } from "./run-integration-worker-protocol.js";
7
7
  import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
8
- import {
9
- ensureLibrettoSetup,
10
- withSessionLogger,
11
- } from "../core/context.js";
8
+ import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
12
9
  import { getPauseSignalPaths } from "../core/pause-signals.js";
13
10
 
14
11
  function parseWorkerRequest(argv: string[]): RunIntegrationWorkerRequest {
package/src/index.ts CHANGED
@@ -2,119 +2,126 @@ import { resolve } from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
 
4
4
  // Logger
5
- export { Logger, defaultLogger, type LoggerApi, type MinimalLogger, type LoggerSink, type LogOptions } from "./shared/logger/logger.js";
6
5
  export {
7
- createFileLogSink,
8
- prettyConsoleSink,
9
- jsonlConsoleSink,
6
+ Logger,
7
+ defaultLogger,
8
+ type LoggerApi,
9
+ type MinimalLogger,
10
+ type LoggerSink,
11
+ type LogOptions,
12
+ } from "./shared/logger/logger.js";
13
+ export {
14
+ createFileLogSink,
15
+ prettyConsoleSink,
16
+ jsonlConsoleSink,
10
17
  } from "./shared/logger/sinks.js";
11
18
 
12
19
  // LLM client interface
13
- export type { LLMClient, Message, MessageContentPart } from "./shared/llm/types.js";
20
+ export type {
21
+ LLMClient,
22
+ Message,
23
+ MessageContentPart,
24
+ } from "./shared/llm/types.js";
14
25
  export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
15
26
  export {
16
- SESSION_STATE_VERSION,
17
- SessionStatusSchema,
18
- SessionStateFileSchema,
19
- parseSessionStateData,
20
- parseSessionStateContent,
21
- serializeSessionState,
22
- type SessionStatus,
23
- type SessionState,
24
- type SessionStateFile,
27
+ SESSION_STATE_VERSION,
28
+ SessionStatusSchema,
29
+ SessionStateFileSchema,
30
+ parseSessionStateData,
31
+ parseSessionStateContent,
32
+ serializeSessionState,
33
+ type SessionStatus,
34
+ type SessionState,
35
+ type SessionStateFile,
25
36
  } from "./shared/state/index.js";
26
37
 
27
38
  // Recovery
28
39
  export { executeRecoveryAgent } from "./runtime/recovery/agent.js";
29
40
  export { attemptWithRecovery } from "./runtime/recovery/recovery.js";
30
41
  export {
31
- detectSubmissionError,
32
- type KnownSubmissionError,
33
- type DetectedSubmissionError,
42
+ detectSubmissionError,
43
+ type KnownSubmissionError,
44
+ type DetectedSubmissionError,
34
45
  } from "./runtime/recovery/errors.js";
35
46
 
36
47
  // AI extraction
37
- export { extractFromPage, type ExtractOptions } from "./runtime/extract/extract.js";
48
+ export {
49
+ extractFromPage,
50
+ type ExtractOptions,
51
+ } from "./runtime/extract/extract.js";
38
52
 
39
53
  // Network helpers
40
54
  export {
41
- pageRequest,
42
- type RequestConfig,
43
- type PageRequestOptions,
55
+ pageRequest,
56
+ type RequestConfig,
57
+ type PageRequestOptions,
44
58
  } from "./runtime/network/network.js";
45
59
 
46
60
  // Download helpers
47
61
  export {
48
- downloadViaClick,
49
- downloadAndSave,
50
- type DownloadResult,
51
- type DownloadViaClickOptions,
52
- type SaveDownloadOptions,
62
+ downloadViaClick,
63
+ downloadAndSave,
64
+ type DownloadResult,
65
+ type DownloadViaClickOptions,
66
+ type SaveDownloadOptions,
53
67
  } from "./runtime/download/download.js";
54
68
 
55
69
  // Debug / Pause
56
70
  export { pause } from "./shared/debug/pause.js";
57
71
 
58
- // Config
59
- export {
60
- isDebugMode,
61
- isDryRun,
62
- shouldPauseBeforeMutation,
63
- } from "./shared/config/config.js";
64
-
65
72
  // Instrumentation
66
73
  export {
67
- instrumentPage,
68
- installInstrumentation,
69
- instrumentContext,
70
- type InstrumentationOptions,
71
- type InstrumentedPage,
74
+ instrumentPage,
75
+ installInstrumentation,
76
+ instrumentContext,
77
+ type InstrumentationOptions,
78
+ type InstrumentedPage,
72
79
  } from "./shared/instrumentation/instrument.js";
73
80
 
74
81
  // Visualization
75
82
  export {
76
- ensureGhostCursor,
77
- moveGhostCursor,
78
- ghostClick,
79
- hideGhostCursor,
80
- type GhostCursorOptions,
83
+ ensureGhostCursor,
84
+ moveGhostCursor,
85
+ ghostClick,
86
+ hideGhostCursor,
87
+ type GhostCursorOptions,
81
88
  } from "./shared/visualization/ghost-cursor.js";
82
89
  export {
83
- ensureHighlightLayer,
84
- showHighlight,
85
- clearHighlights,
86
- type HighlightOptions,
90
+ ensureHighlightLayer,
91
+ showHighlight,
92
+ clearHighlights,
93
+ type HighlightOptions,
87
94
  } from "./shared/visualization/highlight.js";
88
95
 
89
96
  // Run helpers
90
97
  export {
91
- launchBrowser,
92
- type LaunchBrowserArgs,
93
- type BrowserSession,
98
+ launchBrowser,
99
+ type LaunchBrowserArgs,
100
+ type BrowserSession,
94
101
  } from "./shared/run/api.js";
95
102
 
96
103
  // Workflow helpers
97
104
  export {
98
- LibrettoWorkflow,
99
- LIBRETTO_WORKFLOW_BRAND,
100
- workflow,
101
- type LibrettoWorkflowMetadata,
102
- type LibrettoWorkflowContext,
103
- type LibrettoWorkflowHandler,
105
+ LibrettoWorkflow,
106
+ LIBRETTO_WORKFLOW_BRAND,
107
+ workflow,
108
+ type LibrettoWorkflowContext,
109
+ type LibrettoWorkflowHandler,
104
110
  } from "./shared/workflow/workflow.js";
105
111
 
106
112
  const isDirectExecution = (): boolean => {
107
- const entryArg = process.argv[1];
108
- if (!entryArg) {
109
- return false;
110
- }
111
- return pathToFileURL(resolve(entryArg)).href === import.meta.url;
113
+ const entryArg = process.argv[1];
114
+ if (!entryArg) {
115
+ return false;
116
+ }
117
+ return pathToFileURL(resolve(entryArg)).href === import.meta.url;
112
118
  };
113
119
 
114
120
  if (isDirectExecution()) {
115
- void import("./cli/index.js").catch((error: unknown) => {
116
- const message = error instanceof Error ? error.stack ?? error.message : String(error);
117
- process.stderr.write(`${message}\n`);
118
- process.exitCode = 1;
119
- });
121
+ void import("./cli/index.js").catch((error: unknown) => {
122
+ const message =
123
+ error instanceof Error ? (error.stack ?? error.message) : String(error);
124
+ process.stderr.write(`${message}\n`);
125
+ process.exitCode = 1;
126
+ });
120
127
  }
@@ -4,16 +4,16 @@ import type { Page, Download } from "playwright";
4
4
  import type { MinimalLogger } from "../../shared/logger/logger.js";
5
5
 
6
6
  export type DownloadResult = {
7
- /** The raw file contents. */
8
- buffer: Buffer;
9
- /** The filename suggested by the server (Content-Disposition header or URL). */
10
- filename: string;
7
+ /** The raw file contents. */
8
+ buffer: Buffer;
9
+ /** The filename suggested by the server (Content-Disposition header or URL). */
10
+ filename: string;
11
11
  };
12
12
 
13
13
  export type DownloadViaClickOptions = {
14
- logger?: MinimalLogger;
15
- /** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
16
- timeout?: number;
14
+ logger?: MinimalLogger;
15
+ /** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
16
+ timeout?: number;
17
17
  };
18
18
 
19
19
  /**
@@ -24,55 +24,55 @@ export type DownloadViaClickOptions = {
24
24
  * never missed.
25
25
  */
26
26
  export async function downloadViaClick(
27
- page: Page,
28
- selector: string,
29
- options?: DownloadViaClickOptions,
27
+ page: Page,
28
+ selector: string,
29
+ options?: DownloadViaClickOptions,
30
30
  ): Promise<DownloadResult> {
31
- const { logger, timeout = 30_000 } = options ?? {};
31
+ const { logger, timeout = 30_000 } = options ?? {};
32
32
 
33
- const startTime = Date.now();
33
+ const startTime = Date.now();
34
34
 
35
- // 1. Register the download listener BEFORE clicking
36
- const downloadPromise = page.waitForEvent("download", { timeout });
35
+ // 1. Register the download listener BEFORE clicking
36
+ const downloadPromise = page.waitForEvent("download", { timeout });
37
37
 
38
- // 2. Click the element that triggers the download
39
- await page.locator(selector).click();
38
+ // 2. Click the element that triggers the download
39
+ await page.locator(selector).click();
40
40
 
41
- // 3. Await the download event
42
- const download: Download = await downloadPromise;
41
+ // 3. Await the download event
42
+ const download: Download = await downloadPromise;
43
43
 
44
- // 4. Get the suggested filename
45
- const filename = download.suggestedFilename();
44
+ // 4. Get the suggested filename
45
+ const filename = download.suggestedFilename();
46
46
 
47
- // 5. Read the downloaded file into a buffer
48
- const readStream = await download.createReadStream();
49
- if (!readStream) {
50
- throw new Error(
51
- `Download stream unavailable for "${filename}". The browser may have been closed before the download completed.`,
52
- );
53
- }
47
+ // 5. Read the downloaded file into a buffer
48
+ const readStream = await download.createReadStream();
49
+ if (!readStream) {
50
+ throw new Error(
51
+ `Download stream unavailable for "${filename}". The browser may have been closed before the download completed.`,
52
+ );
53
+ }
54
54
 
55
- const chunks: Buffer[] = [];
56
- for await (const chunk of readStream) {
57
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
58
- }
59
- const buffer = Buffer.concat(chunks);
55
+ const chunks: Buffer[] = [];
56
+ for await (const chunk of readStream) {
57
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
58
+ }
59
+ const buffer = Buffer.concat(chunks);
60
60
 
61
- const duration = Date.now() - startTime;
61
+ const duration = Date.now() - startTime;
62
62
 
63
- logger?.info("download:click", {
64
- selector,
65
- filename,
66
- size: buffer.length,
67
- duration,
68
- });
63
+ logger?.info("download:click", {
64
+ selector,
65
+ filename,
66
+ size: buffer.length,
67
+ duration,
68
+ });
69
69
 
70
- return { buffer, filename };
70
+ return { buffer, filename };
71
71
  }
72
72
 
73
73
  export type SaveDownloadOptions = DownloadViaClickOptions & {
74
- /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
75
- savePath?: string;
74
+ /** Absolute or relative path to save the file to. When omitted the suggested filename is used in the current working directory. */
75
+ savePath?: string;
76
76
  };
77
77
 
78
78
  /**
@@ -80,21 +80,25 @@ export type SaveDownloadOptions = DownloadViaClickOptions & {
80
80
  * downloaded file to disk.
81
81
  */
82
82
  export async function downloadAndSave(
83
- page: Page,
84
- selector: string,
85
- options?: SaveDownloadOptions,
83
+ page: Page,
84
+ selector: string,
85
+ options?: SaveDownloadOptions,
86
86
  ): Promise<DownloadResult & { savedTo: string }> {
87
- const { savePath, ...downloadOpts } = options ?? {};
88
- const { buffer, filename } = await downloadViaClick(page, selector, downloadOpts);
89
-
90
- const dest = resolve(savePath ?? filename);
91
- await writeFile(dest, buffer);
92
-
93
- options?.logger?.info("download:saved", {
94
- filename,
95
- savedTo: dest,
96
- size: buffer.length,
97
- });
98
-
99
- return { buffer, filename, savedTo: dest };
87
+ const { savePath, ...downloadOpts } = options ?? {};
88
+ const { buffer, filename } = await downloadViaClick(
89
+ page,
90
+ selector,
91
+ downloadOpts,
92
+ );
93
+
94
+ const dest = resolve(savePath ?? filename);
95
+ await writeFile(dest, buffer);
96
+
97
+ options?.logger?.info("download:saved", {
98
+ filename,
99
+ savedTo: dest,
100
+ size: buffer.length,
101
+ });
102
+
103
+ return { buffer, filename, savedTo: dest };
100
104
  }
@@ -1,7 +1,7 @@
1
1
  export {
2
- downloadViaClick,
3
- downloadAndSave,
4
- type DownloadResult,
5
- type DownloadViaClickOptions,
6
- type SaveDownloadOptions,
2
+ downloadViaClick,
3
+ downloadAndSave,
4
+ type DownloadResult,
5
+ type DownloadViaClickOptions,
6
+ type SaveDownloadOptions,
7
7
  } from "./download.js";
@@ -1,16 +1,19 @@
1
1
  import type { Page } from "playwright";
2
2
  import type z from "zod";
3
- import { type MinimalLogger, defaultLogger } from "../../shared/logger/logger.js";
3
+ import {
4
+ type MinimalLogger,
5
+ defaultLogger,
6
+ } from "../../shared/logger/logger.js";
4
7
  import type { LLMClient } from "../../shared/llm/types.js";
5
8
 
6
9
  export type ExtractOptions<T extends z.ZodType> = {
7
- page: Page;
8
- instruction: string;
9
- schema: T;
10
- llmClient: LLMClient;
11
- logger?: MinimalLogger;
12
- /** Optional CSS selector to scope extraction to a specific element. */
13
- selector?: string;
10
+ page: Page;
11
+ instruction: string;
12
+ schema: T;
13
+ llmClient: LLMClient;
14
+ logger?: MinimalLogger;
15
+ /** Optional CSS selector to scope extraction to a specific element. */
16
+ selector?: string;
14
17
  };
15
18
 
16
19
  /**
@@ -20,48 +23,55 @@ export type ExtractOptions<T extends z.ZodType> = {
20
23
  * matching the provided Zod schema.
21
24
  */
22
25
  export async function extractFromPage<T extends z.ZodType>(
23
- options: ExtractOptions<T>,
26
+ options: ExtractOptions<T>,
24
27
  ): Promise<z.infer<T>> {
25
- const { page, instruction, schema, selector, logger = defaultLogger, llmClient } = options;
28
+ const {
29
+ page,
30
+ instruction,
31
+ schema,
32
+ selector,
33
+ logger = defaultLogger,
34
+ llmClient,
35
+ } = options;
26
36
 
27
- let screenshot: string;
28
- let domContent: string | undefined;
37
+ let screenshot: string;
38
+ let domContent: string | undefined;
29
39
 
30
- if (selector) {
31
- const element = page.locator(selector);
32
- await element.waitFor({ state: "visible", timeout: 10_000 });
40
+ if (selector) {
41
+ const element = page.locator(selector);
42
+ await element.waitFor({ state: "visible", timeout: 10_000 });
33
43
 
34
- const screenshotBuffer = await element.screenshot();
35
- screenshot = screenshotBuffer.toString("base64");
44
+ const screenshotBuffer = await element.screenshot();
45
+ screenshot = screenshotBuffer.toString("base64");
36
46
 
37
- try {
38
- domContent = await element.innerHTML();
39
- if (domContent.length > 30000) {
40
- domContent = domContent.slice(0, 30000) + "\n... [truncated]";
41
- }
42
- } catch {
43
- domContent = undefined;
44
- }
45
- } else {
46
- const cdpClient = await page.context().newCDPSession(page);
47
- await cdpClient.send("Page.enable");
48
- const { data } = await cdpClient.send("Page.captureScreenshot", {
49
- format: "png",
50
- });
51
- screenshot = data;
47
+ try {
48
+ domContent = await element.innerHTML();
49
+ if (domContent.length > 30000) {
50
+ domContent = domContent.slice(0, 30000) + "\n... [truncated]";
51
+ }
52
+ } catch {
53
+ domContent = undefined;
54
+ }
55
+ } else {
56
+ const cdpClient = await page.context().newCDPSession(page);
57
+ await cdpClient.send("Page.enable");
58
+ const { data } = await cdpClient.send("Page.captureScreenshot", {
59
+ format: "png",
60
+ });
61
+ screenshot = data;
52
62
 
53
- try {
54
- const htmlContent = await page.content();
55
- domContent =
56
- htmlContent.length > 50000
57
- ? htmlContent.slice(0, 50000) + "\n... [truncated]"
58
- : htmlContent;
59
- } catch {
60
- domContent = undefined;
61
- }
62
- }
63
+ try {
64
+ const htmlContent = await page.content();
65
+ domContent =
66
+ htmlContent.length > 50000
67
+ ? htmlContent.slice(0, 50000) + "\n... [truncated]"
68
+ : htmlContent;
69
+ } catch {
70
+ domContent = undefined;
71
+ }
72
+ }
63
73
 
64
- const prompt = `You are analyzing a screenshot${selector ? " of a specific element" : ""} from a web page to extract structured data.
74
+ const prompt = `You are analyzing a screenshot${selector ? " of a specific element" : ""} from a web page to extract structured data.
65
75
 
66
76
  Instruction: ${instruction}
67
77
 
@@ -69,24 +79,24 @@ ${domContent ? `Here is the HTML content for additional context:\n<html>\n${domC
69
79
 
70
80
  Extract the requested information from the screenshot and return it in the specified format. Be precise and only extract what is visible.`;
71
81
 
72
- const result = await llmClient.generateObjectFromMessages({
73
- schema,
74
- messages: [
75
- {
76
- role: "user",
77
- content: [
78
- { type: "text", text: prompt },
79
- { type: "image", image: `data:image/png;base64,${screenshot}` },
80
- ],
81
- },
82
- ],
83
- temperature: 0,
84
- });
82
+ const result = await llmClient.generateObjectFromMessages({
83
+ schema,
84
+ messages: [
85
+ {
86
+ role: "user",
87
+ content: [
88
+ { type: "text", text: prompt },
89
+ { type: "image", image: `data:image/png;base64,${screenshot}` },
90
+ ],
91
+ },
92
+ ],
93
+ temperature: 0,
94
+ });
85
95
 
86
- logger.info("extractFromPage completed", {
87
- selector,
88
- instruction: instruction.slice(0, 100),
89
- });
96
+ logger.info("extractFromPage completed", {
97
+ selector,
98
+ instruction: instruction.slice(0, 100),
99
+ });
90
100
 
91
- return result;
101
+ return result;
92
102
  }
@@ -1,5 +1,5 @@
1
1
  export {
2
- pageRequest,
3
- type RequestConfig,
4
- type PageRequestOptions,
2
+ pageRequest,
3
+ type RequestConfig,
4
+ type PageRequestOptions,
5
5
  } from "./network.js";