libretto 0.1.5 → 0.2.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 (168) hide show
  1. package/README.md +215 -17
  2. package/bin/libretto.mjs +18 -0
  3. package/dist/cli/cli.js +203 -0
  4. package/dist/cli/commands/ai.js +21 -0
  5. package/dist/cli/commands/browser.js +59 -0
  6. package/dist/cli/commands/execution.js +422 -0
  7. package/dist/cli/commands/logs.js +93 -0
  8. package/dist/cli/commands/snapshot.js +106 -0
  9. package/dist/cli/core/ai-config.js +149 -0
  10. package/dist/cli/core/browser.js +523 -0
  11. package/dist/cli/core/context.js +113 -0
  12. package/dist/cli/core/pause-signals.js +29 -0
  13. package/dist/cli/core/session-telemetry.js +491 -0
  14. package/dist/cli/core/session.js +183 -0
  15. package/dist/cli/core/snapshot-analyzer.js +492 -0
  16. package/dist/cli/core/telemetry.js +362 -0
  17. package/dist/cli/index.js +13 -0
  18. package/dist/cli/workers/run-integration-runtime.js +222 -0
  19. package/dist/cli/workers/run-integration-worker-protocol.js +0 -0
  20. package/dist/cli/workers/run-integration-worker.js +83 -0
  21. package/dist/index.cjs +123 -0
  22. package/dist/index.d.cts +19 -0
  23. package/dist/index.d.ts +19 -0
  24. package/dist/index.js +107 -0
  25. package/dist/runtime/download/download.cjs +70 -0
  26. package/dist/runtime/download/download.d.cts +35 -0
  27. package/dist/runtime/download/download.d.ts +35 -0
  28. package/dist/runtime/download/download.js +45 -0
  29. package/dist/runtime/download/index.cjs +30 -0
  30. package/dist/runtime/download/index.d.cts +3 -0
  31. package/dist/runtime/download/index.d.ts +3 -0
  32. package/dist/runtime/download/index.js +8 -0
  33. package/dist/runtime/extract/extract.cjs +88 -0
  34. package/dist/runtime/extract/extract.d.cts +23 -0
  35. package/dist/runtime/extract/extract.d.ts +23 -0
  36. package/dist/runtime/extract/extract.js +64 -0
  37. package/dist/runtime/extract/index.cjs +28 -0
  38. package/dist/runtime/extract/index.d.cts +5 -0
  39. package/dist/runtime/extract/index.d.ts +5 -0
  40. package/dist/runtime/extract/index.js +4 -0
  41. package/dist/runtime/network/index.cjs +28 -0
  42. package/dist/runtime/network/index.d.cts +4 -0
  43. package/dist/runtime/network/index.d.ts +4 -0
  44. package/dist/runtime/network/index.js +6 -0
  45. package/dist/runtime/network/network.cjs +91 -0
  46. package/dist/runtime/network/network.d.cts +28 -0
  47. package/dist/runtime/network/network.d.ts +28 -0
  48. package/dist/runtime/network/network.js +67 -0
  49. package/dist/runtime/recovery/agent.cjs +223 -0
  50. package/dist/runtime/recovery/agent.d.cts +13 -0
  51. package/dist/runtime/recovery/agent.d.ts +13 -0
  52. package/dist/runtime/recovery/agent.js +199 -0
  53. package/dist/runtime/recovery/errors.cjs +124 -0
  54. package/dist/runtime/recovery/errors.d.cts +31 -0
  55. package/dist/runtime/recovery/errors.d.ts +31 -0
  56. package/dist/runtime/recovery/errors.js +100 -0
  57. package/dist/runtime/recovery/index.cjs +34 -0
  58. package/dist/runtime/recovery/index.d.cts +7 -0
  59. package/dist/runtime/recovery/index.d.ts +7 -0
  60. package/dist/runtime/recovery/index.js +10 -0
  61. package/dist/runtime/recovery/recovery.cjs +55 -0
  62. package/dist/runtime/recovery/recovery.d.cts +12 -0
  63. package/dist/runtime/recovery/recovery.d.ts +12 -0
  64. package/dist/runtime/recovery/recovery.js +31 -0
  65. package/dist/shared/config/config.cjs +44 -0
  66. package/dist/shared/config/config.d.cts +10 -0
  67. package/dist/shared/config/config.d.ts +10 -0
  68. package/dist/shared/config/config.js +18 -0
  69. package/dist/shared/config/index.cjs +32 -0
  70. package/dist/shared/config/index.d.cts +1 -0
  71. package/dist/shared/config/index.d.ts +1 -0
  72. package/dist/shared/config/index.js +10 -0
  73. package/dist/shared/debug/index.cjs +32 -0
  74. package/dist/shared/debug/index.d.cts +2 -0
  75. package/dist/shared/debug/index.d.ts +2 -0
  76. package/dist/shared/debug/index.js +10 -0
  77. package/dist/shared/debug/pause.cjs +56 -0
  78. package/dist/shared/debug/pause.d.cts +23 -0
  79. package/dist/shared/debug/pause.d.ts +23 -0
  80. package/dist/shared/debug/pause.js +30 -0
  81. package/dist/shared/instrumentation/errors.cjs +81 -0
  82. package/dist/shared/instrumentation/errors.d.cts +12 -0
  83. package/dist/shared/instrumentation/errors.d.ts +12 -0
  84. package/dist/shared/instrumentation/errors.js +57 -0
  85. package/dist/shared/instrumentation/index.cjs +35 -0
  86. package/dist/shared/instrumentation/index.d.cts +6 -0
  87. package/dist/shared/instrumentation/index.d.ts +6 -0
  88. package/dist/shared/instrumentation/index.js +12 -0
  89. package/dist/shared/instrumentation/instrument.cjs +206 -0
  90. package/dist/shared/instrumentation/instrument.d.cts +32 -0
  91. package/dist/shared/instrumentation/instrument.d.ts +32 -0
  92. package/dist/shared/instrumentation/instrument.js +190 -0
  93. package/dist/shared/llm/client.cjs +139 -0
  94. package/dist/shared/llm/client.d.cts +6 -0
  95. package/dist/shared/llm/client.d.ts +6 -0
  96. package/dist/shared/llm/client.js +115 -0
  97. package/dist/shared/llm/index.cjs +28 -0
  98. package/dist/shared/llm/index.d.cts +3 -0
  99. package/dist/shared/llm/index.d.ts +3 -0
  100. package/dist/shared/llm/index.js +4 -0
  101. package/dist/shared/llm/types.cjs +16 -0
  102. package/dist/shared/llm/types.d.cts +34 -0
  103. package/dist/shared/llm/types.d.ts +34 -0
  104. package/dist/shared/llm/types.js +0 -0
  105. package/dist/shared/logger/index.cjs +37 -0
  106. package/dist/shared/logger/index.d.cts +2 -0
  107. package/dist/shared/logger/index.d.ts +2 -0
  108. package/dist/shared/logger/index.js +13 -0
  109. package/dist/shared/logger/logger.cjs +213 -0
  110. package/dist/shared/logger/logger.d.cts +82 -0
  111. package/dist/shared/logger/logger.d.ts +82 -0
  112. package/dist/shared/logger/logger.js +188 -0
  113. package/dist/shared/logger/sinks.cjs +160 -0
  114. package/dist/shared/logger/sinks.d.cts +9 -0
  115. package/dist/shared/logger/sinks.d.ts +9 -0
  116. package/dist/shared/logger/sinks.js +124 -0
  117. package/dist/shared/paths/paths.cjs +104 -0
  118. package/dist/shared/paths/paths.d.cts +10 -0
  119. package/dist/shared/paths/paths.d.ts +10 -0
  120. package/dist/shared/paths/paths.js +73 -0
  121. package/dist/shared/run/api.cjs +35 -0
  122. package/dist/shared/run/api.d.cts +3 -0
  123. package/dist/shared/run/api.d.ts +3 -0
  124. package/dist/shared/run/api.js +12 -0
  125. package/dist/shared/run/browser.cjs +98 -0
  126. package/dist/shared/run/browser.d.cts +22 -0
  127. package/dist/shared/run/browser.d.ts +22 -0
  128. package/dist/shared/run/browser.js +74 -0
  129. package/dist/shared/state/index.cjs +38 -0
  130. package/dist/shared/state/index.d.cts +2 -0
  131. package/dist/shared/state/index.d.ts +2 -0
  132. package/dist/shared/state/index.js +16 -0
  133. package/dist/shared/state/session-state.cjs +85 -0
  134. package/dist/shared/state/session-state.d.cts +34 -0
  135. package/dist/shared/state/session-state.d.ts +34 -0
  136. package/dist/shared/state/session-state.js +56 -0
  137. package/dist/shared/visualization/ghost-cursor.cjs +174 -0
  138. package/dist/shared/visualization/ghost-cursor.d.cts +37 -0
  139. package/dist/shared/visualization/ghost-cursor.d.ts +37 -0
  140. package/dist/shared/visualization/ghost-cursor.js +145 -0
  141. package/dist/shared/visualization/highlight.cjs +134 -0
  142. package/dist/shared/visualization/highlight.d.cts +22 -0
  143. package/dist/shared/visualization/highlight.d.ts +22 -0
  144. package/dist/shared/visualization/highlight.js +108 -0
  145. package/dist/shared/visualization/index.cjs +45 -0
  146. package/dist/shared/visualization/index.d.cts +3 -0
  147. package/dist/shared/visualization/index.d.ts +3 -0
  148. package/dist/shared/visualization/index.js +24 -0
  149. package/dist/shared/workflow/workflow.cjs +47 -0
  150. package/dist/shared/workflow/workflow.d.cts +33 -0
  151. package/dist/shared/workflow/workflow.d.ts +33 -0
  152. package/dist/shared/workflow/workflow.js +21 -0
  153. package/package.json +125 -26
  154. package/.npmignore +0 -2
  155. package/bin/libretto +0 -31
  156. package/lib/connect.js +0 -34
  157. package/lib/export.js +0 -224
  158. package/lib/import.js +0 -166
  159. package/lib/index.js +0 -8
  160. package/lib/log.js +0 -9
  161. package/lib/validate.js +0 -20
  162. package/makefile +0 -8
  163. package/src/connect.coffee +0 -25
  164. package/src/export.coffee +0 -222
  165. package/src/import.coffee +0 -166
  166. package/src/index.coffee +0 -3
  167. package/src/log.coffee +0 -3
  168. package/src/validate.coffee +0 -10
