libretto 0.6.12 → 0.6.13

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 (46) hide show
  1. package/README.md +3 -8
  2. package/README.template.md +3 -8
  3. package/dist/cli/cli.js +0 -23
  4. package/dist/cli/commands/browser.js +1 -7
  5. package/dist/cli/commands/setup.js +1 -294
  6. package/dist/cli/commands/snapshot.js +9 -99
  7. package/dist/cli/commands/status.js +1 -41
  8. package/dist/cli/core/browser.js +3 -3
  9. package/dist/cli/core/config.js +3 -6
  10. package/dist/cli/core/daemon/daemon.js +25 -64
  11. package/dist/cli/core/daemon/snapshot.js +2 -29
  12. package/dist/cli/core/experiments.js +1 -28
  13. package/dist/cli/index.js +0 -2
  14. package/dist/cli/router.js +0 -2
  15. package/dist/shared/instrumentation/instrument.js +4 -4
  16. package/docs/releasing.md +8 -6
  17. package/package.json +2 -1
  18. package/skills/libretto/SKILL.md +17 -19
  19. package/skills/libretto/references/configuration-file-reference.md +6 -12
  20. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  21. package/skills/libretto-readonly/SKILL.md +2 -9
  22. package/src/cli/cli.ts +0 -24
  23. package/src/cli/commands/browser.ts +1 -7
  24. package/src/cli/commands/setup.ts +1 -380
  25. package/src/cli/commands/snapshot.ts +8 -136
  26. package/src/cli/commands/status.ts +1 -49
  27. package/src/cli/core/browser.ts +3 -3
  28. package/src/cli/core/config.ts +3 -6
  29. package/src/cli/core/daemon/daemon.ts +25 -67
  30. package/src/cli/core/daemon/ipc.ts +5 -16
  31. package/src/cli/core/daemon/snapshot.ts +1 -43
  32. package/src/cli/core/experiments.ts +9 -38
  33. package/src/cli/core/resolve-model.ts +5 -0
  34. package/src/cli/core/workflow-runtime.ts +1 -0
  35. package/src/cli/index.ts +0 -1
  36. package/src/cli/router.ts +0 -2
  37. package/src/shared/instrumentation/instrument.ts +4 -4
  38. package/dist/cli/commands/ai.js +0 -110
  39. package/dist/cli/core/ai-model.js +0 -195
  40. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  41. package/dist/cli/core/snapshot-analyzer.js +0 -667
  42. package/scripts/summarize-evals.mjs +0 -135
  43. package/src/cli/commands/ai.ts +0 -144
  44. package/src/cli/core/ai-model.ts +0 -301
  45. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  46. package/src/cli/core/snapshot-analyzer.ts +0 -856
@@ -63,7 +63,7 @@ import {
63
63
  } from "../browser.js";
64
64
  import { handlePages } from "./pages.js";
65
65
  import { handleExec, handleReadonlyExec } from "./exec.js";
66
- import { handleCompactSnapshot, handleSnapshot } from "./snapshot.js";
66
+ import { handleCompactSnapshot } from "./snapshot.js";
67
67
  import { librettoCommand } from "../../../shared/package-manager.js";
68
68
  import type { Snapshot } from "../../../shared/snapshot/types.js";
69
69
  import { snapshot } from "../../../shared/snapshot/capture-snapshot.js";
@@ -271,9 +271,7 @@ class BrowserDaemon {
271
271
  });
272
272
  }
273
273
 
274
- if (experiments["compact-snapshot-format"]) {
275
- await context.addInitScript(installPageStabilityWaiter);
276
- }
274
+ await context.addInitScript(installPageStabilityWaiter);
277
275
 
278
276
  // IPC server — typed handlers are attached per client connection so one
279
277
  // daemon lifetime can serve multiple CLI invocations.
@@ -295,19 +293,15 @@ class BrowserDaemon {
295
293
  wrapPageForActionLogging(p, session);
296
294
  daemon.trackPage(p);
297
295
  }
298
- if (experiments["compact-snapshot-format"]) {
299
- await Promise.all(
300
- initialPages.map((initialPage) =>
301
- daemon.installCompactSnapshotWaiter(initialPage),
302
- ),
303
- );
304
- }
296
+ await Promise.all(
297
+ initialPages.map((initialPage) =>
298
+ daemon.installCompactSnapshotWaiter(initialPage),
299
+ ),
300
+ );
305
301
  context.on("page", (newPage) => {
306
302
  wrapPageForActionLogging(newPage, session);
307
303
  daemon.trackPage(newPage);
308
- if (experiments["compact-snapshot-format"]) {
309
- void daemon.installCompactSnapshotWaiter(newPage);
310
- }
304
+ void daemon.installCompactSnapshotWaiter(newPage);
311
305
  });
