libretto 0.5.0 → 0.5.2

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 (122) hide show
  1. package/README.md +109 -35
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +34 -29
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +21 -4
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +207 -37
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +434 -174
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +20 -4
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +17 -69
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/dom-semantics.d.ts +8 -0
  38. package/dist/shared/dom-semantics.js +69 -0
  39. package/dist/shared/instrumentation/instrument.js +101 -5
  40. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  41. package/dist/shared/llm/client.js +3 -1
  42. package/dist/shared/logger/index.js +4 -1
  43. package/dist/shared/run/api.js +3 -1
  44. package/dist/shared/run/browser.js +47 -3
  45. package/dist/shared/state/session-state.d.ts +2 -1
  46. package/dist/shared/state/session-state.js +5 -2
  47. package/dist/shared/visualization/ghost-cursor.js +36 -14
  48. package/dist/shared/visualization/highlight.js +9 -6
  49. package/dist/shared/workflow/workflow.d.ts +4 -5
  50. package/dist/shared/workflow/workflow.js +3 -5
  51. package/package.json +6 -2
  52. package/scripts/check-skills-sync.mjs +25 -0
  53. package/scripts/compare-eval-summary.mjs +47 -0
  54. package/scripts/postinstall.mjs +15 -15
  55. package/scripts/prepare-release.sh +97 -0
  56. package/scripts/skills-libretto.mjs +103 -0
  57. package/scripts/summarize-evals.mjs +135 -0
  58. package/scripts/sync-skills.mjs +12 -0
  59. package/skills/libretto/SKILL.md +132 -54
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +210 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/execution.ts +233 -102
  69. package/src/cli/commands/init.ts +37 -33
  70. package/src/cli/commands/logs.ts +7 -7
  71. package/src/cli/commands/shared.ts +36 -37
  72. package/src/cli/commands/snapshot.ts +44 -59
  73. package/src/cli/core/ai-config.ts +24 -4
  74. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  75. package/src/cli/core/browser.ts +260 -49
  76. package/src/cli/core/context.ts +7 -2
  77. package/src/cli/core/session-telemetry.ts +449 -197
  78. package/src/cli/core/session.ts +21 -7
  79. package/src/cli/core/snapshot-analyzer.ts +26 -46
  80. package/src/cli/core/snapshot-api-config.ts +170 -175
  81. package/src/cli/core/telemetry.ts +39 -4
  82. package/src/cli/framework/simple-cli.ts +144 -77
  83. package/src/cli/router.ts +13 -21
  84. package/src/cli/workers/run-integration-runtime.ts +36 -9
  85. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  86. package/src/cli/workers/run-integration-worker.ts +1 -4
  87. package/src/index.ts +73 -66
  88. package/src/runtime/download/download.ts +62 -58
  89. package/src/runtime/download/index.ts +5 -5
  90. package/src/runtime/extract/extract.ts +71 -61
  91. package/src/runtime/network/index.ts +3 -3
  92. package/src/runtime/network/network.ts +99 -93
  93. package/src/runtime/recovery/agent.ts +217 -212
  94. package/src/runtime/recovery/errors.ts +107 -104
  95. package/src/runtime/recovery/index.ts +3 -3
  96. package/src/runtime/recovery/recovery.ts +38 -35
  97. package/src/shared/condense-dom/condense-dom.ts +27 -82
  98. package/src/shared/config/config.ts +0 -19
  99. package/src/shared/config/index.ts +0 -5
  100. package/src/shared/debug/pause.ts +57 -51
  101. package/src/shared/dom-semantics.ts +68 -0
  102. package/src/shared/instrumentation/errors.ts +64 -62
  103. package/src/shared/instrumentation/index.ts +5 -5
  104. package/src/shared/instrumentation/instrument.ts +339 -209
  105. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  106. package/src/shared/llm/client.ts +181 -174
  107. package/src/shared/llm/types.ts +39 -39
  108. package/src/shared/logger/index.ts +11 -4
  109. package/src/shared/logger/logger.ts +312 -306
  110. package/src/shared/logger/sinks.ts +118 -114
  111. package/src/shared/paths/paths.ts +50 -49
  112. package/src/shared/paths/repo-root.ts +17 -17
  113. package/src/shared/run/api.ts +5 -1
  114. package/src/shared/run/browser.ts +65 -3
  115. package/src/shared/state/index.ts +9 -9
  116. package/src/shared/state/session-state.ts +46 -43
  117. package/src/shared/visualization/ghost-cursor.ts +180 -149
  118. package/src/shared/visualization/highlight.ts +89 -86
  119. package/src/shared/visualization/index.ts +13 -13
  120. package/src/shared/workflow/workflow.ts +19 -25
  121. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  122. package/skills/libretto/references/user-action-log.md +0 -31
@@ -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));
@@ -295,9 +385,9 @@ async function runResume(session, logger, sessionState) {
295
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
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 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";
@@ -9,8 +17,8 @@ import {
9
17
  loadSnapshotEnv,
10
18
  resolveSnapshotApiModel
11
19
  } from "../core/snapshot-api-config.js";
12
- import { SimpleCLI } from "../framework/simple-cli.js";
13
20
  import { hasProviderCredentials } from "../../shared/llm/client.js";
21
+ import { SimpleCLI } from "../framework/simple-cli.js";
14
22
  const PROVIDER_CHOICES = [
15
23
  {
16
24
  key: "1",
@@ -44,15 +52,6 @@ function promptUser(rl, question) {
44
52
  });
45
53
  });
