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.
package/dist/cli/cli.js CHANGED
@@ -1,62 +1,40 @@
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
  function renderUsage(app) {
4
6
  return `${app.renderHelp()}
5
7
 
6
8
  Options:
7
9
  --session <name> Use a named session (auto-generated for open/run if omitted)
8
10
 
9
- Examples:
10
- libretto open https://linkedin.com
11
-
12
- # ... manually log in ...
13
- libretto save linkedin.com
14
- # Next time you open linkedin.com, you'll be logged in automatically
15
-
16
- libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
17
- libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
18
- libretto readonly-exec "return await page.title()" --session test1
19
- libretto connect http://127.0.0.1:9222 --read-only --session test1
20
- libretto run ./integration.ts --read-only --session test1
21
- libretto status
22
- libretto ai configure openai
23
- libretto ai configure anthropic
24
- libretto ai configure gemini
25
- libretto ai configure vertex
26
- libretto ai configure openai/gpt-4o
27
- libretto snapshot
28
- libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
29
- libretto resume --session my-session
30
- libretto close
31
- libretto close --all
32
- libretto close --all --force
33
-
34
- # Multiple sessions
35
- libretto open https://site1.com --session test1
36
- libretto open https://site2.com --session test2
37
- libretto exec "return await page.title()" --session test1
38
-
39
- Available in exec:
40
- page, context, state, browser, networkLog, actionLog
41
-
42
- Available in readonly-exec:
43
- page, state, snapshot, scrollBy, get
44
-
45
- Profiles:
46
- Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
47
- They persist cookies, localStorage, and session data across browser launches.
48
- Local profiles are machine-local and are not shared with other users/environments.
49
- Sessions can expire; if run fails auth, log in again and re-save the profile.
50
-
51
- Sessions:
52
- Session state is stored in .libretto/sessions/<session>/state.json
53
- CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
54
- Each session runs an isolated browser instance on a dynamic port.
55
- Session mode is stored per session as read-only or write-access.
56
- Use --read-only on open, connect, or run to create a read-only session.
57
- Session mode is enforced by Libretto commands, not by raw CDP clients outside Libretto.
11
+ Docs (agent-friendly): https://libretto.sh/docs
58
12
  `;
59
13
  }
