libretto 0.6.10 → 0.6.12

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 (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -1,326 +0,0 @@
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
- getDefaultWorkflowFromModuleExports,
9
- getWorkflowsFromModuleExports,
10
- instrumentContext,
11
- launchBrowser,
12
- type ExportedLibrettoWorkflow,
13
- type LibrettoWorkflowContext,
14
- } from "../../index.js";
15
- import type { LoggerApi } from "../../shared/logger/index.js";
16
- import { parseSessionStateContent } from "../../shared/state/index.js";
17
- import {
18
- getProfilePath,
19
- normalizeDomain,
20
- normalizeUrl,
21
- } from "../core/browser.js";
22
- import {
23
- getSessionActionsLogPath,
24
- getSessionDir,
25
- getSessionNetworkLogPath,
26
- getSessionStatePath,
27
- } from "../core/context.js";
28
- import {
29
- getPauseSignalPaths,
30
- removeSignalIfExists,
31
- } from "../core/pause-signals.js";
32
- import { installSessionTelemetry } from "../core/session-telemetry.js";
33
- import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
34
-
35
- type LoadedLibrettoWorkflow = ExportedLibrettoWorkflow & {
36
- run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
37
- };
38
-
39
- type RunIntegrationOutcome =
40
- | { status: "completed" }
41
- | { status: "failed-held" };
42
-
43
- const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
44
- const TSCONFIG_HINT =
45
- "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
46
-
47
- function isTsxCompileError(error: unknown): error is Error {
48
- return (
49
- error instanceof Error &&
50
- (error.name === "TransformError" ||
51
- error.message.startsWith("Cannot resolve tsconfig at path:"))
52
- );
53
- }
54
-
55
- function mirrorStdoutToFile(filePath: string): () => void {
56
- const stdout = process.stdout as NodeJS.WriteStream & {
57
- write: (...args: any[]) => boolean;
58
- };
59
- const originalWrite = stdout.write.bind(stdout);
60
-
61
- stdout.write = ((chunk: unknown, ...args: unknown[]) => {
62
- try {
63
- const buffer = Buffer.isBuffer(chunk)
64
- ? chunk
65
- : Buffer.from(String(chunk), "utf8");
66
- appendFileSync(filePath, buffer);
67
- } catch {
68
- // Ignore log mirroring failures; primary stdout should still flow.
69
- }
70
- return originalWrite(chunk, ...args);
71
- }) as typeof stdout.write;
72
-
73
- return () => {
74
- stdout.write = originalWrite as typeof stdout.write;
75
- };
76
- }
77
-
78
- function readSessionStatePid(session: string): number | null {
79
- const statePath = getSessionStatePath(session);
80
- if (!existsSync(statePath)) return null;
81
-
82
- try {
83
- return (
84
- parseSessionStateContent(readFileSync(statePath, "utf8"), statePath)
85
- .pid ?? null
86
- );
87
- } catch {
88
- return null;
89
- }
90
- }
91
-
92
- async function waitForFailureSessionRelease(args: {
93
- session: string;
94
- expectedPid: number;
95
- logger: LoggerApi;
96
- }): Promise<void> {
97
- const { session, expectedPid, logger } = args;
98
- logger.info("run-failure-session-hold", { session, expectedPid });
99
-
100
- while (true) {
101
- const currentPid = readSessionStatePid(session);
102
- if (currentPid !== expectedPid) {
103
- logger.info("run-failure-session-released", {
104
- session,
105
- expectedPid,
106
- currentPid,
107
- });
108
- return;
109
- }
110
- await new Promise((resolveWait) =>
111
- setTimeout(resolveWait, FAILURE_HOLD_POLL_INTERVAL_MS),
112
- );
113
- }
114
- }
115
-
116
- function getMissingLocalAuthProfileError(args: {
117
- normalizedDomain: string;
118
- profilePath: string;
119
- session: string;
120
- }): string {
121
- return [
122
- `Local auth profile not found for domain "${args.normalizedDomain}".`,
123
- `Expected profile file: ${args.profilePath}`,
124
- "To create it:",
125
- ` 1. libretto open https://${args.normalizedDomain} --headed --session ${args.session}`,
126
- " 2. Log in manually in the browser window.",
127
- ` 3. libretto save ${args.normalizedDomain} --session ${args.session}`,
128
- ].join("\n");
129
- }
130
-
131
- function getAbsoluteIntegrationPath(integrationPath: string): string {
132
- const absolutePath = isAbsolute(integrationPath)
133
- ? integrationPath
134
- : resolve(cwd(), integrationPath);
135
- if (!existsSync(absolutePath)) {
136
- throw new Error(`Integration file does not exist: ${absolutePath}`);
137
- }
138
- return absolutePath;
139
- }
140
-
141
- async function loadDefaultWorkflow(
142
- absolutePath: string,
143
- ): Promise<LoadedLibrettoWorkflow> {
144
- let loadedModule: Record<string, unknown>;
145
- try {
146
- loadedModule = (await import(pathToFileURL(absolutePath).href)) as Record<
147
- string,
148
- unknown
149
- >;
150
- } catch (error) {
151
- const message = error instanceof Error ? error.message : String(error);
152
- const compileHint = isTsxCompileError(error) ? `\n${TSCONFIG_HINT}` : "";
153
- throw new Error(
154
- `Failed to import integration module at ${absolutePath}: ${message}${compileHint}`,
155
- );
156
- }
157
-
158
- const defaultWorkflow = getDefaultWorkflowFromModuleExports(loadedModule);
159
- if (defaultWorkflow) {
160
- return defaultWorkflow as LoadedLibrettoWorkflow;
161
- }
162
-
163
- const availableWorkflowNames = getWorkflowsFromModuleExports(loadedModule).map(
164
- (candidate) => candidate.name,
165
- );
166
-
167
- if (availableWorkflowNames.length === 0) {
168
- throw new Error(
169
- `No default-exported workflow found in ${absolutePath}. Export the workflow with \`export default workflow("name", handler)\`.`,
170
- );
171
- }
172
-
173
- throw new Error(
174
- `No default-exported workflow found in ${absolutePath}. libretto run only uses the file's default export. Available named workflows: ${availableWorkflowNames.join(", ")}`,
175
- );
176
- }
177
-
178
- export async function installHeadedWorkflowVisualization(args: {
179
- context: BrowserContext;
180
- logger: LoggerApi;
181
- instrument?: typeof instrumentContext;
182
- }): Promise<void> {
183
- await (args.instrument ?? instrumentContext)(args.context, {
184
- visualize: true,
185
- logger: args.logger,
186
- });
187
- }
188
-
189
- async function runIntegrationInternal(
190
- args: RunIntegrationWorkerRequest,
191
- options: {
192
- logger: LoggerApi;
193
- },
194
- ): Promise<RunIntegrationOutcome> {
195
- const { logger } = options;
196
- const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
197
-
198
- const workflow = await loadDefaultWorkflow(absolutePath);
199
- const signalPaths = getPauseSignalPaths(args.session);
200
- await removeSignalIfExists(signalPaths.pausedSignalPath);
201
- await removeSignalIfExists(signalPaths.resumeSignalPath);
202
- await removeSignalIfExists(signalPaths.completedSignalPath);
203
- await removeSignalIfExists(signalPaths.failedSignalPath);
204
- const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
205
-
206
- console.log(
207
- `Running workflow "${workflow.name}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
208
- );
209
-
210
- const integrationLogger = logger.withScope("integration-run", {
211
- integrationPath: absolutePath,
212
- workflowName: workflow.name,
213
- session: args.session,
214
- });
215
-
216
- // Resolve auth profile from CLI flag (--auth-profile <domain>)
217
- const authProfileDomain = args.authProfileDomain;
218
- const normalizedAuthProfileDomain = authProfileDomain
219
- ? normalizeDomain(normalizeUrl(authProfileDomain))
220
- : undefined;
221
- const storageStatePath = normalizedAuthProfileDomain
222
- ? getProfilePath(normalizedAuthProfileDomain)
223
- : undefined;
224
- if (
225
- normalizedAuthProfileDomain &&
226
- storageStatePath &&
227
- !existsSync(storageStatePath)
228
- ) {
229
- throw new Error(
230
- getMissingLocalAuthProfileError({
231
- normalizedDomain: normalizedAuthProfileDomain,
232
- profilePath: storageStatePath,
233
- session: args.session,
234
- }),
235
- );
236
- }
237
- const browserSession = await launchBrowser({
238
- sessionName: args.session,
239
- headless: args.headless,
240
- storageStatePath,
241
- viewport: args.viewport,
242
- accessMode: args.accessMode,
243
- cdpEndpoint: args.cdpEndpoint,
244
- provider: args.provider,
245
- });
246
- if (!args.headless && args.visualize !== false) {
247
- await installHeadedWorkflowVisualization({
248
- context: browserSession.context,
249
- logger: integrationLogger,
250
- });
251
- }
252
- const actionsLogPath = getSessionActionsLogPath(args.session);
253
- const networkLogPath = getSessionNetworkLogPath(args.session);
254
- await installSessionTelemetry({
255
- context: browserSession.context,
256
- initialPage: browserSession.page,
257
- includeUserDomActions: true,
258
- logAction: (entry) => {
259
- appendFileSync(actionsLogPath, JSON.stringify(entry) + "\n");
260
- },
261
- logNetwork: (entry) => {
262
- appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
263
- },
264
- });
265
-
266
- // tsx/esbuild injects __name() wrappers when keepNames is true. Playwright
267
- // serializes callbacks via Function#toString() into the browser context which
268
- // lacks __name, causing ReferenceError. Inject a no-op polyfill into every page.
269
- await browserSession.context.addInitScript(() => {
270
- (globalThis as Record<string, unknown>).__name = (
271
- target: unknown,
272
- value: string,
273
- ) => Object.defineProperty(target as object, "name", { value, configurable: true });
274
- });
275
-
276
- const workflowContext: LibrettoWorkflowContext = {
277
- session: args.session,
278
- page: browserSession.page,
279
- };
280
-
281
- try {
282
- try {
283
- await workflow.run(workflowContext, args.params ?? {});
284
- } catch (error) {
285
- const errorMessage =
286
- error instanceof Error ? error.message : String(error);
287
- await writeFile(
288
- signalPaths.failedSignalPath,
289
- JSON.stringify(
290
- {
291
- failedAt: new Date().toISOString(),
292
- message: errorMessage,
293
- phase: "workflow",
294
- },
295
- null,
296
- 2,
297
- ),
298
- "utf8",
299
- );
300
- await waitForFailureSessionRelease({
301
- session: args.session,
302
- expectedPid: process.pid,
303
- logger,
304
- });
305
- return { status: "failed-held" };
306
- }
307
- await writeFile(
308
- signalPaths.completedSignalPath,
309
- JSON.stringify({ completedAt: new Date().toISOString() }, null, 2),
310
- "utf8",
311
- );
312
- return { status: "completed" };
313
- } finally {
314
- restoreStdout();
315
- await browserSession.close();
316
- }
317
- }
318
-
319
- export async function runIntegrationFromFileInWorker(
320
- args: RunIntegrationWorkerRequest,
321
- logger: LoggerApi,
322
- ): Promise<RunIntegrationOutcome> {
323
- return await runIntegrationInternal(args, {
324
- logger,
325
- });
326
- }
@@ -1,19 +0,0 @@
1
- import { z } from "zod";
2
- import { SessionAccessModeSchema } from "../../shared/state/index.js";
3
-
4
- export const RunIntegrationWorkerRequestSchema = z.object({
5
- integrationPath: 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
- accessMode: SessionAccessModeSchema.default("write-access"),
13
- cdpEndpoint: z.string().optional(),
14
- provider: z.object({ name: z.string(), sessionId: z.string() }).optional(),
15
- });
16
-
17
- export type RunIntegrationWorkerRequest = z.infer<
18
- typeof RunIntegrationWorkerRequestSchema
19
- >;
@@ -1,72 +0,0 @@
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();