46
54
  }
47
- function askYesNo(question) {
48
- const rl = createInterface({ input: process.stdin, output: process.stdout });
49
- return new Promise((resolve) => {
50
- rl.question(`${question} (y/N) `, (answer) => {
51
- rl.close();
52
- resolve(answer.trim().toLowerCase() === "y");
53
- });
54
- });
55
- }
56
55
  function safeReadAiConfig() {
57
56
  try {
58
57
  return readAiConfig();
@@ -83,7 +82,9 @@ function printSnapshotApiStatus() {
83
82
  printInvalidAiConfigWarning();
84
83
  if (selection && hasProviderCredentials(selection.provider)) {
85
84
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
86
- console.log(" Snapshot objectives will use the API analyzer by default.");
85
+ console.log(
86
+ " Snapshot objectives will use the API analyzer by default."
87
+ );
87
88
  console.log(" No further action required.");
88
89
  return;
89
90
  }
@@ -98,7 +99,9 @@ function printSnapshotApiStatus() {
98
99
  console.log(
99
100
  " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
100
101
  );
101
- console.log(" Run `npx libretto init` interactively to set up credentials.");
102
+ console.log(
103
+ " Run `npx libretto init` interactively to set up credentials."
104
+ );
102
105
  }
103
106
  async function runInteractiveApiSetup() {
104
107
  const config = safeReadAiConfig();
@@ -110,7 +113,9 @@ async function runInteractiveApiSetup() {
110
113
  printInvalidAiConfigWarning();
111
114
  if (selection && hasProviderCredentials(selection.provider)) {
112
115
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
113
- console.log(" Snapshot objectives will use the API analyzer by default.");
116
+ console.log(
117
+ " Snapshot objectives will use the API analyzer by default."
118
+ );
114
119
  return;
115
120
  }
116
121
  console.log(" \u2717 No snapshot API credentials detected.\n");
@@ -119,14 +124,18 @@ async function runInteractiveApiSetup() {
119
124
  output: process.stdout
120
125
  });
121
126
  try {
122
- console.log(" Which API provider would you like to use for snapshot analysis?\n");
127
+ console.log(
128
+ " Which API provider would you like to use for snapshot analysis?\n"
129
+ );
123
130
  for (const choice of PROVIDER_CHOICES) {
124
131
  console.log(` ${choice.key}) ${choice.label}`);
125
132
  }
126
133
  console.log(" s) Skip for now\n");
127
134
  const answer = await promptUser(rl, " Choice: ");
128
135
  if (answer.toLowerCase() === "s" || !answer) {
129
- console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
136
+ console.log(
137
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto init`."
138
+ );
130
139
  console.log(" Or add credentials directly to your .env file:");
131
140
  console.log(" OPENAI_API_KEY=...");
132
141
  console.log(" ANTHROPIC_API_KEY=...");
@@ -215,22 +224,15 @@ function detectAgentDirs(root) {
215
224
  if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
216
225
  return dirs;
217
226
  }
218
- async function copySkills() {
227
+ function copySkills() {
219
228
  const agentDirs = detectAgentDirs(REPO_ROOT);
220
229
  if (agentDirs.length === 0) {
221
- console.log("\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping.");
230
+ console.log(
231
+ "\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
232
+ );
222
233
  return;
223
234
  }
224
235
  const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
225
- const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
226
- const existing = destinations.filter((d) => existsSync(d));
227
- const verb = existing.length > 0 ? "Overwrite" : "Install";
228
- const proceed = await askYesNo(`
229
- ${verb} libretto skills in ${dirNames}?`);
230
- if (!proceed) {
231
- console.log(" Skipping skill copy.");
232
- return;
233
- }
234
236
  let sourceDir;
235
237
  try {
236
238
  sourceDir = getPackageSkillsDir();
@@ -246,7 +248,9 @@ ${verb} libretto skills in ${dirNames}?`);
246
248
  }
247
249
  cpSync(sourceDir, skillDest, { recursive: true });
248
250
  const fileCount = readdirSync(skillDest).length;
249
- console.log(` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`);
251
+ console.log(
252
+ ` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`
253
+ );
250
254
  }
251
255
  }
252
256
  const initInput = SimpleCLI.input({
@@ -267,10 +271,11 @@ const initCommand = SimpleCLI.command({
267
271
  } else {
268
272
  console.log("\nSkipping browser installation (--skip-browsers)");
269
273
  }
274
+ copySkills();
270
275
  if (process.stdin.isTTY) {
271
- await copySkills();
272
276
  await runInteractiveApiSetup();
273
277
  } else {
278
+ loadSnapshotEnv();
274
279
  printSnapshotApiStatus();
275
280
  }
276
281
  console.log("\n\u2713 libretto init complete");
@@ -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;
@@ -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.");