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
@@ -6,8 +6,10 @@ import { z } from "zod";
6
6
  import { installInstrumentation } from "../../shared/instrumentation/index.js";
7
7
  import {
8
8
  connect,
9
- disconnectBrowser
9
+ disconnectBrowser,
10
+ resolveViewport
10
11
  } from "../core/browser.js";
12
+ import { parseViewportArg } from "./browser.js";
11
13
  import { getPauseSignalPaths } from "../core/pause-signals.js";
12
14
  import {
13
15
  assertSessionAvailableForStart,
@@ -22,10 +24,10 @@ import {
22
24
  } from "../core/telemetry.js";
23
25
  import { SimpleCLI } from "../framework/simple-cli.js";
24
26
  import {
25
- loadSessionStateMiddleware,
26
27
  pageOption,
27
- resolveSessionMiddleware,
28
- sessionOption
28
+ sessionOption,
29
+ withAutoSession,
30
+ withRequiredSession
29
31
  } from "./shared.js";
30
32
  const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
31
33
  const require2 = moduleBuiltin.createRequire(import.meta.url);
@@ -69,23 +71,91 @@ function compileExecFunction(code, helperNames) {
69
71
  }).constructor;
70
72
  return new AsyncFunction(...helperNames, code);
71
73
  }
74
+ function stripEmptyCatchHandlers(code) {
75
+ const catchRe = /\??\s*\.catch\(\s*\(\)\s*=>\s*\{\s*\}\s*\)/g;
76
+ let strippedCount = 0;
77
+ let result = "";
78
+ let i = 0;
79
+ while (i < code.length) {
80
+ if (code[i] === "/" && code[i + 1] === "/") {
81
+ const end = code.indexOf("\n", i);
82
+ const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
83
+ result += slice;
84
+ i += slice.length;
85
+ continue;
86
+ }
87
+ if (code[i] === "/" && code[i + 1] === "*") {
88
+ const end = code.indexOf("*/", i + 2);
89
+ const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
90
+ result += slice;
91
+ i += slice.length;
92
+ continue;
93
+ }
94
+ if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
95
+ const quote = code[i];
96
+ let j = i + 1;
97
+ while (j < code.length) {
98
+ if (code[j] === "\\" && quote !== "`") {
99
+ j += 2;
100
+ continue;
101
+ }
102
+ if (code[j] === "\\" && quote === "`") {
103
+ j += 2;
104
+ continue;
105
+ }
106
+ if (code[j] === quote) {
107
+ j++;
108
+ break;
109
+ }
110
+ if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
111
+ let depth = 1;
112
+ j += 2;
113
+ while (j < code.length && depth > 0) {
114
+ if (code[j] === "{") depth++;
115
+ else if (code[j] === "}") depth--;
116
+ j++;
117
+ }
118
+ continue;
119
+ }
120
+ j++;
121
+ }
122
+ result += code.slice(i, j);
123
+ i = j;
124
+ continue;
125
+ }
126
+ catchRe.lastIndex = i;
127
+ const match = catchRe.exec(code);
128
+ if (match && match.index === i) {
129
+ strippedCount++;
130
+ i += match[0].length;
131
+ continue;
132
+ }
133
+ result += code[i];
134
+ i++;
135
+ }
136
+ return { cleaned: result, strippedCount };
137
+ }
72
138
  async function runExec(code, session, logger, visualize = false, pageId) {
139
+ const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
140
+ if (strippedCount > 0) {
141
+ console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
142
+ }
73
143
  logger.info("exec-start", {
74
144
  session,
75
- codeLength: code.length,
76
- codePreview: code.slice(0, 200),
145
+ codeLength: cleanedCode.length,
146
+ codePreview: cleanedCode.slice(0, 200),
77
147
  visualize,
78
148
  pageId
79
149
  });
80
- const { browser, context, page, pageId: resolvedPageId } = await connect(
81
- session,
82
- logger,
83
- 1e4,
84
- {
85
- pageId,
86
- requireSinglePage: true
87
- }
88
- );
150
+ const {
151
+ browser,
152
+ context,
153
+ page,
154
+ pageId: resolvedPageId
155
+ } = await connect(session, logger, 1e4, {
156
+ pageId,
157
+ requireSinglePage: true
158
+ });
89
159
  const STALL_THRESHOLD_MS = 6e4;
90
160
  let lastActivityTs = Date.now();
91
161
  const onActivity = () => {
@@ -97,10 +167,10 @@ async function runExec(code, session, logger, visualize = false, pageId) {
97
167
  logger.warn("exec-stall-warning", {
98
168
  session,
99
169
  silenceMs,
100
- codePreview: code.slice(0, 200)
170
+ codePreview: cleanedCode.slice(0, 200)
101
171
  });
102
172
  console.warn(
103
- `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${code.slice(0, 100)}...)`
173
+ `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`
104
174
  );
105
175
  }
106
176
  }, STALL_THRESHOLD_MS);