14
+ function printSetupAudit() {
15
+ warnIfInstalledSkillOutOfDate();
16
+ const status = resolveAiSetupStatus();
17
+ switch (status.kind) {
18
+ case "ready":
19
+ console.log(`\u2713 Snapshot model: ${status.model}`);
20
+ break;
21
+ case "configured-missing-credentials":
22
+ console.log(
23
+ `\u2717 ${status.provider} configured (model: ${status.model}), but credentials are missing. Run \`npx libretto setup\` to repair.`
24
+ );
25
+ break;
26
+ case "invalid-config":
27
+ console.log(
28
+ `\u2717 AI config is invalid. Run \`npx libretto setup\` to reconfigure.`
29
+ );
30
+ break;
31
+ case "unconfigured":
32
+ console.log(
33
+ `\u2717 No AI model configured. Run \`npx libretto setup\` or \`npx libretto ai configure\` to set up.`
34
+ );
35
+ break;
36
+ }
37
+ }
60
38
  function isRootHelpRequest(rawArgs) {
61
39
  if (rawArgs.length === 0) return true;
62
40
  if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
@@ -70,6 +48,7 @@ async function runLibrettoCLI() {
70
48
  try {
71
49
  if (isRootHelpRequest(rawArgs)) {
72
50
  console.log(renderUsage(app));
51
+ printSetupAudit();
73
52
  return;
74
53
  }
75
54
  const result = await app.run(rawArgs);
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import {
3
- readAiConfig,
4
- writeAiConfig,
5
- clearAiConfig
3
+ readSnapshotModel,
4
+ writeSnapshotModel,
5
+ clearSnapshotModel
6
6
  } from "../core/config.js";
7
7
  import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
8
8
  import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
@@ -21,10 +21,9 @@ const CONFIGURE_PROVIDERS = [
21
21
  function formatConfigureProviders(separator = " | ") {
22
22
  return CONFIGURE_PROVIDERS.join(separator);
23
23
  }
24
- function printAiConfig(config, configPath) {
25
- console.log(`Model: ${config.model}`);
24
+ function printSnapshotModelConfig(model, configPath) {
25
+ console.log(`Snapshot model: ${model}`);
26
26
  console.log(`Config file: ${configPath}`);
27
- console.log(`Updated at: ${config.updatedAt}`);
28
27
  }
29
28
  function resolveModelFromInput(input) {
30
29
  const trimmed = input.trim();
@@ -38,25 +37,25 @@ function runAiConfigure(input, options = {}) {
38
37
  const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
39
38
  const presetArg = input.preset?.trim();
40
39
  if (!presetArg && !input.clear) {
41
- const config2 = readAiConfig(configPath);
42
- if (!config2) {
40
+ const model2 = readSnapshotModel(configPath);
41
+ if (!model2) {
43
42
  console.log(
44
- `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
43
+ `No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
45
44
  );
46
45
  console.log(
47
46
  "Provider credentials still come from your shell or .env file."
48
47
  );
49
48
  return;
50
49
  }
51
- printAiConfig(config2, configPath);
50
+ printSnapshotModelConfig(model2, configPath);
52
51
  return;
53
52
  }
54
53
  if (input.clear) {
55
- const removed = clearAiConfig(configPath);
54
+ const removed = clearSnapshotModel(configPath);
56
55
  if (removed) {
57
- console.log(`Cleared AI config: ${configPath}`);
56
+ console.log(`Cleared snapshot model config: ${configPath}`);
58
57
  } else {
59
- console.log("No AI config was set.");
58
+ console.log("No snapshot model was set.");
60
59
  }
61
60
  return;
62
61
  }
@@ -71,9 +70,9 @@ function runAiConfigure(input, options = {}) {
71
70
  `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
72
71
  );
73
72
  }
74
- const config = writeAiConfig(model, configPath);
75
- console.log("AI config saved.");
76
- printAiConfig(config, configPath);
73
+ writeSnapshotModel(model, configPath);
74
+ console.log("Snapshot model saved.");
75
+ printSnapshotModelConfig(model, configPath);
77
76
  }
78
77
  const aiConfigureInput = SimpleCLI.input({
79
78
  positionals: [
@@ -199,7 +199,11 @@ const sessionModeCommand = SimpleCLI.command({
199
199
  console.log(`Session "${ctx.session}" mode set to ${nextState.mode}.`);
200
200
  });
201
201
  const closeInput = SimpleCLI.input({
202
- positionals: [],
202
+ positionals: [
203
+ SimpleCLI.positional("session", z.string().optional(), {
204
+ help: "Session name to close"
205
+ })
206
+ ],
203
207
  named: {
204
208
  session: sessionOption(),
205
209
  all: SimpleCLI.flag({
@@ -211,7 +215,7 @@ const closeInput = SimpleCLI.input({
211
215
  }
212
216
  }).refine(
213
217
  (input) => input.all || input.session,
214
- `Usage: libretto close --session <name>
218
+ `Usage: libretto close <session>
215
219
  Usage: libretto close --all [--force]`
216
220
  );
217
221
  const closeCommand = SimpleCLI.command({
@@ -32,27 +32,40 @@ async function postJson(apiUrl, apiKey, path, input = {}) {
32
32
  }
33
33
  async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
34
34
  const start = Date.now();
35
+ const workflowWaitMs = 6e4;
35
36
  let status = "building";
37
+ let workflows = null;
38
+ let readyAt = null;
36
39
  let deployment;
37
- while (status === "building" && Date.now() - start < maxWaitMs) {
40
+ while (Date.now() - start < maxWaitMs) {
41
+ if (status !== "building" && status !== "ready") break;
42
+ if (status === "ready" && workflows?.length) break;
43
+ if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
38
44
  await new Promise((r) => setTimeout(r, pollIntervalMs));
39
- const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
45
+ const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
40
46
  id: deploymentId
41
47
  });
42
48
  const body = await res.json();
43
49
  if (res.status !== 200) {
44
50
  throw new Error(
45
- `Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`
51
+ `Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`
46
52
  );
47
53
  }
48
54
  status = body.json.status;
55
+ workflows = body.json.workflows;
49
56
  deployment = body.json;
57
+ if (status === "ready" && readyAt === null) readyAt = Date.now();
50
58
  process.stdout.write(".");
51
59
  }
52
60
  console.log();
53
61
  if (!deployment) {
54
62
  throw new Error("Deployment timed out before receiving a status update.");
55
63
  }
64
+ if (status === "ready" && !workflows?.length) {
65
+ throw new Error(
66
+ "Build completed but workflow discovery failed due to a server-side error. Please redeploy."
67
+ );
68
+ }
56
69
  return deployment;
57
70
  }
58
71
  const deployInput = SimpleCLI.input({
@@ -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
- rmSync,
9
- writeFileSync
6
+ rmSync
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,
@@ -19,7 +16,6 @@ import {
19
16
  } from "../core/context.js";
20
17
  import {
21
18
  DEFAULT_SNAPSHOT_MODELS,
22
- loadSnapshotEnv,
23
19
  resolveAiSetupStatus
24
20
  } from "../core/ai-model.js";
25
21
  import { SimpleCLI } from "../framework/simple-cli.js";
@@ -70,7 +66,7 @@ function sourceEnvVar(source) {
70
66
  }
71
67
  function ensurePinnedDefaultModel(status) {
72
68
  if (status.source !== "config") {
73
- writeAiConfig(status.model);
69
+ writeSnapshotModel(status.model);
74
70
  return { ...status, source: "config" };
75
71
  }
76
72
  return status;
@@ -103,7 +99,7 @@ function buildRepairPlan(status) {
103
99
  provider: status.provider,
104
100
  model: status.model,
105
101
  envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
106
- choices: ["enter-matching-credential", "switch-provider", "skip"]
102
+ choices: ["switch-provider", "skip"]
107
103
  };
108
104
  }
109
105
  if (status.kind === "invalid-config") {
@@ -156,50 +152,7 @@ function printSnapshotApiStatus() {
156
152
  );
157
153
  return false;
158
154
  }
159
- function writeEnvVar(envVar, value, envPath) {
160
- let envContent = "";
161
- if (existsSync(envPath)) {
162
- envContent = readFileSync(envPath, "utf-8");
163
- }
164
- const envLine = `${envVar}=${value}`;
165
- if (envContent.includes(`${envVar}=`)) {
166
- const updated = envContent.replace(
167
- new RegExp(`^${envVar}=.*$`, "m"),
168
- () => envLine
169
- );
170
- writeFileSync(envPath, updated);
171
- console.log(`
172
- \u2713 Updated ${envVar} in ${envPath}`);
173
- } else {
174
- const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
175
- appendFileSync(envPath, `${separator}${envLine}
176
- `);
177
- console.log(`
178
- \u2713 Added ${envVar} to ${envPath}`);
179
- }
180
- process.env[envVar] = value;
181
- }
182
- async function promptForCredential(rl, choice, envPath, modelOverride) {
183
- console.log(`
184
- ${choice.label} selected.`);
185
- console.log(`${choice.envHint}
186
- `);
187
- const apiKeyValue = await promptUser(rl, `Enter your ${choice.envVar}: `);
188
- if (!apiKeyValue) {
189
- console.log("\nNo value entered. Skipping API key setup.");
190
- return false;
191
- }
192
- writeEnvVar(choice.envVar, apiKeyValue, envPath);
193
- loadSnapshotEnv();
194
- const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
195
- writeAiConfig(model);
196
- console.log(`\u2713 Snapshot API ready: ${model}`);
197
- console.log(
198
- "To change: npx libretto ai configure openai | anthropic | gemini | vertex"
199
- );
200
- return true;
201
- }
202
- async function promptProviderSelection(rl, envPath) {
155
+ async function promptProviderSelection(rl) {
203
156
  console.log(
204
157
  "Which model provider would you like to use for snapshot analysis?\n"
205
158
  );
@@ -218,7 +171,14 @@ async function promptProviderSelection(rl, envPath) {
218
171
  Unknown choice "${answer}". Skipping API setup.`);
219
172
  return false;
220
173
  }
221
- return promptForCredential(rl, selected, envPath);
174
+ const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
175
+ writeSnapshotModel(model);
176
+ console.log(`
177
+ \u2713 ${selected.label} selected (model: ${model}).`);
178
+ console.log(`
179
+ Add ${selected.envVar} to your .env file:`);
180
+ console.log(` ${selected.envHint}`);
181
+ return true;
222
182
  }
223
183
  function printSkipMessage() {
224
184
  console.log(
@@ -234,7 +194,6 @@ function printSkipMessage() {
234
194
  }
235
195
  async function runInteractiveApiSetup() {
236
196
  const status = resolveAiSetupStatus();
237
- const envPath = join(REPO_ROOT, ".env");
238
197
  console.log(
239
198
  "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
240
199
  );
@@ -252,23 +211,15 @@ async function runInteractiveApiSetup() {
252
211
  try {
253
212
  if (plan.kind === "repair-missing-credentials") {
254
213
  console.log(formatMissingCredentialsMessage(plan));
214
+ console.log(`
215
+ Add ${plan.envVar} to your .env file to fix this.`);
255
216
  console.log("");
256
- console.log("How would you like to fix this?\n");
257
- console.log(` 1) Enter ${plan.envVar}`);
258
- console.log(" 2) Switch to a different provider");
217
+ console.log("Or switch to a different provider:\n");
218
+ console.log(" 1) Switch to a different provider");
259
219
  console.log(" s) Skip for now\n");
260
220
  const answer = await promptUser(rl, "Choice: ");
261
221
  if (answer === "1") {
262
- const matchingChoice = PROVIDER_CHOICES.find(
263
- (c) => c.provider === plan.provider
264
- );
265
- if (matchingChoice) {
266
- await promptForCredential(rl, matchingChoice, envPath, plan.model);
267
- }
268
- return;
269
- }
270
- if (answer === "2") {
271
- await promptProviderSelection(rl, envPath);
222
+ await promptProviderSelection(rl);
272
223
  return;
273
224
  }
274
225
  printSkipMessage();
@@ -279,11 +230,11 @@ async function runInteractiveApiSetup() {
279
230
  console.log(
280
231
  "\nWould you like to reconfigure with a fresh provider selection?\n"
281
232
  );
282
- await promptProviderSelection(rl, envPath);
233
+ await promptProviderSelection(rl);
283
234
  return;
284
235
  }
285
236
  console.log("\u2717 No snapshot API credentials detected.\n");
286
- await promptProviderSelection(rl, envPath);
237
+ await promptProviderSelection(rl);
287
238
  } finally {
288
239
  rl.close();
289
240
  }
@@ -7,7 +7,7 @@ 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/config.js";
10
+ import { readSnapshotModel } from "../core/config.js";
11
11
  import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
12
12
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
13
13
  function generateSnapshotRunId() {
@@ -187,8 +187,8 @@ async function captureScreenshot(session, logger, pageId) {
187
187
  async function runSnapshot(session, logger, pageId, objective, context) {
188
188
  const normalizedObjective = objective.trim();
189
189
  const normalizedContext = context.trim();
190
- const configuredAi = readAiConfig();
191
- resolveSnapshotApiModelOrThrow(configuredAi);
190
+ const snapshotModel = readSnapshotModel();
191
+ resolveSnapshotApiModelOrThrow(snapshotModel);
192
192
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
193
193
  session,
194
194
  logger,
@@ -206,7 +206,7 @@ async function runSnapshot(session, logger, pageId, objective, context) {
206
206
  htmlPath,
207
207
  condensedHtmlPath
208
208
  };
209
- await runApiInterpret(interpretArgs, logger, configuredAi);
209
+ await runApiInterpret(interpretArgs, logger, snapshotModel);
210
210
  }
211
211
  const snapshotInput = SimpleCLI.input({
212
212
  positionals: [],
@@ -6,7 +6,7 @@ function printAiStatus(status) {
6
6
  console.log("AI configuration:");
7
7
  switch (status.kind) {
8
8
  case "ready":
9
- console.log(` \u2713 Model: ${status.model}`);
9
+ console.log(` \u2713 Snapshot model: ${status.model}`);
10
10
  if (status.source === "config") {
11
11
  console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
12
12
  } else {
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { 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,
@@ -155,20 +155,20 @@ function inferAutoSnapshotModel() {
155
155
  }
156
156
  return null;
157
157
  }
158
- function resolveSnapshotApiModel(config = readAiConfig()) {
158
+ function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
159
159
  loadSnapshotEnv();
160
- if (config?.model) {
161
- const { provider } = parseModel(config.model);
160
+ if (snapshotModel) {
161
+ const { provider } = parseModel(snapshotModel);
162
162
  return {
163
- model: config.model,
163
+ model: snapshotModel,
164
164
  provider,
165
165
  source: "config"
166
166
  };
167
167
  }
168
168
  return inferAutoSnapshotModel();
169
169
  }
170
- function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
171
- const selection = resolveSnapshotApiModel(config);
170
+ function resolveSnapshotApiModelOrThrow(snapshotModel = readSnapshotModel()) {
171
+ const selection = resolveSnapshotApiModel(snapshotModel);
172
172
  if (!selection) {
173
173
  throw new SnapshotApiUnavailableError(noSnapshotApiConfiguredMessage());
174
174
  }
@@ -182,9 +182,9 @@ function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
182
182
  function isSnapshotApiUnavailableError(error) {
183
183
  return error instanceof SnapshotApiUnavailableError;
184
184
  }
185
- function readAiConfigSafely(configPath) {
185
+ function readSnapshotModelSafely(configPath) {
186
186
  try {
187
- return { ok: true, config: readAiConfig(configPath) };
187
+ return { ok: true, model: readSnapshotModel(configPath) };
188
188
  } catch (err) {
189
189
  return {
190
190
  ok: false,
@@ -194,14 +194,14 @@ function readAiConfigSafely(configPath) {
194
194
  }
195
195
  function resolveAiSetupStatus(configPath = LIBRETTO_CONFIG_PATH) {
196
196
  loadSnapshotEnv();
197
- const configResult = readAiConfigSafely(configPath);
198
- if (!configResult.ok) {
199
- return { kind: "invalid-config", message: configResult.message };
197
+ const result = readSnapshotModelSafely(configPath);
198
+ if (!result.ok) {
199
+ return { kind: "invalid-config", message: result.message };
200
200
  }
201
- if (configResult.config) {
201
+ if (result.model) {
202
202
  let selection;
203
203
  try {
204
- selection = resolveSnapshotApiModel(configResult.config);
204
+ selection = resolveSnapshotApiModel(result.model);
205
205
  } catch (err) {
206
206
  return {
207
207
  kind: "invalid-config",
@@ -7,10 +7,10 @@ import {
7
7
  getMimeType,
8
8
  readFileAsBase64
9
9
  } from "./snapshot-analyzer.js";
10
- import { readAiConfig } from "./config.js";
10
+ import { readSnapshotModel } from "./config.js";
11
11
  import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
12
- async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
13
- const selection = resolveSnapshotApiModelOrThrow(configuredAi);
12
+ async function runApiInterpret(args, logger, snapshotModel = readSnapshotModel()) {
13
+ const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
14
14
  logger.info("api-interpret-start", {
15
15
  objective: args.objective,
16
16
  pngPath: args.pngPath,
@@ -4,10 +4,6 @@ import { z } from "zod";
4
4
  import { SessionAccessModeSchema } from "../../shared/state/index.js";
5
5
  import { LIBRETTO_CONFIG_PATH } from "./context.js";
6
6
  const CURRENT_CONFIG_VERSION = 1;
7
- const AiConfigSchema = z.object({
8
- model: z.string().min(1),
9
- updatedAt: z.string()
10
- });
11
7
  const ViewportConfigSchema = z.object({
12
8
  width: z.number().int().min(1),
13
9
  height: z.number().int().min(1)
@@ -18,7 +14,7 @@ const WindowPositionConfigSchema = z.object({
18
14
  });
19
15
  const LibrettoConfigSchema = z.object({
20
16
  version: z.literal(CURRENT_CONFIG_VERSION),
21
- ai: AiConfigSchema.optional(),
17
+ snapshotModel: z.string().min(1).optional(),
22
18
  viewport: ViewportConfigSchema.optional(),
23
19
  windowPosition: WindowPositionConfigSchema.optional(),
24
20
  provider: z.string().optional(),
@@ -31,10 +27,7 @@ function formatExpectedConfigExample() {
31
27
  return JSON.stringify(
32
28
  {
33
29
  version: CURRENT_CONFIG_VERSION,
34
- ai: {
35
- model: "openai/gpt-5.4",
36
- updatedAt: "2026-01-01T00:00:00.000Z"
37
- },
30
+ snapshotModel: "openai/gpt-5.4",
38
31
  viewport: {
39
32
  width: 1280,
40
33
  height: 800
@@ -52,14 +45,14 @@ function formatExpectedConfigExample() {
52
45
  function invalidConfigError(configPath, detail) {
53
46
  return new Error(
54
47
  [
55
- `AI config is invalid at ${configPath}.`,
48
+ `Config is invalid at ${configPath}.`,
56
49
  detail ? `Problems:
57
50
  ${detail}` : null,
58
51
  "Expected config example:",
59
52
  formatExpectedConfigExample(),
60
53
  "Notes:",
61
- ' - "ai", "viewport", "windowPosition", and "sessionMode" are optional.',
62
- ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
54
+ ' - "snapshotModel", "viewport", "windowPosition", and "sessionMode" are optional.',
55
+ ' - "snapshotModel" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
63
56
  "Fix the file to match this shape, or delete it and rerun:",
64
57
  ` npx libretto ai configure openai | anthropic | gemini | vertex`
65
58
  ].filter(Boolean).join("\n")
@@ -93,34 +86,30 @@ function writeLibrettoConfig(config, configPath = LIBRETTO_CONFIG_PATH) {
93
86
  writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
94
87
  return parsed;
95
88
  }
96
- function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
97
- return readLibrettoConfig(configPath).ai ?? null;
89
+ function readSnapshotModel(configPath = LIBRETTO_CONFIG_PATH) {
90
+ return readLibrettoConfig(configPath).snapshotModel ?? null;
98
91
  }
99
- function writeAiConfig(model, configPath = LIBRETTO_CONFIG_PATH) {
92
+ function writeSnapshotModel(model, configPath = LIBRETTO_CONFIG_PATH) {
100
93
  let librettoConfig;
101
94
  try {
102
95
  librettoConfig = readLibrettoConfig(configPath);
103
96
  } catch {
104
97
  librettoConfig = { version: CURRENT_CONFIG_VERSION };
105
98
  }
106
- const ai = AiConfigSchema.parse({
107
- model,
108
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
109
- });
110
99
  writeLibrettoConfig(
111
100
  {
112
101
  ...librettoConfig,
113
102
  version: CURRENT_CONFIG_VERSION,
114
- ai
103
+ snapshotModel: model
115
104
  },
116
105
  configPath
117
106
  );
118
- return ai;
107
+ return model;
119
108
  }
120
- function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
109
+ function clearSnapshotModel(configPath = LIBRETTO_CONFIG_PATH) {
121
110
  const librettoConfig = readLibrettoConfig(configPath);
122
- if (!librettoConfig.ai) return false;
123
- const { ai: _ai, ...rest } = librettoConfig;
111
+ if (!librettoConfig.snapshotModel) return false;
112
+ const { snapshotModel: _, ...rest } = librettoConfig;
124
113
  writeLibrettoConfig(
125
114
  {
126
115
  ...rest
@@ -130,14 +119,13 @@ function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
130
119
  return true;
131
120
  }
132
121
  export {
133
- AiConfigSchema,
134
122
  CURRENT_CONFIG_VERSION,
135
123
  LibrettoConfigSchema,
136
124
  ViewportConfigSchema,
137
125
  WindowPositionConfigSchema,
138
- clearAiConfig,
139
- readAiConfig,
126
+ clearSnapshotModel,
140
127
  readLibrettoConfig,
141
- writeAiConfig,
142
- writeLibrettoConfig
128
+ readSnapshotModel,
129
+ writeLibrettoConfig,
130
+ writeSnapshotModel
143
131
  };
@@ -762,14 +762,17 @@ function buildInputNormalizer(definition) {
762
762
  toKebabCase(key),
763
763
  key
764
764
  ].filter((candidate) => candidate.length > 0);
765
- let value = void 0;
765
+ let found = false;
766
766
  for (const candidate of normalizedCandidates) {
767
767
  if (Object.prototype.hasOwnProperty.call(named, candidate)) {
768
- value = named[candidate];
768
+ output[key] = named[candidate];
769
+ found = true;
769
770
  break;
770
771
  }
771
772
  }
772
- output[key] = value;
773
+ if (!found && !Object.prototype.hasOwnProperty.call(output, key)) {
774
+ output[key] = void 0;
775
+ }
773
776
  }
774
777
  return output;
775
778
  };
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
+ "homepage": "https://libretto.sh",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "https://github.com/saffron-health/libretto"
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
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 Works