@@ -0,0 +1,422 @@
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { fork } from "node:child_process";
3
+ import * as moduleBuiltin from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ import { installInstrumentation } from "../../shared/instrumentation/index.js";
6
+ import {
7
+ connect,
8
+ disconnectBrowser
9
+ } from "../core/browser.js";
10
+ import { getPauseSignalPaths } from "../core/pause-signals.js";
11
+ import {
12
+ assertSessionAvailableForStart,
13
+ readSessionStateOrThrow,
14
+ setSessionStatus
15
+ } from "../core/session.js";
16
+ import {
17
+ readActionLog,
18
+ readNetworkLog,
19
+ wrapPageForActionLogging
20
+ } from "../core/telemetry.js";
21
+ const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
22
+ function withSuppressedStripTypeScriptWarning(action) {
23
+ const mutableProcess = process;
24
+ const originalEmitWarning = mutableProcess.emitWarning;
25
+ mutableProcess.emitWarning = (...args) => {
26
+ const warning = args[0];
27
+ const typeOrOptions = args[1];
28
+ const warningMessage = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
29
+ const warningType = typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type ?? "" : "";
30
+ if (warningType === "ExperimentalWarning" && warningMessage.includes("stripTypeScriptTypes")) {
31
+ return;
32
+ }
33
+ originalEmitWarning(...args);
34
+ };
35
+ try {
36
+ return action();
37
+ } finally {
38
+ mutableProcess.emitWarning = originalEmitWarning;
39
+ }
40
+ }
41
+ function compileTypeScriptExecFunction(code, helperNames) {
42
+ if (!stripTypeScriptTypes) return null;
43
+ const wrappedSource = `(async function __librettoExec(${helperNames.join(", ")}) {
44
+ ${code}
45
+ })`;
46
+ const jsSource = withSuppressedStripTypeScriptWarning(
47
+ () => stripTypeScriptTypes(wrappedSource, { mode: "strip" })
48
+ );
49
+ const createFunction = new Function(
50
+ `return ${jsSource}`
51
+ );
52
+ return createFunction();
53
+ }
54
+ function compileExecFunction(code, helperNames) {
55
+ const typeStripped = compileTypeScriptExecFunction(code, helperNames);
56
+ if (typeStripped) return typeStripped;
57
+ const AsyncFunction = Object.getPrototypeOf(async function() {
58
+ }).constructor;
59
+ return new AsyncFunction(...helperNames, code);
60
+ }
61
+ async function runExec(code, session, logger, visualize = false, pageId) {
62
+ readSessionStateOrThrow(session);
63
+ logger.info("exec-start", {
64
+ session,
65
+ codeLength: code.length,
66
+ codePreview: code.slice(0, 200),
67
+ visualize,
68
+ pageId
69
+ });
70
+ const { browser, context, page, pageId: resolvedPageId } = await connect(
71
+ session,
72
+ logger,
73
+ 1e4,
74
+ {
75
+ pageId,
76
+ requireSinglePage: true
77
+ }
78
+ );
79
+ const STALL_THRESHOLD_MS = 6e4;
80
+ let lastActivityTs = Date.now();
81
+ const onActivity = () => {
82
+ lastActivityTs = Date.now();
83
+ };
84
+ const stallInterval = setInterval(() => {
85
+ const silenceMs = Date.now() - lastActivityTs;
86
+ if (silenceMs >= STALL_THRESHOLD_MS) {
87
+ logger.warn("exec-stall-warning", {
88
+ session,
89
+ silenceMs,
90
+ codePreview: code.slice(0, 200)
91
+ });
92
+ console.warn(
93
+ `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${code.slice(0, 100)}...)`
94
+ );
95
+ }
96
+ }, STALL_THRESHOLD_MS);
97
+ const execStartTs = Date.now();
98
+ const sigintHandler = () => {
99
+ logger.info("exec-interrupted", {
100
+ session,
101
+ duration: Date.now() - execStartTs,
102
+ codePreview: code.slice(0, 200)
103
+ });
104
+ };
105
+ process.on("SIGINT", sigintHandler);
106
+ wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
107
+ if (visualize) {
108
+ await installInstrumentation(page, { visualize: true, logger });
109
+ }
110
+ try {
111
+ const execState = {};
112
+ const networkLog = (opts = {}) => {
113
+ return readNetworkLog(session, opts);
114
+ };
115
+ const actionLog = (opts = {}) => {
116
+ return readActionLog(session, opts);
117
+ };
118
+ const helpers = {
119
+ page,
120
+ context,
121
+ state: execState,
122
+ browser,
123
+ networkLog,
124
+ actionLog,
125
+ console,
126
+ setTimeout,
127
+ setInterval,
128
+ clearTimeout,
129
+ clearInterval,
130
+ fetch,
131
+ URL,
132
+ Buffer
133
+ };
134
+ const helperNames = Object.keys(helpers);
135
+ const fn = compileExecFunction(code, helperNames);
136
+ const result = await fn(...Object.values(helpers));
137
+ logger.info("exec-success", { session, hasResult: result !== void 0 });
138
+ if (result !== void 0) {
139
+ console.log(
140
+ typeof result === "string" ? result : JSON.stringify(result, null, 2)
141
+ );
142
+ }
143
+ } catch (err) {
144
+ logger.error("exec-error", {
145
+ error: err,
146
+ session,
147
+ codePreview: code.slice(0, 200)
148
+ });
149
+ throw err;
150
+ } finally {
151
+ clearInterval(stallInterval);
152
+ process.removeListener("SIGINT", sigintHandler);
153
+ disconnectBrowser(browser, logger, session);
154
+ }
155
+ }
156
+ function parseJsonArg(label, raw) {
157
+ try {
158
+ return JSON.parse(raw);
159
+ } catch (error) {
160
+ throw new Error(
161
+ `Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`
162
+ );
163
+ }
164
+ }
165
+ function isProcessRunning(pid) {
166
+ try {
167
+ process.kill(pid, 0);
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ function readJsonFileIfExists(path) {
174
+ if (!existsSync(path)) return null;
175
+ try {
176
+ return JSON.parse(readFileSync(path, "utf8"));
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function readFailureMessage(path) {
182
+ const raw = readJsonFileIfExists(path);
183
+ if (!raw || typeof raw !== "object") return null;
184
+ const message = raw.message;
185
+ return typeof message === "string" ? message : null;
186
+ }
187
+ async function waitForFailureMessage(path, timeoutMs = 1e3) {
188
+ const deadline = Date.now() + timeoutMs;
189
+ while (Date.now() < deadline) {
190
+ const message = readFailureMessage(path);
191
+ if (message) return message;
192
+ await new Promise((resolveWait) => setTimeout(resolveWait, 25));
193
+ }
194
+ return readFailureMessage(path);
195
+ }
196
+ function streamOutputSince(path, offset) {
197
+ if (!existsSync(path)) return offset;
198
+ const output = readFileSync(path);
199
+ if (output.length <= offset) return output.length;
200
+ process.stdout.write(output.subarray(offset));
201
+ return output.length;
202
+ }
203
+ function clearSignalIfExists(path) {
204
+ if (!existsSync(path)) return;
205
+ try {
206
+ unlinkSync(path);
207
+ } catch {
208
+ }
209
+ }
210
+ async function waitForWorkflowOutcome(args) {
211
+ const signalPaths = getPauseSignalPaths(args.session);
212
+ if (args.pid <= 0) {
213
+ return { status: "exited" };
214
+ }
215
+ let outputOffset = 0;
216
+ while (true) {
217
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
218
+ if (existsSync(signalPaths.failedSignalPath)) {
219
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
220
+ const message = await waitForFailureMessage(signalPaths.failedSignalPath);
221
+ return { status: "failed", message: message ?? void 0 };
222
+ }
223
+ if (existsSync(signalPaths.completedSignalPath)) {
224
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
225
+ return { status: "completed" };
226
+ }
227
+ if (existsSync(signalPaths.pausedSignalPath)) {
228
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
229
+ return { status: "paused" };
230
+ }
231
+ if (!isProcessRunning(args.pid)) {
232
+ outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
233
+ return { status: "exited" };
234
+ }
235
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
236
+ }
237
+ }
238
+ async function runResume(session, logger) {
239
+ const state = readSessionStateOrThrow(session);
240
+ const {
241
+ pausedSignalPath,
242
+ resumeSignalPath,
243
+ completedSignalPath,
244
+ failedSignalPath,
245
+ outputSignalPath
246
+ } = getPauseSignalPaths(session);
247
+ if (!existsSync(pausedSignalPath)) {
248
+ throw new Error(
249
+ `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call ctx.pause() first.`
250
+ );
251
+ }
252
+ if (!isProcessRunning(state.pid)) {
253
+ throw new Error(
254
+ `No active paused workflow found for session "${session}" (worker pid ${state.pid} is not running).`
255
+ );
256
+ }
257
+ clearSignalIfExists(pausedSignalPath);
258
+ clearSignalIfExists(outputSignalPath);
259
+ clearSignalIfExists(completedSignalPath);
260
+ clearSignalIfExists(failedSignalPath);
261
+ setSessionStatus(session, "active", logger);
262
+ writeFileSync(
263
+ resumeSignalPath,
264
+ JSON.stringify(
265
+ {
266
+ resumedAt: (/* @__PURE__ */ new Date()).toISOString(),
267
+ sourcePid: process.pid
268
+ },
269
+ null,
270
+ 2
271
+ ),
272
+ "utf8"
273
+ );
274
+ console.log(`Resume signal sent for session "${session}".`);
275
+ const outcome = await waitForWorkflowOutcome({
276
+ session,
277
+ pid: state.pid
278
+ });
279
+ if (outcome.status === "completed") {
280
+ setSessionStatus(session, "completed", logger);
281
+ console.log("Integration completed.");
282
+ return;
283
+ }
284
+ if (outcome.status === "failed") {
285
+ setSessionStatus(session, "failed", logger);
286
+ throw new Error(
287
+ outcome.message ? `Workflow failed after resume: ${outcome.message}` : "Workflow failed after resume."
288
+ );
289
+ }
290
+ if (outcome.status === "exited") {
291
+ setSessionStatus(session, "exited", logger);
292
+ throw new Error(
293
+ `Workflow process for session "${session}" exited before reporting completion or pause.`
294
+ );
295
+ }
296
+ setSessionStatus(session, "paused", logger);
297
+ console.log("Workflow paused.");
298
+ }
299
+ async function runIntegrationFromFile(args, logger) {
300
+ assertSessionAvailableForStart(args.session, logger);
301
+ const signalPaths = getPauseSignalPaths(args.session);
302
+ clearSignalIfExists(signalPaths.pausedSignalPath);
303
+ clearSignalIfExists(signalPaths.resumeSignalPath);
304
+ clearSignalIfExists(signalPaths.completedSignalPath);
305
+ clearSignalIfExists(signalPaths.failedSignalPath);
306
+ clearSignalIfExists(signalPaths.outputSignalPath);
307
+ const workerEntryPath = fileURLToPath(
308
+ new URL("../workers/run-integration-worker.js", import.meta.url)
309
+ );
310
+ const payload = JSON.stringify(args);
311
+ const worker = fork(workerEntryPath, [payload], {
312
+ detached: true,
313
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
314
+ env: process.env
315
+ });
316
+ worker.disconnect();
317
+ worker.unref();
318
+ const outcome = await waitForWorkflowOutcome({
319
+ session: args.session,
320
+ pid: worker.pid ?? 0
321
+ });
322
+ if (outcome.status === "paused") {
323
+ setSessionStatus(args.session, "paused", logger);
324
+ console.log("Workflow paused.");
325
+ return;
326
+ }
327
+ if (outcome.status === "failed") {
328
+ setSessionStatus(args.session, "failed", logger);
329
+ throw new Error(outcome.message ?? "Workflow failed during run.");
330
+ }
331
+ if (outcome.status === "exited") {
332
+ setSessionStatus(args.session, "exited", logger);
333
+ throw new Error(
334
+ "Workflow process exited before reporting completion or pause during run."
335
+ );
336
+ }
337
+ setSessionStatus(args.session, "completed", logger);
338
+ }
339
+ function registerExecutionCommands(yargs, logger) {
340
+ return yargs.command(
341
+ "exec [code..]",
342
+ "Execute Playwright TypeScript code",
343
+ (cmd) => cmd.option("visualize", { type: "boolean", default: false }).option("page", { type: "string" }),
344
+ async (argv) => {
345
+ const codeParts = Array.isArray(argv.code) ? argv.code : argv.code ? [String(argv.code)] : [];
346
+ const code = codeParts.join(" ");
347
+ if (!code) {
348
+ throw new Error(
349
+ "Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
350
+ );
351
+ }
352
+ await runExec(
353
+ code,
354
+ String(argv.session),
355
+ logger,
356
+ Boolean(argv.visualize),
357
+ argv.page ? String(argv.page) : void 0
358
+ );
359
+ }
360
+ ).command(
361
+ "run [integrationFile] [integrationExport]",
362
+ "Run an exported Libretto workflow from a file",
363
+ (cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("debug", { type: "boolean" }),
364
+ async (argv) => {
365
+ const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless] [--debug]";
366
+ const integrationPath = argv.integrationFile;
367
+ const exportName = argv.integrationExport;
368
+ if (!integrationPath || !exportName) {
369
+ throw new Error(usage);
370
+ }
371
+ const session = String(argv.session);
372
+ const rawInlineParams = argv.params;
373
+ const paramsFile = argv["params-file"];
374
+ if (rawInlineParams && paramsFile) {
375
+ throw new Error("Pass either --params or --params-file, not both.");
376
+ }
377
+ const params = (() => {
378
+ if (paramsFile) {
379
+ let content;
380
+ try {
381
+ content = readFileSync(paramsFile, "utf8");
382
+ } catch {
383
+ throw new Error(
384
+ `Could not read --params-file "${paramsFile}". Ensure the file exists and is readable.`
385
+ );
386
+ }
387
+ return parseJsonArg("--params-file", content);
388
+ }
389
+ if (rawInlineParams) {
390
+ return parseJsonArg("--params", rawInlineParams);
391
+ }
392
+ return {};
393
+ })();
394
+ const hasHeadedFlag = Boolean(argv.headed);
395
+ const hasHeadlessFlag = Boolean(argv.headless);
396
+ if (hasHeadedFlag && hasHeadlessFlag) {
397
+ throw new Error("Cannot pass both --headed and --headless.");
398
+ }
399
+ const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
400
+ const debugFlag = argv.debug;
401
+ const debugMode = debugFlag !== void 0 ? debugFlag : process.env.LIBRETTO_DEBUG === "true";
402
+ await runIntegrationFromFile({
403
+ integrationPath,
404
+ exportName,
405
+ session,
406
+ params,
407
+ headless: headlessMode ?? false,
408
+ debug: debugMode
409
+ }, logger);
410
+ }
411
+ ).command(
412
+ "resume",
413
+ "Resume a paused workflow for the current session",
414
+ (cmd) => cmd,
415
+ async (argv) => {
416
+ await runResume(String(argv.session), logger);
417
+ }
418
+ );
419
+ }
420
+ export {
421
+ registerExecutionCommands
422
+ };
@@ -0,0 +1,93 @@
1
+ import { listOpenPages } from "../core/browser.js";
2
+ import { withSessionLogger } from "../core/context.js";
3
+ import {
4
+ clearActionLog,
5
+ clearNetworkLog,
6
+ formatActionEntry,
7
+ formatNetworkEntry,
8
+ readActionLog,
9
+ readNetworkLog
10
+ } from "../core/telemetry.js";
11
+ async function resolvePageId(session, pageId) {
12
+ if (!pageId) return void 0;
13
+ const pages = await withSessionLogger(
14
+ session,
15
+ async (logger) => listOpenPages(session, logger)
16
+ );
17
+ const foundPage = pages.find((page) => page.id === pageId);
18
+ if (!foundPage) {
19
+ throw new Error(
20
+ `Page "${pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
21
+ );
22
+ }
23
+ return pageId;
24
+ }
25
+ function registerLogCommands(yargs) {
26
+ return yargs.command(
27
+ "network",
28
+ "View captured network requests",
29
+ (cmd) => cmd.option("last", { type: "number" }).option("filter", { type: "string" }).option("method", { type: "string" }).option("page", { type: "string" }).option("clear", { type: "boolean", default: false }),
30
+ async (argv) => {
31
+ const session = String(argv.session);
32
+ if (argv.clear) {
33
+ clearNetworkLog(session);
34
+ console.log("Network log cleared.");
35
+ return;
36
+ }
37
+ const pageId = await resolvePageId(
38
+ session,
39
+ argv.page ? String(argv.page) : void 0
40
+ );
41
+ const entries = readNetworkLog(session, {
42
+ last: typeof argv.last === "number" ? argv.last : void 0,
43
+ filter: argv.filter,
44
+ method: argv.method,
45
+ pageId
46
+ });
47
+ if (entries.length === 0) {
48
+ console.log("No network requests captured.");
49
+ return;
50
+ }
51
+ for (const entry of entries) {
52
+ console.log(formatNetworkEntry(entry));
53
+ }
54
+ console.log(`
55
+ ${entries.length} request(s) shown.`);
56
+ }
57
+ ).command(
58
+ "actions",
59
+ "View captured actions",
60
+ (cmd) => cmd.option("last", { type: "number" }).option("filter", { type: "string" }).option("action", { type: "string" }).option("source", { type: "string" }).option("page", { type: "string" }).option("clear", { type: "boolean", default: false }),
61
+ async (argv) => {
62
+ const session = String(argv.session);
63
+ if (argv.clear) {
64
+ clearActionLog(session);
65
+ console.log("Action log cleared.");
66
+ return;
67
+ }
68
+ const pageId = await resolvePageId(
69
+ session,
70
+ argv.page ? String(argv.page) : void 0
71
+ );
72
+ const entries = readActionLog(session, {
73
+ last: typeof argv.last === "number" ? argv.last : void 0,
74
+ filter: argv.filter,
75
+ action: argv.action,
76
+ source: argv.source,
77
+ pageId
78
+ });
79
+ if (entries.length === 0) {
80
+ console.log("No actions captured.");
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ console.log(formatActionEntry(entry));
85
+ }
86
+ console.log(`
87
+ ${entries.length} action(s) shown.`);
88
+ }
89
+ );
90
+ }
91
+ export {
92
+ registerLogCommands
93
+ };
@@ -0,0 +1,106 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { connect, disconnectBrowser } from "../core/browser.js";
3
+ import { getSessionSnapshotRunDir } from "../core/context.js";
4
+ import {
5
+ canAnalyzeSnapshots,
6
+ runInterpret
7
+ } from "../core/snapshot-analyzer.js";
8
+ const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
9
+ function generateSnapshotRunId() {
10
+ return `snapshot-${Date.now()}`;
11
+ }
12
+ async function captureScreenshot(session, logger, pageId) {
13
+ logger.info("screenshot-start", { session, pageId });
14
+ const snapshotRunId = generateSnapshotRunId();
15
+ const snapshotRunDir = getSessionSnapshotRunDir(session, snapshotRunId);
16
+ mkdirSync(snapshotRunDir, { recursive: true });
17
+ const { browser, page } = await connect(session, logger, 1e4, {
18
+ pageId,
19
+ requireSinglePage: true
20
+ });
21
+ try {
22
+ const title = await page.title();
23
+ const pageUrl = page.url();
24
+ const pngPath = `${snapshotRunDir}/page.png`;
25
+ const htmlPath = `${snapshotRunDir}/page.html`;
26
+ await page.screenshot({ path: pngPath });
27
+ const htmlContent = await page.content();
28
+ const fs = await import("node:fs/promises");
29
+ await fs.writeFile(htmlPath, htmlContent);
30
+ logger.info("screenshot-success", {
31
+ session,
32
+ pageUrl,
33
+ title,
34
+ pngPath,
35
+ htmlPath,
36
+ snapshotRunId
37
+ });
38
+ return { pngPath, htmlPath, baseName: snapshotRunId };
39
+ } catch (err) {
40
+ let pageAlive = false;
41
+ let browserConnected = false;
42
+ try {
43
+ browserConnected = browser.isConnected();
44
+ pageAlive = !page.isClosed();
45
+ } catch {
46
+ }
47
+ logger.error("screenshot-error", {
48
+ error: err,
49
+ session,
50
+ pageAlive,
51
+ browserConnected,
52
+ pageUrl: page.url()
53
+ });
54
+ throw err;
55
+ } finally {
56
+ disconnectBrowser(browser, logger, session);
57
+ }
58
+ }
59
+ async function runSnapshot(session, logger, pageId, objective, context) {
60
+ const { pngPath, htmlPath } = await captureScreenshot(session, logger, pageId);
61
+ console.log("Screenshot saved:");
62
+ console.log(` PNG: ${pngPath}`);
63
+ console.log(` HTML: ${htmlPath}`);
64
+ const normalizedObjective = objective?.trim();
65
+ const normalizedContext = context?.trim();
66
+ if (!normalizedObjective && !normalizedContext) {
67
+ console.log("Use --objective flag to analyze snapshots.");
68
+ return;
69
+ }
70
+ if (!normalizedObjective) {
71
+ throw new Error(
72
+ "Couldn't run analysis: --objective is required when providing --context."
73
+ );
74
+ }
75
+ if (!canAnalyzeSnapshots()) {
76
+ throw new Error(
77
+ "Couldn't run analysis: no AI config set. Run 'libretto-cli ai configure codex' (or claude/gemini) to enable analysis."
78
+ );
79
+ }
80
+ await runInterpret({
81
+ objective: normalizedObjective,
82
+ session,
83
+ context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
84
+ pngPath,
85
+ htmlPath
86
+ }, logger);
87
+ }
88
+ function registerSnapshotCommands(yargs, logger) {
89
+ return yargs.command(
90
+ "snapshot",
91
+ "Capture PNG + HTML; analyze when --objective is provided (--context optional)",
92
+ (cmd) => cmd.option("page", { type: "string" }).option("objective", { type: "string" }).option("context", { type: "string" }),
93
+ async (argv) => {
94
+ await runSnapshot(
95
+ String(argv.session),
96
+ logger,
97
+ argv.page ? String(argv.page) : void 0,
98
+ argv.objective,
99
+ argv.context
100
+ );
101
+ }
102
+ );
103
+ }
104
+ export {
105
+ registerSnapshotCommands
106
+ };