@@ -109,7 +179,7 @@ async function runExec(code, session, logger, visualize = false, pageId) {
109
179
  logger.info("exec-interrupted", {
110
180
  session,
111
181
  duration: Date.now() - execStartTs,
112
- codePreview: code.slice(0, 200)
182
+ codePreview: cleanedCode.slice(0, 200)
113
183
  });
114
184
  };
115
185
  process.on("SIGINT", sigintHandler);
@@ -142,19 +212,21 @@ async function runExec(code, session, logger, visualize = false, pageId) {
142
212
  Buffer
143
213
  };
144
214
  const helperNames = Object.keys(helpers);
145
- const fn = compileExecFunction(code, helperNames);
215
+ const fn = compileExecFunction(cleanedCode, helperNames);
146
216
  const result = await fn(...Object.values(helpers));
147
217
  logger.info("exec-success", { session, hasResult: result !== void 0 });
148
218
  if (result !== void 0) {
149
219
  console.log(
150
220
  typeof result === "string" ? result : JSON.stringify(result, null, 2)
151
221
  );
222
+ } else {
223
+ console.log("Executed successfully");
152
224
  }
153
225
  } catch (err) {
154
226
  logger.error("exec-error", {
155
227
  error: err,
156
228
  session,
157
- codePreview: code.slice(0, 200)
229
+ codePreview: cleanedCode.slice(0, 200)
158
230
  });
159
231
  throw err;
160
232
  } finally {
@@ -191,6 +263,7 @@ async function stopExistingFailedRunSession(session, logger) {
191
263
  port: existingState.port
192
264
  });
193
265
  clearSessionState(session, logger);
266
+ if (existingState.pid == null) return;
194
267
  const stopDeadline = Date.now() + 3e3;
195
268
  while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
196
269
  await new Promise((resolveWait) => setTimeout(resolveWait, 100));
@@ -257,10 +330,18 @@ async function waitForWorkflowOutcome(args) {
257
330
  }
258
331
  let outputOffset = 0;
