libretto 0.6.11 → 0.6.13

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 (130) hide show
  1. package/README.md +7 -8
  2. package/README.template.md +7 -8
  3. package/dist/cli/cli.js +0 -22
  4. package/dist/cli/commands/browser.js +18 -24
  5. package/dist/cli/commands/execution.js +254 -234
  6. package/dist/cli/commands/experiments.js +100 -0
  7. package/dist/cli/commands/setup.js +3 -310
  8. package/dist/cli/commands/shared.js +10 -0
  9. package/dist/cli/commands/snapshot.js +46 -64
  10. package/dist/cli/commands/status.js +1 -40
  11. package/dist/cli/core/browser.js +303 -124
  12. package/dist/cli/core/config.js +5 -6
  13. package/dist/cli/core/context.js +4 -0
  14. package/dist/cli/core/daemon/config.js +0 -6
  15. package/dist/cli/core/daemon/daemon.js +497 -90
  16. package/dist/cli/core/daemon/ipc.js +170 -129
  17. package/dist/cli/core/daemon/snapshot.js +48 -9
  18. package/dist/cli/core/experiments.js +39 -0
  19. package/dist/cli/core/session.js +5 -4
  20. package/dist/cli/core/skill-version.js +2 -1
  21. package/dist/cli/core/workflow-runner/runner.js +147 -0
  22. package/dist/cli/core/workflow-runtime.js +60 -0
  23. package/dist/cli/index.js +0 -2
  24. package/dist/cli/router.js +4 -3
  25. package/dist/shared/debug/pause-handler.d.ts +9 -0
  26. package/dist/shared/debug/pause-handler.js +15 -0
  27. package/dist/shared/debug/pause.d.ts +1 -2
  28. package/dist/shared/debug/pause.js +13 -36
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  31. package/dist/shared/ipc/child-process-transport.js +60 -0
  32. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  33. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  34. package/dist/shared/ipc/ipc.d.ts +46 -0
  35. package/dist/shared/ipc/ipc.js +165 -0
  36. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  37. package/dist/shared/ipc/ipc.spec.js +114 -0
  38. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  39. package/dist/shared/ipc/socket-transport.js +143 -0
  40. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  41. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  42. package/dist/shared/package-manager.d.ts +7 -0
  43. package/dist/shared/package-manager.js +60 -0
  44. package/dist/shared/paths/paths.d.ts +1 -8
  45. package/dist/shared/paths/paths.js +1 -49
  46. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  47. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  48. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  49. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  50. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  51. package/dist/shared/snapshot/render-snapshot.js +651 -0
  52. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  53. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  54. package/dist/shared/snapshot/types.d.ts +40 -0
  55. package/dist/shared/snapshot/types.js +0 -0
  56. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  58. package/dist/shared/state/session-state.d.ts +1 -0
  59. package/dist/shared/state/session-state.js +1 -0
  60. package/docs/experiments.md +67 -0
  61. package/docs/releasing.md +8 -6
  62. package/package.json +5 -2
  63. package/skills/libretto/SKILL.md +19 -19
  64. package/skills/libretto/references/configuration-file-reference.md +6 -12
  65. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  66. package/skills/libretto-readonly/SKILL.md +2 -9
  67. package/src/cli/AGENTS.md +7 -0
  68. package/src/cli/cli.ts +0 -23
  69. package/src/cli/commands/browser.ts +14 -18
  70. package/src/cli/commands/execution.ts +303 -271
  71. package/src/cli/commands/experiments.ts +120 -0
  72. package/src/cli/commands/setup.ts +3 -400
  73. package/src/cli/commands/shared.ts +20 -0
  74. package/src/cli/commands/snapshot.ts +54 -94
  75. package/src/cli/commands/status.ts +1 -48
  76. package/src/cli/core/browser.ts +372 -150
  77. package/src/cli/core/config.ts +4 -5
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +645 -107
  81. package/src/cli/core/daemon/ipc.ts +319 -214
  82. package/src/cli/core/daemon/snapshot.ts +71 -15
  83. package/src/cli/core/experiments.ts +56 -0
  84. package/src/cli/core/resolve-model.ts +5 -0
  85. package/src/cli/core/session.ts +5 -4
  86. package/src/cli/core/skill-version.ts +2 -1
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +86 -0
  89. package/src/cli/index.ts +0 -1
  90. package/src/cli/router.ts +4 -3
  91. package/src/shared/debug/pause-handler.ts +20 -0
  92. package/src/shared/debug/pause.ts +14 -48
  93. package/src/shared/instrumentation/instrument.ts +4 -4
  94. package/src/shared/ipc/AGENTS.md +24 -0
  95. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  96. package/src/shared/ipc/child-process-transport.ts +96 -0
  97. package/src/shared/ipc/ipc.spec.ts +161 -0
  98. package/src/shared/ipc/ipc.ts +288 -0
  99. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  100. package/src/shared/ipc/socket-transport.ts +189 -0
  101. package/src/shared/package-manager.ts +76 -0
  102. package/src/shared/paths/paths.ts +0 -72
  103. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  104. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  105. package/src/shared/snapshot/render-snapshot.ts +962 -0
  106. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  107. package/src/shared/snapshot/types.ts +43 -0
  108. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  109. package/src/shared/state/session-state.ts +1 -0
  110. package/dist/cli/commands/ai.js +0 -109
  111. package/dist/cli/core/ai-model.js +0 -192
  112. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  113. package/dist/cli/core/daemon/index.js +0 -16
  114. package/dist/cli/core/daemon/spawn.js +0 -90
  115. package/dist/cli/core/pause-signals.js +0 -29
  116. package/dist/cli/core/snapshot-analyzer.js +0 -666
  117. package/dist/cli/workers/run-integration-runtime.js +0 -235
  118. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  119. package/dist/cli/workers/run-integration-worker.js +0 -64
  120. package/scripts/summarize-evals.mjs +0 -135
  121. package/src/cli/commands/ai.ts +0 -143
  122. package/src/cli/core/ai-model.ts +0 -298
  123. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  124. package/src/cli/core/daemon/index.ts +0 -24
  125. package/src/cli/core/daemon/spawn.ts +0 -171
  126. package/src/cli/core/pause-signals.ts +0 -35
  127. package/src/cli/core/snapshot-analyzer.ts +0 -855
  128. package/src/cli/workers/run-integration-runtime.ts +0 -326
  129. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  130. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ import { librettoCommand } from "../../shared/package-manager.js";
