libretto 0.5.3-experimental.5 → 0.5.3

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 (126) hide show
  1. package/README.md +114 -37
  2. package/README.template.md +160 -0
  3. package/dist/cli/cli.js +22 -97
  4. package/dist/cli/commands/browser.js +86 -59
  5. package/dist/cli/commands/deploy.js +148 -0
  6. package/dist/cli/commands/execution.js +218 -96
  7. package/dist/cli/commands/init.js +34 -29
  8. package/dist/cli/commands/logs.js +4 -5
  9. package/dist/cli/commands/shared.js +30 -29
  10. package/dist/cli/commands/snapshot.js +26 -39
  11. package/dist/cli/core/ai-config.js +21 -4
  12. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  13. package/dist/cli/core/browser.js +207 -37
  14. package/dist/cli/core/context.js +4 -1
  15. package/dist/cli/core/deploy-artifact.js +687 -0
  16. package/dist/cli/core/session-telemetry.js +434 -174
  17. package/dist/cli/core/session.js +21 -8
  18. package/dist/cli/core/snapshot-analyzer.js +14 -31
  19. package/dist/cli/core/snapshot-api-config.js +2 -6
  20. package/dist/cli/core/telemetry.js +20 -4
  21. package/dist/cli/framework/simple-cli.js +144 -43
  22. package/dist/cli/router.js +16 -21
  23. package/dist/cli/workers/run-integration-runtime.js +25 -45
  24. package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
  25. package/dist/cli/workers/run-integration-worker.js +1 -4
  26. package/dist/index.d.ts +1 -2
  27. package/dist/index.js +13 -10
  28. package/dist/runtime/download/download.js +5 -1
  29. package/dist/runtime/extract/extract.js +11 -2
  30. package/dist/runtime/network/network.js +8 -1
  31. package/dist/runtime/recovery/agent.js +6 -2
  32. package/dist/runtime/recovery/errors.js +3 -1
  33. package/dist/runtime/recovery/recovery.js +3 -1
  34. package/dist/shared/condense-dom/condense-dom.js +17 -69
  35. package/dist/shared/config/config.d.ts +1 -9
  36. package/dist/shared/config/config.js +0 -18
  37. package/dist/shared/config/index.d.ts +2 -1
  38. package/dist/shared/config/index.js +0 -10
  39. package/dist/shared/debug/pause.js +9 -3
  40. package/dist/shared/dom-semantics.d.ts +8 -0
  41. package/dist/shared/dom-semantics.js +69 -0
  42. package/dist/shared/instrumentation/instrument.js +101 -5
  43. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  44. package/dist/shared/llm/client.js +3 -1
  45. package/dist/shared/logger/index.js +4 -1
  46. package/dist/shared/run/api.js +3 -1
  47. package/dist/shared/run/browser.js +47 -3
  48. package/dist/shared/state/session-state.d.ts +2 -1
  49. package/dist/shared/state/session-state.js +5 -2
  50. package/dist/shared/visualization/ghost-cursor.js +36 -14
  51. package/dist/shared/visualization/highlight.js +9 -6
  52. package/dist/shared/workflow/workflow.d.ts +18 -10
  53. package/dist/shared/workflow/workflow.js +50 -5
  54. package/package.json +14 -6
  55. package/scripts/generate-changelog.ts +132 -0
  56. package/scripts/postinstall.mjs +4 -3
  57. package/scripts/skills-libretto.mjs +2 -88
  58. package/scripts/summarize-evals.mjs +32 -10
  59. package/skills/libretto/SKILL.md +132 -62
  60. package/skills/libretto/references/action-logs.md +101 -0
  61. package/skills/libretto/references/auth-profiles.md +1 -2
  62. package/skills/libretto/references/code-generation-rules.md +176 -0
  63. package/skills/libretto/references/configuration-file-reference.md +53 -0
  64. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  65. package/skills/libretto/references/site-security-review.md +143 -0
  66. package/src/cli/cli.ts +23 -110
  67. package/src/cli/commands/browser.ts +94 -70
  68. package/src/cli/commands/deploy.ts +198 -0
  69. package/src/cli/commands/execution.ts +251 -111
  70. package/src/cli/commands/init.ts +37 -33
  71. package/src/cli/commands/logs.ts +7 -7
  72. package/src/cli/commands/shared.ts +36 -37
  73. package/src/cli/commands/snapshot.ts +44 -59
  74. package/src/cli/core/ai-config.ts +24 -4
  75. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  76. package/src/cli/core/browser.ts +260 -49
  77. package/src/cli/core/context.ts +7 -2
  78. package/src/cli/core/deploy-artifact.ts +938 -0
  79. package/src/cli/core/session-telemetry.ts +449 -197
  80. package/src/cli/core/session.ts +21 -7
  81. package/src/cli/core/snapshot-analyzer.ts +26 -46
  82. package/src/cli/core/snapshot-api-config.ts +170 -175
  83. package/src/cli/core/telemetry.ts +39 -4
  84. package/src/cli/framework/simple-cli.ts +281 -98
  85. package/src/cli/router.ts +15 -21
  86. package/src/cli/workers/run-integration-runtime.ts +35 -57
  87. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  88. package/src/cli/workers/run-integration-worker.ts +1 -4
  89. package/src/index.ts +77 -67
  90. package/src/runtime/download/download.ts +62 -58
  91. package/src/runtime/download/index.ts +5 -5
  92. package/src/runtime/extract/extract.ts +71 -61
  93. package/src/runtime/network/index.ts +3 -3
  94. package/src/runtime/network/network.ts +99 -93
  95. package/src/runtime/recovery/agent.ts +217 -212
  96. package/src/runtime/recovery/errors.ts +107 -104
  97. package/src/runtime/recovery/index.ts +3 -3
  98. package/src/runtime/recovery/recovery.ts +38 -35
  99. package/src/shared/condense-dom/condense-dom.ts +27 -82
  100. package/src/shared/config/config.ts +0 -19
  101. package/src/shared/config/index.ts +0 -5
  102. package/src/shared/debug/pause.ts +57 -51
  103. package/src/shared/dom-semantics.ts +68 -0
  104. package/src/shared/instrumentation/errors.ts +64 -62
  105. package/src/shared/instrumentation/index.ts +5 -5
  106. package/src/shared/instrumentation/instrument.ts +339 -209
  107. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  108. package/src/shared/llm/client.ts +181 -174
  109. package/src/shared/llm/types.ts +39 -39
  110. package/src/shared/logger/index.ts +11 -4
  111. package/src/shared/logger/logger.ts +312 -306
  112. package/src/shared/logger/sinks.ts +118 -114
  113. package/src/shared/paths/paths.ts +50 -49
  114. package/src/shared/paths/repo-root.ts +17 -17
  115. package/src/shared/run/api.ts +5 -1
  116. package/src/shared/run/browser.ts +65 -3
  117. package/src/shared/state/index.ts +9 -9
  118. package/src/shared/state/session-state.ts +46 -43
  119. package/src/shared/visualization/ghost-cursor.ts +180 -149
  120. package/src/shared/visualization/highlight.ts +89 -86
  121. package/src/shared/visualization/index.ts +13 -13
  122. package/src/shared/workflow/workflow.ts +107 -30
  123. package/scripts/check-skills-sync.mjs +0 -23
  124. package/scripts/prepare-release.sh +0 -97
  125. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
  126. package/skills/libretto/references/user-action-log.md +0 -31