312
306
 
313
307
  // Navigate after telemetry is installed (so we capture the initial
@@ -659,40 +653,23 @@ class BrowserDaemon {
659
653
  private async runSnapshot(
660
654
  args: Parameters<CliToDaemonApi["snapshot"]>[0],
661
655
  ): Promise<ReturnType<CliToDaemonApi["snapshot"]>> {
662
- if (args.mode === "compact") {
663
- if (!this.experiments["compact-snapshot-format"]) {
664
- throw new Error(
665
- `The compact-snapshot-format experiment is not enabled for session "${this.session}". ` +
666
- `Close and reopen the session after running ${librettoCommand("experiments enable compact-snapshot-format")}.`,
667
- );
668
- }
669
- const targetPage = this.resolveTargetPage(args.pageId);
670
- const result = await this.withRequestTimeout(() =>
671
- handleCompactSnapshot(
672
- targetPage,
673
- this.session,
674
- this.logger,
675
- {
676
- pageId: args.pageId,
677
- cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
678
- useCachedSnapshot: args.useCachedSnapshot,
679
- },
680
- ),
681
- );
682
- if (!args.useCachedSnapshot) {
683
- this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
684
- }
685
- return result;
686
- }
687
-
688
- return this.withRequestTimeout(() =>
689
- handleSnapshot(
690
- this.resolveTargetPage(args.pageId),
656
+ const targetPage = this.resolveTargetPage(args.pageId);
657
+ const result = await this.withRequestTimeout(() =>
658
+ handleCompactSnapshot(
659
+ targetPage,
691
660
  this.session,
692
661
  this.logger,
693
- args.pageId,
662
+ {
663
+ pageId: args.pageId,
664
+ cachedSnapshot: this.latestCompactSnapshotByPage.get(targetPage),
665
+ useCachedSnapshot: args.useCachedSnapshot,
666
+ },
694
667
  ),
695
668
  );
669
+ if (!args.useCachedSnapshot) {
670
+ this.latestCompactSnapshotByPage.set(targetPage, result.snapshot);
671
+ }
672
+ return result;
696
673
  }
697
674
 
698
675
  private async withRequestTimeout<T>(
@@ -720,26 +697,7 @@ class BrowserDaemon {
720
697
  private async runExec(
721
698
  args: Parameters<CliToDaemonApi["exec"]>[0],
722
699
  ): Promise<DaemonExecResult> {
723
- if (this.experiments["compact-snapshot-format"]) {
724
- return this.runCompactExec(args);
725
- }
726
-
727
- try {
728
- const data = await this.withRequestTimeout(() =>
729
- handleExec(
730
- this.resolveTargetPage(args.pageId),
731
- args.code,
732
- this.context,
733
- this.browser,
734
- this.execState,
735
- this.session,
736
- args.visualize,
737
- ),
738
- );
739
- return { ok: true, data };
740
- } catch (error) {
741
- return this.createExecErrorResult(error);
742
- }
700
+ return this.runCompactExec(args);
743
701
  }
744
702
 
745
703
  private async runCompactExec(
@@ -831,17 +789,17 @@ class BrowserDaemon {
831
789
  context: this.context,
832
790
  logger: this.logger,
833
791
  onLog: (event) => {
834
- this.broadcast("workflowOutput", event);
792
+ void this.broadcast("workflowOutput", event);
835
793
  },
836
794
  onOutcome: (outcome) => {
837
795
  if (outcome.state === "paused") {
838
- this.broadcast("workflowPaused", {
796
+ void this.broadcast("workflowPaused", {
839
797
  pausedAt: outcome.pausedAt,
840
798
  url: outcome.url,
841
799
  });
842
800
  return;
843
801
  }
844
- this.broadcast(
802
+ void this.broadcast(
845
803
  "workflowFinished",
846
804
  outcome.result === "completed"
847
805
  ? { result: "completed", completedAt: outcome.completedAt }
@@ -25,9 +25,10 @@ export type DaemonExecArgs = {
25
25
 
26
26
  export type DaemonReadonlyExecArgs = { code: string; pageId?: string };
27
27
 
28
- export type DaemonSnapshotArgs =
29
- | { mode?: "legacy"; pageId?: string; useCachedSnapshot?: false }
30
- | { mode: "compact"; pageId?: string; useCachedSnapshot?: boolean };
28
+ export type DaemonSnapshotArgs = {
29
+ pageId?: string;
30
+ useCachedSnapshot?: boolean;
31
+ };
31
32
 
32
33
  export type DaemonExecSuccess = {
33
34
  result: unknown;
@@ -35,24 +36,12 @@ export type DaemonExecSuccess = {
35
36
  snapshotDiff?: SnapshotDiff;
36
37
  };
37
38
 
38
- export type DaemonLegacySnapshotResult = {
39
- pngPath: string;
40
- htmlPath: string;
41
- snapshotRunId: string;
42
- pageUrl: string;
43
- title: string;
44
- };
45
-
46
- export type DaemonCompactSnapshotResult = {
39
+ export type DaemonSnapshotResult = {
47
40
  mode: "compact";
48
41
  pngPath: string;
49
42
  snapshot: Snapshot;
50
43
  };
51
44
 
52
- export type DaemonSnapshotResult =
53
- | DaemonLegacySnapshotResult
54
- | DaemonCompactSnapshotResult;
55
-
56
45
  export type DaemonCloseResult = { replayUrl?: string };
57
46
 
58
47
  export type DaemonCommandResult<T> =
@@ -1,5 +1,5 @@
1
1
  import type { Page } from "playwright";
2
- import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
3
  import type { LoggerApi } from "../../../shared/logger/index.js";
4
4
  import { getSessionSnapshotRunDir } from "../context.js";
5
5
  import {
@@ -25,48 +25,6 @@ type SnapshotScreenshot = {
25
25
  title: string;
26
26
  };
27
27
 
28
- export async function handleSnapshot(
29
- targetPage: Page,
30
- session: string,
31
- logger: LoggerApi,
32
- pageId?: string,
33
- ): Promise<{
34
- pngPath: string;
35
- htmlPath: string;
36
- snapshotRunId: string;
37
- pageUrl: string;
38
- title: string;
39
- }> {
40
- const screenshot = await captureSnapshotScreenshot(
41
- targetPage,
42
- session,
43
- logger,
44
- pageId,
45
- );
46
- const htmlPath = `${getSessionSnapshotRunDir(
47
- session,
48
- screenshot.snapshotRunId,
49
- )}/page.html`;
50
-
51
- // Capture HTML content.
52
- const htmlContent = await targetPage.content();
53
- writeFileSync(htmlPath, htmlContent);
54
-
55
- logger.info("screenshot-success", {
56
- session,
57
- pageUrl: screenshot.pageUrl,
58
- title: screenshot.title,
59
- pngPath: screenshot.pngPath,
60
- htmlPath,
61
- snapshotRunId: screenshot.snapshotRunId,
62
- });
63
-
64
- return {
65
- ...screenshot,
66
- htmlPath,
67
- };
68
- }
69
-
70
28
  export async function handleCompactSnapshot(
71
29
  targetPage: Page,
72
30
  session: string,
@@ -4,45 +4,16 @@ import {
4
4
  writeLibrettoConfig,
5
5
  } from "./config.js";
6
6
 
7
- export const EXPERIMENTS = {
8
- "compact-snapshot-format": {
9
- title: "Compact snapshot format",
10
- oneSentenceDescription:
11
- "Use compact accessibility snapshots and exec page-change diffs without an AI sub-agent.",
12
- docs: [
13
- "Compact snapshot format changes how agents should use snapshot and exec after the experiment is enabled.",
14
- "",
15
- "Compared with the skill's documented behavior:",
16
- " - Run libretto snapshot --session <name> without --objective or --context.",
17
- " - Snapshot output is a screenshot path plus a compact accessibility tree; it does not use the PNG + HTML + AI analysis path.",
18
- " - Run libretto snapshot <ref> --session <name> to inspect a subtree from the latest full compact snapshot.",
19
- " - Run libretto exec normally; after successful mutations, Libretto prints page-change diffs from compact snapshots without AI analysis.",
20
- " - If a session was already open before enabling the experiment, close and reopen it before relying on this behavior.",
21
- "",
22
- "Full compact snapshot:",
23
- " libretto snapshot --session <name>",
24
- "",
25
- "Cached subtree snapshot:",
26
- " libretto snapshot <ref> --session <name>",
27
- "",
28
- "Run an unscoped snapshot before using refs. Subtree snapshots capture a fresh screenshot but reuse the latest cached tree.",
29
- "",
30
- "Notes:",
31
- " - Use ref forms printed in the tree, such as l16. Numeric-suffix aliases such as e16 also match l16.",
32
- ].join("\n"),
33
- defaultValue: false,
34
- },
35
- } as const satisfies Record<
36
- string,
37
- {
38
- title: string;
39
- oneSentenceDescription: string;
40
- docs?: string;
41
- defaultValue: boolean;
42
- }
43
- >;
7
+ export type ExperimentMetadata = {
8
+ title: string;
9
+ oneSentenceDescription: string;
10
+ docs?: string;
11
+ defaultValue: boolean;
12
+ };
13
+
14
+ export const EXPERIMENTS: Readonly<Record<string, ExperimentMetadata>> = {};
44
15
 
45
- export type ExperimentName = keyof typeof EXPERIMENTS;
16
+ export type ExperimentName = string;
46
17
  export type Experiments = Record<ExperimentName, boolean>;
47
18
 
48
19
  export function isExperimentName(name: string): name is ExperimentName {
@@ -105,6 +105,7 @@ async function getProviderModel(
105
105
  if (!apiKey) {
106
106
  throw new Error(missingProviderCredentialsMessage(provider));
107
107
  }
108
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
108
109
  const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
109
110
  const google = createGoogleGenerativeAI({ apiKey });
110
111
  return google(modelId);
@@ -114,6 +115,7 @@ async function getProviderModel(
114
115
  if (!project) {
115
116
  throw new Error(missingProviderCredentialsMessage(provider));
116
117
  }
118
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
117
119
  const { createVertex } = await import("@ai-sdk/google-vertex");
118
120
  const vertex = createVertex({
119
121
  project,
@@ -126,6 +128,7 @@ async function getProviderModel(
126
128
  if (!apiKey) {
127
129
  throw new Error(missingProviderCredentialsMessage(provider));
128
130
  }
131
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
129
132
  const { createAnthropic } = await import("@ai-sdk/anthropic");
130
133
  const anthropic = createAnthropic({ apiKey });
131
134
  return anthropic(modelId);
@@ -135,6 +138,7 @@ async function getProviderModel(
135
138
  if (!apiKey) {
136
139
  throw new Error(missingProviderCredentialsMessage(provider));
137
140
  }
141
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
138
142
  const { createOpenAI } = await import("@ai-sdk/openai");
139
143
  const openai = createOpenAI({ apiKey });
140
144
  return openai(modelId);
@@ -144,6 +148,7 @@ async function getProviderModel(
144
148
  if (!apiKey) {
145
149
  throw new Error(missingProviderCredentialsMessage(provider));
146
150
  }
151
+ // @lintc-ignore Human-approved: we don't want to import unless the user is using that subagent.
147
152
  const { createOpenAI } = await import("@ai-sdk/openai");
148
153
  const openrouter = createOpenAI({
149
154
  apiKey,
@@ -37,6 +37,7 @@ export async function loadDefaultWorkflow(
37
37
  ): Promise<ExportedLibrettoWorkflow> {
38
38
  let loadedModule: Record<string, unknown>;
39
39
  try {
40
+ // @lintc-ignore Human-approved: user workflow files must be loaded dynamically from the CLI argument.
40
41
  loadedModule = (await import(pathToFileURL(absolutePath).href)) as Record<
41
42
  string,
42
43
  unknown
package/src/cli/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { runLibrettoCLI } from "./cli.js";
3
3
 
4
- export { runClose } from "./commands/browser.js";
5
4
  export { runLibrettoCLI };
6
5
 
7
6
  void runLibrettoCLI();
package/src/cli/router.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { aiCommands } from "./commands/ai.js";
2
1
  import { authCommands } from "./commands/auth.js";
3
2
  import { billingCommands } from "./commands/billing.js";
4
3
  import { browserCommands } from "./commands/browser.js";
@@ -16,7 +15,6 @@ export const cliRoutes = {
16
15
  deploy: deployCommand,
17
16
  experiments: experimentsCommand,
18
17
  ...executionCommands,
19
- ai: aiCommands,
20
18
  auth: authCommands,
21
19
  billing: billingCommands,
22
20
  setup: setupCommand,
@@ -131,12 +131,12 @@ function wrapLocatorActions(
131
131
  try {
132
132
  const result = await orig(...args);
133
133
  if (opts.visualize) {
134
- enqueue(page, () => visualizeAfterAction(page));
134
+ void enqueue(page, () => visualizeAfterAction(page));
135
135
  }
136
136
  return result;
137
137
  } catch (err: any) {
138
138
  if (opts.visualize) {
139
- enqueue(page, () => visualizeAfterAction(page));
139
+ void enqueue(page, () => visualizeAfterAction(page));
140
140
  }
141
141
  // Enrich timeout errors for pointer actions
142
142
  if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
@@ -323,12 +323,12 @@ export async function installInstrumentation(
323
323
  try {
324
324
  const result = await orig(...args);
325
325
  if (visualize) {
326
- enqueue(page, () => visualizeAfterAction(page));
326
+ void enqueue(page, () => visualizeAfterAction(page));
327
327
  }
328
328
  return result;
329
329
  } catch (err: any) {
330
330
  if (visualize) {
331
- enqueue(page, () => visualizeAfterAction(page));
331
+ void enqueue(page, () => visualizeAfterAction(page));
332
332
  }
333
333
  if (
334
334
  POINTER_ACTIONS.has(method) &&
@@ -1,110 +0,0 @@
1
- import { z } from "zod";
2
- import {
3
- readSnapshotModel,
4
- writeSnapshotModel,
5
- clearSnapshotModel
6
- } from "../core/config.js";
7
- import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
8
- import { DEFAULT_SNAPSHOT_MODELS } from "../core/ai-model.js";
9
- import { librettoCommand } from "../../shared/package-manager.js";
10
- import { SimpleCLI } from "../framework/simple-cli.js";
11
- const PROVIDER_ALIASES = {
12
- claude: DEFAULT_SNAPSHOT_MODELS.anthropic,
13
- gemini: DEFAULT_SNAPSHOT_MODELS.google,
14
- google: DEFAULT_SNAPSHOT_MODELS.google
15
- };
16
- const CONFIGURE_PROVIDERS = [
17
- "openai",
18
- "anthropic",
19
- "gemini",
20
- "vertex"
21
- ];
22
- function formatConfigureProviders(separator = " | ") {
23
- return CONFIGURE_PROVIDERS.join(separator);
24
- }
25
- function printSnapshotModelConfig(model, configPath) {
26
- console.log(`Snapshot model: ${model}`);
27
- console.log(`Config file: ${configPath}`);
28
- }
29
- function resolveModelFromInput(input) {
30
- const trimmed = input.trim();
31
- if (!trimmed) return null;
32
- if (trimmed.includes("/")) return trimmed;
33
- const normalized = trimmed.toLowerCase();
34
- return DEFAULT_SNAPSHOT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
35
- }
36
- function runAiConfigure(input, options = {}) {
37
- const configureCommandName = options.configureCommandName ?? librettoCommand("ai configure");
38
- const configPath = options.configPath ?? LIBRETTO_CONFIG_PATH;
39
- const presetArg = input.preset?.trim();
40
- if (!presetArg && !input.clear) {
41
- const model2 = readSnapshotModel(configPath);
42
- if (!model2) {
43
- console.log(
44
- `No snapshot model set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
45
- );
46
- console.log(
47
- "Provider credentials still come from your shell or .env file."
48
- );
49
- return;
50
- }
51
- printSnapshotModelConfig(model2, configPath);
52
- return;
53
- }
54
- if (input.clear) {
55
- const removed = clearSnapshotModel(configPath);
56
- if (removed) {
57
- console.log(`Cleared snapshot model config: ${configPath}`);
58
- } else {
59
- console.log("No snapshot model was set.");
60
- }
61
- return;
62
- }
63
- const model = resolveModelFromInput(presetArg);
64
- if (!model) {
65
- console.log(
66
- `Usage: ${configureCommandName} <${CONFIGURE_PROVIDERS.join("|")}|provider/model-id>
67
- ${configureCommandName}
68
- ${configureCommandName} --clear`
69
- );
70
- throw new Error(
71
- `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
72
- );
73
- }
74
- writeSnapshotModel(model, configPath);
75
- console.log("Snapshot model saved.");
76
- printSnapshotModelConfig(model, configPath);
77
- }
78
- const aiConfigureInput = SimpleCLI.input({
79
- positionals: [
80
- SimpleCLI.positional("preset", z.string().optional(), {
81
- help: "Provider shorthand or provider/model-id"
82
- })
83
- ],
84
- named: {
85
- clear: SimpleCLI.flag({ help: "Clear existing AI config" })
86
- }
87
- });
88
- const aiCommands = SimpleCLI.group({
89
- description: "AI commands",
90
- routes: {
91
- configure: SimpleCLI.command({
92
- description: "Configure AI runtime"
93
- }).input(aiConfigureInput).handle(async ({ input }) => {
94
- runAiConfigure(
95
- {
96
- clear: input.clear,
97
- preset: input.preset
98
- },
99
- {
100
- configureCommandName: librettoCommand("ai configure")
101
- }
102
- );
103
- })
104
- }
105
- });
106
- export {
107
- aiCommands,
108
- aiConfigureInput,
109
- runAiConfigure
110
- };