libretto 0.3.2 → 0.4.1
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 +83 -223
- package/dist/cli/commands/ai.js +32 -18
- package/dist/cli/commands/browser.js +126 -85
- package/dist/cli/commands/execution.js +147 -108
- package/dist/cli/commands/init.js +234 -131
- package/dist/cli/commands/logs.js +90 -65
- package/dist/cli/commands/shared.js +50 -0
- package/dist/cli/commands/snapshot.js +62 -37
- 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/cli/framework/simple-cli.js +776 -0
- package/dist/cli/router.js +29 -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 +7 -4
- /package/{.agents/skills → skills}/libretto/SKILL.md +0 -0
- /package/{.agents/skills → skills}/libretto/code-generation-rules.md +0 -0
- /package/{.agents/skills → skills}/libretto/integration-approach-selection.md +0 -0
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { connect, disconnectBrowser } from "../core/browser.js";
|
|
3
4
|
import { getSessionSnapshotRunDir } from "../core/context.js";
|
|
5
|
+
import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
|
|
4
6
|
import { readSessionState } from "../core/session.js";
|
|
7
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
5
8
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
loadSessionStateMiddleware,
|
|
10
|
+
pageOption,
|
|
11
|
+
resolveSessionMiddleware,
|
|
12
|
+
sessionOption
|
|
13
|
+
} from "./shared.js";
|
|
14
|
+
import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
|
|
15
|
+
import { readAiConfig } from "../core/ai-config.js";
|
|
9
16
|
const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
|
|
10
17
|
const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
|
|
11
18
|
function generateSnapshotRunId() {
|
|
@@ -97,6 +104,7 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
97
104
|
}
|
|
98
105
|
const pngPath = `${snapshotRunDir}/page.png`;
|
|
99
106
|
const htmlPath = `${snapshotRunDir}/page.html`;
|
|
107
|
+
const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
|
|
100
108
|
const restoreViewport = resolveSnapshotViewport(session, logger);
|
|
101
109
|
const viewportMetrics = await readSnapshotViewportMetrics(page);
|
|
102
110
|
logger.info("screenshot-viewport-metrics", {
|
|
@@ -132,15 +140,23 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
132
140
|
const htmlContent = await page.content();
|
|
133
141
|
const fs = await import("node:fs/promises");
|
|
134
142
|
await fs.writeFile(htmlPath, htmlContent);
|
|
143
|
+
const condenseResult = condenseDom(htmlContent);
|
|
144
|
+
await fs.writeFile(condensedHtmlPath, condenseResult.html);
|
|
135
145
|
logger.info("screenshot-success", {
|
|
136
146
|
session,
|
|
137
147
|
pageUrl,
|
|
138
148
|
title,
|
|
139
149
|
pngPath,
|
|
140
150
|
htmlPath,
|
|
141
|
-
|
|
151
|
+
condensedHtmlPath,
|
|
152
|
+
snapshotRunId,
|
|
153
|
+
domCondenseStats: {
|
|
154
|
+
originalLength: condenseResult.originalLength,
|
|
155
|
+
condensedLength: condenseResult.condensedLength,
|
|
156
|
+
reductions: condenseResult.reductions
|
|
157
|
+
}
|
|
142
158
|
});
|
|
143
|
-
return { pngPath, htmlPath, baseName: snapshotRunId };
|
|
159
|
+
return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
|
|
144
160
|
} catch (err) {
|
|
145
161
|
let pageAlive = false;
|
|
146
162
|
let browserConnected = false;
|
|
@@ -168,50 +184,59 @@ async function captureScreenshot(session, logger, pageId) {
|
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
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
187
|
const normalizedObjective = objective?.trim();
|
|
176
188
|
const normalizedContext = context?.trim();
|
|
177
|
-
if (!normalizedObjective &&
|
|
178
|
-
console.log("Use --objective flag to analyze snapshots.");
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (!normalizedObjective) {
|
|
189
|
+
if (!normalizedObjective && normalizedContext) {
|
|
182
190
|
throw new Error(
|
|
183
191
|
"Couldn't run analysis: --objective is required when providing --context."
|
|
184
192
|
);
|
|
185
193
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
194
|
+
const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
|
|
195
|
+
session,
|
|
196
|
+
logger,
|
|
197
|
+
pageId
|
|
198
|
+
);
|
|
199
|
+
console.log("Screenshot saved:");
|
|
200
|
+
console.log(` PNG: ${pngPath}`);
|
|
201
|
+
console.log(` HTML: ${htmlPath}`);
|
|
202
|
+
console.log(` Condensed HTML: ${condensedHtmlPath}`);
|
|
203
|
+
if (!normalizedObjective) {
|
|
204
|
+
console.log("Use --objective flag to analyze snapshots.");
|
|
205
|
+
return;
|
|
190
206
|
}
|
|
191
|
-
|
|
207
|
+
const interpretArgs = {
|
|
192
208
|
objective: normalizedObjective,
|
|
193
209
|
session,
|
|
194
210
|
context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
|
|
195
211
|
pngPath,
|
|
196
|
-
htmlPath
|
|
197
|
-
|
|
212
|
+
htmlPath,
|
|
213
|
+
condensedHtmlPath
|
|
214
|
+
};
|
|
215
|
+
await runApiInterpret(interpretArgs, logger, readAiConfig());
|
|
198
216
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
217
|
+
const snapshotInput = SimpleCLI.input({
|
|
218
|
+
positionals: [],
|
|
219
|
+
named: {
|
|
220
|
+
session: sessionOption(),
|
|
221
|
+
page: pageOption(),
|
|
222
|
+
objective: SimpleCLI.option(z.string().optional()),
|
|
223
|
+
context: SimpleCLI.option(z.string().optional())
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
function createSnapshotCommand(logger) {
|
|
227
|
+
return SimpleCLI.command({
|
|
228
|
+
description: "Capture PNG + HTML; analyze when --objective is provided (--context optional)"
|
|
229
|
+
}).input(snapshotInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
|
|
230
|
+
await runSnapshot(
|
|
231
|
+
ctx.session,
|
|
232
|
+
logger,
|
|
233
|
+
input.page,
|
|
234
|
+
input.objective,
|
|
235
|
+
input.context
|
|
236
|
+
);
|
|
237
|
+
});
|
|
214
238
|
}
|
|
215
239
|
export {
|
|
216
|
-
|
|
240
|
+
createSnapshotCommand,
|
|
241
|
+
snapshotInput
|
|
217
242
|
};
|
|
@@ -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");
|