libretto 0.6.2 → 0.6.4

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.
@@ -19,7 +19,7 @@ Libretto reads workspace config from `.libretto/config.json`.
19
19
 
20
20
  ## Supported Settings
21
21
 
22
- - `ai.model` selects the configured analysis model for `snapshot`.
22
+ - `snapshotModel` selects the configured analysis model for `snapshot`.
23
23
  - `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
24
24
  - Viewport precedence is: CLI `--viewport`, then `.libretto/config.json`, then the default `1366x768`.
25
25
  - `sessionMode` sets the default session access mode for new sessions created by `open`, `connect`, and `run`. Must be `"read-only"` or `"write-access"`. When omitted, defaults to `"write-access"`. Pass `--read-only` or `--write-access` to `open`, `connect`, or `run` to override when creating a session.
@@ -29,10 +29,7 @@ Example:
29
29
  ```json
30
30
  {
31
31
  "version": 1,
32
- "ai": {
33
- "model": "openai/gpt-5.4",
34
- "updatedAt": "2026-01-01T00:00:00.000Z"
35
- },
32
+ "snapshotModel": "openai/gpt-5.4",
36
33
  "viewport": {
37
34
  "width": 1280,
38
35
  "height": 800
@@ -47,7 +44,7 @@ Example:
47
44
  npx libretto setup # first-time onboarding, auto-pins default model
48
45
  npx libretto status # inspect AI config and open sessions
49
46
  npx libretto ai configure openai # explicitly change provider/model
50
- npx libretto open https://app.example.com --viewport 1440x900
47
+ npx libretto open https://example.com --viewport 1440x900
51
48
  npx libretto run ./integration.ts --viewport 1440x900
52
49
  ```
53
50
 
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.2"
7
+ version: "0.6.4"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
package/src/cli/cli.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { resolveAiSetupStatus } from "./core/ai-model.js";
1
2
  import { ensureLibrettoSetup } from "./core/context.js";
2
3
  import { createCLIApp } from "./router.js";
4
+ import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
3
5
 
4
6
  function renderUsage(app: ReturnType<typeof createCLIApp>): string {
5
7
  return `${app.renderHelp()}
@@ -7,56 +9,34 @@ function renderUsage(app: ReturnType<typeof createCLIApp>): string {
7
9
  Options:
8
10
  --session <name> Use a named session (auto-generated for open/run if omitted)
9
11
 
10
- Examples:
11
- libretto open https://linkedin.com
12
-
13
- # ... manually log in ...
14
- libretto save linkedin.com
15
- # Next time you open linkedin.com, you'll be logged in automatically
16
-
17
- libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
18
- libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
19
- libretto readonly-exec "return await page.title()" --session test1
20
- libretto connect http://127.0.0.1:9222 --read-only --session test1
21
- libretto run ./integration.ts --read-only --session test1
22
- libretto status
23
- libretto ai configure openai
24
- libretto ai configure anthropic
25
- libretto ai configure gemini
26
- libretto ai configure vertex
27
- libretto ai configure openai/gpt-4o
28
- libretto snapshot
29
- libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
30
- libretto resume --session my-session
31
- libretto close
32
- libretto close --all
33
- libretto close --all --force
34
-
35
- # Multiple sessions
36
- libretto open https://site1.com --session test1
37
- libretto open https://site2.com --session test2
38
- libretto exec "return await page.title()" --session test1
39
-
40
- Available in exec:
41
- page, context, state, browser, networkLog, actionLog
42
-
43
- Available in readonly-exec:
44
- page, state, snapshot, scrollBy, get
12
+ Docs (agent-friendly): https://libretto.sh/docs
13
+ `;
14
+ }
45
15
 
46
- Profiles:
47
- Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
48
- They persist cookies, localStorage, and session data across browser launches.
49
- Local profiles are machine-local and are not shared with other users/environments.
50
- Sessions can expire; if run fails auth, log in again and re-save the profile.
16
+ function printSetupAudit(): void {
17
+ warnIfInstalledSkillOutOfDate();
51
18
 
52
- Sessions:
53
- Session state is stored in .libretto/sessions/<session>/state.json
54
- CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
55
- Each session runs an isolated browser instance on a dynamic port.
56
- Session mode is stored per session as read-only or write-access.
57
- Use --read-only on open, connect, or run to create a read-only session.
58
- Session mode is enforced by Libretto commands, not by raw CDP clients outside Libretto.
59
- `;
19
+ const status = resolveAiSetupStatus();
20
+ switch (status.kind) {
21
+ case "ready":
22
+ console.log(`✓ Snapshot model: ${status.model}`);
23
+ break;
24
+ case "configured-missing-credentials":
25
+ console.log(
26
+ `✗ ${status.provider} configured (model: ${status.model}), but credentials are missing. Run \`npx libretto setup\` to repair.`,
27
+ );
28
+ break;
29
+ case "invalid-config":
30
+ console.log(
31
+ `✗ AI config is invalid. Run \`npx libretto setup\` to reconfigure.`,
32
+ );
33
+ break;
34
+ case "unconfigured":
35
+ console.log(
36
+ `✗ No AI model configured. Run \`npx libretto setup\` or \`npx libretto ai configure\` to set up.`,
37
+ );
38
+ break;
39
+ }
60
40
  }
61
41
 
62
42
  function isRootHelpRequest(rawArgs: readonly string[]): boolean {
@@ -74,6 +54,7 @@ export async function runLibrettoCLI(): Promise<void> {
74
54
  try {
75
55
  if (isRootHelpRequest(rawArgs)) {
76
56
  console.log(renderUsage(app));
57
+ printSetupAudit();
77
58
  return;
78
59
  }
79
60
 
@@ -1,10 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import {
3
3
  CURRENT_CONFIG_VERSION,
4
- readAiConfig,
5
- writeAiConfig,
6
- clearAiConfig,
7
- type AiConfig,
4
+ readSnapshotModel,
5
+ writeSnapshotModel,
6
+ clearSnapshotModel,
8
7
  } from "../core/config.js";
9
8
  import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
10
9
  import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
@@ -27,10 +26,9 @@ function formatConfigureProviders(separator = " | "): string {
27
26
  return CONFIGURE_PROVIDERS.join(separator);
28
27
  }
29
28
 
30
- function printAiConfig(config: AiConfig, configPath: string): void {
31
- console.log(`Model: ${config.model}`);
29
+ function printSnapshotModelConfig(model: string, configPath: string): void {
30
+ console.log(`Snapshot model: ${model}`);
32
31
  console.log(`Config file: ${configPath}`);
33
- console.log(`Updated at: ${config.updatedAt}`);
34
32
  }
35
33
 
36
34
  /**
@@ -71,26 +69,26 @@ export function runAiConfigure(
71
69
  const presetArg = input.preset?.trim();
72
70
 
73
71
  if (!presetArg && !input.clear) {
74
- const config = readAiConfig(configPath);
75
- if (!config) {
72
+ const model = readSnapshotModel(configPath);
73
+ if (!model) {
76
74
  console.log(
77
- `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
75
+ `No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
78
76
  );
79
77
  console.log(
80
78
  "Provider credentials still come from your shell or .env file.",
81
79
  );
82
80
  return;
83
81
  }
84
- printAiConfig(config, configPath);
82
+ printSnapshotModelConfig(model, configPath);
85
83
  return;
86
84
  }
87
85
 
88
86
  if (input.clear) {
89
- const removed = clearAiConfig(configPath);
87
+ const removed = clearSnapshotModel(configPath);
90
88
  if (removed) {
91
- console.log(`Cleared AI config: ${configPath}`);
89
+ console.log(`Cleared snapshot model config: ${configPath}`);
92
90
  } else {
93
- console.log("No AI config was set.");
91
+ console.log("No snapshot model was set.");
94
92
  }
95
93
  return;
96
94
  }
@@ -107,9 +105,9 @@ export function runAiConfigure(
107
105
  );
108
106
  }
109
107
 
110
- const config = writeAiConfig(model, configPath);
111
- console.log("AI config saved.");
112
- printAiConfig(config, configPath);
108
+ writeSnapshotModel(model, configPath);
109
+ console.log("Snapshot model saved.");
110
+ printSnapshotModelConfig(model, configPath);
113
111
  }
114
112
 
115
113
  export const aiConfigureInput = SimpleCLI.input({
@@ -242,7 +242,11 @@ export const sessionModeCommand = SimpleCLI.command({
242
242
  });
243
243
 
244
244
  export const closeInput = SimpleCLI.input({
245
- positionals: [],
245
+ positionals: [
246
+ SimpleCLI.positional("session", z.string().optional(), {
247
+ help: "Session name to close",
248
+ }),
249
+ ],
246
250
  named: {
247
251
  session: sessionOption(),
248
252
  all: SimpleCLI.flag({
@@ -254,7 +258,7 @@ export const closeInput = SimpleCLI.input({
254
258
  },
255
259
  }).refine(
256
260
  (input) => input.all || input.session,
257
- `Usage: libretto close --session <name>\nUsage: libretto close --all [--force]`,
261
+ `Usage: libretto close <session>\nUsage: libretto close --all [--force]`,
258
262
  );
259
263
 
260
264
  export const closeCommand = SimpleCLI.command({
@@ -60,23 +60,32 @@ async function pollDeployment(
60
60
  maxWaitMs: number,
61
61
  ): Promise<DeploymentResponse["json"]> {
62
62
  const start = Date.now();
63
+ const workflowWaitMs = 60_000;
63
64
  let status: DeploymentStatus = "building";
65
+ let workflows: string[] | null | undefined = null;
66
+ let readyAt: number | null = null;
64
67
  let deployment: DeploymentResponse["json"] | undefined;
65
68
 
66
- while (status === "building" && Date.now() - start < maxWaitMs) {
69
+ while (Date.now() - start < maxWaitMs) {
70
+ if (status !== "building" && status !== "ready") break;
71
+ if (status === "ready" && workflows?.length) break;
72
+ if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
73
+
67
74
  await new Promise((r) => setTimeout(r, pollIntervalMs));
68
75
 
69
- const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
76
+ const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
70
77
  id: deploymentId,
71
78
  });
72
79
  const body = (await res.json()) as DeploymentResponse;
73
80
  if (res.status !== 200) {
74
81
  throw new Error(
75
- `Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`,
82
+ `Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`,
76
83
  );
77
84
  }
78
85
  status = body.json.status;
86
+ workflows = body.json.workflows;
79
87
  deployment = body.json;
88
+ if (status === "ready" && readyAt === null) readyAt = Date.now();
80
89
  process.stdout.write(".");
81
90
  }
82
91
  console.log();
@@ -85,6 +94,12 @@ async function pollDeployment(
85
94
  throw new Error("Deployment timed out before receiving a status update.");
86
95
  }
87
96
 
97
+ if (status === "ready" && !workflows?.length) {
98
+ throw new Error(
99
+ "Build completed but workflow discovery failed due to a server-side error. Please redeploy.",
100
+ );
101
+ }
102
+
88
103
  return deployment;
89
104
  }
90
105
 
@@ -1,17 +1,14 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import {
3
- appendFileSync,
4
3
  cpSync,
5
4
  existsSync,
6
5
  readdirSync,
7
- readFileSync,
8
6
  rmSync,
9
- writeFileSync,
10
7
  } from "node:fs";
11
8
  import { spawnSync } from "node:child_process";
12
9
  import { basename, dirname, join } from "node:path";
13
10
  import { fileURLToPath } from "node:url";
14
- import { writeAiConfig } from "../core/config.js";
11
+ import { writeSnapshotModel } from "../core/config.js";
15
12
  import {
16
13
  ensureLibrettoSetup,
17
14
  LIBRETTO_CONFIG_PATH,
@@ -20,7 +17,6 @@ import {
20
17
  import {
21
18
  type AiSetupStatus,
22
19
  DEFAULT_SNAPSHOT_MODELS,
23
- loadSnapshotEnv,
24
20
  resolveAiSetupStatus,
25
21
  } from "../core/ai-model.js";
26
22
  import type { Provider } from "../core/resolve-model.js";
@@ -97,7 +93,7 @@ function ensurePinnedDefaultModel(
97
93
  status: AiSetupStatus & { kind: "ready" },
98
94
  ): AiSetupStatus & { kind: "ready" } {
99
95
  if (status.source !== "config") {
100
- writeAiConfig(status.model);
96
+ writeSnapshotModel(status.model);
101
97
  return { ...status, source: "config" as const };
102
98
  }
103
99
  return status;
@@ -127,10 +123,7 @@ function printInvalidAiConfigWarning(status: AiSetupStatus): void {
127
123
 
128
124
  // ── Repair plan helpers (exported for testing) ──────────────────────────────
129
125
 
130
- export type RepairChoice =
131
- | "enter-matching-credential"
132
- | "switch-provider"
133
- | "skip";
126
+ export type RepairChoice = "switch-provider" | "skip";
134
127
 
135
128
  export type RepairPlan =
136
129
  | {
@@ -155,7 +148,7 @@ export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
155
148
  provider: status.provider,
156
149
  model: status.model,
157
150
  envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
158
- choices: ["enter-matching-credential", "switch-provider", "skip"],
151
+ choices: ["switch-provider", "skip"],
159
152
  };
160
153
  }
161
154
  if (status.kind === "invalid-config") {
@@ -223,72 +216,13 @@ function printSnapshotApiStatus(): boolean {
223
216
  }
224
217
 
225
218
  /**
226
- * Write an env var to the .env file and update process.env.
227
- */
228
- function writeEnvVar(envVar: string, value: string, envPath: string): void {
229
- let envContent = "";
230
- if (existsSync(envPath)) {
231
- envContent = readFileSync(envPath, "utf-8");
232
- }
233
-
234
- const envLine = `${envVar}=${value}`;
235
- if (envContent.includes(`${envVar}=`)) {
236
- const updated = envContent.replace(
237
- new RegExp(`^${envVar}=.*$`, "m"),
238
- () => envLine,
239
- );
240
- writeFileSync(envPath, updated);
241
- console.log(`\n✓ Updated ${envVar} in ${envPath}`);
242
- } else {
243
- const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
244
- appendFileSync(envPath, `${separator}${envLine}\n`);
245
- console.log(`\n✓ Added ${envVar} to ${envPath}`);
246
- }
247
-
248
- process.env[envVar] = value;
249
- }
250
-
251
- /**
252
- * Prompt the user to enter a credential for a specific provider and pin its model.
253
- * When modelOverride is provided (e.g. during repair), preserves the existing model
254
- * instead of resetting to the provider default.
255
- * Returns true if credential was entered successfully.
256
- */
257
- async function promptForCredential(
258
- rl: ReturnType<typeof createInterface>,
259
- choice: ProviderChoice,
260
- envPath: string,
261
- modelOverride?: string,
262
- ): Promise<boolean> {
263
- console.log(`\n${choice.label} selected.`);
264
- console.log(`${choice.envHint}\n`);
265
-
266
- const apiKeyValue = await promptUser(rl, `Enter your ${choice.envVar}: `);
267
-
268
- if (!apiKeyValue) {
269
- console.log("\nNo value entered. Skipping API key setup.");
270
- return false;
271
- }
272
-
273
- writeEnvVar(choice.envVar, apiKeyValue, envPath);
274
- loadSnapshotEnv();
275
-
276
- const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
277
- writeAiConfig(model);
278
- console.log(`✓ Snapshot API ready: ${model}`);
279
- console.log(
280
- "To change: npx libretto ai configure openai | anthropic | gemini | vertex",
281
- );
282
- return true;
283
- }
284
-
285
- /**
286
- * Run the full provider selection menu and credential entry.
219
+ * Run the full provider selection menu.
220
+ * Pins the selected provider's default model to config and prints
221
+ * instructions for the user to add the credential to .env themselves.
287
222
  * Returns true if a provider was successfully configured.
288
223
  */
289
224
  async function promptProviderSelection(
290
225
  rl: ReturnType<typeof createInterface>,
291
- envPath: string,
292
226
  ): Promise<boolean> {
293
227
  console.log(
294
228
  "Which model provider would you like to use for snapshot analysis?\n",
@@ -311,7 +245,12 @@ async function promptProviderSelection(
311
245
  return false;
312
246
  }
313
247
 
314
- return promptForCredential(rl, selected, envPath);
248
+ const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
249
+ writeSnapshotModel(model);
250
+ console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
251
+ console.log(`\nAdd ${selected.envVar} to your .env file:`);
252
+ console.log(` ${selected.envHint}`);
253
+ return true;
315
254
  }
316
255
 
317
256
  function printSkipMessage(): void {
@@ -329,7 +268,6 @@ function printSkipMessage(): void {
329
268
 
330
269
  async function runInteractiveApiSetup(): Promise<void> {
331
270
  const status = resolveAiSetupStatus();
332
- const envPath = join(REPO_ROOT, ".env");
333
271
 
334
272
  console.log(
335
273
  "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
@@ -353,26 +291,16 @@ async function runInteractiveApiSetup(): Promise<void> {
353
291
  // ── Repair: configured provider with missing credentials ──
354
292
  if (plan.kind === "repair-missing-credentials") {
355
293
  console.log(formatMissingCredentialsMessage(plan));
294
+ console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
356
295
  console.log("");
357
- console.log("How would you like to fix this?\n");
358
- console.log(` 1) Enter ${plan.envVar}`);
359
- console.log(" 2) Switch to a different provider");
296
+ console.log("Or switch to a different provider:\n");
297
+ console.log(" 1) Switch to a different provider");
360
298
  console.log(" s) Skip for now\n");
361
299
 
362
300
  const answer = await promptUser(rl, "Choice: ");
363
301
 
364
302
  if (answer === "1") {
365
- const matchingChoice = PROVIDER_CHOICES.find(
366
- (c) => c.provider === plan.provider,
367
- );
368
- if (matchingChoice) {
369
- await promptForCredential(rl, matchingChoice, envPath, plan.model);
370
- }
371
- return;
372
- }
373
-
374
- if (answer === "2") {
375
- await promptProviderSelection(rl, envPath);
303
+ await promptProviderSelection(rl);
376
304
  return;
377
305
  }
378
306
 
@@ -387,13 +315,13 @@ async function runInteractiveApiSetup(): Promise<void> {
387
315
  console.log(
388
316
  "\nWould you like to reconfigure with a fresh provider selection?\n",
389
317
  );
390
- await promptProviderSelection(rl, envPath);
318
+ await promptProviderSelection(rl);
391
319
  return;
392
320
  }
393
321
 
394
322
  // ── Unconfigured: standard first-run flow ──
395
323
  console.log("✗ No snapshot API credentials detected.\n");
396
- await promptProviderSelection(rl, envPath);
324
+ await promptProviderSelection(rl);
397
325
  } finally {
398
326
  rl.close();
399
327
  }
@@ -12,7 +12,7 @@ import {
12
12
  import { SimpleCLI } from "../framework/simple-cli.js";
13
13
  import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
14
14
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
15
- import { readAiConfig } from "../core/config.js";
15
+ import { readSnapshotModel } from "../core/config.js";
16
16
  import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
17
17
 
18
18
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
@@ -256,8 +256,8 @@ async function runSnapshot(
256
256
  const normalizedObjective = objective.trim();
257
257
  const normalizedContext = context.trim();
258
258
 
259
- const configuredAi = readAiConfig();
260
- resolveSnapshotApiModelOrThrow(configuredAi);
259
+ const snapshotModel = readSnapshotModel();
260
+ resolveSnapshotApiModelOrThrow(snapshotModel);
261
261
 
262
262
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
263
263
  session,
@@ -283,7 +283,7 @@ async function runSnapshot(
283
283
  // The legacy CLI-agent path (spawning codex/claude/gemini as a subprocess) is preserved
284
284
  // in snapshot-analyzer.ts — to switch back, replace this call with:
285
285
  // await runInterpret(interpretArgs, logger);
286
- await runApiInterpret(interpretArgs, logger, configuredAi);
286
+ await runApiInterpret(interpretArgs, logger, snapshotModel);
287
287
  }
288
288
 
289
289
  export const snapshotInput = SimpleCLI.input({
@@ -10,7 +10,7 @@ function printAiStatus(status: AiSetupStatus): void {
10
10
 
11
11
  switch (status.kind) {
12
12
  case "ready":
13
- console.log(` ✓ Model: ${status.model}`);
13
+ console.log(` ✓ Snapshot model: ${status.model}`);
14
14
  if (status.source === "config") {
15
15
  console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
16
16
  } else {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { type AiConfig, readAiConfig } from "./config.js";
3
+ import { readSnapshotModel } from "./config.js";
4
4
  import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
5
5
  import {
6
6
  hasProviderCredentials,
@@ -221,18 +221,18 @@ function inferAutoSnapshotModel(): SnapshotApiModelSelection | null {
221
221
  * Resolve which API model to use for snapshot analysis.
222
222
  *
223
223
  * Priority:
224
- * 1. Model from .libretto/config.json ai.model field (set via `ai configure`)
224
+ * 1. snapshotModel from .libretto/config.json (set via `ai configure`)
225
225
  * 2. Auto-detect from available API credentials in env
226
226
  */
227
227
  export function resolveSnapshotApiModel(
228
- config: AiConfig | null = readAiConfig(),
228
+ snapshotModel: string | null = readSnapshotModel(),
229
229
  ): SnapshotApiModelSelection | null {
230
230
  loadSnapshotEnv();
231
231
 
232
- if (config?.model) {
233
- const { provider } = parseModel(config.model);
232
+ if (snapshotModel) {
233
+ const { provider } = parseModel(snapshotModel);
234
234
  return {
235
- model: config.model,
235
+ model: snapshotModel,
236
236
  provider,
237
237
  source: "config",
238
238
  };
@@ -242,9 +242,9 @@ export function resolveSnapshotApiModel(
242
242
  }
243
243
 
244
244
  export function resolveSnapshotApiModelOrThrow(
245
- config: AiConfig | null = readAiConfig(),
245
+ snapshotModel: string | null = readSnapshotModel(),
246
246
  ): SnapshotApiModelSelection {
247
- const selection = resolveSnapshotApiModel(config);
247
+ const selection = resolveSnapshotApiModel(snapshotModel);
248
248
  if (!selection) {
249
249
  throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
250
250
  }
@@ -288,14 +288,14 @@ export type AiSetupStatus =
288
288
  | { kind: "unconfigured" };
289
289
 
290
290
  /**
291
- * Read AI config without throwing on invalid files.
292
- * Returns the config or an error message.
291
+ * Read snapshot model without throwing on invalid files.
292
+ * Returns the model string or an error message.
293
293
  */
294
- function readAiConfigSafely(
294
+ function readSnapshotModelSafely(
295
295
  configPath: string,
296
- ): { ok: true; config: AiConfig | null } | { ok: false; message: string } {
296
+ ): { ok: true; model: string | null } | { ok: false; message: string } {
297
297
  try {
298
- return { ok: true, config: readAiConfig(configPath) };
298
+ return { ok: true, model: readSnapshotModel(configPath) };
299
299
  } catch (err) {
300
300
  return {
301
301
  ok: false,
@@ -312,25 +312,25 @@ function readAiConfigSafely(
312
312
  * that the throwing APIs collapse into errors.
313
313
  *
314
314
  * 1. If config read throws → `invalid-config`.
315
- * 2. If config has an `ai` block → check credentials for that provider.
316
- * 3. If no config or no `ai` block → auto-detect from env via existing resolver.
315
+ * 2. If config has a `snapshotModel` → check credentials for that provider.
316
+ * 3. If no `snapshotModel` → auto-detect from env via existing resolver.
317
317
  */
318
318
  export function resolveAiSetupStatus(
319
319
  configPath: string = LIBRETTO_CONFIG_PATH,
320
320
  ): AiSetupStatus {
321
321
  loadSnapshotEnv();
322
322
 
323
- const configResult = readAiConfigSafely(configPath);
323
+ const result = readSnapshotModelSafely(configPath);
324
324
 
325
- if (!configResult.ok) {
326
- return { kind: "invalid-config", message: configResult.message };
325
+ if (!result.ok) {
326
+ return { kind: "invalid-config", message: result.message };
327
327
  }
328
328
 
329
- // Config exists with an ai block — use it directly to check credentials
330
- if (configResult.config) {
329
+ // Config has a snapshotModel — use it directly to check credentials
330
+ if (result.model) {
331
331
  let selection: SnapshotApiModelSelection | null;
332
332
  try {
333
- selection = resolveSnapshotApiModel(configResult.config);
333
+ selection = resolveSnapshotApiModel(result.model);
334
334
  } catch (err) {
335
335
  return {
336
336
  kind: "invalid-config",
@@ -356,7 +356,7 @@ export function resolveAiSetupStatus(
356
356
  };
357
357
  }
358
358
 
359
- // No ai config — fall back to env auto-detect via existing resolver
359
+ // No snapshotModel — fall back to env auto-detect via existing resolver
360
360
  const envSelection = resolveSnapshotApiModel(null);
361
361
  if (envSelection && hasProviderCredentials(envSelection.provider)) {
362
362
  return {
@@ -18,15 +18,15 @@ import {
18
18
  type InterpretResult,
19
19
  type InterpretArgs,
20
20
  } from "./snapshot-analyzer.js";
21
- import { readAiConfig, type AiConfig } from "./config.js";
21
+ import { readSnapshotModel } from "./config.js";
22
22
  import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
23
23
 
24
24
  export async function runApiInterpret(
25
25
  args: InterpretArgs,
26
26
  logger: LoggerApi,
27
- configuredAi: AiConfig | null = readAiConfig(),
27
+ snapshotModel: string | null = readSnapshotModel(),
28
28
  ): Promise<void> {
29
- const selection = resolveSnapshotApiModelOrThrow(configuredAi);
29
+ const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
30
30
 
31
31
  logger.info("api-interpret-start", {
32
32
  objective: args.objective,