3
+ import {
4
+ EXPERIMENTS,
5
+ isExperimentName,
6
+ resolveExperiments,
7
+ setExperimentEnabled,
8
+ type ExperimentName,
9
+ type Experiments,
10
+ } from "../core/experiments.js";
11
+ import { SimpleCLI } from "../framework/simple-cli.js";
12
+
13
+ const experimentNames = Object.keys(EXPERIMENTS) as ExperimentName[];
14
+
15
+ const experimentsUsage = [
16
+ "Usage:",
17
+ ` ${librettoCommand("experiments")}`,
18
+ ` ${librettoCommand("experiments describe <experiment>")}`,
19
+ ` ${librettoCommand("experiments enable <experiment>")}`,
20
+ ` ${librettoCommand("experiments disable <experiment>")}`,
21
+ ].join("\n");
22
+
23
+ export const experimentsInput = SimpleCLI.input({
24
+ positionals: [
25
+ SimpleCLI.positional("action", z.string().optional(), {
26
+ help: "Action to apply",
27
+ }),
28
+ SimpleCLI.positional("experiment", z.string().optional(), {
29
+ help: "Experiment name",
30
+ }),
31
+ ],
32
+ named: {},
33
+ });
34
+
35
+ function formatAvailableExperiments(): string {
36
+ return [
37
+ "Available experiments:",
38
+ ...experimentNames.map((name) => ` ${name}`),
39
+ ].join("\n");
40
+ }
41
+
42
+ function experimentUsageError(message: string): Error {
43
+ return new Error(
44
+ [message, "", experimentsUsage, "", formatAvailableExperiments()].join(
45
+ "\n",
46
+ ),
47
+ );
48
+ }
49
+
50
+ function printExperiments(experiments: Experiments): void {
51
+ console.log("Libretto experiments:");
52
+ for (const name of experimentNames) {
53
+ const metadata = EXPERIMENTS[name];
54
+ console.log(
55
+ `- ${name}: ${experiments[name] ? "enabled" : "disabled"} — ${metadata.title}`,
56
+ );
57
+ console.log(` ${metadata.oneSentenceDescription}`);
58
+ }
59
+ }
60
+
61
+ function printExperimentDescription(
62
+ name: ExperimentName,
63
+ experiments: Experiments,
64
+ ): void {
65
+ const metadata = EXPERIMENTS[name];
66
+ console.log(`${metadata.title} (${name})`);
67
+ console.log(`Status: ${experiments[name] ? "enabled" : "disabled"}`);
68
+ console.log("");
69
+ if (experiments[name]) {
70
+ console.log(
71
+ "Since this experiment is enabled, Libretto’s expected usage deviates from the skill. Use these instructions where they differ:",
72
+ );
73
+ console.log("");
74
+ }
75
+ console.log(metadata.docs ?? metadata.oneSentenceDescription);
76
+ }
77
+
78
+ export const experimentsCommand = SimpleCLI.command({
79
+ description: "List or update Libretto experiment flags",
80
+ })
81
+ .input(experimentsInput)
82
+ .handle(async ({ input }) => {
83
+ if (!input.action) {
84
+ printExperiments(resolveExperiments());
85
+ return;
86
+ }
87
+
88
+ if (
89
+ input.action !== "describe" &&
90
+ input.action !== "enable" &&
91
+ input.action !== "disable"
92
+ ) {
93
+ throw experimentUsageError(`Unknown experiments action "${input.action}".`);
94
+ }
95
+
96
+ if (!input.experiment) {
97
+ throw experimentUsageError(
98
+ `Missing experiment name for ${input.action}.`,
99
+ );
100
+ }
101
+
102
+ if (!isExperimentName(input.experiment)) {
103
+ throw experimentUsageError(`Unknown experiment "${input.experiment}".`);
104
+ }
105
+
106
+ if (input.action === "describe") {
107
+ printExperimentDescription(input.experiment, resolveExperiments());
108
+ return;
109
+ }
110
+
111
+ const experiments = setExperimentEnabled(
112
+ input.experiment,
113
+ input.action === "enable",
114
+ );
115
+ console.log(`Experiment "${input.experiment}" ${input.action}d.`);
116
+ if (input.action === "enable") {
117
+ console.log("");
118
+ printExperimentDescription(input.experiment, experiments);
119
+ }
120
+ });
@@ -1,401 +1,15 @@
1
- import { createInterface } from "node:readline";
2
- import {
3
- cpSync,
4
- existsSync,
5
- readdirSync,
6
- rmSync,
7
- } from "node:fs";
1
+ import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
8
2
  import { spawnSync } from "node:child_process";