@@ -1,5 +1,13 @@
1
1
  import { createInterface } from "node:readline";
2
- import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import {
3
+ appendFileSync,
4
+ cpSync,
5
+ existsSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
3
11
  import { spawnSync } from "node:child_process";
4
12
  import { basename, dirname, join } from "node:path";
5
13
  import { fileURLToPath } from "node:url";
@@ -9,8 +17,8 @@ import {
9
17
  loadSnapshotEnv,
10
18
  resolveSnapshotApiModel,
11
19
  } from "../core/snapshot-api-config.js";
12
- import { SimpleCLI } from "../framework/simple-cli.js";
13
20
  import { hasProviderCredentials } from "../../shared/llm/client.js";
21
+ import { SimpleCLI } from "../framework/simple-cli.js";
14
22
 
15
23
  type ProviderChoice = {
16
24
  key: string;
@@ -42,7 +50,8 @@ const PROVIDER_CHOICES: ProviderChoice[] = [
42
50
  key: "4",
43
51
  label: "Google Vertex AI",
44
52
  envVar: "GOOGLE_CLOUD_PROJECT",
45
- envHint: "Requires gcloud auth application-default login and a GCP project ID",
53
+ envHint:
54
+ "Requires gcloud auth application-default login and a GCP project ID",
46
55
  },
47
56
  ];
48
57
 
@@ -57,16 +66,6 @@ function promptUser(
57
66
  });
