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.
Files changed (31) hide show
  1. package/dist/cli/cli.js +83 -223
  2. package/dist/cli/commands/ai.js +32 -18
  3. package/dist/cli/commands/browser.js +126 -85
  4. package/dist/cli/commands/execution.js +147 -108
  5. package/dist/cli/commands/init.js +234 -131
  6. package/dist/cli/commands/logs.js +90 -65
  7. package/dist/cli/commands/shared.js +50 -0
  8. package/dist/cli/commands/snapshot.js +62 -37
  9. package/dist/cli/core/ai-config.js +29 -44
  10. package/dist/cli/core/api-snapshot-analyzer.js +74 -0
  11. package/dist/cli/core/context.js +1 -1
  12. package/dist/cli/core/snapshot-analyzer.js +200 -87
  13. package/dist/cli/core/snapshot-api-config.js +137 -0
  14. package/dist/cli/framework/simple-cli.js +776 -0
  15. package/dist/cli/router.js +29 -0
  16. package/dist/shared/condense-dom/condense-dom.cjs +462 -0
  17. package/dist/shared/condense-dom/condense-dom.d.cts +34 -0
  18. package/dist/shared/condense-dom/condense-dom.d.ts +34 -0
  19. package/dist/shared/condense-dom/condense-dom.js +438 -0
  20. package/dist/shared/llm/ai-sdk-adapter.cjs +5 -1
  21. package/dist/shared/llm/ai-sdk-adapter.js +5 -1
  22. package/dist/shared/llm/client.cjs +106 -27
  23. package/dist/shared/llm/client.d.cts +8 -1
  24. package/dist/shared/llm/client.d.ts +8 -1
  25. package/dist/shared/llm/client.js +89 -23
  26. package/dist/shared/llm/types.d.cts +2 -1
  27. package/dist/shared/llm/types.d.ts +2 -1
  28. package/package.json +7 -4
  29. /package/{.agents/skills → skills}/libretto/SKILL.md +0 -0
  30. /package/{.agents/skills → skills}/libretto/code-generation-rules.md +0 -0
  31. /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
- canAnalyzeSnapshots,
7
- runInterpret
8
- } from "../core/snapshot-analyzer.js";
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
- snapshotRunId
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 && !normalizedContext) {
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
- if (!canAnalyzeSnapshots()) {
187
- throw new Error(
188
- "Couldn't run analysis: no AI config set. Run 'libretto-cli ai configure codex' (or claude/gemini) to enable analysis."
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
- await runInterpret({
207
+ const interpretArgs = {
192
208
  objective: normalizedObjective,
193
209
  session,
194
210
  context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
195
211
  pngPath,
196
- htmlPath
197
- }, logger);
212
+ htmlPath,
213
+ condensedHtmlPath
214
+ };
215
+ await runApiInterpret(interpretArgs, logger, readAiConfig());
198
216
  }
199
- function registerSnapshotCommands(yargs, logger) {
200
- return yargs.command(
201
- "snapshot",
202
- "Capture PNG + HTML; analyze when --objective is provided (--context optional)",
203
- (cmd) => cmd.option("page", { type: "string" }).option("objective", { type: "string" }).option("context", { type: "string" }),
204
- async (argv) => {
205
- await runSnapshot(
206
- String(argv.session),
207
- logger,
208
- argv.page ? String(argv.page) : void 0,
209
- argv.objective,
210
- argv.context
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
- registerSnapshotCommands
240
+ createSnapshotCommand,
241
+ snapshotInput
217
242
  };
@@ -1,13 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
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
- preset: AiPresetSchema,
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 AI_CONFIG_PRESETS = {
23
- codex: ["codex", "exec", "--skip-git-repo-check", "--sandbox", "read-only"],
24
- claude: [join(homedir(), ".claude", "local", "claude"), "-p"],
25
- gemini: ["gemini", "--output-format", "json"]
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 quoteShellArg(value) {
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
- preset,
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(`AI preset: ${config.preset}`);
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 printConfigureUsage(commandName) {
97
- console.log(
98
- `Usage: ${commandName} <codex|claude|gemini> [-- <command prefix...>]
99
- ${commandName}
100
- ${commandName} --clear`
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-cli ai configure";
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
- const customPrefix = (input.customPrefix ?? []).filter(Boolean);
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} codex' to set one.`);
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 parsedPreset = AiPresetSchema.safeParse(presetArg);
127
- if (!parsedPreset.success) {
128
- printConfigureUsage(configureCommandName);
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
- "Missing or invalid preset. Use one of: codex, claude, gemini."
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
- if (input.customPrefix && input.customPrefix.length > 0 && customPrefix.length === 0) {
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
+ };
@@ -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");