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.
@@ -1,24 +1,29 @@
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();
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 AI_CONFIG_PRESETS = {
18
- codex: ["codex", "exec", "--skip-git-repo-check", "--sandbox", "read-only"],
19
- claude: [join(homedir(), ".claude", "local", "claude"), "-p"],
20
- 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"
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 quoteShellArg(value) {
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
- preset,
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
- version: librettoConfig.version
76
+ ...rest
79
77
  },
80
78
  configPath
81
79
  );
82
80
  return true;
83
81
  }
84
82
  function printAiConfig(config, configPath) {
85
- console.log(`AI preset: ${config.preset}`);
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 printConfigureUsage(commandName) {
91
- console.log(
92
- `Usage: ${commandName} <codex|claude|gemini> [-- <command prefix...>]
93
- ${commandName}
94
- ${commandName} --clear`
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-cli ai configure";
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
- const customPrefix = (input.customPrefix ?? []).filter(Boolean);
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} codex' to set one.`);
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 parsedPreset = AiPresetSchema.safeParse(presetArg);
121
- if (!parsedPreset.success) {
122
- 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
+ );
123
122
  throw new Error(
124
- "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".`
125
124
  );
126
125
  }
127
- if (input.customPrefix && input.customPrefix.length > 0 && customPrefix.length === 0) {
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
+ };
@@ -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
- async function runOpen(rawUrl, headed, session, logger) {
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
- logger.info("open-start", { url, headed, session });
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: 1366, height: 768 },
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,
@@ -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");