libretto 0.5.4 → 0.5.6

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 (101) hide show
  1. package/README.md +23 -10
  2. package/README.template.md +23 -10
  3. package/dist/cli/cli.js +10 -0
  4. package/dist/cli/commands/ai.js +77 -2
  5. package/dist/cli/commands/browser.js +71 -6
  6. package/dist/cli/commands/execution.js +101 -44
  7. package/dist/cli/commands/setup.js +376 -0
  8. package/dist/cli/commands/snapshot.js +2 -2
  9. package/dist/cli/commands/status.js +62 -0
  10. package/dist/cli/core/{snapshot-api-config.js → ai-model.js} +81 -7
  11. package/dist/cli/core/api-snapshot-analyzer.js +7 -5
  12. package/dist/cli/core/browser.js +81 -42
  13. package/dist/cli/core/{ai-config.js → config.js} +13 -79
  14. package/dist/cli/core/context.js +1 -25
  15. package/dist/cli/core/deploy-artifact.js +121 -61
  16. package/dist/cli/core/readonly-exec.js +231 -0
  17. package/dist/{shared/llm/client.js → cli/core/resolve-model.js} +4 -68
  18. package/dist/cli/core/session.js +44 -0
  19. package/dist/cli/core/skill-version.js +73 -0
  20. package/dist/cli/core/telemetry.js +1 -54
  21. package/dist/cli/index.js +1 -7
  22. package/dist/cli/router.js +4 -4
  23. package/dist/cli/workers/run-integration-runtime.js +29 -25
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/index.d.ts +2 -4
  26. package/dist/index.js +2 -2
  27. package/dist/runtime/extract/extract.d.ts +2 -2
  28. package/dist/runtime/extract/extract.js +4 -2
  29. package/dist/runtime/extract/index.d.ts +1 -1
  30. package/dist/runtime/recovery/agent.d.ts +2 -3
  31. package/dist/runtime/recovery/agent.js +5 -3
  32. package/dist/runtime/recovery/errors.d.ts +2 -3
  33. package/dist/runtime/recovery/errors.js +4 -2
  34. package/dist/runtime/recovery/index.d.ts +1 -2
  35. package/dist/runtime/recovery/recovery.d.ts +2 -3
  36. package/dist/runtime/recovery/recovery.js +3 -3
  37. package/dist/shared/debug/pause.js +4 -21
  38. package/dist/shared/run/api.d.ts +2 -0
  39. package/dist/shared/run/browser.d.ts +4 -1
  40. package/dist/shared/run/browser.js +5 -3
  41. package/dist/shared/state/index.d.ts +1 -1
  42. package/dist/shared/state/index.js +2 -0
  43. package/dist/shared/state/session-state.d.ts +10 -1
  44. package/dist/shared/state/session-state.js +3 -0
  45. package/dist/shared/workflow/workflow.d.ts +2 -3
  46. package/dist/shared/workflow/workflow.js +16 -9
  47. package/package.json +3 -4
  48. package/scripts/postinstall.mjs +13 -11
  49. package/scripts/skills-libretto.mjs +14 -4
  50. package/skills/AGENTS.md +11 -0
  51. package/skills/libretto/SKILL.md +30 -9
  52. package/skills/libretto/references/auth-profiles.md +1 -1
  53. package/skills/libretto/references/code-generation-rules.md +6 -6
  54. package/skills/libretto/references/configuration-file-reference.md +11 -6
  55. package/skills/libretto-readonly/SKILL.md +95 -0
  56. package/src/cli/cli.ts +10 -0
  57. package/src/cli/commands/ai.ts +111 -1
  58. package/src/cli/commands/browser.ts +81 -7
  59. package/src/cli/commands/execution.ts +128 -61
  60. package/src/cli/commands/setup.ts +499 -0
  61. package/src/cli/commands/snapshot.ts +2 -2
  62. package/src/cli/commands/status.ts +77 -0
  63. package/src/cli/core/{snapshot-api-config.ts → ai-model.ts} +154 -14
  64. package/src/cli/core/api-snapshot-analyzer.ts +7 -5
  65. package/src/cli/core/browser.ts +107 -45
  66. package/src/cli/core/{ai-config.ts → config.ts} +13 -108
  67. package/src/cli/core/context.ts +1 -45
  68. package/src/cli/core/deploy-artifact.ts +141 -71
  69. package/src/cli/core/readonly-exec.ts +284 -0
  70. package/src/{shared/llm/client.ts → cli/core/resolve-model.ts} +3 -85
  71. package/src/cli/core/session.ts +62 -2
  72. package/src/cli/core/skill-version.ts +93 -0
  73. package/src/cli/core/telemetry.ts +0 -52
  74. package/src/cli/index.ts +0 -6
  75. package/src/cli/router.ts +4 -4
  76. package/src/cli/workers/run-integration-runtime.ts +36 -31
  77. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  78. package/src/index.ts +1 -7
  79. package/src/runtime/extract/extract.ts +6 -5
  80. package/src/runtime/recovery/agent.ts +5 -4
  81. package/src/runtime/recovery/errors.ts +4 -3
  82. package/src/runtime/recovery/recovery.ts +4 -4
  83. package/src/shared/debug/pause.ts +4 -23
  84. package/src/shared/run/browser.ts +5 -1
  85. package/src/shared/state/index.ts +2 -0
  86. package/src/shared/state/session-state.ts +3 -0
  87. package/src/shared/workflow/workflow.ts +24 -15
  88. package/dist/cli/commands/init.js +0 -286
  89. package/dist/cli/commands/logs.js +0 -117
  90. package/dist/shared/llm/ai-sdk-adapter.d.ts +0 -22
  91. package/dist/shared/llm/ai-sdk-adapter.js +0 -49
  92. package/dist/shared/llm/client.d.ts +0 -13
  93. package/dist/shared/llm/index.d.ts +0 -5
  94. package/dist/shared/llm/index.js +0 -6
  95. package/dist/shared/llm/types.d.ts +0 -67
  96. package/dist/shared/llm/types.js +0 -0
  97. package/src/cli/commands/init.ts +0 -331
  98. package/src/cli/commands/logs.ts +0 -128
  99. package/src/shared/llm/ai-sdk-adapter.ts +0 -81
  100. package/src/shared/llm/index.ts +0 -3
  101. package/src/shared/llm/types.ts +0 -63
