libretto 0.6.12 → 0.6.14

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 (82) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -11,7 +11,7 @@ import {
11
11
  } from "../core/browser.js";
12
12
  import { resolveProviderName } from "../core/providers/index.js";
13
13
  import { readLibrettoConfig } from "../core/config.js";
14
- import { createLoggerForSession, withSessionLogger } from "../core/context.js";
14
+ import { createLoggerForSession } from "../core/context.js";
15
15
  import { librettoCommand } from "../../shared/package-manager.js";
16
16
  import {
17
17
  type SessionAccessMode,
@@ -20,7 +20,7 @@ import {
20
20
  validateSessionName,
21
21
  } from "../core/session.js";
22
22
  import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
23
- import { SimpleCLI } from "../framework/simple-cli.js";
23
+ import { SimpleCLI } from "affordance";
24
24
  import {
25
25
  sessionOption,
26
26
  withAutoSession,
@@ -63,8 +63,8 @@ function resolveRequestedSessionMode(
63
63
 
64
64
  export const openInput = SimpleCLI.input({
65
65
  positionals: [
66
- SimpleCLI.positional("url", z.string().optional(), {
67
- help: "URL to open",
66
+ SimpleCLI.positional("url", z.string().default("about:blank"), {
67
+ help: "URL to open (defaults to about:blank)",
68
68
  }),
69
69
  ],
70
70
  named: {
@@ -92,10 +92,6 @@ export const openInput = SimpleCLI.input({
92
92
  }),
93
93
  },
94
94
  })
95
- .refine(
96
- (input) => Boolean(input.url),
97
- `Usage: ${librettoCommand("open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]")}`,
98
- )
99
95
  .refine(
100
96
  (input) => !(input.headed && input.headless),
101
97
  "Cannot pass both --headed and --headless.",
@@ -119,7 +115,7 @@ export const openCommand = SimpleCLI.command({
119
115
  if (providerName === "local") {
120
116
  const headed = input.headed || !input.headless;
121
117
  const viewport = parseViewportArg(input.viewport);
122
- await runOpen(input.url!, headed, ctx.session, ctx.logger, {
118
+ await runOpen(input.url, headed, ctx.session, ctx.logger, {
123
119
  viewport,
124
120
  accessMode: resolveRequestedSessionMode(
125
121
  input.readOnly,
@@ -130,7 +126,7 @@ export const openCommand = SimpleCLI.command({
130
126
  });
131
127
  } else {
132
128
  await runOpenWithProvider(
133
- input.url!,
129
+ input.url,
134
130
  providerName,
135
131
  ctx.session,
136
132
  ctx.logger,
@@ -295,9 +291,3 @@ export const browserCommands = {
295
291
  "session-mode": sessionModeCommand,
296
292
  close: closeCommand,
297
293
  };
298
-
299
- export async function runClose(session: string): Promise<void> {
300
- await withSessionLogger(session, async (logger) => {
301
- await runCloseWithLogger(session, logger);
302
- });
303
- }
@@ -1,8 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { HOSTED_API_URL } from "../core/auth-fetch.js";
3
+ import {
4
+ orpcCall,
5
+ resolveApiUrl,
6
+ } from "../core/auth-fetch.js";
4
7
  import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
5
- import { SimpleCLI } from "../framework/simple-cli.js";
8
+ import { readAuthState } from "../core/auth-storage.js";
9
+ import { SimpleCLI } from "affordance";
6
10
 
7
11
  type DeploymentStatus = "building" | "ready" | "failed";
8
12
 
@@ -19,37 +23,50 @@ function generateDeploymentName(): string {
19
23
  return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
20
24
  }
21
25
 
22
- function getConfig() {
23
- const apiKey = process.env.LIBRETTO_API_KEY;
26
+ function deployApiKeyRequiredMessage(hasStoredSession: boolean): string {
27
+ if (hasStoredSession) {
28
+ return [
29
+ "LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
30
+ "You are logged in locally, but deploy endpoints require API-key auth.",
31
+ " • Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
32
+ " • Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`.",
33
+ ].join("\n");
34
+ }
35
+
36
+ return [
37
+ "LIBRETTO_API_KEY is required to deploy to Libretto Cloud.",
38
+ "No local cloud session was found.",
39
+ " • New account: run `libretto cloud auth signup`, then verify your email.",
40
+ " • Existing account: run `libretto cloud auth login`.",
41
+ " • Generate a key: run `libretto cloud auth api-key issue --label <label>`.",
42
+ " • Add it to your project .env file: `LIBRETTO_API_KEY=<issued-key>`.",
43
+ ].join("\n");
44
+ }
45
+
46
+ async function requireDeployApiKey() {
47
+ const apiKey = process.env.LIBRETTO_API_KEY?.trim();
24
48
 
25
49
  if (!apiKey) {
26
- throw new Error(
27
- "LIBRETTO_API_KEY environment variable is required.",
28
- );
50
+ throw new Error(deployApiKeyRequiredMessage(await hasStoredCloudSession()));
29
51
  }
30
52
 
31
- return { apiUrl: HOSTED_API_URL, apiKey };
53
+ return {
54
+ apiUrl: resolveApiUrl(null),
55
+ credential: { source: "env-api-key" as const, apiKey },
56
+ };
32
57
  }
33
58
 
34
- async function postJson(
35
- apiUrl: string,
36
- apiKey: string,
37
- path: string,
38
- input: Record<string, unknown> = {},
39
- ): Promise<Response> {
40
- return fetch(`${apiUrl}${path}`, {
41
- method: "POST",
42
- headers: {
43
- "x-api-key": apiKey,
44
- "Content-Type": "application/json",
45
- },
46
- body: JSON.stringify({ json: input }),
47
- });
59
+ async function hasStoredCloudSession(): Promise<boolean> {
60
+ try {
61
+ return Boolean((await readAuthState())?.session);
62
+ } catch {
63
+ return false;
64
+ }
48
65
  }
49
66
 
50
67
  async function pollDeployment(
51
68
  apiUrl: string,
52
- apiKey: string,
69
+ credential: { source: "env-api-key"; apiKey: string },
53
70
  deploymentId: string,
54
71
  pollIntervalMs: number,
55
72
  maxWaitMs: number,
@@ -68,18 +85,14 @@ async function pollDeployment(
68
85
 
69
86
  await new Promise((r) => setTimeout(r, pollIntervalMs));
70
87
 
71
- const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
72
- id: deploymentId,
88
+ deployment = await orpcCall<DeploymentResponse["json"]>({
89
+ apiUrl,
90
+ path: "/v1/deployments/sync",
91
+ input: { id: deploymentId },
92
+ credential,
73
93
  });
74
- const body = (await res.json()) as DeploymentResponse;
75
- if (res.status !== 200) {
76
- throw new Error(
77
- `Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`,
78
- );
79
- }
80
- status = body.json.status;
81
- workflows = body.json.workflows;
82
- deployment = body.json;
94
+ status = deployment.status;
95
+ workflows = deployment.workflows;
83
96
  if (status === "ready" && readyAt === null) readyAt = Date.now();
84
97
  process.stdout.write(".");
85
98
  }
@@ -132,11 +145,10 @@ export const deployInput = SimpleCLI.input({
132
145
 
133
146
  export const deployCommand = SimpleCLI.command({
134
147
  description: "Deploy workflows to the hosted platform",
135
- experimental: true,
136
148
  })
137
149
  .input(deployInput)
138
150
  .handle(async ({ input }) => {
139
- const { apiUrl, apiKey } = getConfig();
151
+ const { apiUrl, credential } = await requireDeployApiKey();
140
152
  const deploymentName = generateDeploymentName();
141
153
 
142
154
  // Hosted deploy uploads a generated artifact with a deploy entrypoint and
@@ -157,20 +169,14 @@ export const deployCommand = SimpleCLI.command({
157
169
  if (input.description) createPayload.description = input.description;
158
170
 
159
171
  console.log("Uploading deployment...");
160
- const res = await postJson(
172
+ const body = await orpcCall<DeploymentResponse["json"]>({
161
173
  apiUrl,
162
- apiKey,
163
- "/v1/deployments/create",
164
- createPayload,
165
- );
166
- const body = (await res.json()) as DeploymentResponse;
167
- if (res.status !== 200) {
168
- throw new Error(
169
- `Failed to create deployment (${res.status}): ${JSON.stringify(body)}`,
170
- );
171
- }
174
+ path: "/v1/deployments/create",
175
+ input: createPayload,
176
+ credential,
177
+ });
172
178
 
173
- const { deployment_id, status } = body.json;
179
+ const { deployment_id, status } = body;
174
180
  console.log(`Deployment created: ${deployment_id}`);
175
181
  console.log(`Status: ${status}`);
176
182
 
@@ -178,7 +184,7 @@ export const deployCommand = SimpleCLI.command({
178
184
  process.stdout.write("Waiting for build");
179
185
  const deployment = await pollDeployment(
180
186
  apiUrl,
181
- apiKey,
187
+ credential,
182
188
  deployment_id,
183
189
  10_000,
184
190
  5 * 60 * 1000,
@@ -29,7 +29,10 @@ import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
29
29
  import { readLibrettoConfig } from "../core/config.js";
30
30
  import { librettoCommand } from "../../shared/package-manager.js";
31
31
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
32
- import { resolveProviderName } from "../core/providers/index.js";
32
+ import {
33
+ getProviderStartupTimeoutMs,
34
+ resolveProviderName,
35
+ } from "../core/providers/index.js";
33
36
  import { getAbsoluteIntegrationPath } from "../core/workflow-runtime.js";
34
37
  import {
35
38
  compileExecFunction,
@@ -49,7 +52,7 @@ import {
49
52
  } from "../core/telemetry.js";
50
53
  import type { SessionAccessMode } from "../../shared/state/index.js";
51
54
  import type { Experiments } from "../core/experiments.js";
52
- import { SimpleCLI } from "../framework/simple-cli.js";
55
+ import { SimpleCLI } from "affordance";
53
56
  import {
54
57
  pageOption,
55
58
  sessionOption,
@@ -609,7 +612,7 @@ async function runIntegrationFromFile(
609
612
  },
610
613
  logger,
611
614
  logPath: runLogPath,
612
- startupTimeoutMs: 60_000,
615
+ startupTimeoutMs: getProviderStartupTimeoutMs(args.providerName),
613
616
  handlers,
614
617
  });
615
618
 
@@ -8,7 +8,7 @@ import {
8
8
  type ExperimentName,
9
9
  type Experiments,
10
10
  } from "../core/experiments.js";
11
- import { SimpleCLI } from "../framework/simple-cli.js";
11
+ import { SimpleCLI } from "affordance";
12
12
 
13
13
  const experimentNames = Object.keys(EXPERIMENTS) as ExperimentName[];
14
14
 
@@ -1,382 +1,14 @@
1
- import { createInterface } from "node:readline";
2
1
  import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
3
2
  import { spawnSync } from "node:child_process";
4
3
  import { basename, dirname, join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
- import { writeSnapshotModel } from "../core/config.js";
7
5
  import {
8
6
  ensureLibrettoSetup,
9
7
  LIBRETTO_CONFIG_PATH,
10
8
  REPO_ROOT,
11
9
  } from "../core/context.js";
12
- import {
13
- type AiSetupStatus,
14
- DEFAULT_SNAPSHOT_MODELS,
15
- resolveAiSetupStatus,
16
- } from "../core/ai-model.js";
17
- import {
18
- detectProjectPackageManager,
19
- installCommand,
20
- librettoCommand,
21
- } from "../../shared/package-manager.js";
22
- import type { Provider } from "../core/resolve-model.js";
23
- import { SimpleCLI } from "../framework/simple-cli.js";
24
-
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 isSdkInstalled(sdkPackage: string): boolean {
34
- try {
35
- const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
36
- cwd: REPO_ROOT,
37
- stdio: "pipe",
38
- });
39
- return result.status === 0;
40
- } catch {
41
- return false;
42
- }
43
- }
44
-
45
- function installSdkIfNeeded(provider: Provider): void {
46
- const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
47
- if (isSdkInstalled(sdkPackage)) return;
48
-
49
- const pkgManager = detectProjectPackageManager();
50
- const cmd = installCommand(pkgManager);
51
- console.log(`\nInstalling ${sdkPackage}...`);
52
- const result = spawnSync(cmd, [sdkPackage], {
53
- cwd: REPO_ROOT,
54
- stdio: "inherit",
55
- shell: true,
56
- });
57
- if (result.status === 0) {
58
- console.log(`✓ Installed ${sdkPackage}`);
59
- } else {
60
- console.error(
61
- `✗ Failed to install ${sdkPackage}. Install it manually: ${cmd} ${sdkPackage}`,
62
- );
63
- }
64
- }
65
-
66
- export type ProviderChoice = {
67
- key: string;
68
- label: string;
69
- provider: Provider;
70
- envVar: string;
71
- envHint: string;
72
- };
73
-
74
- export const PROVIDER_CHOICES: ProviderChoice[] = [
75
- {
76
- key: "1",
77
- label: "OpenAI",
78
- provider: "openai",
79
- envVar: "OPENAI_API_KEY",
80
- envHint: "Get your key at https://platform.openai.com/api-keys",
81
- },
82
- {
83
- key: "2",
84
- label: "Anthropic",
85
- provider: "anthropic",
86
- envVar: "ANTHROPIC_API_KEY",
87
- envHint: "Get your key at https://console.anthropic.com/settings/keys",
88
- },
89
- {
90
- key: "3",
91
- label: "Google Gemini",
92
- provider: "google",
93
- envVar: "GEMINI_API_KEY",
94
- envHint: "Get your key at https://aistudio.google.com/apikey",
95
- },
96
- {
97
- key: "4",
98
- label: "Google Vertex AI",
99
- provider: "vertex",
100
- envVar: "GOOGLE_CLOUD_PROJECT",
101
- envHint:
102
- "Requires `gcloud auth application-default login` and a GCP project ID",
103
- },
104
- {
105
- key: "5",
106
- label: "OpenRouter",
107
- provider: "openrouter",
108
- envVar: "OPENROUTER_API_KEY",
109
- envHint: "Get your key at https://openrouter.ai/settings/keys",
110
- },
111
- ];
112
-
113
- function promptUser(
114
- rl: ReturnType<typeof createInterface>,
115
- question: string,
116
- ): Promise<string> {
117
- return new Promise((resolve) => {
118
- rl.question(question, (answer) => {
119
- resolve(answer.trim());
120
- });
121
- });
122
- }
123
-
124
- /** Map provider to a human-readable label for status messages. */
125
- function providerLabel(provider: Provider): string {
126
- const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
127
- return choice?.label ?? provider;
128
- }
129
-
130
- /** Extract the env var name from source like "env:GOOGLE_CLOUD_PROJECT". */
131
- function sourceEnvVar(source: string): string | null {
132
- if (source.startsWith("env:")) return source.slice(4);
133
- return null;
134
- }
135
-
136
- /**
137
- * If the workspace has usable credentials but no pinned model in config,
138
- * write the resolved default model to `.libretto/config.json`.
139
- */
140
- function ensurePinnedDefaultModel(
141
- status: AiSetupStatus & { kind: "ready" },
142
- ): AiSetupStatus & { kind: "ready" } {
143
- if (status.source !== "config") {
144
- writeSnapshotModel(status.model);
145
- return { ...status, source: "config" as const };
146
- }
147
- return status;
148
- }
149
-
150
- function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
151
- const envVar = sourceEnvVar(status.source);
152
- if (envVar) {
153
- console.log(
154
- `✓ Detected ${envVar}. Using ${providerLabel(status.provider)}.`,
155
- );
156
- } else {
157
- console.log(`✓ Using ${providerLabel(status.provider)} (${status.model}).`);
158
- }
159
- console.log(
160
- `To change: ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
161
- );
162
- }
163
-
164
- function printInvalidAiConfigWarning(status: AiSetupStatus): void {
165
- if (status.kind !== "invalid-config") return;
166
- console.log("! Existing AI config is invalid:");
167
- for (const line of status.message.split("\n")) {
168
- console.log(` ${line}`);
169
- }
170
- }
171
-
172
- // ── Repair plan helpers (exported for testing) ──────────────────────────────
173
-
174
- export type RepairChoice = "switch-provider" | "skip";
175
-
176
- export type RepairPlan =
177
- | {
178
- kind: "repair-missing-credentials";
179
- provider: Provider;
180
- model: string;
181
- envVar: string;
182
- choices: RepairChoice[];
183
- }
184
- | { kind: "repair-invalid-config"; message: string }
185
- | { kind: "no-repair-needed" };
186
-
187
- /**
188
- * Determine what repair action setup should take for the current AI status.
189
- * Pure function — no I/O, no prompts.
190
- */
191
- export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
192
- if (status.kind === "configured-missing-credentials") {
193
- const choice = PROVIDER_CHOICES.find((c) => c.provider === status.provider);
194
- return {
195
- kind: "repair-missing-credentials",
196
- provider: status.provider,
197
- model: status.model,
198
- envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
199
- choices: ["switch-provider", "skip"],
200
- };
201
- }
202
- if (status.kind === "invalid-config") {
203
- return { kind: "repair-invalid-config", message: status.message };
204
- }
205
- return { kind: "no-repair-needed" };
206
- }
207
-
208
- /**
209
- * Format a provider-specific explanation for missing credentials.
210
- */
211
- export function formatMissingCredentialsMessage(
212
- plan: RepairPlan & { kind: "repair-missing-credentials" },
213
- ): string {
214
- return `✗ ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
215
- }
216
-
217
- function printSnapshotApiStatus(): boolean {
218
- const status = resolveAiSetupStatus();
219
-
220
- console.log(
221
- "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
222
- );
223
-
224
- if (status.kind === "ready") {
225
- console.log();
226
- printHealthySummary(status);
227
- ensurePinnedDefaultModel(status);
228
- return true;
229
- }
230
-
231
- // Provider-specific missing-credentials message
232
- const plan = buildRepairPlan(status);
233
- if (plan.kind === "repair-missing-credentials") {
234
- console.log();
235
- console.log(formatMissingCredentialsMessage(plan));
236
- console.log(
237
- ` To fix: add ${plan.envVar} to .env, or run \`${librettoCommand("setup")}\` interactively to repair.`,
238
- );
239
- return false;
240
- }
241
-
242
- if (plan.kind === "repair-invalid-config") {
243
- printInvalidAiConfigWarning(status);
244
- console.log(
245
- ` Run \`${librettoCommand("setup")}\` interactively to reconfigure.`,
246
- );
247
- return false;
248
- }
249
-
250
- console.log();
251
- console.log("✗ No snapshot API credentials detected.");
252
- console.log(" Add one provider to .env:");
253
- console.log(" OPENAI_API_KEY=...");
254
- console.log(" ANTHROPIC_API_KEY=...");
255
- console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
256
- console.log(
257
- " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
258
- );
259
- console.log(
260
- ` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
261
- );
262
- console.log(
263
- ` Run \`${librettoCommand("setup")}\` interactively to set up credentials.`,
264
- );
265
- return false;
266
- }
267
-
268
- /**
269
- * Run the full provider selection menu.
270
- * Pins the selected provider's default model to config and prints
271
- * instructions for the user to add the credential to .env themselves.
272
- * Returns true if a provider was successfully configured.
273
- */
274
- async function promptProviderSelection(
275
- rl: ReturnType<typeof createInterface>,
276
- ): Promise<boolean> {
277
- console.log(
278
- "Which model provider would you like to use for snapshot analysis?\n",
279
- );
280
- for (const choice of PROVIDER_CHOICES) {
281
- console.log(` ${choice.key}) ${choice.label}`);
282
- }
283
- console.log(" s) Skip for now\n");
284
-
285
- const answer = await promptUser(rl, "Choice: ");
286
-
287
- if (answer.toLowerCase() === "s" || !answer) {
288
- printSkipMessage();
289
- return false;
290
- }
291
-
292
- const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
293
- if (!selected) {
294
- console.log(`\nUnknown choice "${answer}". Skipping API setup.`);
295
- return false;
296
- }
297
-
298
- const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
299
- writeSnapshotModel(model);
300
- console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
301
- console.log(`\nAdd ${selected.envVar} to your .env file:`);
302
- console.log(` ${selected.envHint}`);
303
- installSdkIfNeeded(selected.provider);
304
- return true;
305
- }
306
-
307
- function printSkipMessage(): void {
308
- console.log(
309
- `\nSkipped. You can set up API credentials later by rerunning \`${librettoCommand("setup")}\`.`,
310
- );
311
- console.log("Or add credentials directly to your .env file:");
312
- console.log(" OPENAI_API_KEY=...");
313
- console.log(" ANTHROPIC_API_KEY=...");
314
- console.log(" GEMINI_API_KEY=...");
315
- console.log(
316
- ` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
317
- );
318
- }
319
-
320
- async function runInteractiveApiSetup(): Promise<void> {
321
- const status = resolveAiSetupStatus();
322
-
323
- console.log(
324
- "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
325
- );
326
-
327
- if (status.kind === "ready") {
328
- console.log();
329
- printHealthySummary(status);
330
- ensurePinnedDefaultModel(status);
331
- return;
332
- }
333
-
334
- const plan = buildRepairPlan(status);
335
-
336
- const rl = createInterface({
337
- input: process.stdin,
338
- output: process.stdout,
339
- });
340
-
341
- try {
342
- // ── Repair: configured provider with missing credentials ──
343
- if (plan.kind === "repair-missing-credentials") {
344
- console.log(formatMissingCredentialsMessage(plan));
345
- console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
346
- console.log("");
347
- console.log("Or switch to a different provider:\n");
348
- console.log(" 1) Switch to a different provider");
349
- console.log(" s) Skip for now\n");
350
-
351
- const answer = await promptUser(rl, "Choice: ");
352
-
353
- if (answer === "1") {
354
- await promptProviderSelection(rl);
355
- return;
356
- }
357
-
358
- // skip or empty
359
- printSkipMessage();
360
- return;
361
- }
362
-
363
- // ── Repair: invalid config → let user pick a provider ──
364
- if (plan.kind === "repair-invalid-config") {
365
- printInvalidAiConfigWarning(status);
366
- console.log(
367
- "\nWould you like to reconfigure with a fresh provider selection?\n",
368
- );
369
- await promptProviderSelection(rl);
370
- return;
371
- }
372
-
373
- // ── Unconfigured: standard first-run flow ──
374
- console.log("✗ No snapshot API credentials detected.\n");
375
- await promptProviderSelection(rl);
376
- } finally {
377
- rl.close();
378
- }
379
- }
10
+ import { librettoCommand } from "../../shared/package-manager.js";
11
+ import { SimpleCLI } from "affordance";
380
12
 
381
13
  function installBrowsers(): void {
382
14
  console.log("Installing Playwright Chromium...");
@@ -486,17 +118,6 @@ export const setupCommand = SimpleCLI.command({
486
118
 
487
119
  copySkills();
488
120
 
489
- if (process.stdin.isTTY) {
490
- await runInteractiveApiSetup();
491
- } else {
492
- const ready = printSnapshotApiStatus();
493
- if (!ready) {
494
- console.log(
495
- `\nIf you're an agent, request the user to run \`${librettoCommand("setup")}\`.`,
496
- );
497
- }
498
- }
499
-
500
121
  console.log(`\nConfig set up at ${LIBRETTO_CONFIG_PATH}`);
501
122
  console.log("\n✓ libretto setup complete");
502
123
  });
@@ -14,7 +14,7 @@ import {
14
14
  type SimpleCLIMiddlewareArgs,
15
15
  type SimpleCLIContext,
16
16
  type SimpleCLIMiddleware,
17
- } from "../framework/simple-cli.js";
17
+ } from "affordance";
18
18
 
19
19
  export function sessionOption(help = "Session name") {
20
20
  return SimpleCLI.option(z.string().optional(), { help });