libretto 0.4.4 → 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 (194) hide show
  1. package/README.md +106 -36
  2. package/dist/cli/cli.js +39 -113
  3. package/dist/cli/commands/ai.js +1 -1
  4. package/dist/cli/commands/browser.js +87 -60
  5. package/dist/cli/commands/execution.js +201 -88
  6. package/dist/cli/commands/init.js +30 -8
  7. package/dist/cli/commands/logs.js +5 -6
  8. package/dist/cli/commands/shared.js +30 -29
  9. package/dist/cli/commands/snapshot.js +26 -39
  10. package/dist/cli/core/ai-config.js +9 -2
  11. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  12. package/dist/cli/core/browser.js +141 -33
  13. package/dist/cli/core/context.js +7 -18
  14. package/dist/cli/core/session-telemetry.js +5 -2
  15. package/dist/cli/core/session.js +23 -10
  16. package/dist/cli/core/snapshot-analyzer.js +16 -33
  17. package/dist/cli/core/snapshot-api-config.js +2 -6
  18. package/dist/cli/core/telemetry.js +10 -2
  19. package/dist/cli/framework/simple-cli.js +45 -25
  20. package/dist/cli/router.js +14 -21
  21. package/dist/cli/workers/run-integration-runtime.js +26 -7
  22. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  23. package/dist/cli/workers/run-integration-worker.js +1 -4
  24. package/dist/index.d.ts +1 -2
  25. package/dist/index.js +7 -10
  26. package/dist/runtime/download/download.js +5 -1
  27. package/dist/runtime/extract/extract.js +11 -2
  28. package/dist/runtime/network/network.js +8 -1
  29. package/dist/runtime/recovery/agent.js +6 -2
  30. package/dist/runtime/recovery/errors.js +3 -1
  31. package/dist/runtime/recovery/recovery.js +3 -1
  32. package/dist/shared/condense-dom/condense-dom.js +6 -13
  33. package/dist/shared/config/config.d.ts +1 -9
  34. package/dist/shared/config/config.js +0 -18
  35. package/dist/shared/config/index.d.ts +2 -1
  36. package/dist/shared/config/index.js +0 -10
  37. package/dist/shared/debug/pause.js +9 -3
  38. package/dist/shared/instrumentation/instrument.js +101 -5
  39. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  40. package/dist/shared/llm/client.js +3 -1
  41. package/dist/shared/logger/index.js +4 -1
  42. package/dist/shared/paths/paths.js +2 -1
  43. package/dist/shared/paths/repo-root.d.ts +3 -0
  44. package/dist/shared/paths/repo-root.js +24 -0
  45. package/dist/shared/run/api.js +3 -1
  46. package/dist/shared/run/browser.js +7 -2
  47. package/dist/shared/state/session-state.d.ts +2 -1
  48. package/dist/shared/state/session-state.js +5 -2
  49. package/dist/shared/visualization/ghost-cursor.js +19 -10
  50. package/dist/shared/visualization/highlight.js +9 -6
  51. package/dist/shared/workflow/workflow.d.ts +4 -5
  52. package/dist/shared/workflow/workflow.js +3 -5
  53. package/package.json +11 -8
  54. package/scripts/check-skills-sync.mjs +25 -0
  55. package/scripts/compare-eval-summary.mjs +47 -0
  56. package/scripts/postinstall.mjs +26 -17
  57. package/scripts/prepare-release.sh +97 -0
  58. package/scripts/skills-libretto.mjs +103 -0
  59. package/scripts/summarize-evals.mjs +135 -0
  60. package/scripts/sync-skills.mjs +12 -0
  61. package/skills/libretto/SKILL.md +130 -377
  62. package/skills/libretto/references/auth-profiles.md +30 -0
  63. package/skills/libretto/{code-generation-rules.md → references/code-generation-rules.md} +27 -42
  64. package/skills/libretto/references/configuration-file-reference.md +53 -0
  65. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  66. package/skills/libretto/references/site-security-review.md +143 -0
  67. package/src/cli/cli.ts +86 -0
  68. package/src/cli/commands/ai.ts +35 -0
  69. package/src/cli/commands/browser.ts +189 -0
  70. package/src/cli/commands/execution.ts +822 -0
  71. package/src/cli/commands/init.ts +350 -0
  72. package/src/cli/commands/logs.ts +128 -0
  73. package/src/cli/commands/shared.ts +69 -0
  74. package/src/cli/commands/snapshot.ts +312 -0
  75. package/src/cli/core/ai-config.ts +264 -0
  76. package/src/cli/core/api-snapshot-analyzer.ts +108 -0
  77. package/src/cli/core/browser.ts +976 -0
  78. package/src/cli/core/context.ts +127 -0
  79. package/src/cli/core/pause-signals.ts +35 -0
  80. package/src/cli/core/session-telemetry.ts +564 -0
  81. package/src/cli/core/session.ts +223 -0
  82. package/src/cli/core/snapshot-analyzer.ts +855 -0
  83. package/src/cli/core/snapshot-api-config.ts +231 -0
  84. package/src/cli/core/telemetry.ts +459 -0
  85. package/src/cli/framework/simple-cli.ts +1340 -0
  86. package/src/cli/index.ts +13 -0
  87. package/src/cli/router.ts +20 -0
  88. package/src/cli/workers/run-integration-runtime.ts +338 -0
  89. package/src/cli/workers/run-integration-worker-protocol.ts +16 -0
  90. package/src/cli/workers/run-integration-worker.ts +72 -0
  91. package/src/index.ts +127 -0
  92. package/src/runtime/download/download.ts +104 -0
  93. package/src/runtime/download/index.ts +7 -0
  94. package/src/runtime/extract/extract.ts +102 -0
  95. package/src/runtime/extract/index.ts +1 -0
  96. package/src/runtime/network/index.ts +5 -0
  97. package/src/runtime/network/network.ts +119 -0
  98. package/{dist/runtime/recovery/agent.cjs → src/runtime/recovery/agent.ts} +114 -76
  99. package/src/runtime/recovery/errors.ts +155 -0
  100. package/src/runtime/recovery/index.ts +7 -0
  101. package/src/runtime/recovery/recovery.ts +53 -0
  102. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +249 -124
  103. package/src/shared/config/config.ts +3 -0
  104. package/src/shared/config/index.ts +0 -0
  105. package/src/shared/debug/index.ts +1 -0
  106. package/src/shared/debug/pause.ts +91 -0
  107. package/src/shared/instrumentation/errors.ts +84 -0
  108. package/src/shared/instrumentation/index.ts +9 -0
  109. package/src/shared/instrumentation/instrument.ts +406 -0
  110. package/src/shared/llm/ai-sdk-adapter.ts +81 -0
  111. package/{dist/shared/llm/client.cjs → src/shared/llm/client.ts} +86 -80
  112. package/src/shared/llm/index.ts +3 -0
  113. package/src/shared/llm/types.ts +63 -0
  114. package/src/shared/logger/index.ts +13 -0
  115. package/src/shared/logger/logger.ts +358 -0
  116. package/src/shared/logger/sinks.ts +148 -0
  117. package/src/shared/paths/paths.ts +110 -0
  118. package/src/shared/paths/repo-root.ts +27 -0
  119. package/src/shared/run/api.ts +6 -0
  120. package/src/shared/run/browser.ts +107 -0
  121. package/src/shared/state/index.ts +11 -0
  122. package/src/shared/state/session-state.ts +77 -0
  123. package/src/shared/visualization/ghost-cursor.ts +213 -0
  124. package/src/shared/visualization/highlight.ts +149 -0
  125. package/src/shared/visualization/index.ts +18 -0
  126. package/src/shared/workflow/workflow.ts +36 -0
  127. package/dist/index.cjs +0 -144
  128. package/dist/index.d.cts +0 -21
  129. package/dist/runtime/download/download.cjs +0 -70
  130. package/dist/runtime/download/download.d.cts +0 -35
  131. package/dist/runtime/download/index.cjs +0 -30
  132. package/dist/runtime/download/index.d.cts +0 -3
  133. package/dist/runtime/extract/extract.cjs +0 -88
  134. package/dist/runtime/extract/extract.d.cts +0 -23
  135. package/dist/runtime/extract/index.cjs +0 -28
  136. package/dist/runtime/extract/index.d.cts +0 -5
  137. package/dist/runtime/network/index.cjs +0 -28
  138. package/dist/runtime/network/index.d.cts +0 -4
  139. package/dist/runtime/network/network.cjs +0 -91
  140. package/dist/runtime/network/network.d.cts +0 -28
  141. package/dist/runtime/recovery/agent.d.cts +0 -13
  142. package/dist/runtime/recovery/errors.cjs +0 -124
  143. package/dist/runtime/recovery/errors.d.cts +0 -31
  144. package/dist/runtime/recovery/index.cjs +0 -34
  145. package/dist/runtime/recovery/index.d.cts +0 -7
  146. package/dist/runtime/recovery/recovery.cjs +0 -55
  147. package/dist/runtime/recovery/recovery.d.cts +0 -12
  148. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  149. package/dist/shared/config/config.cjs +0 -44
  150. package/dist/shared/config/config.d.cts +0 -10
  151. package/dist/shared/config/index.cjs +0 -32
  152. package/dist/shared/config/index.d.cts +0 -1
  153. package/dist/shared/debug/index.cjs +0 -28
  154. package/dist/shared/debug/index.d.cts +0 -1
  155. package/dist/shared/debug/pause.cjs +0 -86
  156. package/dist/shared/debug/pause.d.cts +0 -12
  157. package/dist/shared/instrumentation/errors.cjs +0 -81
  158. package/dist/shared/instrumentation/errors.d.cts +0 -12
  159. package/dist/shared/instrumentation/index.cjs +0 -35
  160. package/dist/shared/instrumentation/index.d.cts +0 -6
  161. package/dist/shared/instrumentation/instrument.cjs +0 -206
  162. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  163. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  164. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  165. package/dist/shared/llm/client.d.cts +0 -13
  166. package/dist/shared/llm/index.cjs +0 -31
  167. package/dist/shared/llm/index.d.cts +0 -5
  168. package/dist/shared/llm/types.cjs +0 -16
  169. package/dist/shared/llm/types.d.cts +0 -67
  170. package/dist/shared/logger/index.cjs +0 -37
  171. package/dist/shared/logger/index.d.cts +0 -2
  172. package/dist/shared/logger/logger.cjs +0 -232
  173. package/dist/shared/logger/logger.d.cts +0 -86
  174. package/dist/shared/logger/sinks.cjs +0 -160
  175. package/dist/shared/logger/sinks.d.cts +0 -9
  176. package/dist/shared/paths/paths.cjs +0 -104
  177. package/dist/shared/paths/paths.d.cts +0 -10
  178. package/dist/shared/run/api.cjs +0 -28
  179. package/dist/shared/run/api.d.cts +0 -2
  180. package/dist/shared/run/browser.cjs +0 -98
  181. package/dist/shared/run/browser.d.cts +0 -22
  182. package/dist/shared/state/index.cjs +0 -38
  183. package/dist/shared/state/index.d.cts +0 -2
  184. package/dist/shared/state/session-state.cjs +0 -92
  185. package/dist/shared/state/session-state.d.cts +0 -40
  186. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  187. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  188. package/dist/shared/visualization/highlight.cjs +0 -134
  189. package/dist/shared/visualization/highlight.d.cts +0 -22
  190. package/dist/shared/visualization/index.cjs +0 -45
  191. package/dist/shared/visualization/index.d.cts +0 -3
  192. package/dist/shared/workflow/workflow.cjs +0 -47
  193. package/dist/shared/workflow/workflow.d.cts +0 -21
  194. package/skills/libretto/integration-approach-selection.md +0 -174
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { runLibrettoCLI } from "./cli.js";
3
+ import {
4
+ maybeConfigureLLMClientFactoryFromEnv,
5
+ setLLMClientFactory,
6
+ } from "./core/context.js";
7
+
8
+ export { setLLMClientFactory };
9
+ export { runClose } from "./commands/browser.js";
10
+ export { runLibrettoCLI };
11
+
12
+ maybeConfigureLLMClientFactoryFromEnv();
13
+ void runLibrettoCLI();
@@ -0,0 +1,20 @@
1
+ import { aiCommands } from "./commands/ai.js";
2
+ import { browserCommands } from "./commands/browser.js";
3
+ import { executionCommands } from "./commands/execution.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { logCommands } from "./commands/logs.js";
6
+ import { snapshotCommand } from "./commands/snapshot.js";
7
+ import { SimpleCLI } from "./framework/simple-cli.js";
8
+
9
+ export const cliRoutes = {
10
+ ...browserCommands,
11
+ ...executionCommands,
12
+ ...logCommands,
13
+ ai: aiCommands,
14
+ init: initCommand,
15
+ snapshot: snapshotCommand,
16
+ };
17
+
18
+ export function createCLIApp() {
19
+ return SimpleCLI.define("libretto", cliRoutes);
20
+ }
@@ -0,0 +1,338 @@
1
+ import type { BrowserContext } from "playwright";
2
+ import { appendFileSync, existsSync, readFileSync } from "node:fs";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { cwd } from "node:process";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import {
8
+ instrumentContext,
9
+ launchBrowser,
10
+ type LibrettoWorkflowContext,
11
+ } from "../../index.js";
12
+ import type { LoggerApi } from "../../shared/logger/index.js";
13
+ import { parseSessionStateContent } from "../../shared/state/index.js";
14
+ import { getProfilePath, normalizeDomain } from "../core/browser.js";
15
+ import {
16
+ getSessionActionsLogPath,
17
+ getSessionDir,
18
+ getSessionNetworkLogPath,
19
+ getSessionStatePath,
20
+ } from "../core/context.js";
21
+ import {
22
+ getPauseSignalPaths,
23
+ removeSignalIfExists,
24
+ } from "../core/pause-signals.js";
25
+ import { installSessionTelemetry } from "../core/session-telemetry.js";
26
+ import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
27
+
28
+ const LIBRETTO_WORKFLOW_BRAND = Symbol.for("libretto.workflow");
29
+
30
+ type LoadedLibrettoWorkflow = {
31
+ run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
32
+ };
33
+
34
+ type RunIntegrationOutcome =
35
+ | { status: "completed" }
36
+ | { status: "failed-held" };
37
+
38
+ const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
39
+ const TSCONFIG_HINT =
40
+ "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
41
+
42
+ function isTsxCompileError(error: unknown): error is Error {
43
+ return (
44
+ error instanceof Error &&
45
+ (error.name === "TransformError" ||
46
+ error.message.startsWith("Cannot resolve tsconfig at path:"))
47
+ );
48
+ }
49
+
50
+ function mirrorStdoutToFile(filePath: string): () => void {
51
+ const stdout = process.stdout as NodeJS.WriteStream & {
52
+ write: (...args: any[]) => boolean;
53
+ };
54
+ const originalWrite = stdout.write.bind(stdout);
55
+
56
+ stdout.write = ((chunk: unknown, ...args: unknown[]) => {
57
+ try {
58
+ const buffer = Buffer.isBuffer(chunk)
59
+ ? chunk
60
+ : Buffer.from(String(chunk), "utf8");
61
+ appendFileSync(filePath, buffer);
62
+ } catch {
63
+ // Ignore log mirroring failures; primary stdout should still flow.
64
+ }
65
+ return originalWrite(chunk, ...args);
66
+ }) as typeof stdout.write;
67
+
68
+ return () => {
69
+ stdout.write = originalWrite as typeof stdout.write;
70
+ };
71
+ }
72
+
73
+ function readSessionStatePid(session: string): number | null {
74
+ const statePath = getSessionStatePath(session);
75
+ if (!existsSync(statePath)) return null;
76
+
77
+ try {
78
+ return (
79
+ parseSessionStateContent(readFileSync(statePath, "utf8"), statePath)
80
+ .pid ?? null
81
+ );
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ async function waitForFailureSessionRelease(args: {
88
+ session: string;
89
+ expectedPid: number;
90
+ logger: LoggerApi;
91
+ }): Promise<void> {
92
+ const { session, expectedPid, logger } = args;
93
+ logger.info("run-failure-session-hold", { session, expectedPid });
94
+
95
+ while (true) {
96
+ const currentPid = readSessionStatePid(session);
97
+ if (currentPid !== expectedPid) {
98
+ logger.info("run-failure-session-released", {
99
+ session,
100
+ expectedPid,
101
+ currentPid,
102
+ });
103
+ return;
104
+ }
105
+ await new Promise((resolveWait) =>
106
+ setTimeout(resolveWait, FAILURE_HOLD_POLL_INTERVAL_MS),
107
+ );
108
+ }
109
+ }
110
+
111
+ function isLoadedLibrettoWorkflow(
112
+ value: unknown,
113
+ ): value is LoadedLibrettoWorkflow {
114
+ if (!value || typeof value !== "object") return false;
115
+ const candidate = value as Record<PropertyKey, unknown>;
116
+ return (
117
+ candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
118
+ typeof candidate.run === "function"
119
+ );
120
+ }
121
+
122
+ function resolveLocalAuthProfilePath(domain: string): string {
123
+ return getProfilePath(normalizeDomain(domain));
124
+ }
125
+
126
+ function getMissingLocalAuthProfileError(args: {
127
+ domain: string;
128
+ profilePath: string;
129
+ session: string;
130
+ }): string {
131
+ const normalizedDomain = normalizeDomain(args.domain);
132
+ return [
133
+ `Local auth profile not found for domain "${normalizedDomain}".`,
134
+ `Expected profile file: ${args.profilePath}`,
135
+ "To create it:",
136
+ ` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
137
+ " 2. Log in manually in the browser window.",
138
+ ` 3. libretto save ${normalizedDomain} --session ${args.session}`,
139
+ ].join("\n");
140
+ }
141
+
142
+ function getAbsoluteIntegrationPath(integrationPath: string): string {
143
+ const absolutePath = isAbsolute(integrationPath)
144
+ ? integrationPath
145
+ : resolve(cwd(), integrationPath);
146
+ if (!existsSync(absolutePath)) {
147
+ throw new Error(`Integration file does not exist: ${absolutePath}`);
148
+ }
149
+ return absolutePath;
150
+ }
151
+
152
+ async function loadWorkflowExport(
153
+ absolutePath: string,
154
+ exportName: string,
155
+ ): Promise<LoadedLibrettoWorkflow> {
156
+ let loadedModule: Record<string, unknown>;
157
+ try {
158
+ loadedModule = (await import(pathToFileURL(absolutePath).href)) as Record<
159
+ string,
160
+ unknown
161
+ >;
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ const compileHint = isTsxCompileError(error) ? `\n${TSCONFIG_HINT}` : "";
165
+ throw new Error(
166
+ `Failed to import integration module at ${absolutePath}: ${message}${compileHint}`,
167
+ );
168
+ }
169
+
170
+ const targetExport = loadedModule[exportName];
171
+ if (!targetExport) {
172
+ const availableExports = Object.keys(loadedModule);
173
+ const detail =
174
+ availableExports.length > 0
175
+ ? ` Available exports: ${availableExports.join(", ")}`
176
+ : " The module has no exports.";
177
+ throw new Error(
178
+ `Export "${exportName}" was not found in ${absolutePath}.${detail}`,
179
+ );
180
+ }
181
+
182
+ if (!isLoadedLibrettoWorkflow(targetExport)) {
183
+ throw new Error(
184
+ [
185
+ `Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
186
+ "",
187
+ 'A workflow must be created using the workflow() function from "libretto":',
188
+ "",
189
+ ' import { workflow } from "libretto";',
190
+ "",
191
+ ` export const ${exportName} = workflow<InputType, OutputType>(`,
192
+ " async (ctx, input) => {",
193
+ " // ctx.session — libretto session name",
194
+ " // ctx.page — Playwright Page instance",
195
+ " // ctx.logger — MinimalLogger",
196
+ " // ctx.services — injected dependencies (generic, default {})",
197
+ " // input — JSON-serializable input matching InputType",
198
+ " return output; // must match OutputType",
199
+ " },",
200
+ " );",
201
+ ].join("\n"),
202
+ );
203
+ }
204
+
205
+ return targetExport;
206
+ }
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
+
219
+ async function runIntegrationInternal(
220
+ args: RunIntegrationWorkerRequest,
221
+ options: {
222
+ logger: LoggerApi;
223
+ },
224
+ ): Promise<RunIntegrationOutcome> {
225
+ const { logger } = options;
226
+ const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
227
+ const workflow = await loadWorkflowExport(absolutePath, args.exportName);
228
+ const signalPaths = getPauseSignalPaths(args.session);
229
+ await removeSignalIfExists(signalPaths.pausedSignalPath);
230
+ await removeSignalIfExists(signalPaths.resumeSignalPath);
231
+ await removeSignalIfExists(signalPaths.completedSignalPath);
232
+ await removeSignalIfExists(signalPaths.failedSignalPath);
233
+ const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
234
+
235
+ console.log(
236
+ `Running integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
237
+ );
238
+
239
+ const integrationLogger = logger.withScope("integration-run", {
240
+ integrationPath: absolutePath,
241
+ integrationExport: args.exportName,
242
+ session: args.session,
243
+ });
244
+
245
+ // Resolve auth profile from CLI flag (--auth-profile <domain>)
246
+ const authProfileDomain = args.authProfileDomain;
247
+ const storageStatePath = authProfileDomain
248
+ ? resolveLocalAuthProfilePath(authProfileDomain)
249
+ : undefined;
250
+ if (authProfileDomain && storageStatePath && !existsSync(storageStatePath)) {
251
+ throw new Error(
252
+ getMissingLocalAuthProfileError({
253
+ domain: authProfileDomain,
254
+ profilePath: storageStatePath,
255
+ session: args.session,
256
+ }),
257
+ );
258
+ }
259
+ const browserSession = await launchBrowser({
260
+ sessionName: args.session,
261
+ headless: args.headless,
262
+ storageStatePath,
263
+ viewport: args.viewport,
264
+ });
265
+ if (!args.headless && args.visualize !== false) {
266
+ await installHeadedWorkflowVisualization({
267
+ context: browserSession.context,
268
+ logger: integrationLogger,
269
+ });
270
+ }
271
+ const actionsLogPath = getSessionActionsLogPath(args.session);
272
+ const networkLogPath = getSessionNetworkLogPath(args.session);
273
+ await installSessionTelemetry({
274
+ context: browserSession.context,
275
+ initialPage: browserSession.page,
276
+ includeUserDomActions: true,
277
+ logAction: (entry) => {
278
+ appendFileSync(actionsLogPath, JSON.stringify(entry) + "\n");
279
+ },
280
+ logNetwork: (entry) => {
281
+ appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
282
+ },
283
+ });
284
+
285
+ const workflowContext: LibrettoWorkflowContext = {
286
+ session: args.session,
287
+ logger: integrationLogger,
288
+ page: browserSession.page,
289
+ services: {},
290
+ };
291
+
292
+ try {
293
+ try {
294
+ await workflow.run(workflowContext, args.params ?? {});
295
+ } catch (error) {
296
+ const errorMessage =
297
+ error instanceof Error ? error.message : String(error);
298
+ await writeFile(
299
+ signalPaths.failedSignalPath,
300
+ JSON.stringify(
301
+ {
302
+ failedAt: new Date().toISOString(),
303
+ message: errorMessage,
304
+ phase: "workflow",
305
+ },
306
+ null,
307
+ 2,
308
+ ),
309
+ "utf8",
310
+ );
311
+ await waitForFailureSessionRelease({
312
+ session: args.session,
313
+ expectedPid: process.pid,
314
+ logger,
315
+ });
316
+ return { status: "failed-held" };
317
+ }
318
+ await writeFile(
319
+ signalPaths.completedSignalPath,
320
+ JSON.stringify({ completedAt: new Date().toISOString() }, null, 2),
321
+ "utf8",
322
+ );
323
+ console.log("Integration completed.");
324
+ return { status: "completed" };
325
+ } finally {
326
+ restoreStdout();
327
+ await browserSession.close();
328
+ }
329
+ }
330
+
331
+ export async function runIntegrationFromFileInWorker(
332
+ args: RunIntegrationWorkerRequest,
333
+ logger: LoggerApi,
334
+ ): Promise<RunIntegrationOutcome> {
335
+ return await runIntegrationInternal(args, {
336
+ logger,
337
+ });
338
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ export const RunIntegrationWorkerRequestSchema = z.object({
4
+ integrationPath: z.string().min(1),
5
+ exportName: z.string().min(1),
6
+ session: z.string().min(1),
7
+ params: z.unknown(),
8
+ headless: z.boolean(),
9
+ visualize: z.boolean().default(true),
10
+ authProfileDomain: z.string().optional(),
11
+ viewport: z.object({ width: z.number(), height: z.number() }).optional(),
12
+ });
13
+
14
+ export type RunIntegrationWorkerRequest = z.infer<
15
+ typeof RunIntegrationWorkerRequestSchema
16
+ >;
@@ -0,0 +1,72 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { ZodError } from "zod";
3
+ import {
4
+ RunIntegrationWorkerRequestSchema,
5
+ type RunIntegrationWorkerRequest,
6
+ } from "./run-integration-worker-protocol.js";
7
+ import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
8
+ import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
9
+ import { getPauseSignalPaths } from "../core/pause-signals.js";
10
+
11
+ function parseWorkerRequest(argv: string[]): RunIntegrationWorkerRequest {
12
+ const rawPayload = argv[2];
13
+ if (!rawPayload) {
14
+ throw new Error("Missing worker payload argument.");
15
+ }
16
+
17
+ let parsed: unknown;
18
+ try {
19
+ parsed = JSON.parse(rawPayload);
20
+ } catch (error) {
21
+ throw new Error(
22
+ `Invalid worker payload JSON: ${error instanceof Error ? error.message : String(error)}`,
23
+ );
24
+ }
25
+
26
+ try {
27
+ return RunIntegrationWorkerRequestSchema.parse(parsed);
28
+ } catch (error) {
29
+ if (error instanceof ZodError) {
30
+ const details = error.issues
31
+ .map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`)
32
+ .join("; ");
33
+ throw new Error(`Worker payload is invalid: ${details}`);
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ async function main(): Promise<void> {
40
+ let request: RunIntegrationWorkerRequest | null = null;
41
+ let exitCode = 0;
42
+ try {
43
+ request = parseWorkerRequest(process.argv);
44
+ const workerRequest = request;
45
+ ensureLibrettoSetup();
46
+ await withSessionLogger(workerRequest.session, async (logger) => {
47
+ await runIntegrationFromFileInWorker(workerRequest, logger);
48
+ });
49
+ } catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ if (request) {
52
+ const { failedSignalPath } = getPauseSignalPaths(request.session);
53
+ await writeFile(
54
+ failedSignalPath,
55
+ JSON.stringify(
56
+ {
57
+ failedAt: new Date().toISOString(),
58
+ message,
59
+ phase: "setup",
60
+ },
61
+ null,
62
+ 2,
63
+ ),
64
+ "utf8",
65
+ );
66
+ }
67
+ exitCode = 1;
68
+ }
69
+ process.exit(exitCode);
70
+ }
71
+
72
+ void main();
package/src/index.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { resolve } from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ // Logger
5
+ export {
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,
17
+ } from "./shared/logger/sinks.js";
18
+
19
+ // LLM client interface
20
+ export type {
21
+ LLMClient,
22
+ Message,
23
+ MessageContentPart,
24
+ } from "./shared/llm/types.js";
25
+ export { createLLMClientFromModel } from "./shared/llm/ai-sdk-adapter.js";
26
+ export {
27
+ SESSION_STATE_VERSION,
28
+ SessionStatusSchema,
29
+ SessionStateFileSchema,
30
+ parseSessionStateData,
31
+ parseSessionStateContent,
32
+ serializeSessionState,
33
+ type SessionStatus,
34
+ type SessionState,
35
+ type SessionStateFile,
36
+ } from "./shared/state/index.js";
37
+
38
+ // Recovery
39
+ export { executeRecoveryAgent } from "./runtime/recovery/agent.js";
40
+ export { attemptWithRecovery } from "./runtime/recovery/recovery.js";
41
+ export {
42
+ detectSubmissionError,
43
+ type KnownSubmissionError,
44
+ type DetectedSubmissionError,
45
+ } from "./runtime/recovery/errors.js";
46
+
47
+ // AI extraction
48
+ export {
49
+ extractFromPage,
50
+ type ExtractOptions,
51
+ } from "./runtime/extract/extract.js";
52
+
53
+ // Network helpers
54
+ export {
55
+ pageRequest,
56
+ type RequestConfig,
57
+ type PageRequestOptions,
58
+ } from "./runtime/network/network.js";
59
+
60
+ // Download helpers
61
+ export {
62
+ downloadViaClick,
63
+ downloadAndSave,
64
+ type DownloadResult,
65
+ type DownloadViaClickOptions,
66
+ type SaveDownloadOptions,
67
+ } from "./runtime/download/download.js";
68
+
69
+ // Debug / Pause
70
+ export { pause } from "./shared/debug/pause.js";
71
+
72
+ // Instrumentation
73
+ export {
74
+ instrumentPage,
75
+ installInstrumentation,
76
+ instrumentContext,
77
+ type InstrumentationOptions,
78
+ type InstrumentedPage,
79
+ } from "./shared/instrumentation/instrument.js";
80
+
81
+ // Visualization
82
+ export {
83
+ ensureGhostCursor,
84
+ moveGhostCursor,
85
+ ghostClick,
86
+ hideGhostCursor,
87
+ type GhostCursorOptions,
88
+ } from "./shared/visualization/ghost-cursor.js";
89
+ export {
90
+ ensureHighlightLayer,
91
+ showHighlight,
92
+ clearHighlights,
93
+ type HighlightOptions,
94
+ } from "./shared/visualization/highlight.js";
95
+
96
+ // Run helpers
97
+ export {
98
+ launchBrowser,
99
+ type LaunchBrowserArgs,
100
+ type BrowserSession,
101
+ } from "./shared/run/api.js";
102
+
103
+ // Workflow helpers
104
+ export {
105
+ LibrettoWorkflow,
106
+ LIBRETTO_WORKFLOW_BRAND,
107
+ workflow,
108
+ type LibrettoWorkflowContext,
109
+ type LibrettoWorkflowHandler,
110
+ } from "./shared/workflow/workflow.js";
111
+
112
+ const isDirectExecution = (): boolean => {
113
+ const entryArg = process.argv[1];
114
+ if (!entryArg) {
115
+ return false;
116
+ }
117
+ return pathToFileURL(resolve(entryArg)).href === import.meta.url;
118
+ };
119
+
120
+ if (isDirectExecution()) {
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
+ });
127
+ }
@@ -0,0 +1,104 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import type { Page, Download } from "playwright";
4
+ import type { MinimalLogger } from "../../shared/logger/logger.js";
5
+
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;
11
+ };
12
+
13
+ export type DownloadViaClickOptions = {
14
+ logger?: MinimalLogger;
15
+ /** Timeout in milliseconds for waiting on the download event. Defaults to 30 000. */
16
+ timeout?: number;
17
+ };
18
+
19
+ /**
20
+ * Triggers a file download by clicking a DOM element and intercepts the
21
+ * resulting download using Playwright's download event.
22
+ *
23
+ * The download promise is registered **before** the click so the event is
24
+ * never missed.
25
+ */
26
+ export async function downloadViaClick(
27
+ page: Page,
28
+ selector: string,
29
+ options?: DownloadViaClickOptions,
30
+ ): Promise<DownloadResult> {
31
+ const { logger, timeout = 30_000 } = options ?? {};
32
+
33
+ const startTime = Date.now();
34
+
35
+ // 1. Register the download listener BEFORE clicking
36
+ const downloadPromise = page.waitForEvent("download", { timeout });
37
+
38
+ // 2. Click the element that triggers the download
39
+ await page.locator(selector).click();
40
+
41
+ // 3. Await the download event
42
+ const download: Download = await downloadPromise;
43
+
44
+ // 4. Get the suggested filename
45
+ const filename = download.suggestedFilename();
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
+ }
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);
60
+
61
+ const duration = Date.now() - startTime;
62
+
63
+ logger?.info("download:click", {
64
+ selector,
65
+ filename,
66
+ size: buffer.length,
67
+ duration,
68
+ });
69
+
70
+ return { buffer, filename };
71
+ }
72
+
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;
76
+ };
77
+
78
+ /**
79
+ * Convenience wrapper around {@link downloadViaClick} that also writes the
80
+ * downloaded file to disk.
81
+ */
82
+ export async function downloadAndSave(
83
+ page: Page,
84
+ selector: string,
85
+ options?: SaveDownloadOptions,
86
+ ): Promise<DownloadResult & { savedTo: string }> {
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 };
104
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ downloadViaClick,
3
+ downloadAndSave,
4
+ type DownloadResult,
5
+ type DownloadViaClickOptions,
6
+ type SaveDownloadOptions,
7
+ } from "./download.js";