259
332
  while (true) {
260
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
333
+ outputOffset = streamOutputSince(
334
+ signalPaths.outputSignalPath,
335
+ outputOffset
336
+ );
261
337
  if (existsSync(signalPaths.failedSignalPath)) {
262
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
263
- const failureDetails = await waitForFailureDetails(signalPaths.failedSignalPath);
338
+ outputOffset = streamOutputSince(
339
+ signalPaths.outputSignalPath,
340
+ outputOffset
341
+ );
342
+ const failureDetails = await waitForFailureDetails(
343
+ signalPaths.failedSignalPath
344
+ );
264
345
  return {
265
346
  status: "failed",
266
347
  message: failureDetails?.message,
@@ -268,15 +349,24 @@ async function waitForWorkflowOutcome(args) {
268
349
  };
269
350
  }
270
351
  if (existsSync(signalPaths.completedSignalPath)) {
271
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
352
+ outputOffset = streamOutputSince(
353
+ signalPaths.outputSignalPath,
354
+ outputOffset
355
+ );
272
356
  return { status: "completed" };
273
357
  }
274
358
  if (existsSync(signalPaths.pausedSignalPath)) {
275
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
359
+ outputOffset = streamOutputSince(
360
+ signalPaths.outputSignalPath,
361
+ outputOffset
362
+ );
276
363
  return { status: "paused" };
277
364
  }
278
365
  if (!isProcessRunning(args.pid)) {
279
- outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
366
+ outputOffset = streamOutputSince(
367
+ signalPaths.outputSignalPath,
368
+ outputOffset
369
+ );
280
370
  return { status: "exited" };
281
371
  }
282
372
  await new Promise((resolveWait) => setTimeout(resolveWait, 250));
@@ -292,12 +382,12 @@ async function runResume(session, logger, sessionState) {
292
382
  } = getPauseSignalPaths(session);
293
383
  if (!existsSync(pausedSignalPath)) {
294
384
  throw new Error(
295
- `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause("${session}") first.`
385
+ `Session "${session}" is not paused. Run "libretto run ... --session ${session}" and call pause("${session}") first.`
296
386
  );
297
387
  }
298
- if (!isProcessRunning(sessionState.pid)) {
388
+ if (sessionState.pid == null || !isProcessRunning(sessionState.pid)) {
299
389
  throw new Error(
300
- `No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`
390
+ `No active paused workflow found for session "${session}" (worker pid ${sessionState.pid ?? "unknown"} is not running).`
301
391
  );
302
392
  }
303
393
  clearSignalIfExists(pausedSignalPath);
@@ -359,18 +449,24 @@ async function runIntegrationFromFile(args, logger) {
359
449
  session: args.session,
360
450
  params: args.params,
361
451
  headless: args.headless,
362
- authProfileDomain: args.authProfileDomain
363
- });
364
- const worker = spawn(process.execPath, [
365
- tsxCliPath,
366
- ...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
367
- workerEntryPath,
368
- payload
369
- ], {
370
- detached: true,
371
- stdio: "ignore",
372
- env: process.env
452
+ visualize: args.visualize,
453
+ authProfileDomain: args.authProfileDomain,
454
+ viewport: args.viewport
373
455
  });
456
+ const worker = spawn(
457
+ process.execPath,
458
+ [
459
+ tsxCliPath,
460
+ ...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
461
+ workerEntryPath,
462
+ payload
463
+ ],
464
+ {
465
+ detached: true,
466
+ stdio: "ignore",
467
+ env: process.env
468
+ }
469
+ );
374
470
  worker.unref();
375
471
  const outcome = await waitForWorkflowOutcome({
376
472
  session: args.session,
@@ -409,27 +505,27 @@ const execInput = SimpleCLI.input({
409
505
  ],
410
506
  named: {
411
507
  session: sessionOption(),
412
- visualize: SimpleCLI.flag({ help: "Enable ghost cursor + highlight visualization" }),
508
+ visualize: SimpleCLI.flag({
509
+ help: "Enable ghost cursor + highlight visualization"
510
+ }),
413
511
  page: pageOption()
414
512
  }
415
513
  }).refine(
416
514
  (input) => input.codeParts.length > 0,
417
- "Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
515
+ `Usage: libretto exec <code> [--session <name>] [--visualize]`
418
516
  );
419
- function createExecCommand(logger) {
420
- return SimpleCLI.command({
421
- description: "Execute Playwright TypeScript code"
422
- }).input(execInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
423
- await runExec(
424
- input.codeParts.join(" "),
425
- ctx.session,
426
- logger,
427
- input.visualize,
428
- input.page
429
- );
430
- });
431
- }
432
- const runUsage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
517
+ const execCommand = SimpleCLI.command({
518
+ description: "Execute Playwright TypeScript code"
519
+ }).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
520
+ await runExec(
521
+ input.codeParts.join(" "),
522
+ ctx.session,
523
+ ctx.logger,
524
+ input.visualize,
525
+ input.page
526
+ );
527
+ });
528
+ const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
433
529
  const runInput = SimpleCLI.input({
434
530
  positionals: [
435
531
  SimpleCLI.positional("integrationFile", z.string().optional(), {
@@ -453,15 +549,28 @@ const runInput = SimpleCLI.input({
453
549
  }),
454
550
  headed: SimpleCLI.flag({ help: "Run in headed mode" }),
455
551
  headless: SimpleCLI.flag({ help: "Run in headless mode" }),
552
+ noVisualize: SimpleCLI.flag({
553
+ name: "no-visualize",
554
+ help: "Disable ghost cursor + highlight visualization in headed mode"
555
+ }),
456
556
  authProfile: SimpleCLI.option(z.string().optional(), {
457
557
  name: "auth-profile",
458
558
  help: "Domain for local auth profile (e.g. apps.example.com)"
559
+ }),
560
+ viewport: SimpleCLI.option(z.string().optional(), {
561
+ help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
459
562
  })
460
563
  }
461
564
  }).refine(
462
565
  (input) => Boolean(input.integrationFile && input.integrationExport),
463
566
  runUsage
464
- ).refine((input) => !(input.params && input.paramsFile), "Pass either --params or --params-file, not both.").refine((input) => !(input.headed && input.headless), "Cannot pass both --headed and --headless.");
567
+ ).refine(
568
+ (input) => !(input.params && input.paramsFile),
569
+ "Pass either --params or --params-file, not both."
570
+ ).refine(
571
+ (input) => !(input.headed && input.headless),
572
+ "Cannot pass both --headed and --headless."
573
+ );
465
574
  function resolveRunParams(rawInlineParams, paramsFile) {
466
575
  if (paramsFile) {
467
576
  let content;
@@ -479,51 +588,55 @@ function resolveRunParams(rawInlineParams, paramsFile) {
479
588
  }
480
589
  return {};
481
590
  }
482
- function createRunCommand(logger) {
483
- return SimpleCLI.command({
484
- description: "Run an exported Libretto workflow from a file"
485
- }).input(runInput).use(resolveSessionMiddleware).handle(async ({ input, ctx }) => {
486
- await stopExistingFailedRunSession(ctx.session, logger);
487
- assertSessionAvailableForStart(ctx.session, logger);
488
- const params = resolveRunParams(input.params, input.paramsFile);
489
- const headlessMode = input.headed ? false : input.headless ? true : void 0;
490
- await runIntegrationFromFile({
591
+ const runCommand = SimpleCLI.command({
592
+ description: "Run an exported Libretto workflow from a file"
593
+ }).input(runInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
594
+ await stopExistingFailedRunSession(ctx.session, ctx.logger);
595
+ assertSessionAvailableForStart(ctx.session, ctx.logger);
596
+ const params = resolveRunParams(input.params, input.paramsFile);
597
+ const headlessMode = input.headed ? false : input.headless ? true : void 0;
598
+ const visualize = !input.noVisualize;
599
+ const viewport = resolveViewport(
600
+ parseViewportArg(input.viewport),
601
+ ctx.logger
602
+ );
603
+ await runIntegrationFromFile(
604
+ {
491
605
  integrationPath: input.integrationFile,
492
606
  exportName: input.integrationExport,
493
607
  session: ctx.session,
494
608
  params,
495
609
  tsconfigPath: input.tsconfig,
496
610
  headless: headlessMode ?? false,
497
- authProfileDomain: input.authProfile
498
- }, logger);
499
- });
500
- }
611
+ visualize,
612
+ authProfileDomain: input.authProfile,
613
+ viewport
614
+ },
615
+ ctx.logger
616
+ );
617
+ });
501
618
  const resumeInput = SimpleCLI.input({
502
619
  positionals: [],
503
620
  named: {
504
621
  session: sessionOption()
505
622
  }
506
623
  });
507
- function createResumeCommand(logger) {
508
- return SimpleCLI.command({
509
- description: "Resume a paused workflow for the current session"
510
- }).input(resumeInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ ctx }) => {
511
- await runResume(ctx.session, logger, ctx.sessionState);
512
- });
513
- }
514
- function createExecutionCommands(logger) {
515
- return {
516
- exec: createExecCommand(logger),
517
- run: createRunCommand(logger),
518
- resume: createResumeCommand(logger)
519
- };
520
- }
624
+ const resumeCommand = SimpleCLI.command({
625
+ description: "Resume a paused workflow for the current session"
626
+ }).input(resumeInput).use(withRequiredSession()).handle(async ({ ctx }) => {
627
+ await runResume(ctx.session, ctx.logger, ctx.sessionState);
628
+ });
629
+ const executionCommands = {
630
+ exec: execCommand,
631
+ run: runCommand,
632
+ resume: resumeCommand
633
+ };
521
634
  export {
522
- createExecCommand,
523
- createExecutionCommands,
524
- createResumeCommand,
525
- createRunCommand,
635
+ execCommand,
526
636
  execInput,
637
+ executionCommands,
638
+ resumeCommand,
527
639
  resumeInput,
640
+ runCommand,
528
641
  runInput
529
642
  };
