libretto 0.3.2 → 0.4.0
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/commands/ai.js +3 -5
- package/dist/cli/commands/init.js +157 -114
- package/dist/cli/commands/snapshot.js +32 -22
- package/dist/cli/core/ai-config.js +29 -44
- package/dist/cli/core/api-snapshot-analyzer.js +74 -0
- package/dist/cli/core/context.js +1 -1
- package/dist/cli/core/snapshot-analyzer.js +200 -87
- package/dist/cli/core/snapshot-api-config.js +137 -0
- package/dist/shared/condense-dom/condense-dom.cjs +462 -0
- package/dist/shared/condense-dom/condense-dom.d.cts +34 -0
- package/dist/shared/condense-dom/condense-dom.d.ts +34 -0
- package/dist/shared/condense-dom/condense-dom.js +438 -0
- package/dist/shared/llm/ai-sdk-adapter.cjs +5 -1
- package/dist/shared/llm/ai-sdk-adapter.js +5 -1
- package/dist/shared/llm/client.cjs +106 -27
- package/dist/shared/llm/client.d.cts +8 -1
- package/dist/shared/llm/client.d.ts +8 -1
- package/dist/shared/llm/client.js +89 -23
- package/dist/shared/llm/types.d.cts +2 -1
- package/dist/shared/llm/types.d.ts +2 -1
- package/package.json +6 -1
package/dist/cli/commands/ai.js
CHANGED
|
@@ -2,16 +2,14 @@ import { runAiConfigure } from "../core/ai-config.js";
|
|
|
2
2
|
function registerAICommands(yargs) {
|
|
3
3
|
return yargs.command(
|
|
4
4
|
"ai configure [preset]",
|
|
5
|
-
"Configure AI
|
|
5
|
+
"Configure AI model for snapshot analysis",
|
|
6
6
|
(cmd) => cmd.option("clear", { type: "boolean", default: false }),
|
|
7
7
|
(argv) => {
|
|
8
|
-
const customPrefix = Array.isArray(argv["--"]) ? argv["--"] : [];
|
|
9
8
|
runAiConfigure({
|
|
10
9
|
clear: Boolean(argv.clear),
|
|
11
|
-
preset: argv.preset
|
|
12
|
-
customPrefix
|
|
10
|
+
preset: argv.preset
|
|
13
11
|
}, {
|
|
14
|
-
configureCommandName: "libretto
|
|
12
|
+
configureCommandName: "npx libretto ai configure"
|
|
15
13
|
});
|
|
16
14
|
}
|
|
17
15
|
);
|
|
@@ -1,67 +1,169 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { existsSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import {
|
|
5
|
-
AI_CONFIG_PRESETS,
|
|
6
|
-
AiPresetSchema,
|
|
7
|
-
formatCommandPrefix,
|
|
8
6
|
readAiConfig
|
|
9
7
|
} from "../core/ai-config.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { REPO_ROOT } from "../core/context.js";
|
|
9
|
+
import {
|
|
10
|
+
loadSnapshotEnv,
|
|
11
|
+
resolveSnapshotApiModel
|
|
12
|
+
} from "../core/snapshot-api-config.js";
|
|
13
|
+
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
14
|
+
const PROVIDER_CHOICES = [
|
|
15
|
+
{
|
|
16
|
+
key: "1",
|
|
17
|
+
label: "OpenAI",
|
|
18
|
+
envVar: "OPENAI_API_KEY",
|
|
19
|
+
envHint: "Get your key at https://platform.openai.com/api-keys"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "2",
|
|
23
|
+
label: "Anthropic",
|
|
24
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
25
|
+
envHint: "Get your key at https://console.anthropic.com/settings/keys"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "3",
|
|
29
|
+
label: "Google Gemini",
|
|
30
|
+
envVar: "GEMINI_API_KEY",
|
|
31
|
+
envHint: "Get your key at https://aistudio.google.com/apikey"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "4",
|
|
35
|
+
label: "Google Vertex AI",
|
|
36
|
+
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
37
|
+
envHint: "Requires gcloud auth application-default login and a GCP project ID"
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function promptUser(rl, question) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
rl.question(question, (answer) => {
|
|
43
|
+
resolve(answer.trim());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
13
46
|
}
|
|
14
|
-
function
|
|
47
|
+
function safeReadAiConfig() {
|
|
15
48
|
try {
|
|
16
|
-
|
|
17
|
-
if (!stats.isFile()) return false;
|
|
18
|
-
if (process.platform === "win32") {
|
|
19
|
-
const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
20
|
-
const extensions = pathExt.split(";").map((ext) => ext.trim().toUpperCase()).filter(Boolean);
|
|
21
|
-
const fileExt = extname(filePath).toUpperCase();
|
|
22
|
-
return extensions.includes(fileExt);
|
|
23
|
-
}
|
|
24
|
-
accessSync(filePath, constants.X_OK);
|
|
25
|
-
return true;
|
|
49
|
+
return readAiConfig();
|
|
26
50
|
} catch {
|
|
27
|
-
return
|
|
51
|
+
return null;
|
|
28
52
|
}
|
|
29
53
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const candidates = hasExtension ? [command] : extensions.map(
|
|
43
|
-
(ext) => ext.startsWith(".") ? `${command}${ext}` : `${command}.${ext}`
|
|
44
|
-
);
|
|
45
|
-
return pathEntries.some(
|
|
46
|
-
(dir) => candidates.some((candidate) => isRunnableFile(join(dir, candidate)))
|
|
54
|
+
function printSnapshotApiStatus() {
|
|
55
|
+
const config = safeReadAiConfig();
|
|
56
|
+
const selection = resolveSnapshotApiModel(config);
|
|
57
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
58
|
+
console.log("\nSnapshot analysis:");
|
|
59
|
+
console.log(
|
|
60
|
+
" Libretto uses direct API calls for snapshot analysis when supported credentials are available."
|
|
61
|
+
);
|
|
62
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
63
|
+
if (selection && hasProviderCredentials(selection.provider)) {
|
|
64
|
+
console.log(
|
|
65
|
+
` \u2713 Ready: ${selection.model} (${selection.source})`
|
|
47
66
|
);
|
|
67
|
+
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
68
|
+
console.log(" No further action required.");
|
|
69
|
+
return;
|
|
48
70
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
71
|
+
console.log(" \u2717 No snapshot API credentials detected.");
|
|
72
|
+
console.log(" Add one provider to .env:");
|
|
73
|
+
console.log(" OPENAI_API_KEY=...");
|
|
74
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
75
|
+
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
76
|
+
console.log(
|
|
77
|
+
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
54
78
|
);
|
|
79
|
+
console.log(
|
|
80
|
+
" Or run `npx libretto ai configure <provider>` to set a specific model."
|
|
81
|
+
);
|
|
82
|
+
console.log(" Run `npx libretto init` interactively to set up credentials.");
|
|
55
83
|
}
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
function printDifferentAnalyzerHint(prefix = " ") {
|
|
84
|
+
async function runInteractiveApiSetup() {
|
|
85
|
+
const config = safeReadAiConfig();
|
|
86
|
+
const selection = resolveSnapshotApiModel(config);
|
|
87
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
88
|
+
console.log("\nSnapshot analysis setup:");
|
|
62
89
|
console.log(
|
|
63
|
-
|
|
90
|
+
" Libretto uses direct API calls for snapshot analysis."
|
|
64
91
|
);
|
|
92
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
93
|
+
if (selection && hasProviderCredentials(selection.provider)) {
|
|
94
|
+
console.log(
|
|
95
|
+
` \u2713 Ready: ${selection.model} (${selection.source})`
|
|
96
|
+
);
|
|
97
|
+
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
console.log(" \u2717 No snapshot API credentials detected.\n");
|
|
101
|
+
const rl = createInterface({
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
console.log(" Which API provider would you like to use for snapshot analysis?\n");
|
|
107
|
+
for (const choice of PROVIDER_CHOICES) {
|
|
108
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
109
|
+
}
|
|
110
|
+
console.log(" s) Skip for now\n");
|
|
111
|
+
const answer = await promptUser(rl, " Choice: ");
|
|
112
|
+
if (answer.toLowerCase() === "s" || !answer) {
|
|
113
|
+
console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
|
|
114
|
+
console.log(" Or add credentials directly to your .env file:");
|
|
115
|
+
console.log(" OPENAI_API_KEY=...");
|
|
116
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
117
|
+
console.log(" GEMINI_API_KEY=...");
|
|
118
|
+
console.log(
|
|
119
|
+
" Or run `npx libretto ai configure <provider>` to set a specific model."
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const selected = PROVIDER_CHOICES.find((c) => c.key === answer);
|
|
124
|
+
if (!selected) {
|
|
125
|
+
console.log(`
|
|
126
|
+
Unknown choice "${answer}". Skipping API setup.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.log(`
|
|
130
|
+
${selected.label} selected.`);
|
|
131
|
+
console.log(` ${selected.envHint}
|
|
132
|
+
`);
|
|
133
|
+
const apiKeyValue = await promptUser(rl, ` Enter your ${selected.envVar}: `);
|
|
134
|
+
if (!apiKeyValue) {
|
|
135
|
+
console.log("\n No value entered. Skipping API key setup.");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let envContent = "";
|
|
139
|
+
if (existsSync(envPath)) {
|
|
140
|
+
envContent = readFileSync(envPath, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
const envLine = `${selected.envVar}=${apiKeyValue}`;
|
|
143
|
+
if (envContent.includes(`${selected.envVar}=`)) {
|
|
144
|
+
const updated = envContent.replace(
|
|
145
|
+
new RegExp(`^${selected.envVar}=.*$`, "m"),
|
|
146
|
+
() => envLine
|
|
147
|
+
);
|
|
148
|
+
writeFileSync(envPath, updated);
|
|
149
|
+
console.log(`
|
|
150
|
+
\u2713 Updated ${selected.envVar} in ${envPath}`);
|
|
151
|
+
} else {
|
|
152
|
+
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
153
|
+
appendFileSync(envPath, `${separator}${envLine}
|
|
154
|
+
`);
|
|
155
|
+
console.log(`
|
|
156
|
+
\u2713 Added ${selected.envVar} to ${envPath}`);
|
|
157
|
+
}
|
|
158
|
+
loadSnapshotEnv();
|
|
159
|
+
process.env[selected.envVar] = apiKeyValue;
|
|
160
|
+
const newSelection = resolveSnapshotApiModel(safeReadAiConfig());
|
|
161
|
+
if (newSelection && hasProviderCredentials(newSelection.provider)) {
|
|
162
|
+
console.log(` \u2713 Snapshot API ready: ${newSelection.model}`);
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
rl.close();
|
|
166
|
+
}
|
|
65
167
|
}
|
|
66
168
|
function installBrowsers() {
|
|
67
169
|
console.log("\nInstalling Playwright Chromium...");
|
|
@@ -77,69 +179,6 @@ function installBrowsers() {
|
|
|
77
179
|
);
|
|
78
180
|
}
|
|
79
181
|
}
|
|
80
|
-
function checkAiRuntimeConfiguration() {
|
|
81
|
-
let config = null;
|
|
82
|
-
let configReadError = null;
|
|
83
|
-
try {
|
|
84
|
-
config = readAiConfig();
|
|
85
|
-
} catch (error) {
|
|
86
|
-
configReadError = error instanceof Error ? error.message : String(error);
|
|
87
|
-
}
|
|
88
|
-
const availableCommands = detectAvailableAiRuntimeCommands();
|
|
89
|
-
console.log("\nAI runtime configuration:");
|
|
90
|
-
console.log(
|
|
91
|
-
" Libretto can use your coding agent as a subagent to analyze snapshots and other page signals."
|
|
92
|
-
);
|
|
93
|
-
console.log(
|
|
94
|
-
" This is optional, but it significantly improves page understanding and debugging performance."
|
|
95
|
-
);
|
|
96
|
-
if (configReadError) {
|
|
97
|
-
console.log(` \u2717 Could not read AI config: ${configReadError}`);
|
|
98
|
-
console.log(" Reconfigure with:");
|
|
99
|
-
printAiConfigureCommands(" ");
|
|
100
|
-
printDifferentAnalyzerHint(" ");
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (config) {
|
|
104
|
-
const configuredCommand = config.commandPrefix[0];
|
|
105
|
-
if (!isCommandDefined(configuredCommand)) {
|
|
106
|
-
console.log(
|
|
107
|
-
` \u2717 Configured command not found: ${configuredCommand ?? "(empty)"}`
|
|
108
|
-
);
|
|
109
|
-
if (availableCommands.length > 0) {
|
|
110
|
-
console.log(
|
|
111
|
-
` Detected available commands: ${availableCommands.join(", ")}`
|
|
112
|
-
);
|
|
113
|
-
} else {
|
|
114
|
-
console.log(
|
|
115
|
-
" No codex, claude, or gemini analyzer command was detected on PATH."
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
console.log(" Reconfigure with:");
|
|
119
|
-
printAiConfigureCommands(" ");
|
|
120
|
-
printDifferentAnalyzerHint(" ");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
console.log(
|
|
124
|
-
` \u2713 Configured (${config.preset}): ${formatCommandPrefix(config.commandPrefix)}`
|
|
125
|
-
);
|
|
126
|
-
console.log(" Analysis commands are ready to use.");
|
|
127
|
-
printDifferentAnalyzerHint(" ");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
console.log(" \u2717 No AI config set.");
|
|
131
|
-
if (availableCommands.length > 0) {
|
|
132
|
-
console.log(
|
|
133
|
-
` Detected available commands: ${availableCommands.join(", ")}`
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
console.log(" No codex, claude, or gemini analyzer command was detected on PATH.");
|
|
137
|
-
}
|
|
138
|
-
console.log(" Configure one with:");
|
|
139
|
-
printAiConfigureCommands(" ");
|
|
140
|
-
printDifferentAnalyzerHint(" ");
|
|
141
|
-
console.log(" Optionally provide a custom command prefix with '-- ...'.");
|
|
142
|
-
}
|
|
143
182
|
function registerInitCommand(yargs) {
|
|
144
183
|
return yargs.command(
|
|
145
184
|
"init",
|
|
@@ -149,14 +188,18 @@ function registerInitCommand(yargs) {
|
|
|
149
188
|
default: false,
|
|
150
189
|
describe: "Skip Playwright Chromium installation"
|
|
151
190
|
}),
|
|
152
|
-
(argv) => {
|
|
191
|
+
async (argv) => {
|
|
153
192
|
console.log("Initializing libretto...\n");
|
|
154
193
|
if (!argv["skip-browsers"]) {
|
|
155
194
|
installBrowsers();
|
|
156
195
|
} else {
|
|
157
196
|
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
158
197
|
}
|
|
159
|
-
|
|
198
|
+
if (process.stdin.isTTY) {
|
|
199
|
+
await runInteractiveApiSetup();
|
|
200
|
+
} else {
|
|
201
|
+
printSnapshotApiStatus();
|
|
202
|
+
}
|
|
160
203
|
console.log("\n\u2713 libretto init complete");
|
|
161
204
|
}
|
|
162
205
|
);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { connect, disconnectBrowser } from "../core/browser.js";
|
|
3
3
|
import { getSessionSnapshotRunDir } from "../core/context.js";
|
|
4
|
+
import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
|
|
4
5
|
import { readSessionState } from "../core/session.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
runInterpret
|
|
8
|
-
} from "../core/snapshot-analyzer.js";
|
|
6
|
+
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
7
|
+
import { readAiConfig } from "../core/ai-config.js";
|
|
9
8
|
const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
|
|
10
9
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
11
10
|
function generateSnapshotRunId() {
|
|
@@ -97,6 +96,7 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
97
96
|
}
|
|
98
97
|
const pngPath = `${snapshotRunDir}/page.png`;
|
|
99
98
|
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
99
|
+
const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
|
|
100
100
|
const restoreViewport = resolveSnapshotViewport(session, logger);
|
|
101
101
|
const viewportMetrics = await readSnapshotViewportMetrics(page);
|
|
102
102
|
logger.info("screenshot-viewport-metrics", {
|
|
@@ -132,15 +132,23 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
132
132
|
const htmlContent = await page.content();
|
|
133
133
|
const fs = await import("node:fs/promises");
|
|
134
134
|
await fs.writeFile(htmlPath, htmlContent);
|
|
135
|
+
const condenseResult = condenseDom(htmlContent);
|
|
136
|
+
await fs.writeFile(condensedHtmlPath, condenseResult.html);
|
|
135
137
|
logger.info("screenshot-success", {
|
|
136
138
|
session,
|
|
137
139
|
pageUrl,
|
|
138
140
|
title,
|
|
139
141
|
pngPath,
|
|
140
142
|
htmlPath,
|
|
141
|
-
|
|
143
|
+
condensedHtmlPath,
|
|
144
|
+
snapshotRunId,
|
|
145
|
+
domCondenseStats: {
|
|
146
|
+
originalLength: condenseResult.originalLength,
|
|
147
|
+
condensedLength: condenseResult.condensedLength,
|
|
148
|
+
reductions: condenseResult.reductions
|
|
149
|
+
}
|
|
142
150
|
});
|
|
143
|
-
return { pngPath, htmlPath, baseName: snapshotRunId };
|
|
151
|
+
return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
|
|
144
152
|
} catch (err) {
|
|
145
153
|
let pageAlive = false;
|
|
146
154
|
let browserConnected = false;
|
|
@@ -168,33 +176,35 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
async function runSnapshot(session, logger, pageId, objective, context) {
|
|
171
|
-
const { pngPath, htmlPath } = await captureScreenshot(session, logger, pageId);
|
|
172
|
-
console.log("Screenshot saved:");
|
|
173
|
-
console.log(` PNG: ${pngPath}`);
|
|
174
|
-
console.log(` HTML: ${htmlPath}`);
|
|
175
179
|
const normalizedObjective = objective?.trim();
|
|
176
180
|
const normalizedContext = context?.trim();
|
|
177
|
-
if (!normalizedObjective &&
|
|
178
|
-
console.log("Use --objective flag to analyze snapshots.");
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (!normalizedObjective) {
|
|
181
|
+
if (!normalizedObjective && normalizedContext) {
|
|
182
182
|
throw new Error(
|
|
183
183
|
"Couldn't run analysis: --objective is required when providing --context."
|
|
184
184
|
);
|
|
185
185
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
187
|
+
session,
|
|
188
|
+
logger,
|
|
189
|
+
pageId
|
|
190
|
+
);
|
|
191
|
+
console.log("Screenshot saved:");
|
|
192
|
+
console.log(` PNG: ${pngPath}`);
|
|
193
|
+
console.log(` HTML: ${htmlPath}`);
|
|
194
|
+
console.log(` Condensed HTML: ${condensedHtmlPath}`);
|
|
195
|
+
if (!normalizedObjective) {
|
|
196
|
+
console.log("Use --objective flag to analyze snapshots.");
|
|
197
|
+
return;
|
|
190
198
|
}
|
|
191
|
-
|
|
199
|
+
const interpretArgs = {
|
|
192
200
|
objective: normalizedObjective,
|
|
193
201
|
session,
|
|
194
202
|
context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
|
|
195
203
|
pngPath,
|
|
196
|
-
htmlPath
|
|
197
|
-
|
|
204
|
+
htmlPath,
|
|
205
|
+
condensedHtmlPath
|
|
206
|
+
};
|
|
207
|
+
await runApiInterpret(interpretArgs, logger, readAiConfig());
|
|
198
208
|
}
|
|
199
209
|
function registerSnapshotCommands(yargs, logger) {
|
|
200
210
|
return yargs.command(
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname
|
|
3
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { dirname } from "node:path";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { LIBRETTO_CONFIG_PATH } from "./context.js";
|
|
6
5
|
const CURRENT_CONFIG_VERSION = 1;
|
|
7
|
-
const AiPresetSchema = z.enum(["codex", "claude", "gemini"]);
|
|
8
6
|
const AiConfigSchema = z.object({
|
|
9
|
-
|
|
10
|
-
commandPrefix: z.array(z.string()).min(1),
|
|
7
|
+
model: z.string().min(1),
|
|
11
8
|
updatedAt: z.string()
|
|
12
9
|
}).strict();
|
|
13
10
|
const ViewportConfigSchema = z.object({
|
|
@@ -19,11 +16,14 @@ const LibrettoConfigSchema = z.object({
|
|
|
19
16
|
ai: AiConfigSchema.optional(),
|
|
20
17
|
viewport: ViewportConfigSchema.optional()
|
|
21
18
|
}).passthrough();
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
gemini:
|
|
19
|
+
const DEFAULT_MODELS = {
|
|
20
|
+
openai: "openai/gpt-5.4",
|
|
21
|
+
anthropic: "anthropic/claude-sonnet-4-6",
|
|
22
|
+
gemini: "google/gemini-2.5-flash",
|
|
23
|
+
google: "google/gemini-2.5-flash",
|
|
24
|
+
vertex: "vertex/gemini-2.5-pro"
|
|
26
25
|
};
|
|
26
|
+
const CONFIGURE_PROVIDERS = Object.keys(DEFAULT_MODELS);
|
|
27
27
|
function invalidConfigError(configPath) {
|
|
28
28
|
return new Error(
|
|
29
29
|
`AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
|
|
@@ -51,18 +51,10 @@ function writeLibrettoConfig(config, configPath = LIBRETTO_CONFIG_PATH) {
|
|
|
51
51
|
function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
|
|
52
52
|
return readLibrettoConfig(configPath).ai ?? null;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
55
|
-
if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) return value;
|
|
56
|
-
return JSON.stringify(value);
|
|
57
|
-
}
|
|
58
|
-
function formatCommandPrefix(prefix) {
|
|
59
|
-
return prefix.map((arg) => quoteShellArg(arg)).join(" ");
|
|
60
|
-
}
|
|
61
|
-
function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH) {
|
|
54
|
+
function writeAiConfig(model, configPath = LIBRETTO_CONFIG_PATH) {
|
|
62
55
|
const librettoConfig = readLibrettoConfig(configPath);
|
|
63
56
|
const ai = AiConfigSchema.parse({
|
|
64
|
-
|
|
65
|
-
commandPrefix,
|
|
57
|
+
model,
|
|
66
58
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
67
59
|
});
|
|
68
60
|
writeLibrettoConfig(
|
|
@@ -88,27 +80,24 @@ function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
|
|
|
88
80
|
return true;
|
|
89
81
|
}
|
|
90
82
|
function printAiConfig(config, configPath) {
|
|
91
|
-
console.log(`
|
|
92
|
-
console.log(`Command prefix: ${formatCommandPrefix(config.commandPrefix)}`);
|
|
83
|
+
console.log(`Model: ${config.model}`);
|
|
93
84
|
console.log(`Config file: ${configPath}`);
|
|
94
85
|
console.log(`Updated at: ${config.updatedAt}`);
|
|
95
86
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
87
|
+
function resolveModelFromInput(input) {
|
|
88
|
+
const trimmed = input.trim();
|
|
89
|
+
if (!trimmed) return null;
|
|
90
|
+
if (trimmed.includes("/")) return trimmed;
|
|
91
|
+
return DEFAULT_MODELS[trimmed.toLowerCase()] ?? null;
|
|
102
92
|
}
|
|
103
93
|
function runAiConfigure(input, options = {}) {
|
|
104
|
-
const configureCommandName = options.configureCommandName ?? "libretto
|
|
94
|
+
const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
|
|
105
95
|
const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
|
|
106
96
|
const presetArg = input.preset?.trim();
|
|
107
|
-
|
|
108
|
-
if (!presetArg && customPrefix.length === 0 && !input.clear) {
|
|
97
|
+
if (!presetArg && !input.clear) {
|
|
109
98
|
const config2 = readAiConfig(configPath);
|
|
110
99
|
if (!config2) {
|
|
111
|
-
console.log(`No AI config set. Run '${configureCommandName}
|
|
100
|
+
console.log(`No AI config set. Run '${configureCommandName} openai' to set one.`);
|
|
112
101
|
return;
|
|
113
102
|
}
|
|
114
103
|
printAiConfig(config2, configPath);
|
|
@@ -123,31 +112,27 @@ function runAiConfigure(input, options = {}) {
|
|
|
123
112
|
}
|
|
124
113
|
return;
|
|
125
114
|
}
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
|
|
115
|
+
const model = resolveModelFromInput(presetArg);
|
|
116
|
+
if (!model) {
|
|
117
|
+
console.log(
|
|
118
|
+
`Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>
|
|
119
|
+
${configureCommandName}
|
|
120
|
+
${configureCommandName} --clear`
|
|
121
|
+
);
|
|
129
122
|
throw new Error(
|
|
130
|
-
|
|
123
|
+
`Invalid provider or model. Use one of: ${CONFIGURE_PROVIDERS.join(", ")}, or a full model string like "openai/gpt-4o".`
|
|
131
124
|
);
|
|
132
125
|
}
|
|
133
|
-
|
|
134
|
-
throw new Error("Custom command prefix cannot be empty.");
|
|
135
|
-
}
|
|
136
|
-
const preset = parsedPreset.data;
|
|
137
|
-
const commandPrefix = customPrefix.length > 0 ? customPrefix : AI_CONFIG_PRESETS[preset];
|
|
138
|
-
const config = writeAiConfig(preset, commandPrefix, configPath);
|
|
126
|
+
const config = writeAiConfig(model, configPath);
|
|
139
127
|
console.log("AI config saved.");
|
|
140
128
|
printAiConfig(config, configPath);
|
|
141
129
|
}
|
|
142
130
|
export {
|
|
143
|
-
AI_CONFIG_PRESETS,
|
|
144
131
|
AiConfigSchema,
|
|
145
|
-
AiPresetSchema,
|
|
146
132
|
CURRENT_CONFIG_VERSION,
|
|
147
133
|
LibrettoConfigSchema,
|
|
148
134
|
ViewportConfigSchema,
|
|
149
135
|
clearAiConfig,
|
|
150
|
-
formatCommandPrefix,
|
|
151
136
|
readAiConfig,
|
|
152
137
|
readLibrettoConfig,
|
|
153
138
|
runAiConfigure,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createLLMClient } from "../../shared/llm/client.js";
|
|
3
|
+
import {
|
|
4
|
+
formatInterpretationOutput,
|
|
5
|
+
InterpretResultSchema,
|
|
6
|
+
buildInlinePromptSelection,
|
|
7
|
+
getMimeType,
|
|
8
|
+
readFileAsBase64
|
|
9
|
+
} from "./snapshot-analyzer.js";
|
|
10
|
+
import { readAiConfig } from "./ai-config.js";
|
|
11
|
+
import {
|
|
12
|
+
resolveSnapshotApiModelOrThrow
|
|
13
|
+
} from "./snapshot-api-config.js";
|
|
14
|
+
async function runApiInterpret(args, logger, configuredAi = readAiConfig()) {
|
|
15
|
+
const selection = resolveSnapshotApiModelOrThrow(configuredAi);
|
|
16
|
+
logger.info("api-interpret-start", {
|
|
17
|
+
objective: args.objective,
|
|
18
|
+
pngPath: args.pngPath,
|
|
19
|
+
htmlPath: args.htmlPath,
|
|
20
|
+
condensedHtmlPath: args.condensedHtmlPath,
|
|
21
|
+
model: selection.model,
|
|
22
|
+
modelSource: selection.source
|
|
23
|
+
});
|
|
24
|
+
const fullHtmlContent = readFileSync(args.htmlPath, "utf-8");
|
|
25
|
+
const condensedHtmlContent = readFileSync(args.condensedHtmlPath, "utf-8");
|
|
26
|
+
const promptSelection = buildInlinePromptSelection(
|
|
27
|
+
args,
|
|
28
|
+
fullHtmlContent,
|
|
29
|
+
condensedHtmlContent,
|
|
30
|
+
selection.model
|
|
31
|
+
);
|
|
32
|
+
logger.info("api-interpret-dom-selection", {
|
|
33
|
+
configuredModel: promptSelection.stats.configuredModel,
|
|
34
|
+
fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
|
|
35
|
+
condensedDomEstimatedTokens: promptSelection.stats.condensedDomEstimatedTokens,
|
|
36
|
+
contextWindowTokens: promptSelection.budget.contextWindowTokens,
|
|
37
|
+
promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
|
|
38
|
+
selectedDom: promptSelection.domSource,
|
|
39
|
+
selectedHtmlEstimatedTokens: promptSelection.htmlEstimatedTokens,
|
|
40
|
+
selectedPromptEstimatedTokens: promptSelection.promptEstimatedTokens,
|
|
41
|
+
selectionReason: promptSelection.selectionReason,
|
|
42
|
+
truncated: promptSelection.truncated
|
|
43
|
+
});
|
|
44
|
+
const imageBase64 = readFileAsBase64(args.pngPath);
|
|
45
|
+
const imageMimeType = getMimeType(args.pngPath);
|
|
46
|
+
const imageBytes = Buffer.from(imageBase64, "base64");
|
|
47
|
+
const client = createLLMClient(selection.model);
|
|
48
|
+
const result = await client.generateObjectFromMessages({
|
|
49
|
+
schema: InterpretResultSchema,
|
|
50
|
+
messages: [
|
|
51
|
+
{
|
|
52
|
+
role: "user",
|
|
53
|
+
content: [
|
|
54
|
+
{ type: "text", text: promptSelection.prompt },
|
|
55
|
+
{
|
|
56
|
+
type: "image",
|
|
57
|
+
image: imageBytes,
|
|
58
|
+
mediaType: imageMimeType
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
temperature: 0.1
|
|
64
|
+
});
|
|
65
|
+
const parsed = InterpretResultSchema.parse(result);
|
|
66
|
+
logger.info("api-interpret-success", {
|
|
67
|
+
selectorCount: parsed.selectors.length,
|
|
68
|
+
answer: parsed.answer.slice(0, 200)
|
|
69
|
+
});
|
|
70
|
+
console.log(formatInterpretationOutput(parsed, "Interpretation (via API):"));
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
runApiInterpret
|
|
74
|
+
};
|
package/dist/cli/core/context.js
CHANGED
|
@@ -86,7 +86,7 @@ function getLLMClientFactory() {
|
|
|
86
86
|
}
|
|
87
87
|
function maybeConfigureLLMClientFactoryFromEnv() {
|
|
88
88
|
if (llmClientFactory) return;
|
|
89
|
-
const hasAnyCreds = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
|
|
89
|
+
const hasAnyCreds = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
90
90
|
if (!hasAnyCreds) return;
|
|
91
91
|
setLLMClientFactory(async (_logger, model) => {
|
|
92
92
|
const { createLLMClient } = await import("../../shared/llm/index.js");
|