58
67
  }
59
68
 
60
- function askYesNo(question: string): Promise<boolean> {
61
- const rl = createInterface({ input: process.stdin, output: process.stdout });
62
- return new Promise((resolve) => {
63
- rl.question(`${question} (y/N) `, (answer) => {
64
- rl.close();
65
- resolve(answer.trim().toLowerCase() === "y");
66
- });
67
- });
68
- }
69
-
70
69
  function safeReadAiConfig(): ReturnType<typeof readAiConfig> {
71
70
  try {
72
71
  return readAiConfig();
@@ -101,7 +100,9 @@ function printSnapshotApiStatus(): void {
101
100
 
102
101
  if (selection && hasProviderCredentials(selection.provider)) {
103
102
  console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
104
- console.log(" Snapshot objectives will use the API analyzer by default.");
103
+ console.log(
104
+ " Snapshot objectives will use the API analyzer by default.",
105
+ );
105
106
  console.log(" No further action required.");
106
107
  return;
107
108
  }
@@ -117,7 +118,9 @@ function printSnapshotApiStatus(): void {
117
118
  console.log(
118
119
  " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model.",
119
120
  );
120
- console.log(" Run `npx libretto init` interactively to set up credentials.");
121
+ console.log(
122
+ " Run `npx libretto init` interactively to set up credentials.",
123
+ );
121
124
  }
122
125
 
123
126
  async function runInteractiveApiSetup(): Promise<void> {
@@ -132,7 +135,9 @@ async function runInteractiveApiSetup(): Promise<void> {
132
135
 
133
136
  if (selection && hasProviderCredentials(selection.provider)) {
134
137
  console.log(` ✓ Ready: ${selection.model} (${selection.source})`);
135
- console.log(" Snapshot objectives will use the API analyzer by default.");
138
+ console.log(
139
+ " Snapshot objectives will use the API analyzer by default.",
140
+ );
136
141
  return;
137
142
  }
138
143
 
@@ -144,7 +149,9 @@ async function runInteractiveApiSetup(): Promise<void> {
144
149
  });
145
150
 
146
151
  try {
147
- console.log(" Which API provider would you like to use for snapshot analysis?\n");
152
+ console.log(
153
+ " Which API provider would you like to use for snapshot analysis?\n",
154
+ );
148
155
  for (const choice of PROVIDER_CHOICES) {
149
156
  console.log(` ${choice.key}) ${choice.label}`);
150
157
  }
@@ -153,7 +160,9 @@ async function runInteractiveApiSetup(): Promise<void> {
153
160
  const answer = await promptUser(rl, " Choice: ");
154
161
 
155
162
  if (answer.toLowerCase() === "s" || !answer) {
156
- console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
163
+ console.log(
164
+ "\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.",
165
+ );
157
166
  console.log(" Or add credentials directly to your .env file:");
158
167
  console.log(" OPENAI_API_KEY=...");
159
168
  console.log(" ANTHROPIC_API_KEY=...");
@@ -251,26 +260,17 @@ function detectAgentDirs(root: string): string[] {
251
260
  return dirs;
252
261
  }
253
262
 
254
- async function copySkills(): Promise<void> {
263
+ function copySkills(): void {
255
264
  const agentDirs = detectAgentDirs(REPO_ROOT);
256
265
 
257
266
  if (agentDirs.length === 0) {
258
- console.log("\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.");
267
+ console.log(
268
+ "\nSkills: No .agents/ or .claude/ directory found in repo root — skipping.",
269
+ );
259
270
  return;
260
271
  }
261
272
 
262
273
  const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
263
- const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
264
- // Say "Overwrite" if skills already exist in ANY target dir — skills must
265
- // be identical across coding agents, so we always copy to all of them.
266
- const existing = destinations.filter((d) => existsSync(d));
267
- const verb = existing.length > 0 ? "Overwrite" : "Install";
268
-
269
- const proceed = await askYesNo(`\n${verb} libretto skills in ${dirNames}?`);
270
- if (!proceed) {
271
- console.log(" Skipping skill copy.");
272
- return;
273
- }
274
274
 
275
275
  let sourceDir: string;
276
276
  try {
@@ -289,7 +289,9 @@ async function copySkills(): Promise<void> {
289
289
  }
290
290
  cpSync(sourceDir, skillDest, { recursive: true });
291
291
  const fileCount = readdirSync(skillDest).length;
292
- console.log(` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`);
292
+ console.log(
293
+ ` ✓ Copied ${fileCount} skill files to ${name}/skills/libretto/`,
294
+ );
293
295
  }
294
296
  }
295
297
 
@@ -316,10 +318,12 @@ export const initCommand = SimpleCLI.command({
316
318
  console.log("\nSkipping browser installation (--skip-browsers)");
317
319
  }
318
320
 
321
+ copySkills();
322
+
319
323
  if (process.stdin.isTTY) {
320
- await copySkills();
321
324
  await runInteractiveApiSetup();
322
325
  } else {
326
+ loadSnapshotEnv();
323
327
  printSnapshotApiStatus();
324
328
  }
325
329
 
@@ -12,13 +12,15 @@ import {
12
12
  import { SimpleCLI } from "../framework/simple-cli.js";
13
13
  import {
14
14
  integerOption,
15
- loadSessionStateMiddleware,
16
15
  pageOption,
17
- resolveSessionMiddleware,
18
16
  sessionOption,
17
+ withRequiredSession,
19
18
  } from "./shared.js";
20
19
 
21
- async function resolvePageId(session: string, pageId?: string): Promise<string | undefined> {
20
+ async function resolvePageId(
21
+ session: string,
22
+ pageId?: string,
23
+ ): Promise<string | undefined> {
22
24
  if (!pageId) return undefined;
23
25
  const pages = await withSessionLogger(session, async (logger) =>
24
26
  listOpenPages(session, logger),
@@ -48,8 +50,7 @@ export const networkCommand = SimpleCLI.command({
48
50
  description: "View captured network requests",
49
51
  })
50
52
  .input(networkInput)
51
- .use(resolveSessionMiddleware)
52
- .use(loadSessionStateMiddleware)
53
+ .use(withRequiredSession())
53
54
  .handle(async ({ input, ctx }) => {
54
55
  if (input.clear) {
55
56
  clearNetworkLog(ctx.session);
@@ -93,8 +94,7 @@ export const actionsCommand = SimpleCLI.command({
93
94
  description: "View captured actions",
94
95
  })
95
96
  .input(actionsInput)
96
- .use(resolveSessionMiddleware)
97
- .use(loadSessionStateMiddleware)
97
+ .use(withRequiredSession())
98
98
  .handle(async ({ input, ctx }) => {
99
99
  if (input.clear) {
100
100
  clearActionLog(ctx.session);
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
+ import type { LoggerApi } from "../../shared/logger/index.js";
3
+ import { createLoggerForSession } from "../core/context.js";
2
4
  import {
3
- SESSION_DEFAULT,
5
+ generateSessionName,
4
6
  readSessionStateOrThrow,
5
7
  type SessionState,
6
8
  validateSessionName,
@@ -10,21 +12,8 @@ import {
10
12
  type SimpleCLIMiddleware,
11
13
  } from "../framework/simple-cli.js";
12
14
 
13
- export function createSessionSchema() {
14
- return z.string().default(SESSION_DEFAULT).superRefine((value, ctx) => {
15
- try {
16
- validateSessionName(value);
17
- } catch (err) {
18
- ctx.addIssue({
19
- code: z.ZodIssueCode.custom,
20
- message: err instanceof Error ? err.message : String(err),
21
- });
22
- }
23
- });
24
- }
25
-
26
- export function sessionOption(help = "Use a named session") {
27
- return SimpleCLI.option(createSessionSchema(), { help });
15
+ export function sessionOption(help = "Session name") {
16
+ return SimpleCLI.option(z.string().optional(), { help });
28
17
  }
29
18
 
30
19
  export function pageOption(help = "Target a specific page id") {
@@ -35,36 +24,46 @@ export function integerOption(help?: string) {
35
24
  return SimpleCLI.option(z.coerce.number().int().optional(), { help });
36
25
  }
37
26
 
38
- export type SessionInput = {
39
- session: string;
40
- };
41
-
42
27
  export type SessionContext = {
43
28
  session: string;
29
+ logger: LoggerApi;
44
30
  };
45
31
 
46
32
  export type SessionStateContext = SessionContext & {
47
33
  sessionState: SessionState;
48
34
  };
49
35
 
50
- export const resolveSessionMiddleware: SimpleCLIMiddleware<
51
- SessionInput,
36
+ export function withRequiredSession(): SimpleCLIMiddleware<
37
+ { session?: string },
52
38
  {},
53
- SessionContext
54
- > = async ({ input, ctx }) => {
55
- return {
56
- ...ctx,
57
- session: input.session,
39
+ SessionStateContext
40
+ > {
41
+ return async ({ input, ctx }) => {
42
+ if (!input.session) {
43
+ throw new Error("Missing required option --session.");
44
+ }
45
+ validateSessionName(input.session);
46
+ const logger = createLoggerForSession(input.session);
47
+ return {
48
+ ...ctx,
49
+ session: input.session,
50
+ logger,
51
+ sessionState: readSessionStateOrThrow(input.session),
52
+ };
58
53
  };
59
- };
54
+ }
60
55
 
61
- export const loadSessionStateMiddleware: SimpleCLIMiddleware<
62
- SessionInput,
63
- SessionContext,
64
- SessionStateContext
65
- > = async ({ ctx }) => {
66
- return {
67
- ...ctx,
68
- sessionState: readSessionStateOrThrow(ctx.session),
56
+ export function withAutoSession(): SimpleCLIMiddleware<
57
+ { session?: string },
58
+ {},
59
+ SessionContext
60
+ > {
61
+ return async ({ input, ctx }) => {
62
+ const session = input.session ?? generateSessionName();
63
+ if (input.session) {
64
+ validateSessionName(input.session);
65
+ }
66
+ const logger = createLoggerForSession(session);
67
+ return { ...ctx, session, logger };
69
68
  };
70
- };
69
+ }
@@ -10,17 +10,11 @@ import {
10
10
  type ScreenshotPair,
11
11
  } from "../core/snapshot-analyzer.js";
12
12
  import { SimpleCLI } from "../framework/simple-cli.js";
13
- import {
14
- loadSessionStateMiddleware,
15
- pageOption,
16
- resolveSessionMiddleware,
17
- sessionOption,
18
- } from "./shared.js";
13
+ import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
19
14
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
20
15
  import { readAiConfig } from "../core/ai-config.js";
21
16
  import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
22
17
 
23
- const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
24
18
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
25
19
 
26
20
  function generateSnapshotRunId(): string {
@@ -38,28 +32,28 @@ function isZeroViewport(value: number | null): boolean {
38
32
  return typeof value === "number" && value <= 0;
39
33
  }
40
34
 
41
- function shouldForceSnapshotViewport(metrics: SnapshotViewportMetrics): boolean {
35
+ function shouldForceSnapshotViewport(
36
+ metrics: SnapshotViewportMetrics,
37
+ ): boolean {
42
38
  return (
43
- isZeroViewport(metrics.configuredWidth)
44
- || isZeroViewport(metrics.configuredHeight)
45
- || isZeroViewport(metrics.innerWidth)
46
- || isZeroViewport(metrics.innerHeight)
39
+ isZeroViewport(metrics.configuredWidth) ||
40
+ isZeroViewport(metrics.configuredHeight) ||
41
+ isZeroViewport(metrics.innerWidth) ||
42
+ isZeroViewport(metrics.innerHeight)
47
43
  );
48
44
  }
49
45
 
50
46
  function isZeroWidthScreenshotError(error: unknown): boolean {
51
47
  return (
52
- error instanceof Error
53
- && error.message.includes("Cannot take screenshot with 0 width")
48
+ error instanceof Error &&
49
+ error.message.includes("Cannot take screenshot with 0 width")
54
50
  );
55
51
  }
56
52
 
57
- async function readSnapshotViewportMetrics(
58
- page: {
59
- viewportSize(): { width: number; height: number } | null;
60
- evaluate<T>(pageFunction: () => T | Promise<T>): Promise<T>;
61
- },
62
- ): Promise<SnapshotViewportMetrics> {
53
+ async function readSnapshotViewportMetrics(page: {
54
+ viewportSize(): { width: number; height: number } | null;
55
+ evaluate<T>(pageFunction: () => T | Promise<T>): Promise<T>;
56
+ }): Promise<SnapshotViewportMetrics> {
63
57
  const configuredViewport = page.viewportSize();
64
58
  let innerWidth: number | null = null;
65
59
  let innerHeight: number | null = null;
@@ -161,6 +155,12 @@ async function captureScreenshot(
161
155
  const htmlPath = `${snapshotRunDir}/page.html`;
162
156
  const condensedHtmlPath = `${snapshotRunDir}/page.condensed.html`;
163
157
 
158
+ const RENDER_SETTLE_TIMEOUT_MS = 10_000;
159
+ await Promise.race([
160
+ page.waitForLoadState("networkidle").catch(() => {}),
161
+ new Promise((resolve) => setTimeout(resolve, RENDER_SETTLE_TIMEOUT_MS)),
162
+ ]);
163
+
164
164
  const restoreViewport = resolveSnapshotViewport(session, logger);
165
165
  const viewportMetrics = await readSnapshotViewportMetrics(page);
166
166
  logger.info("screenshot-viewport-metrics", {
@@ -249,22 +249,15 @@ async function captureScreenshot(
249
249
  async function runSnapshot(
250
250
  session: string,
251
251
  logger: LoggerApi,
252
- pageId?: string,
253
- objective?: string,
254
- context?: string,
252
+ pageId: string | undefined,
253
+ objective: string,
254
+ context: string,
255
255
  ): Promise<void> {
256
- const normalizedObjective = objective?.trim();
257
- const normalizedContext = context?.trim();
258
- if (!normalizedObjective && normalizedContext) {
259
- throw new Error(
260
- "Couldn't run analysis: --objective is required when providing --context.",
261
- );
262
- }
256
+ const normalizedObjective = objective.trim();
257
+ const normalizedContext = context.trim();
263
258
 
264
- const configuredAi = normalizedObjective ? readAiConfig() : null;
265
- if (normalizedObjective) {
266
- resolveSnapshotApiModelOrThrow(configuredAi);
267
- }
259
+ const configuredAi = readAiConfig();
260
+ resolveSnapshotApiModelOrThrow(configuredAi);
268
261
 
269
262
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
270
263
  session,
@@ -277,15 +270,10 @@ async function runSnapshot(
277
270
  console.log(` HTML: ${htmlPath}`);
278
271
  console.log(` Condensed HTML: ${condensedHtmlPath}`);
279
272
 
280
- if (!normalizedObjective) {
281
- console.log("Use --objective flag to analyze snapshots.");
282
- return;
283
- }
284
-
285
273
  const interpretArgs: InterpretArgs = {
286
274
  objective: normalizedObjective,
287
275
  session,
288
- context: normalizedContext ?? DEFAULT_SNAPSHOT_CONTEXT,
276
+ context: normalizedContext,
289
277
  pngPath,
290
278
  htmlPath,
291
279
  condensedHtmlPath,
@@ -303,25 +291,22 @@ export const snapshotInput = SimpleCLI.input({
303
291
  named: {
304
292
  session: sessionOption(),
305
293
  page: pageOption(),
306
- objective: SimpleCLI.option(z.string().optional()),
307
- context: SimpleCLI.option(z.string().optional()),
294
+ objective: SimpleCLI.option(z.string()),
295
+ context: SimpleCLI.option(z.string()),
308
296
  },
309
297
  });
310
298
 
311
- export function createSnapshotCommand(logger: LoggerApi) {
312
- return SimpleCLI.command({
313
- description: "Capture PNG + HTML; analyze when --objective is provided (--context optional)",
314
- })
315
- .input(snapshotInput)
316
- .use(resolveSessionMiddleware)
317
- .use(loadSessionStateMiddleware)
318
- .handle(async ({ input, ctx }) => {
319
- await runSnapshot(
320
- ctx.session,
321
- logger,
322
- input.page,
323
- input.objective,
324
- input.context,
325
- );
326
- });
327
- }
299
+ export const snapshotCommand = SimpleCLI.command({
300
+ description: "Capture PNG + HTML and analyze with --objective and --context",
301
+ })
302
+ .input(snapshotInput)
303
+ .use(withRequiredSession())
304
+ .handle(async ({ input, ctx }) => {
305
+ await runSnapshot(
306
+ ctx.session,
307
+ ctx.logger,
308
+ input.page,
309
+ input.objective,
310
+ input.context,
311
+ );
312
+ });
@@ -29,11 +29,18 @@ export const ViewportConfigSchema = z.object({
29
29
  });
30
30
  export type ViewportConfig = z.infer<typeof ViewportConfigSchema>;
31
31
 
32
+ export const WindowPositionConfigSchema = z.object({
33
+ x: z.number().int(),
34
+ y: z.number().int(),
35
+ });
36
+ export type WindowPositionConfig = z.infer<typeof WindowPositionConfigSchema>;
37
+
32
38
  export const LibrettoConfigSchema = z
33
39
  .object({
34
40
  version: z.literal(CURRENT_CONFIG_VERSION),
35
41
  ai: AiConfigSchema.optional(),
36
42
  viewport: ViewportConfigSchema.optional(),
43
+ windowPosition: WindowPositionConfigSchema.optional(),
37
44
  })
38
45
  .passthrough();
39
46
  export type LibrettoConfig = z.infer<typeof LibrettoConfigSchema>;
@@ -51,7 +58,12 @@ const PROVIDER_ALIASES: Record<string, string> = {
51
58
  google: DEFAULT_MODELS.gemini,
52
59
  };
53
60
 
54
- const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"] as const;
61
+ const CONFIGURE_PROVIDERS = [
62
+ "openai",
63
+ "anthropic",
64
+ "gemini",
65
+ "vertex",
66
+ ] as const;
55
67
 
56
68
  function formatConfigureProviders(separator = " | "): string {
57
69
  return CONFIGURE_PROVIDERS.join(separator);
@@ -75,6 +87,10 @@ function formatExpectedConfigExample(): string {
75
87
  width: 1280,
76
88
  height: 800,
77
89
  },
90
+ windowPosition: {
91
+ x: 1600,
92
+ y: 120,
93
+ },
78
94
  },
79
95
  null,
80
96
  2,
@@ -89,11 +105,13 @@ function invalidConfigError(configPath: string, detail?: string): Error {
89
105
  "Expected config example:",
90
106
  formatExpectedConfigExample(),
91
107
  "Notes:",
92
- ' - "ai" and "viewport" are optional.',
108
+ ' - "ai", "viewport", and "windowPosition" are optional.',
93
109
  ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
94
110
  "Fix the file to match this shape, or delete it and rerun:",
95
111
  ` npx libretto ai configure ${formatConfigureProviders()}`,
96
- ].filter(Boolean).join("\n"),
112
+ ]
113
+ .filter(Boolean)
114
+ .join("\n"),
97
115
  );
98
116
  }
99
117
 
@@ -220,7 +238,9 @@ export function runAiConfigure(
220
238
  console.log(
221
239
  `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`,
222
240
  );
223
- console.log("Provider credentials still come from your shell or .env file.");
241
+ console.log(
242
+ "Provider credentials still come from your shell or .env file.",
243
+ );
224
244
  return;
225
245
  }
226
246
  printAiConfig(config, configPath);
@@ -10,7 +10,6 @@ import { readFileSync } from "node:fs";
10
10
  import type { LoggerApi } from "../../shared/logger/index.js";
11
11
  import { createLLMClient } from "../../shared/llm/client.js";
12
12
  import {
13
- formatInterpretationOutput,
14
13
  InterpretResultSchema,
15
14
  buildInlinePromptSelection,
16
15
  getMimeType,
@@ -19,9 +18,7 @@ import {
19
18
  type InterpretArgs,
20
19
  } from "./snapshot-analyzer.js";
21
20
  import { readAiConfig, type AiConfig } from "./ai-config.js";
22
- import {
23
- resolveSnapshotApiModelOrThrow,
24
- } from "./snapshot-api-config.js";
21
+ import { resolveSnapshotApiModelOrThrow } from "./snapshot-api-config.js";
25
22
 
26
23
  export async function runApiInterpret(
27
24
  args: InterpretArgs,
@@ -52,7 +49,8 @@ export async function runApiInterpret(
52
49
  logger.info("api-interpret-dom-selection", {
53
50
  configuredModel: promptSelection.stats.configuredModel,
54
51
  fullDomEstimatedTokens: promptSelection.stats.fullDomEstimatedTokens,
55
- condensedDomEstimatedTokens: promptSelection.stats.condensedDomEstimatedTokens,
52
+ condensedDomEstimatedTokens:
53
+ promptSelection.stats.condensedDomEstimatedTokens,
56
54
  contextWindowTokens: promptSelection.budget.contextWindowTokens,
57
55
  promptBudgetTokens: promptSelection.budget.promptBudgetTokens,
58
56
  selectedDom: promptSelection.domSource,
@@ -93,5 +91,18 @@ export async function runApiInterpret(
93
91
  answer: parsed.answer.slice(0, 200),
94
92
  });
95
93
 
96
- console.log(formatInterpretationOutput(parsed, "Interpretation (via API):"));
94
+ console.log("");
95
+ console.log("Analysis:");
96
+ console.log(parsed.answer);
97
+ if (parsed.selectors.length > 0) {
98
+ console.log("");
99
+ console.log("Selectors:");
100
+ parsed.selectors.forEach((selector, index) => {
101
+ console.log(` ${index + 1}. ${selector.label}: ${selector.selector}`);
102
+ });
103
+ }
104
+ if (parsed.notes?.trim()) {
105
+ console.log("");
106
+ console.log(`Notes: ${parsed.notes.trim()}`);
107
+ }
97
108
  }