libretto 0.1.4 → 0.2.0

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