libretto 0.6.3 → 0.6.5
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 +1 -1
- package/dist/cli/commands/ai.js +15 -16
- package/dist/cli/commands/setup.js +83 -69
- package/dist/cli/commands/snapshot.js +4 -4
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/ai-model.js +14 -14
- package/dist/cli/core/api-snapshot-analyzer.js +3 -3
- package/dist/cli/core/config.js +17 -29
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto/references/configuration-file-reference.md +3 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +1 -1
- package/src/cli/commands/ai.ts +15 -17
- package/src/cli/commands/setup.ts +87 -91
- package/src/cli/commands/snapshot.ts +4 -4
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/ai-model.ts +22 -22
- package/src/cli/core/api-snapshot-analyzer.ts +3 -3
- package/src/cli/core/config.ts +15 -40
package/dist/cli/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ function printSetupAudit() {
|
|
|
16
16
|
const status = resolveAiSetupStatus();
|
|
17
17
|
switch (status.kind) {
|
|
18
18
|
case "ready":
|
|
19
|
-
console.log(`\u2713
|
|
19
|
+
console.log(`\u2713 Snapshot model: ${status.model}`);
|
|
20
20
|
break;
|
|
21
21
|
case "configured-missing-credentials":
|
|
22
22
|
console.log(
|
package/dist/cli/commands/ai.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
25
|
-
console.log(`
|
|
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
|
|
42
|
-
if (!
|
|
40
|
+
const model2 = readSnapshotModel(configPath);
|
|
41
|
+
if (!model2) {
|
|
43
42
|
console.log(
|
|
44
|
-
`No
|
|
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
|
-
|
|
50
|
+
printSnapshotModelConfig(model2, configPath);
|
|
52
51
|
return;
|
|
53
52
|
}
|
|
54
53
|
if (input.clear) {
|
|
55
|
-
const removed =
|
|
54
|
+
const removed = clearSnapshotModel(configPath);
|
|
56
55
|
if (removed) {
|
|
57
|
-
console.log(`Cleared
|
|
56
|
+
console.log(`Cleared snapshot model config: ${configPath}`);
|
|
58
57
|
} else {
|
|
59
|
-
console.log("No
|
|
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
|
-
|
|
75
|
-
console.log("
|
|
76
|
-
|
|
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: [
|
|
@@ -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
|
-
|
|
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 {
|
|
11
|
+
import { writeSnapshotModel } from "../core/config.js";
|
|
15
12
|
import {
|
|
16
13
|
ensureLibrettoSetup,
|
|
17
14
|
LIBRETTO_CONFIG_PATH,
|
|
@@ -19,10 +16,64 @@ 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";
|
|
22
|
+
const PROVIDER_SDK_PACKAGES = {
|
|
23
|
+
openai: "@ai-sdk/openai",
|
|
24
|
+
anthropic: "@ai-sdk/anthropic",
|
|
25
|
+
google: "@ai-sdk/google",
|
|
26
|
+
vertex: "@ai-sdk/google-vertex"
|
|
27
|
+
};
|
|
28
|
+
function detectPackageManager() {
|
|
29
|
+
if (existsSync(join(REPO_ROOT, "pnpm-lock.yaml"))) return "pnpm";
|
|
30
|
+
if (existsSync(join(REPO_ROOT, "yarn.lock"))) return "yarn";
|
|
31
|
+
if (existsSync(join(REPO_ROOT, "bun.lockb"))) return "bun";
|
|
32
|
+
return "npm";
|
|
33
|
+
}
|
|
34
|
+
function installCommand(pkgManager) {
|
|
35
|
+
switch (pkgManager) {
|
|
36
|
+
case "yarn":
|
|
37
|
+
return "yarn add";
|
|
38
|
+
case "bun":
|
|
39
|
+
return "bun add";
|
|
40
|
+
case "pnpm":
|
|
41
|
+
return "pnpm add";
|
|
42
|
+
default:
|
|
43
|
+
return "npm install";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isSdkInstalled(sdkPackage) {
|
|
47
|
+
try {
|
|
48
|
+
const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
|
|
49
|
+
cwd: REPO_ROOT,
|
|
50
|
+
stdio: "pipe"
|
|
51
|
+
});
|
|
52
|
+
return result.status === 0;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function installSdkIfNeeded(provider) {
|
|
58
|
+
const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
|
|
59
|
+
if (isSdkInstalled(sdkPackage)) return;
|
|
60
|
+
const pkgManager = detectPackageManager();
|
|
61
|
+
const cmd = installCommand(pkgManager);
|
|
62
|
+
console.log(`
|
|
63
|
+
Installing ${sdkPackage}...`);
|
|
64
|
+
const result = spawnSync(cmd, [sdkPackage], {
|
|
65
|
+
cwd: REPO_ROOT,
|
|
66
|
+
stdio: "inherit",
|
|
67
|
+
shell: true
|
|
68
|
+
});
|
|
69
|
+
if (result.status === 0) {
|
|
70
|
+
console.log(`\u2713 Installed ${sdkPackage}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.error(
|
|
73
|
+
`\u2717 Failed to install ${sdkPackage}. Install it manually: ${cmd} ${sdkPackage}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
26
77
|
const PROVIDER_CHOICES = [
|
|
27
78
|
{
|
|
28
79
|
key: "1",
|
|
@@ -70,7 +121,7 @@ function sourceEnvVar(source) {
|
|
|
70
121
|
}
|
|
71
122
|
function ensurePinnedDefaultModel(status) {
|
|
72
123
|
if (status.source !== "config") {
|
|
73
|
-
|
|
124
|
+
writeSnapshotModel(status.model);
|
|
74
125
|
return { ...status, source: "config" };
|
|
75
126
|
}
|
|
76
127
|
return status;
|
|
@@ -103,7 +154,7 @@ function buildRepairPlan(status) {
|
|
|
103
154
|
provider: status.provider,
|
|
104
155
|
model: status.model,
|
|
105
156
|
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
106
|
-
choices: ["
|
|
157
|
+
choices: ["switch-provider", "skip"]
|
|
107
158
|
};
|
|
108
159
|
}
|
|
109
160
|
if (status.kind === "invalid-config") {
|
|
@@ -156,50 +207,7 @@ function printSnapshotApiStatus() {
|
|
|
156
207
|
);
|
|
157
208
|
return false;
|
|
158
209
|
}
|
|
159
|
-
function
|
|
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) {
|
|
210
|
+
async function promptProviderSelection(rl) {
|
|
203
211
|
console.log(
|
|
204
212
|
"Which model provider would you like to use for snapshot analysis?\n"
|
|
205
213
|
);
|
|
@@ -218,7 +226,15 @@ async function promptProviderSelection(rl, envPath) {
|
|
|
218
226
|
Unknown choice "${answer}". Skipping API setup.`);
|
|
219
227
|
return false;
|
|
220
228
|
}
|
|
221
|
-
|
|
229
|
+
const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
|
|
230
|
+
writeSnapshotModel(model);
|
|
231
|
+
console.log(`
|
|
232
|
+
\u2713 ${selected.label} selected (model: ${model}).`);
|
|
233
|
+
console.log(`
|
|
234
|
+
Add ${selected.envVar} to your .env file:`);
|
|
235
|
+
console.log(` ${selected.envHint}`);
|
|
236
|
+
installSdkIfNeeded(selected.provider);
|
|
237
|
+
return true;
|
|
222
238
|
}
|
|
223
239
|
function printSkipMessage() {
|
|
224
240
|
console.log(
|
|
@@ -234,7 +250,6 @@ function printSkipMessage() {
|
|
|
234
250
|
}
|
|
235
251
|
async function runInteractiveApiSetup() {
|
|
236
252
|
const status = resolveAiSetupStatus();
|
|
237
|
-
const envPath = join(REPO_ROOT, ".env");
|
|
238
253
|
console.log(
|
|
239
254
|
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
|
|
240
255
|
);
|
|
@@ -252,23 +267,15 @@ async function runInteractiveApiSetup() {
|
|
|
252
267
|
try {
|
|
253
268
|
if (plan.kind === "repair-missing-credentials") {
|
|
254
269
|
console.log(formatMissingCredentialsMessage(plan));
|
|
270
|
+
console.log(`
|
|
271
|
+
Add ${plan.envVar} to your .env file to fix this.`);
|
|
255
272
|
console.log("");
|
|
256
|
-
console.log("
|
|
257
|
-
console.log(
|
|
258
|
-
console.log(" 2) Switch to a different provider");
|
|
273
|
+
console.log("Or switch to a different provider:\n");
|
|
274
|
+
console.log(" 1) Switch to a different provider");
|
|
259
275
|
console.log(" s) Skip for now\n");
|
|
260
276
|
const answer = await promptUser(rl, "Choice: ");
|
|
261
277
|
if (answer === "1") {
|
|
262
|
-
|
|
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);
|
|
278
|
+
await promptProviderSelection(rl);
|
|
272
279
|
return;
|
|
273
280
|
}
|
|
274
281
|
printSkipMessage();
|
|
@@ -279,11 +286,11 @@ async function runInteractiveApiSetup() {
|
|
|
279
286
|
console.log(
|
|
280
287
|
"\nWould you like to reconfigure with a fresh provider selection?\n"
|
|
281
288
|
);
|
|
282
|
-
await promptProviderSelection(rl
|
|
289
|
+
await promptProviderSelection(rl);
|
|
283
290
|
return;
|
|
284
291
|
}
|
|
285
292
|
console.log("\u2717 No snapshot API credentials detected.\n");
|
|
286
|
-
await promptProviderSelection(rl
|
|
293
|
+
await promptProviderSelection(rl);
|
|
287
294
|
} finally {
|
|
288
295
|
rl.close();
|
|
289
296
|
}
|
|
@@ -322,6 +329,13 @@ function detectAgentDirs(root) {
|
|
|
322
329
|
function copySkills() {
|
|
323
330
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
324
331
|
if (agentDirs.length === 0) {
|
|
332
|
+
console.log(
|
|
333
|
+
"\n\u26A0 No .agents/ or .claude/ directory found. Libretto skills were not installed."
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
" Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:"
|
|
337
|
+
);
|
|
338
|
+
console.log(` mkdir ${join(REPO_ROOT, ".claude")}`);
|
|
325
339
|
return;
|
|
326
340
|
}
|
|
327
341
|
let skillsRoot;
|
|
@@ -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 {
|
|
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
|
|
191
|
-
resolveSnapshotApiModelOrThrow(
|
|
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,
|
|
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
|
|
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 {
|
|
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(
|
|
158
|
+
function resolveSnapshotApiModel(snapshotModel = readSnapshotModel()) {
|
|
159
159
|
loadSnapshotEnv();
|
|
160
|
-
if (
|
|
161
|
-
const { provider } = parseModel(
|
|
160
|
+
if (snapshotModel) {
|
|
161
|
+
const { provider } = parseModel(snapshotModel);
|
|
162
162
|
return {
|
|
163
|
-
model:
|
|
163
|
+
model: snapshotModel,
|
|
164
164
|
provider,
|
|
165
165
|
source: "config"
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
return inferAutoSnapshotModel();
|
|
169
169
|
}
|
|
170
|
-
function resolveSnapshotApiModelOrThrow(
|
|
171
|
-
const selection = resolveSnapshotApiModel(
|
|
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
|
|
185
|
+
function readSnapshotModelSafely(configPath) {
|
|
186
186
|
try {
|
|
187
|
-
return { ok: true,
|
|
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
|
|
198
|
-
if (!
|
|
199
|
-
return { kind: "invalid-config", message:
|
|
197
|
+
const result = readSnapshotModelSafely(configPath);
|
|
198
|
+
if (!result.ok) {
|
|
199
|
+
return { kind: "invalid-config", message: result.message };
|
|
200
200
|
}
|
|
201
|
-
if (
|
|
201
|
+
if (result.model) {
|
|
202
202
|
let selection;
|
|
203
203
|
try {
|
|
204
|
-
selection = resolveSnapshotApiModel(
|
|
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 {
|
|
10
|
+
import { readSnapshotModel } from "./config.js";
|
|
11
11
|
import { resolveSnapshotApiModelOrThrow } from "./ai-model.js";
|
|
12
|
-
async function runApiInterpret(args, logger,
|
|
13
|
-
const selection = resolveSnapshotApiModelOrThrow(
|
|
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,
|
package/dist/cli/core/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
-
' - "
|
|
62
|
-
' - "
|
|
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
|
|
97
|
-
return readLibrettoConfig(configPath).
|
|
89
|
+
function readSnapshotModel(configPath = LIBRETTO_CONFIG_PATH) {
|
|
90
|
+
return readLibrettoConfig(configPath).snapshotModel ?? null;
|
|
98
91
|
}
|
|
99
|
-
function
|
|
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
|
-
|
|
103
|
+
snapshotModel: model
|
|
115
104
|
},
|
|
116
105
|
configPath
|
|
117
106
|
);
|
|
118
|
-
return
|
|
107
|
+
return model;
|
|
119
108
|
}
|
|
120
|
-
function
|
|
109
|
+
function clearSnapshotModel(configPath = LIBRETTO_CONFIG_PATH) {
|
|
121
110
|
const librettoConfig = readLibrettoConfig(configPath);
|
|
122
|
-
if (!librettoConfig.
|
|
123
|
-
const {
|
|
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
|
-
|
|
139
|
-
readAiConfig,
|
|
126
|
+
clearSnapshotModel,
|
|
140
127
|
readLibrettoConfig,
|
|
141
|
-
|
|
142
|
-
writeLibrettoConfig
|
|
128
|
+
readSnapshotModel,
|
|
129
|
+
writeLibrettoConfig,
|
|
130
|
+
writeSnapshotModel
|
|
143
131
|
};
|
package/package.json
CHANGED
package/skills/libretto/SKILL.md
CHANGED
|
@@ -19,7 +19,7 @@ Libretto reads workspace config from `.libretto/config.json`.
|
|
|
19
19
|
|
|
20
20
|
## Supported Settings
|
|
21
21
|
|
|
22
|
-
- `
|
|
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
|
-
"
|
|
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://
|
|
47
|
+
npx libretto open https://example.com --viewport 1440x900
|
|
51
48
|
npx libretto run ./integration.ts --viewport 1440x900
|
|
52
49
|
```
|
|
53
50
|
|
package/src/cli/cli.ts
CHANGED
|
@@ -19,7 +19,7 @@ function printSetupAudit(): void {
|
|
|
19
19
|
const status = resolveAiSetupStatus();
|
|
20
20
|
switch (status.kind) {
|
|
21
21
|
case "ready":
|
|
22
|
-
console.log(`✓
|
|
22
|
+
console.log(`✓ Snapshot model: ${status.model}`);
|
|
23
23
|
break;
|
|
24
24
|
case "configured-missing-credentials":
|
|
25
25
|
console.log(
|
package/src/cli/commands/ai.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
3
|
CURRENT_CONFIG_VERSION,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
31
|
-
console.log(`
|
|
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
|
|
75
|
-
if (!
|
|
72
|
+
const model = readSnapshotModel(configPath);
|
|
73
|
+
if (!model) {
|
|
76
74
|
console.log(
|
|
77
|
-
`No
|
|
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
|
-
|
|
82
|
+
printSnapshotModelConfig(model, configPath);
|
|
85
83
|
return;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
if (input.clear) {
|
|
89
|
-
const removed =
|
|
87
|
+
const removed = clearSnapshotModel(configPath);
|
|
90
88
|
if (removed) {
|
|
91
|
-
console.log(`Cleared
|
|
89
|
+
console.log(`Cleared snapshot model config: ${configPath}`);
|
|
92
90
|
} else {
|
|
93
|
-
console.log("No
|
|
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
|
-
|
|
111
|
-
console.log("
|
|
112
|
-
|
|
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({
|
|
@@ -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 {
|
|
11
|
+
import { writeSnapshotModel } from "../core/config.js";
|
|
15
12
|
import {
|
|
16
13
|
ensureLibrettoSetup,
|
|
17
14
|
LIBRETTO_CONFIG_PATH,
|
|
@@ -20,12 +17,71 @@ 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";
|
|
27
23
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
28
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
|
+
};
|
|
31
|
+
|
|
32
|
+
function detectPackageManager(): string {
|
|
33
|
+
if (existsSync(join(REPO_ROOT, "pnpm-lock.yaml"))) return "pnpm";
|
|
34
|
+
if (existsSync(join(REPO_ROOT, "yarn.lock"))) return "yarn";
|
|
35
|
+
if (existsSync(join(REPO_ROOT, "bun.lockb"))) return "bun";
|
|
36
|
+
return "npm";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function installCommand(pkgManager: string): string {
|
|
40
|
+
switch (pkgManager) {
|
|
41
|
+
case "yarn":
|
|
42
|
+
return "yarn add";
|
|
43
|
+
case "bun":
|
|
44
|
+
return "bun add";
|
|
45
|
+
case "pnpm":
|
|
46
|
+
return "pnpm add";
|
|
47
|
+
default:
|
|
48
|
+
return "npm install";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isSdkInstalled(sdkPackage: string): boolean {
|
|
53
|
+
try {
|
|
54
|
+
const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
|
|
55
|
+
cwd: REPO_ROOT,
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
});
|
|
58
|
+
return result.status === 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function installSdkIfNeeded(provider: Provider): void {
|
|
65
|
+
const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
|
|
66
|
+
if (isSdkInstalled(sdkPackage)) return;
|
|
67
|
+
|
|
68
|
+
const pkgManager = detectPackageManager();
|
|
69
|
+
const cmd = installCommand(pkgManager);
|
|
70
|
+
console.log(`\nInstalling ${sdkPackage}...`);
|
|
71
|
+
const result = spawnSync(cmd, [sdkPackage], {
|
|
72
|
+
cwd: REPO_ROOT,
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
shell: true,
|
|
75
|
+
});
|
|
76
|
+
if (result.status === 0) {
|
|
77
|
+
console.log(`✓ Installed ${sdkPackage}`);
|
|
78
|
+
} else {
|
|
79
|
+
console.error(
|
|
80
|
+
`✗ Failed to install ${sdkPackage}. Install it manually: ${cmd} ${sdkPackage}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
29
85
|
export type ProviderChoice = {
|
|
30
86
|
key: string;
|
|
31
87
|
label: string;
|
|
@@ -97,7 +153,7 @@ function ensurePinnedDefaultModel(
|
|
|
97
153
|
status: AiSetupStatus & { kind: "ready" },
|
|
98
154
|
): AiSetupStatus & { kind: "ready" } {
|
|
99
155
|
if (status.source !== "config") {
|
|
100
|
-
|
|
156
|
+
writeSnapshotModel(status.model);
|
|
101
157
|
return { ...status, source: "config" as const };
|
|
102
158
|
}
|
|
103
159
|
return status;
|
|
@@ -127,10 +183,7 @@ function printInvalidAiConfigWarning(status: AiSetupStatus): void {
|
|
|
127
183
|
|
|
128
184
|
// ── Repair plan helpers (exported for testing) ──────────────────────────────
|
|
129
185
|
|
|
130
|
-
export type RepairChoice =
|
|
131
|
-
| "enter-matching-credential"
|
|
132
|
-
| "switch-provider"
|
|
133
|
-
| "skip";
|
|
186
|
+
export type RepairChoice = "switch-provider" | "skip";
|
|
134
187
|
|
|
135
188
|
export type RepairPlan =
|
|
136
189
|
| {
|
|
@@ -155,7 +208,7 @@ export function buildRepairPlan(status: AiSetupStatus): RepairPlan {
|
|
|
155
208
|
provider: status.provider,
|
|
156
209
|
model: status.model,
|
|
157
210
|
envVar: choice?.envVar ?? `${status.provider.toUpperCase()}_API_KEY`,
|
|
158
|
-
choices: ["
|
|
211
|
+
choices: ["switch-provider", "skip"],
|
|
159
212
|
};
|
|
160
213
|
}
|
|
161
214
|
if (status.kind === "invalid-config") {
|
|
@@ -223,72 +276,13 @@ function printSnapshotApiStatus(): boolean {
|
|
|
223
276
|
}
|
|
224
277
|
|
|
225
278
|
/**
|
|
226
|
-
*
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
279
|
+
* Run the full provider selection menu.
|
|
280
|
+
* Pins the selected provider's default model to config and prints
|
|
281
|
+
* instructions for the user to add the credential to .env themselves.
|
|
287
282
|
* Returns true if a provider was successfully configured.
|
|
288
283
|
*/
|
|
289
284
|
async function promptProviderSelection(
|
|
290
285
|
rl: ReturnType<typeof createInterface>,
|
|
291
|
-
envPath: string,
|
|
292
286
|
): Promise<boolean> {
|
|
293
287
|
console.log(
|
|
294
288
|
"Which model provider would you like to use for snapshot analysis?\n",
|
|
@@ -311,7 +305,13 @@ async function promptProviderSelection(
|
|
|
311
305
|
return false;
|
|
312
306
|
}
|
|
313
307
|
|
|
314
|
-
|
|
308
|
+
const model = DEFAULT_SNAPSHOT_MODELS[selected.provider];
|
|
309
|
+
writeSnapshotModel(model);
|
|
310
|
+
console.log(`\n✓ ${selected.label} selected (model: ${model}).`);
|
|
311
|
+
console.log(`\nAdd ${selected.envVar} to your .env file:`);
|
|
312
|
+
console.log(` ${selected.envHint}`);
|
|
313
|
+
installSdkIfNeeded(selected.provider);
|
|
314
|
+
return true;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
function printSkipMessage(): void {
|
|
@@ -329,7 +329,6 @@ function printSkipMessage(): void {
|
|
|
329
329
|
|
|
330
330
|
async function runInteractiveApiSetup(): Promise<void> {
|
|
331
331
|
const status = resolveAiSetupStatus();
|
|
332
|
-
const envPath = join(REPO_ROOT, ".env");
|
|
333
332
|
|
|
334
333
|
console.log(
|
|
335
334
|
"\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables.",
|
|
@@ -353,26 +352,16 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
353
352
|
// ── Repair: configured provider with missing credentials ──
|
|
354
353
|
if (plan.kind === "repair-missing-credentials") {
|
|
355
354
|
console.log(formatMissingCredentialsMessage(plan));
|
|
355
|
+
console.log(`\nAdd ${plan.envVar} to your .env file to fix this.`);
|
|
356
356
|
console.log("");
|
|
357
|
-
console.log("
|
|
358
|
-
console.log(
|
|
359
|
-
console.log(" 2) Switch to a different provider");
|
|
357
|
+
console.log("Or switch to a different provider:\n");
|
|
358
|
+
console.log(" 1) Switch to a different provider");
|
|
360
359
|
console.log(" s) Skip for now\n");
|
|
361
360
|
|
|
362
361
|
const answer = await promptUser(rl, "Choice: ");
|
|
363
362
|
|
|
364
363
|
if (answer === "1") {
|
|
365
|
-
|
|
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);
|
|
364
|
+
await promptProviderSelection(rl);
|
|
376
365
|
return;
|
|
377
366
|
}
|
|
378
367
|
|
|
@@ -387,13 +376,13 @@ async function runInteractiveApiSetup(): Promise<void> {
|
|
|
387
376
|
console.log(
|
|
388
377
|
"\nWould you like to reconfigure with a fresh provider selection?\n",
|
|
389
378
|
);
|
|
390
|
-
await promptProviderSelection(rl
|
|
379
|
+
await promptProviderSelection(rl);
|
|
391
380
|
return;
|
|
392
381
|
}
|
|
393
382
|
|
|
394
383
|
// ── Unconfigured: standard first-run flow ──
|
|
395
384
|
console.log("✗ No snapshot API credentials detected.\n");
|
|
396
|
-
await promptProviderSelection(rl
|
|
385
|
+
await promptProviderSelection(rl);
|
|
397
386
|
} finally {
|
|
398
387
|
rl.close();
|
|
399
388
|
}
|
|
@@ -441,6 +430,13 @@ function copySkills(): void {
|
|
|
441
430
|
const agentDirs = detectAgentDirs(REPO_ROOT);
|
|
442
431
|
|
|
443
432
|
if (agentDirs.length === 0) {
|
|
433
|
+
console.log(
|
|
434
|
+
"\n⚠ No .agents/ or .claude/ directory found. Libretto skills were not installed.",
|
|
435
|
+
);
|
|
436
|
+
console.log(
|
|
437
|
+
" Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:",
|
|
438
|
+
);
|
|
439
|
+
console.log(` mkdir ${join(REPO_ROOT, ".claude")}`);
|
|
444
440
|
return;
|
|
445
441
|
}
|
|
446
442
|
|
|
@@ -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 {
|
|
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
|
|
260
|
-
resolveSnapshotApiModelOrThrow(
|
|
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,
|
|
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(` ✓
|
|
13
|
+
console.log(` ✓ Snapshot model: ${status.model}`);
|
|
14
14
|
if (status.source === "config") {
|
|
15
15
|
console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
|
|
16
16
|
} else {
|
package/src/cli/core/ai-model.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
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.
|
|
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
|
-
|
|
228
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
229
229
|
): SnapshotApiModelSelection | null {
|
|
230
230
|
loadSnapshotEnv();
|
|
231
231
|
|
|
232
|
-
if (
|
|
233
|
-
const { provider } = parseModel(
|
|
232
|
+
if (snapshotModel) {
|
|
233
|
+
const { provider } = parseModel(snapshotModel);
|
|
234
234
|
return {
|
|
235
|
-
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
|
-
|
|
245
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
246
246
|
): SnapshotApiModelSelection {
|
|
247
|
-
const selection = resolveSnapshotApiModel(
|
|
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
|
|
292
|
-
* Returns the
|
|
291
|
+
* Read snapshot model without throwing on invalid files.
|
|
292
|
+
* Returns the model string or an error message.
|
|
293
293
|
*/
|
|
294
|
-
function
|
|
294
|
+
function readSnapshotModelSafely(
|
|
295
295
|
configPath: string,
|
|
296
|
-
): { ok: true;
|
|
296
|
+
): { ok: true; model: string | null } | { ok: false; message: string } {
|
|
297
297
|
try {
|
|
298
|
-
return { ok: true,
|
|
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
|
|
316
|
-
* 3. If no
|
|
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
|
|
323
|
+
const result = readSnapshotModelSafely(configPath);
|
|
324
324
|
|
|
325
|
-
if (!
|
|
326
|
-
return { kind: "invalid-config", message:
|
|
325
|
+
if (!result.ok) {
|
|
326
|
+
return { kind: "invalid-config", message: result.message };
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
// Config
|
|
330
|
-
if (
|
|
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(
|
|
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
|
|
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 {
|
|
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
|
-
|
|
27
|
+
snapshotModel: string | null = readSnapshotModel(),
|
|
28
28
|
): Promise<void> {
|
|
29
|
-
const selection = resolveSnapshotApiModelOrThrow(
|
|
29
|
+
const selection = resolveSnapshotApiModelOrThrow(snapshotModel);
|
|
30
30
|
|
|
31
31
|
logger.info("api-interpret-start", {
|
|
32
32
|
objective: args.objective,
|
package/src/cli/core/config.ts
CHANGED
|
@@ -6,24 +6,6 @@ import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
|
6
6
|
|
|
7
7
|
export const CURRENT_CONFIG_VERSION = 1;
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* AI configuration schema.
|
|
11
|
-
*
|
|
12
|
-
* The `model` field is a provider/model-id string (e.g. "openai/gpt-5.4",
|
|
13
|
-
* "anthropic/claude-sonnet-4-6", "google/gemini-3-flash-preview", "vertex/gemini-2.5-pro").
|
|
14
|
-
*
|
|
15
|
-
* Legacy note: earlier versions stored a `preset` (codex|claude|gemini) and
|
|
16
|
-
* `commandPrefix` (CLI args to spawn a sub-agent process). That approach has
|
|
17
|
-
* been replaced by direct API calls via the Vercel AI SDK. The legacy CLI-agent
|
|
18
|
-
* code is preserved in snapshot-analyzer.ts but is not wired into the snapshot
|
|
19
|
-
* command.
|
|
20
|
-
*/
|
|
21
|
-
export const AiConfigSchema = z.object({
|
|
22
|
-
model: z.string().min(1),
|
|
23
|
-
updatedAt: z.string(),
|
|
24
|
-
});
|
|
25
|
-
export type AiConfig = z.infer<typeof AiConfigSchema>;
|
|
26
|
-
|
|
27
9
|
export const ViewportConfigSchema = z.object({
|
|
28
10
|
width: z.number().int().min(1),
|
|
29
11
|
height: z.number().int().min(1),
|
|
@@ -39,7 +21,7 @@ export type WindowPositionConfig = z.infer<typeof WindowPositionConfigSchema>;
|
|
|
39
21
|
export const LibrettoConfigSchema = z
|
|
40
22
|
.object({
|
|
41
23
|
version: z.literal(CURRENT_CONFIG_VERSION),
|
|
42
|
-
|
|
24
|
+
snapshotModel: z.string().min(1).optional(),
|
|
43
25
|
viewport: ViewportConfigSchema.optional(),
|
|
44
26
|
windowPosition: WindowPositionConfigSchema.optional(),
|
|
45
27
|
provider: z.string().optional(),
|
|
@@ -58,10 +40,7 @@ function formatExpectedConfigExample(): string {
|
|
|
58
40
|
return JSON.stringify(
|
|
59
41
|
{
|
|
60
42
|
version: CURRENT_CONFIG_VERSION,
|
|
61
|
-
|
|
62
|
-
model: "openai/gpt-5.4",
|
|
63
|
-
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
64
|
-
},
|
|
43
|
+
snapshotModel: "openai/gpt-5.4",
|
|
65
44
|
viewport: {
|
|
66
45
|
width: 1280,
|
|
67
46
|
height: 800,
|
|
@@ -80,13 +59,13 @@ function formatExpectedConfigExample(): string {
|
|
|
80
59
|
function invalidConfigError(configPath: string, detail?: string): Error {
|
|
81
60
|
return new Error(
|
|
82
61
|
[
|
|
83
|
-
`
|
|
62
|
+
`Config is invalid at ${configPath}.`,
|
|
84
63
|
detail ? `Problems:\n${detail}` : null,
|
|
85
64
|
"Expected config example:",
|
|
86
65
|
formatExpectedConfigExample(),
|
|
87
66
|
"Notes:",
|
|
88
|
-
' - "
|
|
89
|
-
' - "
|
|
67
|
+
' - "snapshotModel", "viewport", "windowPosition", and "sessionMode" are optional.',
|
|
68
|
+
' - "snapshotModel" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
|
|
90
69
|
"Fix the file to match this shape, or delete it and rerun:",
|
|
91
70
|
` npx libretto ai configure openai | anthropic | gemini | vertex`,
|
|
92
71
|
]
|
|
@@ -132,16 +111,16 @@ export function writeLibrettoConfig(
|
|
|
132
111
|
return parsed;
|
|
133
112
|
}
|
|
134
113
|
|
|
135
|
-
export function
|
|
114
|
+
export function readSnapshotModel(
|
|
136
115
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
137
|
-
):
|
|
138
|
-
return readLibrettoConfig(configPath).
|
|
116
|
+
): string | null {
|
|
117
|
+
return readLibrettoConfig(configPath).snapshotModel ?? null;
|
|
139
118
|
}
|
|
140
119
|
|
|
141
|
-
export function
|
|
120
|
+
export function writeSnapshotModel(
|
|
142
121
|
model: string,
|
|
143
122
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
144
|
-
):
|
|
123
|
+
): string {
|
|
145
124
|
let librettoConfig: LibrettoConfig;
|
|
146
125
|
try {
|
|
147
126
|
librettoConfig = readLibrettoConfig(configPath);
|
|
@@ -150,27 +129,23 @@ export function writeAiConfig(
|
|
|
150
129
|
// overwrite a broken file instead of throwing.
|
|
151
130
|
librettoConfig = { version: CURRENT_CONFIG_VERSION };
|
|
152
131
|
}
|
|
153
|
-
const ai = AiConfigSchema.parse({
|
|
154
|
-
model,
|
|
155
|
-
updatedAt: new Date().toISOString(),
|
|
156
|
-
});
|
|
157
132
|
writeLibrettoConfig(
|
|
158
133
|
{
|
|
159
134
|
...librettoConfig,
|
|
160
135
|
version: CURRENT_CONFIG_VERSION,
|
|
161
|
-
|
|
136
|
+
snapshotModel: model,
|
|
162
137
|
},
|
|
163
138
|
configPath,
|
|
164
139
|
);
|
|
165
|
-
return
|
|
140
|
+
return model;
|
|
166
141
|
}
|
|
167
142
|
|
|
168
|
-
export function
|
|
143
|
+
export function clearSnapshotModel(
|
|
169
144
|
configPath: string = LIBRETTO_CONFIG_PATH,
|
|
170
145
|
): boolean {
|
|
171
146
|
const librettoConfig = readLibrettoConfig(configPath);
|
|
172
|
-
if (!librettoConfig.
|
|
173
|
-
const {
|
|
147
|
+
if (!librettoConfig.snapshotModel) return false;
|
|
148
|
+
const { snapshotModel: _, ...rest } = librettoConfig;
|
|
174
149
|
writeLibrettoConfig(
|
|
175
150
|
{
|
|
176
151
|
...rest,
|