@@ -13,15 +13,19 @@ import { parseViewportArg } from "./browser.js";
13
13
  import { getPauseSignalPaths } from "../core/pause-signals.js";
14
14
  import {
15
15
  assertSessionAvailableForStart,
16
+ assertSessionAllowsCommand,
16
17
  clearSessionState,
17
18
  readSessionState,
18
19
  setSessionStatus
19
20
  } from "../core/session.js";
21
+ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
20
22
  import {
21
23
  readActionLog,
22
24
  readNetworkLog,
23
25
  wrapPageForActionLogging
24
26
  } from "../core/telemetry.js";
27
+ import { readLibrettoConfig } from "../core/config.js";
28
+ import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
25
29
  import { SimpleCLI } from "../framework/simple-cli.js";
26
30
  import {
27
31
  pageOption,
@@ -135,12 +139,15 @@ function stripEmptyCatchHandlers(code) {
135
139
  }
136
140
  return { cleaned: result, strippedCount };
137
141
  }
138
- async function runExec(code, session, logger, visualize = false, pageId) {
142
+ async function runExec(code, session, logger, options = {}) {
143
+ const visualize = options.visualize ?? false;
144
+ const pageId = options.pageId;
145
+ const mode = options.mode ?? "exec";
139
146
  const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
140
147
  if (strippedCount > 0) {
141
148
  console.log("(Stripped `.catch(() => {})` \u2014 letting errors bubble up)");
142
149
  }
143
- logger.info("exec-start", {
150
+ logger.info(`${mode}-start`, {
144
151
  session,
145
152
  codeLength: cleanedCode.length,
146
153
  codePreview: cleanedCode.slice(0, 200),
@@ -164,57 +171,61 @@ async function runExec(code, session, logger, visualize = false, pageId) {
164
171
  const stallInterval = setInterval(() => {
165
172
  const silenceMs = Date.now() - lastActivityTs;
166
173
  if (silenceMs >= STALL_THRESHOLD_MS) {
167
- logger.warn("exec-stall-warning", {
174
+ logger.warn(`${mode}-stall-warning`, {
168
175
  session,
169
176
  silenceMs,
170
177
  codePreview: cleanedCode.slice(0, 200)
171
178
  });
172
179
  console.warn(
173
- `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 exec may be hung (code: ${cleanedCode.slice(0, 100)}...)`
180
+ `[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1e3)}s \u2014 ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`
174
181
  );
175
182
  }
176
183
  }, STALL_THRESHOLD_MS);
177
184
  const execStartTs = Date.now();
178
185
  const sigintHandler = () => {
179
- logger.info("exec-interrupted", {
186
+ logger.info(`${mode}-interrupted`, {
180
187
  session,
181
188
  duration: Date.now() - execStartTs,
182
189
  codePreview: cleanedCode.slice(0, 200)
183
190
  });
184
191
  };
185
192
  process.on("SIGINT", sigintHandler);
186
- wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
187
- if (visualize) {
193
+ if (mode === "exec") {
194
+ wrapPageForActionLogging(page, session, resolvedPageId, onActivity);
195
+ }
196
+ if (visualize && mode === "exec") {
188
197
  await installInstrumentation(page, { visualize: true, logger });
189
198
  }
190
199
  try {
191
- const execState = {};
192
- const networkLog = (opts = {}) => {
193
- return readNetworkLog(session, opts);
194
- };
195
- const actionLog = (opts = {}) => {
196
- return readActionLog(session, opts);
197
- };
198
- const helpers = {
199
- page,
200
- context,
201
- state: execState,
202
- browser,
203
- networkLog,
204
- actionLog,
205
- console,
206
- setTimeout,
207
- setInterval,
208
- clearTimeout,
209
- clearInterval,
210
- fetch,
211
- URL,
212
- Buffer
213
- };
200
+ const helpers = mode === "readonly-exec" ? createReadonlyExecHelpers(page, { onActivity }) : (() => {
201
+ const execState = {};
202
+ const networkLog = (opts = {}) => {
203
+ return readNetworkLog(session, opts);
204
+ };
205
+ const actionLog = (opts = {}) => {
206
+ return readActionLog(session, opts);
207
+ };
208
+ return {
209
+ page,
210
+ context,
211
+ state: execState,
212
+ browser,
213
+ networkLog,
214
+ actionLog,
215
+ console,
216
+ setTimeout,
217
+ setInterval,
218
+ clearTimeout,
219
+ clearInterval,
220
+ fetch,
221
+ URL,
222
+ Buffer
223
+ };
224
+ })();
214
225
  const helperNames = Object.keys(helpers);
215
226
  const fn = compileExecFunction(cleanedCode, helperNames);
216
227
  const result = await fn(...Object.values(helpers));
217
- logger.info("exec-success", { session, hasResult: result !== void 0 });
228
+ logger.info(`${mode}-success`, { session, hasResult: result !== void 0 });
218
229
  if (result !== void 0) {
219
230
  console.log(
220
231
  typeof result === "string" ? result : JSON.stringify(result, null, 2)
@@ -223,7 +234,7 @@ async function runExec(code, session, logger, visualize = false, pageId) {
223
234
  console.log("Executed successfully");
224
235
  }
225
236
  } catch (err) {
226
- logger.error("exec-error", {
237
+ logger.error(`${mode}-error`, {
227
238
  error: err,
228
239
  session,
229
240
  codePreview: cleanedCode.slice(0, 200)
@@ -445,13 +456,13 @@ async function runIntegrationFromFile(args, logger) {
445
456
  );
446
457
  const payload = JSON.stringify({
447
458
  integrationPath: args.integrationPath,
448
- workflowName: args.workflowName,
449
459
  session: args.session,
450
460
  params: args.params,
451
461
  headless: args.headless,
452
462
  visualize: args.visualize,
453
463
  authProfileDomain: args.authProfileDomain,
454
- viewport: args.viewport
464
+ viewport: args.viewport,
465
+ accessMode: args.accessMode
455
466
  });
456
467
  const worker = spawn(
457
468
  process.execPath,
@@ -526,6 +537,7 @@ const execInput = SimpleCLI.input({
526
537
  const execCommand = SimpleCLI.command({
527
538
  description: "Execute Playwright TypeScript code"
528
539
  }).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
540
+ assertSessionAllowsCommand(ctx.sessionState, "exec", ["write-access"]);
529
541
  const code = input.code;
530
542
  const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
531
543
  if (codeFromArgsOrStdin === null) {
@@ -537,18 +549,48 @@ const execCommand = SimpleCLI.command({
537
549
  codeFromArgsOrStdin,
538
550
  ctx.session,
539
551
  ctx.logger,
540
- input.visualize,
541
- input.page
552
+ {
553
+ visualize: input.visualize,
554
+ pageId: input.page,
555
+ mode: "exec"
556
+ }
542
557
  );
543
558
  });
544
- const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
559
+ const readonlyExecInput = SimpleCLI.input({
560
+ positionals: [
561
+ SimpleCLI.positional("code", z.string().optional(), {
562
+ help: "Read-only Playwright TypeScript code to execute"
563
+ })
564
+ ],
565
+ named: {
566
+ session: sessionOption(),
567
+ page: pageOption()
568
+ }
569
+ }).refine(
570
+ (input) => input.code !== void 0,
571
+ `Usage: libretto readonly-exec <code|-> [--session <name>] [--page <id>]
572
+ echo '<code>' | libretto readonly-exec - [--session <name>] [--page <id>]`
573
+ );
574
+ const readonlyExecCommand = SimpleCLI.command({
575
+ description: "Execute read-only Playwright inspection code"
576
+ }).input(readonlyExecInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
577
+ const code = input.code;
578
+ const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
579
+ if (codeFromArgsOrStdin === null) {
580
+ throw new Error(
581
+ "Missing stdin input for `readonly-exec -`. Pipe inspection code into stdin."
582
+ );
583
+ }
584
+ await runExec(codeFromArgsOrStdin, ctx.session, ctx.logger, {
585
+ pageId: input.page,
586
+ mode: "readonly-exec"
587
+ });
588
+ });
589
+ const runUsage = `Usage: libretto run <integrationFile> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--read-only|--write-access] [--no-visualize] [--viewport WxH]`;
545
590
  const runInput = SimpleCLI.input({
546
591
  positionals: [
547
592
  SimpleCLI.positional("integrationFile", z.string().optional(), {
548
593
  help: "Path to the integration file"
549
- }),
550
- SimpleCLI.positional("workflowName", z.string().optional(), {
551
- help: "Workflow name to run (from workflow(name, handler))"
552
594
  })
553
595
  ],
554
596
  named: {
@@ -565,6 +607,14 @@ const runInput = SimpleCLI.input({
565
607
  }),
566
608
  headed: SimpleCLI.flag({ help: "Run in headed mode" }),
567
609
  headless: SimpleCLI.flag({ help: "Run in headless mode" }),
610
+ readOnly: SimpleCLI.flag({
611
+ name: "read-only",
612
+ help: "Create the session in read-only mode"
613
+ }),
614
+ writeAccess: SimpleCLI.flag({
615
+ name: "write-access",
616
+ help: "Create the session in write-access mode (overrides config default)"
617
+ }),
568
618
  noVisualize: SimpleCLI.flag({
569
619
  name: "no-visualize",
570
620
  help: "Disable ghost cursor + highlight visualization in headed mode"
@@ -578,7 +628,7 @@ const runInput = SimpleCLI.input({
578
628
  })
579
629
  }
580
630
  }).refine(
581
- (input) => Boolean(input.integrationFile && input.workflowName),
631
+ (input) => Boolean(input.integrationFile),
582
632
  runUsage
583
633
  ).refine(
584
634
  (input) => !(input.params && input.paramsFile),
@@ -586,6 +636,9 @@ const runInput = SimpleCLI.input({
586
636
  ).refine(
587
637
  (input) => !(input.headed && input.headless),
588
638
  "Cannot pass both --headed and --headless."
639
+ ).refine(
640
+ (input) => !(input.readOnly && input.writeAccess),
641
+ "Cannot pass both --read-only and --write-access."
589
642
  );
590
643
  function resolveRunParams(rawInlineParams, paramsFile) {
591
644
  if (paramsFile) {
@@ -605,8 +658,9 @@ function resolveRunParams(rawInlineParams, paramsFile) {
605
658
  return {};
606
659
  }
607
660
  const runCommand = SimpleCLI.command({
608
- description: "Run an exported Libretto workflow from a file"
661
+ description: "Run the default-exported Libretto workflow from a file"
609
662
  }).input(runInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
663
+ warnIfInstalledSkillOutOfDate();
610
664
  await stopExistingFailedRunSession(ctx.session, ctx.logger);
611
665
  assertSessionAvailableForStart(ctx.session, ctx.logger);
612
666
  const params = resolveRunParams(input.params, input.paramsFile);
@@ -619,14 +673,14 @@ const runCommand = SimpleCLI.command({
619
673
  await runIntegrationFromFile(
620
674
  {
621
675
  integrationPath: input.integrationFile,
622
- workflowName: input.workflowName,
623
676
  session: ctx.session,
624
677
  params,
625
678
  tsconfigPath: input.tsconfig,
626
679
  headless: headlessMode ?? false,
627
680
  visualize,
628
681
  authProfileDomain: input.authProfile,
629
- viewport
682
+ viewport,
683
+ accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access"
630
684
  },
631
685
  ctx.logger
632
686
  );
@@ -644,6 +698,7 @@ const resumeCommand = SimpleCLI.command({
644
698
  });
645
699
  const executionCommands = {
646
700
  exec: execCommand,
701
+ "readonly-exec": readonlyExecCommand,
647
702
  run: runCommand,
648
703
  resume: resumeCommand
649
704
  };
@@ -651,6 +706,8 @@ export {
651
706
  execCommand,
652
707
  execInput,
653
708
  executionCommands,
709
+ readonlyExecCommand,
710
+ readonlyExecInput,
654
711
  resumeCommand,
655
712
  resumeInput,
656
713
  runCommand,
@@ -0,0 +1,376 @@
1
+ import { createInterface } from "node:readline";
2
+ import {
3
+ appendFileSync,
4
+ cpSync,
5
+ existsSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync
10
+ } from "node:fs";
11
+ import { spawnSync } from "node:child_process";
12
+ import { basename, dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { writeAiConfig } from "../core/config.js";
15
+ import {
16
+ ensureLibrettoSetup,
17
+ LIBRETTO_CONFIG_PATH,
18
+ REPO_ROOT
19
+ } from "../core/context.js";
20
+ import {
21
+ DEFAULT_SNAPSHOT_MODELS,
22
+ loadSnapshotEnv,
23
+ resolveAiSetupStatus
24
+ } from "../core/ai-model.js";
25
+ import { SimpleCLI } from "../framework/simple-cli.js";
26
+ const PROVIDER_CHOICES = [
27
+ {
28
+ key: "1",
29
+ label: "OpenAI",
30
+ provider: "openai",
31
+ envVar: "OPENAI_API_KEY",
32
+ envHint: "Get your key at https://platform.openai.com/api-keys"
33
+ },
34
+ {
35
+ key: "2",
36
+ label: "Anthropic",
37
+ provider: "anthropic",
38
+ envVar: "ANTHROPIC_API_KEY",
39
+ envHint: "Get your key at https://console.anthropic.com/settings/keys"
40
+ },
41
+ {
42
+ key: "3",
43
+ label: "Google Gemini",
44
+ provider: "google",
45
+ envVar: "GEMINI_API_KEY",
46
+ envHint: "Get your key at https://aistudio.google.com/apikey"
47
+ },
48
+ {
49
+ key: "4",
50
+ label: "Google Vertex AI",
51
+ provider: "vertex",
52
+ envVar: "GOOGLE_CLOUD_PROJECT",
53
+ envHint: "Requires `gcloud auth application-default login` and a GCP project ID"
54
+ }
55
+ ];
56
+ function promptUser(rl, question) {
57
+ return new Promise((resolve) => {
58
+ rl.question(question, (answer) => {
59
+ resolve(answer.trim());
60
+ });
61
+ });
62
+ }
63
+ function ensurePinnedDefaultModel(status) {
64
+ if (status.source !== "config") {
65
+ writeAiConfig(status.model);
66
+ return { ...status, source: "config" };
67
+ }
68
+ return status;
69
+ }
70
+ function printHealthySummary(status) {
71
+ console.log(` \u2713 Model: ${status.model}`);
72
+ console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
73
+ console.log(
74
+ " To change: npx libretto ai configure openai | anthropic | gemini | vertex"
75
+ );
76
+ }
77
+ function printInvalidAiConfigWarning(status) {
78
+ if (status.kind !== "invalid-config") return;
79
+ console.log(" ! Existing AI config is invalid:");
80
+ for (const line of status.message.split("\n")) {
81
+ console.log(` ${line}`);
82
+ }
83
+ }
84
+ function buildRepairPlan(status) {
85
+ if (status.kind === "configured-missing-credentials") {
86
+ const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
87
+ return {
88
+ kind: "repair-missing-credentials",
89
+ provider: status.provider,
90
+ model: status.model,
91
+ envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
92
+ choices: ["enter-matching-credential", "switch-provider", "skip"]
93
+ };
94
+ }
95
+ if (status.kind === "invalid-config") {
96
+ return { kind: "repair-invalid-config", message: status.message };
97
+ }
98
+ return { kind: "no-repair-needed" };
99
+ }
100
+ function formatMissingCredentialsMessage(plan) {
101
+ return [
102
+ ` \u2717 ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`
103
+ ].join("\n");
104
+ }
105
+ function printSnapshotApiStatus() {
106
+ const status = resolveAiSetupStatus();
107
+ const envPath = join(REPO_ROOT, ".env");
108
+ console.log("\nSnapshot analysis:");
109
+ console.log(
110
+ " Libretto uses direct API calls for snapshot analysis when supported credentials are available."
111
+ );
112
+ console.log(` Credentials are loaded from process env and ${envPath}.`);
113
+ if (status.kind === "ready") {
114
+ const pinned = ensurePinnedDefaultModel(status);
115
+ printHealthySummary(pinned);
116
+ return true;
117
+ }
118
+ const plan = buildRepairPlan(status);
119
+ if (plan.kind === "repair-missing-credentials") {
120
+ console.log(formatMissingCredentialsMessage(plan));
121
+ console.log(
122
+ ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`
123
+ );
124
+ return false;
125
+ }
126
+ if (plan.kind === "repair-invalid-config") {
127
+ printInvalidAiConfigWarning(status);
128
+ console.log(" Run `npx libretto setup` interactively to reconfigure.");
129
+ return false;
130
+ }
131
+ console.log(" \u2717 No snapshot API credentials detected.");
132
+ console.log(" Add one provider to .env:");
133
+ console.log(" OPENAI_API_KEY=...");
134
+ console.log(" ANTHROPIC_API_KEY=...");
135
+ console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
136
+ console.log(
137
+ " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
138
+ );
139
+ console.log(
140
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
141
+ );
142
+ console.log(
143
+ " Run `npx libretto setup` interactively to set up credentials."
144
+ );
145
+ return false;
146
+ }
147
+ function writeEnvVar(envVar, value, envPath) {
148
+ let envContent = "";
149
+ if (existsSync(envPath)) {
150
+ envContent = readFileSync(envPath, "utf-8");
151
+ }
152
+ const envLine = `${envVar}=${value}`;
153
+ if (envContent.includes(`${envVar}=`)) {
154
+ const updated = envContent.replace(
155
+ new RegExp(`^${envVar}=.*$`, "m"),
156
+ () => envLine
157
+ );
158
+ writeFileSync(envPath, updated);
159
+ console.log(`
160
+ \u2713 Updated ${envVar} in ${envPath}`);
161
+ } else {
162
+ const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
163
+ appendFileSync(envPath, `${separator}${envLine}
164
+ `);
165
+ console.log(`
166
+ \u2713 Added ${envVar} to ${envPath}`);
167
+ }
168
+ process.env[envVar] = value;
169
+ }
170
+ async function promptForCredential(rl, choice, envPath, modelOverride) {
171
+ console.log(`
172
+ ${choice.label} selected.`);
173
+ console.log(` ${choice.envHint}
174
+ `);
175
+ const apiKeyValue = await promptUser(rl, ` Enter your ${choice.envVar}: `);
176
+ if (!apiKeyValue) {
177
+ console.log("\n No value entered. Skipping API key setup.");
178
+ return false;
179
+ }
180
+ writeEnvVar(choice.envVar, apiKeyValue, envPath);
181
+ loadSnapshotEnv();
182
+ const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
183
+ writeAiConfig(model);
184
+ console.log(` \u2713 Snapshot API ready: ${model}`);
185
+ return true;
186
+ }
187
+ async function promptProviderSelection(rl, envPath) {
188
+ console.log(
189
+ " Which API provider would you like to use for snapshot analysis?\n"
190
+ );
191
+ for (const choice of PROVIDER_CHOICES) {
192
+ console.log(` ${choice.key}) ${choice.label}`);
193
+ }
194
+ console.log(" s) Skip for now\n");
195
+ const answer = await promptUser(rl, " Choice: ");
196
+ if (answer.toLowerCase() === "s" || !answer) {
197
+ printSkipMessage();
198
+ return false;
199
+ }
200
+ const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
201
+ if (!selected) {
202
+ console.log(`
203
+ Unknown choice "${answer}". Skipping API setup.`);
204
+ return false;
205
+ }
206
+ return promptForCredential(rl, selected, envPath);
207
+ }
208
+ function printSkipMessage() {
209
+ console.log(
210
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto setup`."
211
+ );
212
+ console.log(" Or add credentials directly to your .env file:");
213
+ console.log(" OPENAI_API_KEY=...");
214
+ console.log(" ANTHROPIC_API_KEY=...");
215
+ console.log(" GEMINI_API_KEY=...");
216
+ console.log(
217
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
218
+ );
219
+ }
220
+ async function runInteractiveApiSetup() {
221
+ const status = resolveAiSetupStatus();
222
+ const envPath = join(REPO_ROOT, ".env");
223
+ console.log("\nSnapshot analysis setup:");
224
+ console.log(" Libretto uses direct API calls for snapshot analysis.");
225
+ console.log(` Credentials are loaded from process env and ${envPath}.`);
226
+ if (status.kind === "ready") {
227
+ const pinned = ensurePinnedDefaultModel(status);
228
+ printHealthySummary(pinned);
229
+ return;
230
+ }
231
+ const plan = buildRepairPlan(status);
232
+ const rl = createInterface({
233
+ input: process.stdin,
234
+ output: process.stdout
235
+ });
236
+ try {
237
+ if (plan.kind === "repair-missing-credentials") {
238
+ console.log(formatMissingCredentialsMessage(plan));
239
+ console.log("");
240
+ console.log(" How would you like to fix this?\n");
241
+ console.log(` 1) Enter ${plan.envVar}`);
242
+ console.log(" 2) Switch to a different provider");
243
+ console.log(" s) Skip for now\n");
244
+ const answer = await promptUser(rl, " Choice: ");
245
+ if (answer === "1") {
246
+ const matchingChoice = PROVIDER_CHOICES.find(
247
+ (c) => c.provider === plan.provider
248
+ );
249
+ if (matchingChoice) {
250
+ await promptForCredential(rl, matchingChoice, envPath, plan.model);
251
+ }
252
+ return;
253
+ }
254
+ if (answer === "2") {
255
+ await promptProviderSelection(rl, envPath);
256
+ return;
257
+ }
258
+ printSkipMessage();
259
+ return;
260
+ }
261
+ if (plan.kind === "repair-invalid-config") {
262
+ printInvalidAiConfigWarning(status);
263
+ console.log(
264
+ "\n Would you like to reconfigure with a fresh provider selection?\n"
265
+ );
266
+ await promptProviderSelection(rl, envPath);
267
+ return;
268
+ }
269
+ console.log(" \u2717 No snapshot API credentials detected.\n");
270
+ await promptProviderSelection(rl, envPath);
271
+ } finally {
272
+ rl.close();
273
+ }
274
+ }
275
+ function installBrowsers() {
276
+ console.log("\nInstalling Playwright Chromium...");
277
+ const result = spawnSync("npx", ["playwright", "install", "chromium"], {
278
+ stdio: "inherit",
279
+ shell: true
280
+ });
281
+ if (result.status === 0) {
282
+ console.log(" \u2713 Playwright Chromium installed");
283
+ } else {
284
+ console.error(
285
+ " \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
286
+ );
287
+ }
288
+ }
289
+ function getPackageSkillsRoot() {
290
+ const thisFile = fileURLToPath(import.meta.url);
291
+ let dir = dirname(thisFile);
292
+ while (dir !== dirname(dir)) {
293
+ if (existsSync(join(dir, "skills", "libretto"))) {
294
+ return join(dir, "skills");
295
+ }
296
+ dir = dirname(dir);
297
+ }
298
+ throw new Error("Could not locate libretto skill files in package");
299
+ }
300
+ function detectAgentDirs(root) {
301
+ const dirs = [];
302
+ if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
303
+ if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
304
+ return dirs;
305
+ }
306
+ function copySkills() {
307
+ const agentDirs = detectAgentDirs(REPO_ROOT);
308
+ if (agentDirs.length === 0) {
309
+ console.log(
310
+ "\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
311
+ );
312
+ return;
313
+ }
314
+ let skillsRoot;
315
+ try {
316
+ skillsRoot = getPackageSkillsRoot();
317
+ } catch (e) {
318
+ console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
319
+ return;
320
+ }
321
+ const skillNames = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
322
+ for (const agentDir of agentDirs) {
323
+ const agentName = basename(agentDir);
324
+ for (const skillName of skillNames) {
325
+ const sourceDir = join(skillsRoot, skillName);
326
+ const skillDest = join(agentDir, "skills", skillName);
327
+ if (existsSync(skillDest)) {
328
+ rmSync(skillDest, { recursive: true });
329
+ }
330
+ cpSync(sourceDir, skillDest, { recursive: true });
331
+ const fileCount = readdirSync(skillDest).length;
332
+ console.log(
333
+ ` \u2713 Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`
334
+ );
335
+ }
336
+ }
337
+ }
338
+ const setupInput = SimpleCLI.input({
339
+ positionals: [],
340
+ named: {
341
+ skipBrowsers: SimpleCLI.flag({
342
+ name: "skip-browsers",
343
+ help: "Skip Playwright Chromium installation"
344
+ })
345
+ }
346
+ });
347
+ const setupCommand = SimpleCLI.command({
348
+ description: "Set up libretto in the current project"
349
+ }).input(setupInput).handle(async ({ input }) => {
350
+ console.log("Setting up libretto...\n");
351
+ ensureLibrettoSetup();
352
+ if (!input.skipBrowsers) {
353
+ installBrowsers();
354
+ } else {
355
+ console.log("\nSkipping browser installation (--skip-browsers)");
356
+ }
357
+ copySkills();
358
+ if (process.stdin.isTTY) {
359
+ await runInteractiveApiSetup();
360
+ } else {
361
+ const ready = printSnapshotApiStatus();
362
+ if (!ready) {
363
+ console.log(
364
+ "\nIf you're an agent, request the user to run `npx libretto setup`."
365
+ );
366
+ }
367
+ }
368
+ console.log("\n\u2713 libretto setup complete");
369
+ });
370
+ export {
371
+ PROVIDER_CHOICES,
372
+ buildRepairPlan,
373
+ formatMissingCredentialsMessage,
374
+ setupCommand,
375
+ setupInput
376
+ };
@@ -7,8 +7,8 @@ import { readSessionState } from "../core/session.js";
7
7
  import { SimpleCLI } from "../framework/simple-cli.js";
8
8
  import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
9
9
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
10
- import { readAiConfig } from "../core/ai-config.js";
11
- import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
10
+ import { readAiConfig } from "../core/config.js";
11
+ import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
12
12
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
13
13
  function generateSnapshotRunId() {
14
14
  return `snapshot-${Date.now()}`;