libretto 0.3.1 → 0.3.2

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 CHANGED
@@ -6,21 +6,18 @@ It is designed for engineering teams that automate workflows in web apps and wan
6
6
 
7
7
  ## Installation
8
8
 
9
- Install Libretto in your project with your favorite package manager:
10
-
11
9
  ```bash
12
- npm install libretto playwright zod
13
- yarn add libretto playwright zod
14
- bun add libretto playwright zod
15
- pnpm add libretto playwright zod
10
+ npm install --save-dev libretto
16
11
  ```
17
12
 
18
- Then initialize Libretto:
13
+ Chromium is downloaded automatically via a `postinstall` script. If postinstall scripts are disabled (e.g. `--ignore-scripts`, common in monorepos), run init manually:
19
14
 
20
15
  ```bash
21
16
  npx libretto init
22
17
  ```
23
18
 
19
+ This installs the Chromium browser binary and optionally configures an AI subagent (Gemini, Claude, or Codex) that can analyze page snapshots without consuming the coding agent's context window.
20
+
24
21
  ## Usage
25
22
 
26
23
  Libretto is usually used through prompts with the Libretto skill.
@@ -56,6 +53,19 @@ npx libretto help
56
53
  npx libretto run ./integration.ts main
57
54
  ```
58
55
 
56
+ ## The `.libretto/` directory
57
+
58
+ Libretto stores local runtime state in a `.libretto/` directory at your project root. Sensitive directories (`sessions/` and `profiles/`) are automatically git-ignored via `.libretto/.gitignore`.
59
+
60
+ - **`profiles/<domain>.json`** — Saved browser sessions (cookies, localStorage) for authenticated sites. Created via `npx libretto save <domain>`. Machine-local and never committed.
61
+ - **`sessions/<name>/`** — Per-session runtime state:
62
+ - `state.json` — Session metadata (debug port, PID, status)
63
+ - `logs.jsonl` — Structured session logs
64
+ - `network.jsonl` — Captured network requests (URLs, methods, headers, response status)
65
+ - `actions.jsonl` — Recorded user actions (clicks, fills, navigations)
66
+ - `snapshots/` — Screenshot PNGs and HTML snapshots captured via `npx libretto snapshot`
67
+ - **`ai.json`** — AI runtime configuration set via `npx libretto ai configure`.
68
+
59
69
  ## Authors
60
70
 
61
71
  Maintained by the team at [Saffron Health](https://saffron.health).
@@ -16,6 +16,9 @@ function registerBrowserCommands(yargs, logger) {
16
16
  }).option("headless", {
17
17
  type: "boolean",
18
18
  default: false
19
+ }).option("viewport", {
20
+ type: "string",
21
+ describe: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
19
22
  }),
20
23
  async (argv) => {
21
24
  const hasHeadedFlag = Boolean(argv.headed);
@@ -27,10 +30,28 @@ function registerBrowserCommands(yargs, logger) {
27
30
  const url = argv.url;
28
31
  if (!url) {
29
32
  throw new Error(
30
- "Usage: libretto-cli open <url> [--headless] [--session <name>]"
33
+ "Usage: libretto-cli open <url> [--headless] [--viewport WxH] [--session <name>]"
31
34
  );
32
35
  }
33
- await runOpen(url, headed, String(argv.session), logger);
36
+ const viewportArg = argv.viewport;
37
+ let viewport;
38
+ if (viewportArg) {
39
+ const match = viewportArg.match(/^(\d+)x(\d+)$/i);
40
+ if (!match) {
41
+ throw new Error(
42
+ "Invalid --viewport format. Expected WIDTHxHEIGHT (e.g. 1920x1080)."
43
+ );
44
+ }
45
+ const w = Number(match[1]);
46
+ const h = Number(match[2]);
47
+ if (w < 1 || h < 1) {
48
+ throw new Error(
49
+ "Invalid --viewport dimensions. Width and height must be at least 1."
50
+ );
51
+ }
52
+ viewport = { width: w, height: h };
53
+ }
54
+ await runOpen(url, headed, String(argv.session), logger, { viewport });
34
55
  }
35
56
  ).command(
36
57
  "save [urlOrDomain]",
@@ -1,14 +1,70 @@
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 { readSessionState } from "../core/session.js";
4
5
  import {
5
6
  canAnalyzeSnapshots,
6
7
  runInterpret
7
8
  } from "../core/snapshot-analyzer.js";
8
9
  const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
10
+ const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
9
11
  function generateSnapshotRunId() {
10
12
  return `snapshot-${Date.now()}`;
11
13
  }
14
+ function isZeroViewport(value) {
15
+ return typeof value === "number" && value <= 0;
16
+ }
17
+ function shouldForceSnapshotViewport(metrics) {
18
+ return isZeroViewport(metrics.configuredWidth) || isZeroViewport(metrics.configuredHeight) || isZeroViewport(metrics.innerWidth) || isZeroViewport(metrics.innerHeight);
19
+ }
20
+ function isZeroWidthScreenshotError(error) {
21
+ return error instanceof Error && error.message.includes("Cannot take screenshot with 0 width");
22
+ }
23
+ async function readSnapshotViewportMetrics(page) {
24
+ const configuredViewport = page.viewportSize();
25
+ let innerWidth = null;
26
+ let innerHeight = null;
27
+ try {
28
+ const innerViewport = await page.evaluate(() => ({
29
+ width: window.innerWidth,
30
+ height: window.innerHeight
31
+ }));
32
+ innerWidth = innerViewport.width;
33
+ innerHeight = innerViewport.height;
34
+ } catch {
35
+ }
36
+ return {
37
+ configuredWidth: configuredViewport?.width ?? null,
38
+ configuredHeight: configuredViewport?.height ?? null,
39
+ innerWidth,
40
+ innerHeight
41
+ };
42
+ }
43
+ function resolveSnapshotViewport(session, logger) {
44
+ const state = readSessionState(session, logger);
45
+ if (state?.viewport) {
46
+ logger.info("screenshot-viewport-from-session-state", {
47
+ session,
48
+ viewport: state.viewport
49
+ });
50
+ return state.viewport;
51
+ }
52
+ logger.info("screenshot-viewport-fallback", {
53
+ session,
54
+ reason: "no viewport in session state",
55
+ viewport: FALLBACK_SNAPSHOT_VIEWPORT
56
+ });
57
+ return FALLBACK_SNAPSHOT_VIEWPORT;
58
+ }
59
+ async function forceSnapshotViewport(page, viewport, logger, session, pageId, reason) {
60
+ await page.setViewportSize(viewport);
61
+ logger.warn("screenshot-viewport-forced", {
62
+ session,
63
+ pageId,
64
+ reason,
65
+ viewport
66
+ });
67
+ }
12
68
  async function captureScreenshot(session, logger, pageId) {
13
69
  logger.info("screenshot-start", { session, pageId });
14
70
  const snapshotRunId = generateSnapshotRunId();
@@ -19,11 +75,60 @@ async function captureScreenshot(session, logger, pageId) {
19
75
  requireSinglePage: true
20
76
  });
21
77
  try {
22
- const title = await page.title();
23
- const pageUrl = page.url();
78
+ let title = null;
79
+ try {
80
+ title = await page.title();
81
+ } catch (error) {
82
+ logger.warn("screenshot-title-read-failed", {
83
+ session,
84
+ pageId,
85
+ error
86
+ });
87
+ }
88
+ let pageUrl = null;
89
+ try {
90
+ pageUrl = page.url();
91
+ } catch (error) {
92
+ logger.warn("screenshot-url-read-failed", {
93
+ session,
94
+ pageId,
95
+ error
96
+ });
97
+ }
24
98
  const pngPath = `${snapshotRunDir}/page.png`;
25
99
  const htmlPath = `${snapshotRunDir}/page.html`;
26
- await page.screenshot({ path: pngPath });
100
+ const restoreViewport = resolveSnapshotViewport(session, logger);
101
+ const viewportMetrics = await readSnapshotViewportMetrics(page);
102
+ logger.info("screenshot-viewport-metrics", {
103
+ session,
104
+ pageId,
105
+ restoreViewport,
106
+ ...viewportMetrics
107
+ });
108
+ await forceSnapshotViewport(
109
+ page,
110
+ restoreViewport,
111
+ logger,
112
+ session,
113
+ pageId,
114
+ shouldForceSnapshotViewport(viewportMetrics) ? "preflight-invalid-viewport" : "preflight-normalize-viewport"
115
+ );
116
+ try {
117
+ await page.screenshot({ path: pngPath });
118
+ } catch (error) {
119
+ if (!isZeroWidthScreenshotError(error)) {
120
+ throw error;
121
+ }
122
+ await forceSnapshotViewport(
123
+ page,
124
+ restoreViewport,
125
+ logger,
126
+ session,
127
+ pageId,
128
+ "retry-after-zero-width-screenshot-error"
129
+ );
130
+ await page.screenshot({ path: pngPath });
131
+ }
27
132
  const htmlContent = await page.content();
28
133
  const fs = await import("node:fs/promises");
29
134
  await fs.writeFile(htmlPath, htmlContent);
@@ -49,7 +154,13 @@ async function captureScreenshot(session, logger, pageId) {
49
154
  session,
50
155
  pageAlive,
51
156
  browserConnected,
52
- pageUrl: page.url()
157
+ pageUrl: (() => {
158
+ try {
159
+ return page.url();
160
+ } catch {
161
+ return null;
162
+ }
163
+ })()
53
164
  });
54
165
  throw err;
55
166
  } finally {
@@ -10,9 +10,14 @@ const AiConfigSchema = z.object({
10
10
  commandPrefix: z.array(z.string()).min(1),
11
11
  updatedAt: z.string()
12
12
  }).strict();
13
+ const ViewportConfigSchema = z.object({
14
+ width: z.number().int().min(1),
15
+ height: z.number().int().min(1)
16
+ });
13
17
  const LibrettoConfigSchema = z.object({
14
18
  version: z.literal(CURRENT_CONFIG_VERSION),
15
- ai: AiConfigSchema.optional()
19
+ ai: AiConfigSchema.optional(),
20
+ viewport: ViewportConfigSchema.optional()
16
21
  }).passthrough();
17
22
  const AI_CONFIG_PRESETS = {
18
23
  codex: ["codex", "exec", "--skip-git-repo-check", "--sandbox", "read-only"],
@@ -73,9 +78,10 @@ function writeAiConfig(preset, commandPrefix, configPath = LIBRETTO_CONFIG_PATH)
73
78
  function clearAiConfig(configPath = LIBRETTO_CONFIG_PATH) {
74
79
  const librettoConfig = readLibrettoConfig(configPath);
75
80
  if (!librettoConfig.ai) return false;
81
+ const { ai: _ai, ...rest } = librettoConfig;
76
82
  writeLibrettoConfig(
77
83
  {
78
- version: librettoConfig.version
84
+ ...rest
79
85
  },
80
86
  configPath
81
87
  );
@@ -139,6 +145,7 @@ export {
139
145
  AiPresetSchema,
140
146
  CURRENT_CONFIG_VERSION,
141
147
  LibrettoConfigSchema,
148
+ ViewportConfigSchema,
142
149
  clearAiConfig,
143
150
  formatCommandPrefix,
144
151
  readAiConfig,
@@ -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,
@@ -66,8 +66,8 @@ class UserCodingAgent {
66
66
  Screenshot file path: ${pngPath}
67
67
  Use the screenshot alongside the HTML snapshot context above.`;
