libretto 0.3.1 → 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/README.md +17 -7
- package/dist/cli/commands/ai.js +3 -5
- package/dist/cli/commands/browser.js +23 -2
- package/dist/cli/commands/init.js +157 -114
- package/dist/cli/commands/snapshot.js +147 -26
- package/dist/cli/core/ai-config.js +38 -46
- package/dist/cli/core/api-snapshot-analyzer.js +74 -0
- package/dist/cli/core/browser.js +21 -4
- package/dist/cli/core/context.js +1 -1
- package/dist/cli/core/snapshot-analyzer.js +295 -104
- package/dist/cli/core/snapshot-api-config.js +137 -0
- package/dist/cli/index.js +1 -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 +4 -3
- package/dist/shared/llm/types.d.ts +4 -3
- package/dist/shared/state/session-state.cjs +8 -1
- package/dist/shared/state/session-state.d.cts +24 -18
- package/dist/shared/state/session-state.d.ts +24 -18
- package/dist/shared/state/session-state.js +7 -1
- package/package.json +39 -33
|
@@ -1,24 +1,29 @@
|
|
|
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();
|
|
10
|
+
const ViewportConfigSchema = z.object({
|
|
11
|
+
width: z.number().int().min(1),
|
|
12
|
+
height: z.number().int().min(1)
|
|
13
|
+
});
|
|
13
14
|
const LibrettoConfigSchema = z.object({
|
|
14
15
|
version: z.literal(CURRENT_CONFIG_VERSION),
|
|
15
|
-
ai: AiConfigSchema.optional()
|
|
16
|
+
ai: AiConfigSchema.optional(),
|
|
17
|
+
viewport: ViewportConfigSchema.optional()
|
|
16
18
|
}).passthrough();
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
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"
|
|
21
25
|
};
|
|
26
|
+
const CONFIGURE_PROVIDERS = Object.keys(DEFAULT_MODELS);
|
|
22
27
|
function invalidConfigError(configPath) {
|
|
23
28
|
return new Error(
|
|
24
29
|
`AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
|
|
@@ -46,18 +51,10 @@ function writeLibrettoConfig(config, configPath = LIBRETTO_CONFIG_PATH) {
|
|
|
46
51
|
function readAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
|
|
47
52
|
return readLibrettoConfig(configPath).ai ?? null;
|
|
48
53
|
}
|
|
49
|
-
function
|
|
50
|
-
if (/^[a-zA-Z0-9_./:@=-]+$/.test(value)) return value;
|
|
51
|
-
return JSON.stringify(value);
|
|
52
|
-
}
|
|
53
|
-
function formatCommandPrefix(prefix) {
|
|
54
|
-
return prefix.map((arg) => quoteShellArg(arg)).join(" ");
|
|
55
|
-
}
|
|
56
|
-
function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH) {
|
|
54
|
+
function writeAiConfig(model, configPath = LIBRETTO_CONFIG_PATH) {
|
|
57
55
|
const librettoConfig = readLibrettoConfig(configPath);
|
|
58
56
|
const ai = AiConfigSchema.parse({
|
|
59
|
-
|
|
60
|
-
commandPrefix,
|
|
57
|
+
model,
|
|
61
58
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
62
59
|
});
|
|
63
60
|
writeLibrettoConfig(
|
|
@@ -73,36 +70,34 @@ function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH)
|
|
|
73
70
|
function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
|
|
74
71
|
const librettoConfig = readLibrettoConfig(configPath);
|
|
75
72
|
if (!librettoConfig.ai) return false;
|
|
73
|
+
const { ai: _ai, ...rest } = librettoConfig;
|
|
76
74
|
writeLibrettoConfig(
|
|
77
75
|
{
|
|
78
|
-
|
|
76
|
+
...rest
|
|
79
77
|
},
|
|
80
78
|
configPath
|
|
81
79
|
);
|
|
82
80
|
return true;
|
|
83
81
|
}
|
|
84
82
|
function printAiConfig(config, configPath) {
|
|
85
|
-
console.log(`
|
|
86
|
-
console.log(`Command prefix: ${formatCommandPrefix(config.commandPrefix)}`);
|
|
83
|
+
console.log(`Model: ${config.model}`);
|
|
87
84
|
console.log(`Config file: ${configPath}`);
|
|
88
85
|
console.log(`Updated at: ${config.updatedAt}`);
|
|
89
86
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
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;
|
|
96
92
|
}
|
|
97
93
|
function runAiConfigure(input, options = {}) {
|
|
98
|
-
const configureCommandName = options.configureCommandName ?? "libretto
|
|
94
|
+
const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
|
|
99
95
|
const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
|
|
100
96
|
const presetArg = input.preset?.trim();
|
|
101
|
-
|
|
102
|
-
if (!presetArg && customPrefix.length === 0 && !input.clear) {
|
|
97
|
+
if (!presetArg && !input.clear) {
|
|
103
98
|
const config2 = readAiConfig(configPath);
|
|
104
99
|
if (!config2) {
|
|
105
|
-
console.log(`No AI config set. Run '${configureCommandName}
|
|
100
|
+
console.log(`No AI config set. Run '${configureCommandName} openai' to set one.`);
|
|
106
101
|
return;
|
|
107
102
|
}
|
|
108
103
|
printAiConfig(config2, configPath);
|
|
@@ -117,30 +112,27 @@ function runAiConfigure(input, options = {}) {
|
|
|
117
112
|
}
|
|
118
113
|
return;
|
|
119
114
|
}
|
|
120
|
-
const
|
|
121
|
-
if (!
|
|
122
|
-
|
|
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
|
+
);
|
|
123
122
|
throw new Error(
|
|
124
|
-
|
|
123
|
+
`Invalid provider or model. Use one of: ${CONFIGURE_PROVIDERS.join(", ")}, or a full model string like "openai/gpt-4o".`
|
|
125
124
|
);
|
|
126
125
|
}
|
|
127
|
-
|
|
128
|
-
throw new Error("Custom command prefix cannot be empty.");
|
|
129
|
-
}
|
|
130
|
-
const preset = parsedPreset.data;
|
|
131
|
-
const commandPrefix = customPrefix.length > 0 ? customPrefix : AI_CONFIG_PRESETS[preset];
|
|
132
|
-
const config = writeAiConfig(preset, commandPrefix, configPath);
|
|
126
|
+
const config = writeAiConfig(model, configPath);
|
|
133
127
|
console.log("AI config saved.");
|
|
134
128
|
printAiConfig(config, configPath);
|
|
135
129
|
}
|
|
136
130
|
export {
|
|
137
|
-
AI_CONFIG_PRESETS,
|
|
138
131
|
AiConfigSchema,
|
|
139
|
-
AiPresetSchema,
|
|
140
132
|
CURRENT_CONFIG_VERSION,
|
|
141
133
|
LibrettoConfigSchema,
|
|
134
|
+
ViewportConfigSchema,
|
|
142
135
|
clearAiConfig,
|
|
143
|
-
formatCommandPrefix,
|
|
144
136
|
readAiConfig,
|
|
145
137
|
readLibrettoConfig,
|
|
146
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/browser.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getSessionNetworkLogPath,
|
|
10
10
|
PROFILES_DIR
|
|
11
11
|
} from "./context.js";
|
|
12
|
+
import { readLibrettoConfig } from "./ai-config.js";
|
|
12
13
|
import {
|
|
13
14
|
assertSessionAvailableForStart,
|
|
14
15
|
clearSessionState,
|
|
@@ -216,9 +217,24 @@ async function runPages(session, logger) {
|
|
|
216
217
|
console.log(` id=${pageSummary.id} url=${pageSummary.url}${activeSuffix}`);
|
|
217
218
|
});
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
+
const DEFAULT_VIEWPORT = { width: 1366, height: 768 };
|
|
221
|
+
function resolveViewport(cliViewport, logger) {
|
|
222
|
+
if (cliViewport) {
|
|
223
|
+
logger.info("viewport-source", { source: "cli", viewport: cliViewport });
|
|
224
|
+
return cliViewport;
|
|
225
|
+
}
|
|
226
|
+
const config = readLibrettoConfig();
|
|
227
|
+
if (config.viewport) {
|
|
228
|
+
logger.info("viewport-source", { source: "config", viewport: config.viewport });
|
|
229
|
+
return config.viewport;
|
|
230
|
+
}
|
|
231
|
+
logger.info("viewport-source", { source: "default", viewport: DEFAULT_VIEWPORT });
|
|
232
|
+
return DEFAULT_VIEWPORT;
|
|
233
|
+
}
|
|
234
|
+
async function runOpen(rawUrl, headed, session, logger, options) {
|
|
220
235
|
const url = normalizeUrl(rawUrl);
|
|
221
|
-
|
|
236
|
+
const viewport = resolveViewport(options?.viewport, logger);
|
|
237
|
+
logger.info("open-start", { url, headed, session, viewport });
|
|
222
238
|
assertSessionAvailableForStart(session, logger);
|
|
223
239
|
const port = await pickFreePort();
|
|
224
240
|
const runLogPath = logFileForSession(session);
|
|
@@ -296,7 +312,7 @@ browser.on('disconnected', () => {
|
|
|
296
312
|
|
|
297
313
|
const context = await browser.newContext({
|
|
298
314
|
${storageStateCode}
|
|
299
|
-
viewport: { width:
|
|
315
|
+
viewport: { width: ${viewport.width}, height: ${viewport.height} },
|
|
300
316
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
|
301
317
|
});
|
|
302
318
|
|
|
@@ -398,7 +414,8 @@ await new Promise(() => {});
|
|
|
398
414
|
pid: child.pid,
|
|
399
415
|
session,
|
|
400
416
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
401
|
-
status: "active"
|
|
417
|
+
status: "active",
|
|
418
|
+
viewport
|
|
402
419
|
}, logger);
|
|
403
420
|
logger.info("open-success", {
|
|
404
421
|
url,
|
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");
|