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 +28 -49
- package/dist/cli/commands/ai.js +15 -16
- package/dist/cli/commands/browser.js +6 -2
- package/dist/cli/commands/deploy.js +16 -3
- package/dist/cli/commands/setup.js +20 -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/dist/cli/framework/simple-cli.js +6 -3
- package/package.json +2 -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 +29 -48
- package/src/cli/commands/ai.ts +15 -17
- package/src/cli/commands/browser.ts +6 -2
- package/src/cli/commands/deploy.ts +18 -3
- package/src/cli/commands/setup.ts +19 -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/src/cli/framework/simple-cli.ts +6 -3
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
|
-
|
|
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);
|
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: [
|
|
@@ -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
|
|
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 (
|
|
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/
|
|
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
|
|
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
|
-
|
|
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,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
|
-
|
|
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: ["
|
|
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
|
|
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
|
-
|
|
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("
|
|
257
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
233
|
+
await promptProviderSelection(rl);
|
|
283
234
|
return;
|
|
284
235
|
}
|
|
285
236
|
console.log("\u2717 No snapshot API credentials detected.\n");
|
|
286
|
-
await promptProviderSelection(rl
|
|
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 {
|
|
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
|
};
|
|
@@ -762,14 +762,17 @@ function buildInputNormalizer(definition) {
|
|
|
762
762
|
toKebabCase(key),
|
|
763
763
|
key
|
|
764
764
|
].filter((candidate) => candidate.length > 0);
|
|
765
|
-
let
|
|
765
|
+
let found = false;
|
|
766
766
|
for (const candidate of normalizedCandidates) {
|
|
767
767
|
if (Object.prototype.hasOwnProperty.call(named, candidate)) {
|
|
768
|
-
|
|
768
|
+
output[key] = named[candidate];
|
|
769
|
+
found = true;
|
|
769
770
|
break;
|
|
770
771
|
}
|
|
771
772
|
}
|
|
772
|
-
output
|
|
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.
|
|
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"
|