9
3
  import { basename, dirname, join } from "node:path";
10
4
  import { fileURLToPath } from "node:url";
11
- import { writeSnapshotModel } from "../core/config.js";
12
5
  import {
13
6
  ensureLibrettoSetup,
14
7
  LIBRETTO_CONFIG_PATH,
15
8
  REPO_ROOT,
16
9
  } from "../core/context.js";
17
- import {
18
- type AiSetupStatus,
19
- DEFAULT_SNAPSHOT_MODELS,
20
- resolveAiSetupStatus,
21
- } from "../core/ai-model.js";
22
- import type { Provider } from "../core/resolve-model.js";
10
+ import { librettoCommand } from "../../shared/package-manager.js";
23
11
  import { SimpleCLI } from "../framework/simple-cli.js";
24
12
 
25
- const PROVIDER_SDK_PACKAGES: Record<Provider, string> = {
26
- openai: "@ai-sdk/openai",
27
- anthropic: "@ai-sdk/anthropic",
28
- google: "@ai-sdk/google",
29
- vertex: "@ai-sdk/google-vertex",
30
- openrouter: "@ai-sdk/openai",
31
- };
32
-
33
- function detectPackageManager(): string {
34
- if (existsSync(join(REPO_ROOT, "pnpm-lock.yaml"))) return "pnpm";
35
- if (existsSync(join(REPO_ROOT, "yarn.lock"))) return "yarn";
36
- if (existsSync(join(REPO_ROOT, "bun.lockb"))) return "bun";
37
- return "npm";
38
- }
39
-
40
- function installCommand(pkgManager: string): string {
41
- switch (pkgManager) {
42
- case "yarn":
43
- return "yarn add";
44
- case "bun":
45
- return "bun add";
46
- case "pnpm":
47
- return "pnpm add";
48
- default:
49
- return "npm install";
50
- }
51
- }
52
-
53
- function isSdkInstalled(sdkPackage: string): boolean {
54
- try {
55
- const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
56
- cwd: REPO_ROOT,
57
- stdio: "pipe",
58
- });
59
- return result.status === 0;
60
- } catch {
61
- return false;
62
- }
63
- }
64
-
65
- function installSdkIfNeeded(provider: Provider): void {
66
- const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
67
- if (isSdkInstalled(sdkPackage)) return;
68
-
69
- const pkgManager = detectPackageManager();
70
- const cmd = installCommand(pkgManager);
71
- console.log(`\nInstalling ${sdkPackage}...`);
72
- const result = spawnSync(cmd, [sdkPackage], {
73
- cwd: REPO_ROOT,
74
- stdio: "inherit",
75
- shell: true,
76
- });
77
- if (result.status === 0) {
78
- console.log(`✓ Installed ${sdkPackage}`);
79
- } else {
80
- console.error(
81
- `✗ Failed to install ${sdkPackage}. Install it manually: ${cmd} ${sdkPackage}`,
82
- );
83
- }
84
- }
85
-
86
- export type ProviderChoice = {
87
- key: string;
88
- label: string;
89
- provider: Provider;
90
- envVar: string;
91
- envHint: string;
92
- };
93
-
94
- export const PROVIDER_CHOICES: ProviderChoice[] = [
95
- {
96
- key: "1",
97
- label: "OpenAI",
98
- provider: "openai",
99
- envVar: "OPENAI_API_KEY",
100
- envHint: "Get your key at https://platform.openai.com/api-keys",
101
- },
102
- {
103
- key: "2",
104
- label: "Anthropic",
105
- provider: "anthropic",
106
- envVar: "ANTHROPIC_API_KEY",
107
- envHint: "Get your key at https://console.anthropic.com/settings/keys",
108
- },
109
- {
110
- key: "3",
111
- label: "Google Gemini",
112
- provider: "google",
113
- envVar: "GEMINI_API_KEY",
114
- envHint: "Get your key at https://aistudio.google.com/apikey",
115
- },
116
- {
117
- key: "4",
118
- label: "Google Vertex AI",
119
- provider: "vertex",
120
- envVar: "GOOGLE_CLOUD_PROJECT",
121
- envHint:
122
- "Requires `gcloud auth application-default login` and a GCP project ID",
123
- },
124
- {
125
- key: "5",
126
- label: "OpenRouter",
127
- provider: "openrouter",
128
- envVar: "OPENROUTER_API_KEY",
129
- envHint: "Get your key at https://openrouter.ai/settings/keys",
130
- },
131
- ];
132
-
133
- function promptUser(
134
- rl: ReturnType<typeof createInterface>,
135
- question: string,
136
- ): Promise<string> {
137
- return new Promise((resolve) => {
138
- rl.question(question, (answer) => {
139
- resolve(answer.trim());
140
- });
141
- });
142
- }
143
-
144
- /** Map provider to a human-readable label for status messages. */
145
- function providerLabel(provider: Provider): string {
146
- const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
147
- return choice?.label ?? provider;
148
- }
149
-
150
- /** Extract the env var name from source like "env:GOOGLE_CLOUD_PROJECT". */
151
- function sourceEnvVar(source: string): string | null {
152
- if (source.startsWith("env:")) return source.slice(4);
153
- return null;
154
- }
155
-
156
- /**
157
- * If the workspace has usable credentials but no pinned model in config,
158
- * write the resolved default model to `.libretto/config.json`.
159
- */
160
- function ensurePinnedDefaultModel(
161
- status: AiSetupStatus & { kind: "ready" },
162
- ): AiSetupStatus & { kind: "ready" } {
163
- if (status.source !== "config") {
164
- writeSnapshotModel(status.model);
165
- return { ...status, source: "config" as const };
166
- }
167
- return status;
168
- }
169
-
170
- function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
171
- const envVar = sourceEnvVar(status.source);
172
- if (envVar) {
173
- console.log(
174
- `✓ Detected ${envVar}. Using ${providerLabel(status.provider)}.`,
175
- );
176
- } else {
177
- console.log(`✓ Using ${providerLabel(status.provider)} (${status.model}).`);
178
- }
179
- console.log(
180
- "To change: npx libretto ai configure openai | anthropic | gemini | vertex | openrouter",
181
- );
182
- }
183
-
184
- function printInvalidAiConfigWarning(status: AiSetupStatus): void {
185
- if (status.kind !== "invalid-config") return;
186
- console.log("! Existing AI config is invalid:");
187
- for (const line of status.message.split("\n")) {
188
- console.log(` ${line}`);
189
- }
190
- }
191
-
192
- // ── Repair plan helpers (exported for testing) ──────────────────────────────
193
-
194
- export type RepairChoice = "switch-provider" | "skip";
195
-
196
- export type RepairPlan =
197
- | {
198
- kind: "repair-missing-credentials";
199
- provider: Provider;
200
- model: string;
201
- envVar: string;
202
- choices: RepairChoice[];
203
- }
204
- | { kind: "repair-invalid-config"; message: string }
205
- | { kind: "no-repair-needed" };
206
-
207
- /**
208
- * Determine what repair action setup should take for the current AI status.
209
- * Pure function — no I/O, no prompts.
210
- */
211
- export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
212
- if (status.kind === "configured-missing-credentials") {
213
- const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
214
- return {
215
- kind: "repair-missing-credentials",
216
- provider: status.provider,
217
- model: status.model,
218
- envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
219
- choices: ["switch-provider", "skip"],
220
- };
221
- }
222
- if (status.kind === "invalid-config") {
223
- return { kind: "repair-invalid-config", message: status.message };
224
- }
225
- return { kind: "no-repair-needed" };
226
- }
227
-
228
- /**
229
- * Format a provider-specific explanation for missing credentials.
230
- */
231
- export function formatMissingCredentialsMessage(
232
- plan: RepairPlan & { kind: "repair-missing-credentials" },
233
- ): string {
234
- return `✗ ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
235
- }
236
-
237
- function printSnapshotApiStatus(): boolean {
238
- const status = resolveAiSetupStatus();
239
-
240
- console.log(
241
- "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
242
- );
243
-
244
- if (status.kind === "ready") {
245
- console.log();
246
- printHealthySummary(status);
247
- ensurePinnedDefaultModel(status);
248
- return true;
249
- }
250
-
251
- // Provider-specific missing-credentials message
252
- const plan = buildRepairPlan(status);
253
- if (plan.kind === "repair-missing-credentials") {
254
- console.log();
255
- console.log(formatMissingCredentialsMessage(plan));
256
- console.log(
257
- ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`,
258
- );
259
- return false;
260
- }
261
-
262
- if (plan.kind === "repair-invalid-config") {
263
- printInvalidAiConfigWarning(status);
264
- console.log(" Run `npx libretto setup` interactively to reconfigure.");
265
- return false;
266
- }
267
-
268
- console.log();
269
- console.log("✗ No snapshot API credentials detected.");
270
- console.log(" Add one provider to .env:");
271
- console.log(" OPENAI_API_KEY=...");
272
- console.log(" ANTHROPIC_API_KEY=...");
273
- console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
274
- console.log(
275
- " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
276
- );
277
- console.log(
278
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model.",
279
- );
280
- console.log(
281
- " Run `npx libretto setup` interactively to set up credentials.",
282
- );
283
- return false;
284
- }
285
-
286
- /**
287
- * Run the full provider selection menu.
288
- * Pins the selected provider's default model to config and prints
289
- * instructions for the user to add the credential to .env themselves.
290
- * Returns true if a provider was successfully configured.
291
- */
292
- async function promptProviderSelection(
293
- rl: ReturnType<typeof createInterface>,
294
- ): Promise<boolean> {
295
- console.log(
296
- "Which model provider would you like to use for snapshot analysis?\n",
297
- );
298
- for (const choice of PROVIDER_CHOICES) {
299
- console.log(` ${choice.key}) ${choice.label}`);
300
- }
301
- console.log(" s) Skip for now\n");
302
-
303
- const answer = await promptUser(rl, "Choice: ");
304
-
305
- if (answer.toLowerCase() === "s" || !answer) {
306
- printSkipMessage();
307
- return false;
308
- }
309
-
310
- const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
311
- if (!selected) {
312
- console.log(`\nUnknown choice "${answer}". Skipping API setup.`);
313
- return false;
314
- }
315
-
316
- const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
317
- writeSnapshotModel(model);
318
- console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
319
- console.log(`\nAdd ${selected.envVar} to your .env file:`);
320
- console.log(` ${selected.envHint}`);
321
- installSdkIfNeeded(selected.provider);
322
- return true;
323
- }
324
-
325
- function printSkipMessage(): void {
326
- console.log(
327
- "\nSkipped. You can set up API credentials later by rerunning `npx libretto setup`.",
328
- );
329
- console.log("Or add credentials directly to your .env file:");
330
- console.log(" OPENAI_API_KEY=...");
331
- console.log(" ANTHROPIC_API_KEY=...");
332
- console.log(" GEMINI_API_KEY=...");
333
- console.log(
334
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model.",
335
- );
336
- }
337
-
338
- async function runInteractiveApiSetup(): Promise<void> {
339
- const status = resolveAiSetupStatus();
340
-
341
- console.log(
342
- "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
343
- );
344
-
345
- if (status.kind === "ready") {
346
- console.log();
347
- printHealthySummary(status);
348
- ensurePinnedDefaultModel(status);
349
- return;
350
- }
351
-
352
- const plan = buildRepairPlan(status);
353
-
354
- const rl = createInterface({
355
- input: process.stdin,
356
- output: process.stdout,
357
- });
358
-
359
- try {
360
- // ── Repair: configured provider with missing credentials ──
361
- if (plan.kind === "repair-missing-credentials") {
362
- console.log(formatMissingCredentialsMessage(plan));
363
- console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
364
- console.log("");
365
- console.log("Or switch to a different provider:\n");
366
- console.log(" 1) Switch to a different provider");
367
- console.log(" s) Skip for now\n");
368
-
369
- const answer = await promptUser(rl, "Choice: ");
370
-
371
- if (answer === "1") {
372
- await promptProviderSelection(rl);
373
- return;
374
- }
375
-
376
- // skip or empty
377
- printSkipMessage();
378
- return;
379
- }
380
-
381
- // ── Repair: invalid config → let user pick a provider ──
382
- if (plan.kind === "repair-invalid-config") {
383
- printInvalidAiConfigWarning(status);
384
- console.log(
385
- "\nWould you like to reconfigure with a fresh provider selection?\n",
386
- );
387
- await promptProviderSelection(rl);
388
- return;
389
- }
390
-
391
- // ── Unconfigured: standard first-run flow ──
392
- console.log("✗ No snapshot API credentials detected.\n");
393
- await promptProviderSelection(rl);
394
- } finally {
395
- rl.close();
396
- }
397
- }
398
-
399
13
  function installBrowsers(): void {
400
14
  console.log("Installing Playwright Chromium...");
401
15
  const result = spawnSync("npx", ["playwright", "install", "chromium"], {
@@ -442,7 +56,7 @@ function copySkills(): void {
442
56
  "\n⚠️ No .agents/ or .claude/ directory found. Libretto skills were not installed.",
443
57
  );
444
58
  console.log(
445
- " Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:",
59
+ ` Create one of these directories in your repo root and rerun \`${librettoCommand("setup")}\` to install skills:`,
446
60
  );
447
61
  console.log(` mkdir ${join(REPO_ROOT, ".claude")}`);
448
62
  return;
@@ -504,17 +118,6 @@ export const setupCommand = SimpleCLI.command({
504
118
 
505
119
  copySkills();
506
120
 
507
- if (process.stdin.isTTY) {
508
- await runInteractiveApiSetup();
509
- } else {
510
- const ready = printSnapshotApiStatus();
511
- if (!ready) {
512
- console.log(
513
- "\nIf you're an agent, request the user to run `npx libretto setup`.",
514
- );
515
- }
516
- }
517
-
518
121
  console.log(`\nConfig set up at ${LIBRETTO_CONFIG_PATH}`);
519
122
  console.log("\n✓ libretto setup complete");
520
123
  });
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { LoggerApi } from "../../shared/logger/index.js";
3
+ import type { Experiments } from "../core/experiments.js";
4
+ import { resolveExperiments } from "../core/experiments.js";
3
5
  import { createLoggerForSession } from "../core/context.js";
4
6
  import {
5
7
  generateSessionName,
@@ -9,6 +11,8 @@ import {
9
11
  } from "../core/session.js";
10
12
  import {
11
13
  SimpleCLI,
14
+ type SimpleCLIMiddlewareArgs,
15
+ type SimpleCLIContext,
12
16
  type SimpleCLIMiddleware,
13
17
  } from "../framework/simple-cli.js";
14
18
 
@@ -33,6 +37,22 @@ export type SessionStateContext = SessionContext & {
33
37
  sessionState: SessionState;
34
38
  };
35
39
 
40
+ export type ExperimentsContext = {
41
+ experiments: Experiments;
42
+ };
43
+
44
+ export function withExperiments() {
45
+ return async <TInput, TContext extends SimpleCLIContext>({
46
+ ctx,
47
+ }: SimpleCLIMiddlewareArgs<
48
+ TInput,
49
+ TContext
50
+ >): Promise<TContext & ExperimentsContext> => ({
51
+ ...ctx,
52
+ experiments: resolveExperiments(),
53
+ });
54
+ }
55
+
36
56
  export function withRequiredSession(): SimpleCLIMiddleware<
37
57
  { session?: string },
38
58
  {},