@@ -1,5 +1,13 @@
1
1
  import { createInterface } from "node:readline";
2
- import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import {
3
+ appendFileSync,
4
+ cpSync,
5
+ existsSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync
10
+ } from "node:fs";
3
11
  import { spawnSync } from "node:child_process";
4
12
  import { basename, dirname, join } from "node:path";
5
13
  import { fileURLToPath } from "node:url";
@@ -83,7 +91,9 @@ function printSnapshotApiStatus() {
83
91
  printInvalidAiConfigWarning();
84
92
  if (selection && hasProviderCredentials(selection.provider)) {
85
93
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
86
- console.log(" Snapshot objectives will use the API analyzer by default.");
94
+ console.log(
95
+ " Snapshot objectives will use the API analyzer by default."
96
+ );
87
97
  console.log(" No further action required.");
88
98
  return;
89
99
  }
@@ -98,7 +108,9 @@ function printSnapshotApiStatus() {
98
108
  console.log(
99
109
  " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
100
110
  );
101
- console.log(" Run `npx libretto init` interactively to set up credentials.");
111
+ console.log(
112
+ " Run `npx libretto init` interactively to set up credentials."
113
+ );
102
114
  }
103
115
  async function runInteractiveApiSetup() {
104
116
  const config = safeReadAiConfig();
@@ -110,7 +122,9 @@ async function runInteractiveApiSetup() {
110
122
  printInvalidAiConfigWarning();
111
123
  if (selection && hasProviderCredentials(selection.provider)) {
112
124
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
113
- console.log(" Snapshot objectives will use the API analyzer by default.");
125
+ console.log(
126
+ " Snapshot objectives will use the API analyzer by default."
127
+ );
114
128
  return;
115
129
  }
116
130
  console.log(" \u2717 No snapshot API credentials detected.\n");
@@ -119,14 +133,18 @@ async function runInteractiveApiSetup() {
119
133
  output: process.stdout
120
134
  });
121
135
  try {
122
- console.log(" Which API provider would you like to use for snapshot analysis?\n");
136
+ console.log(
137
+ " Which API provider would you like to use for snapshot analysis?\n"
138
+ );
123
139
  for (const choice of PROVIDER_CHOICES) {
124
140
  console.log(` ${choice.key}) ${choice.label}`);
125
141
  }
126
142
  console.log(" s) Skip for now\n");
127
143
  const answer = await promptUser(rl, " Choice: ");
128
144
  if (answer.toLowerCase() === "s" || !answer) {
129
- console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
145
+ console.log(
146
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto init`."
147
+ );
130
148
  console.log(" Or add credentials directly to your .env file:");
131
149
  console.log(" OPENAI_API_KEY=...");
132
150
  console.log(" ANTHROPIC_API_KEY=...");
@@ -218,7 +236,9 @@ function detectAgentDirs(root) {
218
236
  async function copySkills() {
219
237
  const agentDirs = detectAgentDirs(REPO_ROOT);
220
238
  if (agentDirs.length === 0) {
221
- console.log("\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping.");
239
+ console.log(
240
+ "\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
241
+ );
222
242
  return;
223
243
  }
224
244
  const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
@@ -246,7 +266,9 @@ ${verb} libretto skills in ${dirNames}?`);
246
266
  }
247
267
  cpSync(sourceDir, skillDest, { recursive: true });
248
268
  const fileCount = readdirSync(skillDest).length;
249
- console.log(` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`);
269
+ console.log(
270
+ ` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`
271
+ );
250
272
  }
251
273
  }
252
274
  const initInput = SimpleCLI.input({
@@ -12,10 +12,9 @@ import {
12
12
  import { SimpleCLI } from "../framework/simple-cli.js";
13
13
  import {
14
14
  integerOption,
15
- loadSessionStateMiddleware,
16
15
  pageOption,
17
- resolveSessionMiddleware,
18
- sessionOption
16
+ sessionOption,
17
+ withRequiredSession
19
18
  } from "./shared.js";
20
19
  async function resolvePageId(session, pageId) {
21
20
  if (!pageId) return void 0;
@@ -26,7 +25,7 @@ async function resolvePageId(session, pageId) {
26
25
  const foundPage = pages.find((page) => page.id === pageId);
27
26
  if (!foundPage) {
28
27
  throw new Error(
29
- `Page "${pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
28
+ `Page "${pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
30
29
  );
31
30
  }
32
31
  return pageId;
@@ -44,7 +43,7 @@ const networkInput = SimpleCLI.input({
44
43
  });
45
44
  const networkCommand = SimpleCLI.command({
46
45
  description: "View captured network requests"
47
- }).input(networkInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
46
+ }).input(networkInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
48
47
  if (input.clear) {
49
48
  clearNetworkLog(ctx.session);
50
49
  console.log("Network log cleared.");
@@ -81,7 +80,7 @@ const actionsInput = SimpleCLI.input({
81
80
  });
82
81
  const actionsCommand = SimpleCLI.command({
83
82
  description: "View captured actions"
84
- }).input(actionsInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
83
+ }).input(actionsInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
85
84
  if (input.clear) {
86
85
  clearActionLog(ctx.session);
87
86
  console.log("Action log cleared.");
@@ -1,26 +1,15 @@
1
1
  import { z } from "zod";
2
+ import { createLoggerForSession } from "../core/context.js";
2
3
  import {
3
- SESSION_DEFAULT,
4
+ generateSessionName,
4
5
  readSessionStateOrThrow,
5
6
  validateSessionName
6
7
  } from "../core/session.js";
7
8
  import {
8
9
  SimpleCLI
9
10
  } from "../framework/simple-cli.js";
10
- function createSessionSchema() {
11
- return z.string().default(SESSION_DEFAULT).superRefine((value, ctx) => {
12
- try {
13
- validateSessionName(value);
14
- } catch (err) {
15
- ctx.addIssue({
16
- code: z.ZodIssueCode.custom,
17
- message: err instanceof Error ? err.message : String(err)
18
- });
19
- }
20
- });
21
- }
22
- function sessionOption(help = "Use a named session") {
23
- return SimpleCLI.option(createSessionSchema(), { help });
11
+ function sessionOption(help = "Session name") {
12
+ return SimpleCLI.option(z.string().optional(), { help });
24
13
  }
25
14
  function pageOption(help = "Target a specific page id") {
26
15
  return SimpleCLI.option(z.string().optional(), { help });
@@ -28,23 +17,35 @@ function pageOption(help = "Target a specific page id") {
28
17
  function integerOption(help) {
29
18
  return SimpleCLI.option(z.coerce.number().int().optional(), { help });
30
19
  }
31
- const resolveSessionMiddleware = async ({ input, ctx }) => {
32
- return {
33
- ...ctx,
34
- session: input.session
20
+ function withRequiredSession() {
21
+ return async ({ input, ctx }) => {
22
+ if (!input.session) {
23
+ throw new Error("Missing required option --session.");
24
+ }
25
+ validateSessionName(input.session);
26
+ const logger = createLoggerForSession(input.session);
27
+ return {
28
+ ...ctx,
29
+ session: input.session,
30
+ logger,
31
+ sessionState: readSessionStateOrThrow(input.session)
32
+ };
35
33
  };
36
- };
37
- const loadSessionStateMiddleware = async ({ ctx }) => {
38
- return {
39
- ...ctx,
40
- sessionState: readSessionStateOrThrow(ctx.session)
34
+ }
35
+ function withAutoSession() {
36
+ return async ({ input, ctx }) => {
37
+ const session = input.session ?? generateSessionName();
38
+ if (input.session) {
39
+ validateSessionName(input.session);
40
+ }
41
+ const logger = createLoggerForSession(session);
42
+ return { ...ctx, session, logger };
41
43
  };
42
- };
44
+ }
43
45
  export {
44
- createSessionSchema,
45
46
  integerOption,
46
- loadSessionStateMiddleware,
47
47
  pageOption,
48
- resolveSessionMiddleware,
49
- sessionOption
48
+ sessionOption,
49
+ withAutoSession,
50
+ withRequiredSession
50
51
  };