68
68
  }
69
- async runAnalyzer(args, stdinText) {
70
- const result = await runExternalCommand(this.command, args, stdinText);
69
+ async runAnalyzer(args, logger, stdinText) {
70
+ const result = await runExternalCommand(this.command, args, logger, stdinText);
71
71
  if (result.exitCode !== 0) {
72
72
  throw new Error(
73
73
  `Analyzer command failed (${formatCommandPrefix([this.command, ...args])}).
@@ -76,13 +76,13 @@ ${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No erro
76
76
  }
77
77
  return result;
78
78
  }
79
- async runAndParse(args, stdinText) {
80
- const result = await this.runAnalyzer(args, stdinText);
79
+ async runAndParse(args, logger, stdinText) {
80
+ const result = await this.runAnalyzer(args, logger, stdinText);
81
81
  return parseInterpretResultFromText(result.stdout);
82
82
  }
83
83
  }
84
84
  class CodexUserCodingAgent extends UserCodingAgent {
85
- async analyzeSnapshot(prompt, pngPath) {
85
+ async analyzeSnapshot(prompt, pngPath, logger) {
86
86
  const tempDir = mkdtempSync(join(tmpdir(), "libretto-cli-analyzer-"));
87
87
  const outputPath = join(
88
88
  tempDir,
@@ -96,9 +96,21 @@ class CodexUserCodingAgent extends UserCodingAgent {
96
96
  pngPath,
97
97
  "-"
98
98
  ];
99
- const result = await this.runAnalyzer(args, prompt);
99
+ logger.info("interpret-analyzer-codex-start", {
100
+ outputPath,
101
+ pngPath,
102
+ promptChars: prompt.length,
103
+ args
104
+ });
105
+ const result = await this.runAnalyzer(args, logger, prompt);
100
106
  let outputText = result.stdout;
101
107
  try {
108
+ logger.info("interpret-analyzer-codex-finish", {
109
+ outputPath,
110
+ outputFileExists: existsSync(outputPath),
111
+ stdoutChars: result.stdout.length,
112
+ stderrChars: result.stderr.length
113
+ });
102
114
  if (existsSync(outputPath)) {
103
115
  outputText = readFileSync(outputPath, "utf-8");
104
116
  }
@@ -109,31 +121,59 @@ class CodexUserCodingAgent extends UserCodingAgent {
109
121
  }
110
122
  }
111
123
  class ClaudeUserCodingAgent extends UserCodingAgent {
112
- async analyzeSnapshot(prompt, pngPath) {
113
- const args = [...this.baseArgs, `${prompt}${this.screenshotHint(pngPath)}`];
114
- return await this.runAndParse(args);
124
+ async analyzeSnapshot(prompt, pngPath, logger) {
125
+ return await this.runAndParse(
126
+ [...this.baseArgs],
127
+ logger,
128
+ `${prompt}${this.screenshotHint(pngPath)}`
129
+ );
115
130
  }
116
131
  }
117
132
  class GeminiUserCodingAgent extends UserCodingAgent {
118
- async analyzeSnapshot(prompt, pngPath) {
119
- const args = [...this.baseArgs, `${prompt}${this.screenshotHint(pngPath)}`];
120
- return await this.runAndParse(args);
133
+ async analyzeSnapshot(prompt, pngPath, logger) {
134
+ return await this.runAndParse(
135
+ [...this.baseArgs],
136
+ logger,
137
+ `${prompt}${this.screenshotHint(pngPath)}`
138
+ );
121
139
  }
122
140
  }
123
- async function runExternalCommand(command, args, stdinText) {
141
+ async function runExternalCommand(command, args, logger, stdinText) {
124
142
  return await new Promise((resolve2, reject) => {
143
+ const startedAt = Date.now();
144
+ logger.info("interpret-analyzer-spawn-start", {
145
+ command,
146
+ args,
147
+ stdinChars: stdinText?.length ?? 0
148
+ });
125
149
  const child = spawn(command, args, {
126
150
  stdio: ["pipe", "pipe", "pipe"]
127
151
  });
128
152
  let stdout = "";
129
153
  let stderr = "";
154
+ let stdinError = null;
130
155
  child.stdout.on("data", (chunk) => {
131
156
  stdout += chunk.toString();
132
157
  });
133
158
  child.stderr.on("data", (chunk) => {
134
159
  stderr += chunk.toString();
135
160
  });
161
+ child.stdin.on("error", (err) => {
162
+ stdinError = err;
163
+ logger.warn("interpret-analyzer-stdin-pipe-error", {
164
+ command,
165
+ args,
166
+ code: stdinError.code ?? null,
167
+ message: stdinError.message,
168
+ hint: stdinError.code === "EPIPE" ? "Child process exited before consuming all stdin data" : "Unexpected stdin write error"
169
+ });
170
+ });
136
171
  child.on("error", (err) => {
172
+ logger.error("interpret-analyzer-spawn-error", {
173
+ command,
174
+ args,
175
+ error: err
176
+ });
137
177
  const error = err;
138
178
  if (error.code === "ENOENT") {
139
179
  reject(
@@ -146,16 +186,41 @@ async function runExternalCommand(command, args, stdinText) {
146
186
  reject(err);
147
187
  });
148
188
  child.on("close", (code) => {
189
+ const stdinNote = formatStdinError(stderr, stdinError);
190
+ const combinedStderr = `${stderr}${stdinNote}`;
191
+ logger.info("interpret-analyzer-spawn-close", {
192
+ command,
193
+ args,
194
+ exitCode: code ?? 1,
195
+ durationMs: Date.now() - startedAt,
196
+ stdoutChars: stdout.length,
197
+ stderrChars: combinedStderr.length,
198
+ stdinErrorCode: stdinError?.code ?? null,
199
+ stdoutPreview: summarizeForLog(stdout),
200
+ stderrPreview: summarizeForLog(combinedStderr)
201
+ });
149
202
  resolve2({
150
203
  exitCode: code ?? 1,
151
204
  stdout,
152
- stderr
205
+ stderr: combinedStderr
153
206
  });
154
207
  });
155
- if (stdinText !== void 0) {
156
- child.stdin.write(stdinText);
208
+ try {
209
+ if (stdinText !== void 0) {
210
+ child.stdin.end(stdinText);
211
+ } else {
212
+ child.stdin.end();
213
+ }
214
+ } catch (err) {
215
+ stdinError = err;
216
+ logger.warn("interpret-analyzer-stdin-write-error", {
217
+ command,
218
+ args,
219
+ code: stdinError.code ?? null,
220
+ message: stdinError.message,
221
+ hint: stdinError.code === "EPIPE" ? "Child process exited before consuming all stdin data" : "Unexpected stdin write error"
222
+ });
157
223
  }
158
- child.stdin.end();
159
224
  });
160
225
  }
161
226
  function stripAnsi(value) {
@@ -164,6 +229,19 @@ function stripAnsi(value) {
164
229
  ""
165
230
  );
166
231
  }
232
+ function summarizeForLog(value, maxChars = 800) {
233
+ const cleaned = stripAnsi(value).trim();
234
+ if (!cleaned) return "";
235
+ if (cleaned.length <= maxChars) return cleaned;
236
+ return `${cleaned.slice(0, maxChars)}\u2026 [truncated ${cleaned.length - maxChars} chars]`;
237
+ }
238
+ function formatStdinError(stderr, error) {
239
+ if (!error) return "";
240
+ const detail = error.code === "EPIPE" ? "Analyzer closed stdin before Libretto finished sending the snapshot prompt." : `Analyzer stdin error: ${error.message}`;
241
+ if (stderr.includes(detail)) return "";
242
+ return `${stderr.endsWith("\n") || stderr.length === 0 ? "" : "\n"}${detail}
243
+ `;
244
+ }
167
245
  function extractJsonObjectCandidates(text) {
168
246
  const candidates = [];
169
247
  const seen = /* @__PURE__ */ new Set();
@@ -430,7 +508,7 @@ ${trimmedHtml}`;
430
508
  preset: configuredAnalyzer.preset,
431
509
  commandPrefix: configuredAnalyzer.commandPrefix
432
510
  });
433
- parsed = await configuredAgent.analyzeSnapshot(prompt, pngPath);
511
+ parsed = await configuredAgent.analyzeSnapshot(prompt, pngPath, logger);
434
512
  } else {
435
513
  const llmClientFactory = getLLMClientFactory();
436
514
  if (!llmClientFactory) {
package/dist/cli/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { runLibrettoCLI } from "./cli.js";
2
3
  import {
3
4
  maybeConfigureLLMClientFactoryFromEnv,
@@ -42,7 +42,7 @@ interface LLMClient {
42
42
  prompt: string;
43
43
  schema: T;
44
44
  temperature?: number;
45
- }): Promise<z.infer<T>>;
45
+ }): Promise<z.output<T>>;
46
46
  /**
47
47
  * Generate a structured object from a conversation-style message array.
48
48
  *
@@ -60,7 +60,7 @@ interface LLMClient {
60
60
  messages: Message[];
61
61
  schema: T;
62
62
  temperature?: number;
63
- }): Promise<z.infer<T>>;
63
+ }): Promise<z.output<T>>;
64
64
  }
65
65
 
66
66
  export type { LLMClient, Message, MessageContentPart };
@@ -42,7 +42,7 @@ interface LLMClient {
42
42
  prompt: string;
43
43
  schema: T;
44
44
  temperature?: number;
45
- }): Promise<z.infer<T>>;
45
+ }): Promise<z.output<T>>;
46
46
  /**
47
47
  * Generate a structured object from a conversation-style message array.
48
48
  *
@@ -60,7 +60,7 @@ interface LLMClient {
60
60
  messages: Message[];
61
61
  schema: T;
62
62
  temperature?: number;
63
- }): Promise<z.infer<T>>;
63
+ }): Promise<z.output<T>>;
64
64
  }
65
65
 
66
66
  export type { LLMClient, Message, MessageContentPart };
@@ -21,6 +21,7 @@ __export(session_state_exports, {
21
21
  SESSION_STATE_VERSION: () => SESSION_STATE_VERSION,
22
22
  SessionStateFileSchema: () => SessionStateFileSchema,
23
23
  SessionStatusSchema: () => SessionStatusSchema,
24
+ SessionViewportSchema: () => SessionViewportSchema,
24
25
  parseSessionStateContent: () => parseSessionStateContent,
25
26
  parseSessionStateData: () => parseSessionStateData,
26
27
  serializeSessionState: () => serializeSessionState
@@ -35,13 +36,18 @@ const SessionStatusSchema = import_zod.z.enum([
35
36
  "failed",
36
37
  "exited"
37
38
  ]);
39
+ const SessionViewportSchema = import_zod.z.object({
40
+ width: import_zod.z.number().int().min(1),
41
+ height: import_zod.z.number().int().min(1)
42
+ });
38
43
  const SessionStateFileSchema = import_zod.z.object({
39
44
  version: import_zod.z.literal(SESSION_STATE_VERSION),
40
45
  port: import_zod.z.number().int().min(0).max(65535),
41
46
  pid: import_zod.z.number().int(),
42
47
  session: import_zod.z.string().min(1),
43
48
  startedAt: import_zod.z.string().datetime({ offset: true }),
44
- status: SessionStatusSchema.optional()
49
+ status: SessionStatusSchema.optional(),
50
+ viewport: SessionViewportSchema.optional()
45
51
  });
46
52
  function formatIssues(error) {
47
53
  return error.issues.map((issue) => {
@@ -79,6 +85,7 @@ function serializeSessionState(state) {
79
85
  SESSION_STATE_VERSION,
80
86
  SessionStateFileSchema,
81
87
  SessionStatusSchema,
88
+ SessionViewportSchema,
82
89
  parseSessionStateContent,
83
90
  parseSessionStateData,
84
91
  serializeSessionState
@@ -1,29 +1,35 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  declare const SESSION_STATE_VERSION = 1;
4
- declare const SessionStatusSchema: z.ZodEnum<["active", "paused", "completed", "failed", "exited"]>;
4
+ declare const SessionStatusSchema: z.ZodEnum<{
5
+ active: "active";
6
+ paused: "paused";
7
+ completed: "completed";
8
+ failed: "failed";
9
+ exited: "exited";
10
+ }>;
11
+ declare const SessionViewportSchema: z.ZodObject<{
12
+ width: z.ZodNumber;
13
+ height: z.ZodNumber;
14
+ }, z.core.$strip>;
5
15
  declare const SessionStateFileSchema: z.ZodObject<{
6
16
  version: z.ZodLiteral<1>;
7
17
  port: z.ZodNumber;
8
18
  pid: z.ZodNumber;
9
19
  session: z.ZodString;
10
20
  startedAt: z.ZodString;
11
- status: z.ZodOptional<z.ZodEnum<["active", "paused", "completed", "failed", "exited"]>>;
12
- }, "strip", z.ZodTypeAny, {
13
- version: 1;
14
- port: number;
15
- pid: number;
16
- session: string;
17
- startedAt: string;
18
- status?: "active" | "paused" | "completed" | "failed" | "exited" | undefined;
19
- }, {
20
- version: 1;
21
- port: number;
22
- pid: number;
23
- session: string;
24
- startedAt: string;
25
- status?: "active" | "paused" | "completed" | "failed" | "exited" | undefined;
26
- }>;
21
+ status: z.ZodOptional<z.ZodEnum<{
22
+ active: "active";
23
+ paused: "paused";
24
+ completed: "completed";
25
+ failed: "failed";
26
+ exited: "exited";
27
+ }>>;
28
+ viewport: z.ZodOptional<z.ZodObject<{
29
+ width: z.ZodNumber;
30
+ height: z.ZodNumber;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>;
27
33
  type SessionStatus = z.infer<typeof SessionStatusSchema>;
28
34
  type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
29
35
  type SessionState = Omit<SessionStateFile, "version">;
@@ -31,4 +37,4 @@ declare function parseSessionStateData(rawState: unknown, source: string): Sessi
31
37
  declare function parseSessionStateContent(content: string, source: string): SessionState;
32
38
  declare function serializeSessionState(state: SessionState): SessionStateFile;
33
39
 
34
- export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
40
+ export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
@@ -1,29 +1,35 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  declare const SESSION_STATE_VERSION = 1;
4
- declare const SessionStatusSchema: z.ZodEnum<["active", "paused", "completed", "failed", "exited"]>;
4
+ declare const SessionStatusSchema: z.ZodEnum<{
5
+ active: "active";
6
+ paused: "paused";
7
+ completed: "completed";
8
+ failed: "failed";
9
+ exited: "exited";
10
+ }>;
11
+ declare const SessionViewportSchema: z.ZodObject<{
12
+ width: z.ZodNumber;
13
+ height: z.ZodNumber;
14
+ }, z.core.$strip>;
5
15
  declare const SessionStateFileSchema: z.ZodObject<{
6
16
  version: z.ZodLiteral<1>;
7
17
  port: z.ZodNumber;
8
18
  pid: z.ZodNumber;
9
19
  session: z.ZodString;
10
20
  startedAt: z.ZodString;
11
- status: z.ZodOptional<z.ZodEnum<["active", "paused", "completed", "failed", "exited"]>>;
12
- }, "strip", z.ZodTypeAny, {
13
- version: 1;
14
- port: number;
15
- pid: number;
16
- session: string;
17
- startedAt: string;
18
- status?: "active" | "paused" | "completed" | "failed" | "exited" | undefined;
19
- }, {
20
- version: 1;
21
- port: number;
22
- pid: number;
23
- session: string;
24
- startedAt: string;
25
- status?: "active" | "paused" | "completed" | "failed" | "exited" | undefined;
26
- }>;
21
+ status: z.ZodOptional<z.ZodEnum<{
22
+ active: "active";
23
+ paused: "paused";
24
+ completed: "completed";
25
+ failed: "failed";
26
+ exited: "exited";
27
+ }>>;
28
+ viewport: z.ZodOptional<z.ZodObject<{
29
+ width: z.ZodNumber;
30
+ height: z.ZodNumber;
31
+ }, z.core.$strip>>;
32
+ }, z.core.$strip>;
27
33
  type SessionStatus = z.infer<typeof SessionStatusSchema>;
28
34
  type SessionStateFile = z.infer<typeof SessionStateFileSchema>;
29
35
  type SessionState = Omit<SessionStateFile, "version">;
@@ -31,4 +37,4 @@ declare function parseSessionStateData(rawState: unknown, source: string): Sessi
31
37
  declare function parseSessionStateContent(content: string, source: string): SessionState;
32
38
  declare function serializeSessionState(state: SessionState): SessionStateFile;
33
39
 
34
- export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
40
+ export { SESSION_STATE_VERSION, type SessionState, type SessionStateFile, SessionStateFileSchema, type SessionStatus, SessionStatusSchema, SessionViewportSchema, parseSessionStateContent, parseSessionStateData, serializeSessionState };
@@ -7,13 +7,18 @@ const SessionStatusSchema = z.enum([
7
7
  "failed",
8
8
  "exited"
9
9
  ]);
10
+ const SessionViewportSchema = z.object({
11
+ width: z.number().int().min(1),
12
+ height: z.number().int().min(1)
13
+ });
10
14
  const SessionStateFileSchema = z.object({
11
15
  version: z.literal(SESSION_STATE_VERSION),
12
16
  port: z.number().int().min(0).max(65535),
13
17
  pid: z.number().int(),
14
18
  session: z.string().min(1),
15
19
  startedAt: z.string().datetime({ offset: true }),
16
- status: SessionStatusSchema.optional()
20
+ status: SessionStatusSchema.optional(),
21
+ viewport: SessionViewportSchema.optional()
17
22
  });
18
23
  function formatIssues(error) {
19
24
  return error.issues.map((issue) => {
@@ -50,6 +55,7 @@ export {
50
55
  SESSION_STATE_VERSION,
51
56
  SessionStateFileSchema,
52
57
  SessionStatusSchema,
58
+ SessionViewportSchema,
53
59
  parseSessionStateContent,
54
60
  parseSessionStateData,
55
61
  serializeSessionState
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,6 +8,7 @@
8
8
  "url": "https://github.com/saffron-health/libretto"
9
9
  },
10
10
  "type": "module",
11
+ "packageManager": "pnpm@9.15.4",
11
12
  "publishConfig": {
12
13
  "access": "public"
13
14
  },
@@ -26,11 +27,23 @@
26
27
  "require": "./dist/index.cjs"
27
28
  }
28
29
  },
30
+ "scripts": {
31
+ "postinstall": "playwright install chromium",
32
+ "build": "pnpm run build:runtime && pnpm run build:cli",
33
+ "build:runtime": "tsup --config tsup.config.ts",
34
+ "build:cli": "tsup --config tsup.cli.config.ts",
35
+ "type-check": "tsc --noEmit",
36
+ "test": "pnpm run build && vitest run",
37
+ "eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
38
+ "benchmark": "pnpm run build && tsx benchmarks/run.ts",
39
+ "test:watch": "vitest",
40
+ "cli": "node dist/index.js",
41
+ "prepack": "pnpm run build"
42
+ },
29
43
  "peerDependencies": {
30
- "@ai-sdk/anthropic": "^3.0.0",
31
- "@ai-sdk/google-vertex": "^4.0.0",
32
- "@ai-sdk/openai": "^3.0.0",
33
- "zod": ">=3.0.0"
44
+ "@ai-sdk/anthropic": "^3.0.58",
45
+ "@ai-sdk/google-vertex": "^4.0.80",
46
+ "@ai-sdk/openai": "^3.0.41"
34
47
  },
35
48
  "peerDependenciesMeta": {
36
49
  "@ai-sdk/anthropic": {
@@ -44,34 +57,22 @@
44
57
  }
45
58
  },
46
59
  "devDependencies": {
47
- "@anthropic-ai/claude-agent-sdk": "^0.2.73",
48
- "@ai-sdk/anthropic": "^3.0.53",
49
- "@ai-sdk/google-vertex": "^4.0.72",
50
- "@ai-sdk/openai": "^3.0.39",
51
- "@types/node": "^25.3.3",
52
- "@types/yargs": "^17.0.33",
53
- "openai": "^6.27.0",
54
- "tsup": "^8.0.0",
55
- "typescript": "^5.7.0",
56
- "vitest": "^4.0.18",
57
- "zod": "^3.25.0"
60
+ "@anthropic-ai/claude-agent-sdk": "^0.2.75",
61
+ "@ai-sdk/anthropic": "^3.0.58",
62
+ "@ai-sdk/google-vertex": "^4.0.80",
63
+ "@ai-sdk/openai": "^3.0.41",
64
+ "@types/node": "^25.5.0",
65
+ "@types/yargs": "^17.0.35",
66
+ "openai": "^6.29.0",
67
+ "tsup": "^8.5.1",
68
+ "typescript": "^5.9.3",
69
+ "vitest": "^4.1.0"
58
70
  },
59
71
  "dependencies": {
60
- "ai": "^6.0.110",
61
- "playwright": "^1.52.0",
62
- "tsx": "^4.19.2",
63
- "yargs": "^17.7.2"
64
- },
65
- "scripts": {
66
- "postinstall": "npx playwright install chromium",
67
- "build": "pnpm run build:runtime && pnpm run build:cli",
68
- "build:runtime": "tsup --config tsup.config.ts",
69
- "build:cli": "tsup --config tsup.cli.config.ts",
70
- "type-check": "tsc --noEmit",
71
- "test": "pnpm run build && vitest run",
72
- "eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
73
- "benchmark": "pnpm run build && tsx benchmarks/run.ts",
74
- "test:watch": "vitest",
75
- "cli": "node dist/index.js"
72
+ "ai": "^6.0.116",
73
+ "playwright": "^1.58.2",
74
+ "tsx": "^4.21.0",
75
+ "yargs": "^18.0.0",
76
+ "zod": "^4.3.6"
76
77
  }
77
- }
78
+ }