libretto 0.6.10 → 0.6.12

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 (119) hide show
  1. package/README.md +4 -0
  2. package/README.template.md +4 -0
  3. package/dist/cli/cli.js +4 -3
  4. package/dist/cli/commands/ai.js +3 -2
  5. package/dist/cli/commands/browser.js +17 -17
  6. package/dist/cli/commands/execution.js +254 -234
  7. package/dist/cli/commands/experiments.js +100 -0
  8. package/dist/cli/commands/setup.js +20 -34
  9. package/dist/cli/commands/shared.js +10 -0
  10. package/dist/cli/commands/snapshot.js +81 -9
  11. package/dist/cli/commands/status.js +5 -4
  12. package/dist/cli/core/ai-model.js +6 -3
  13. package/dist/cli/core/browser.js +300 -121
  14. package/dist/cli/core/config.js +4 -2
  15. package/dist/cli/core/context.js +4 -0
  16. package/dist/cli/core/daemon/config.js +0 -6
  17. package/dist/cli/core/daemon/daemon.js +535 -89
  18. package/dist/cli/core/daemon/ipc.js +170 -129
  19. package/dist/cli/core/daemon/snapshot.js +72 -6
  20. package/dist/cli/core/experiments.js +66 -0
  21. package/dist/cli/core/session.js +5 -4
  22. package/dist/cli/core/skill-version.js +2 -1
  23. package/dist/cli/core/snapshot-analyzer.js +4 -3
  24. package/dist/cli/core/workflow-runner/runner.js +147 -0
  25. package/dist/cli/core/workflow-runtime.js +60 -0
  26. package/dist/cli/router.js +4 -1
  27. package/dist/shared/debug/pause-handler.d.ts +9 -0
  28. package/dist/shared/debug/pause-handler.js +15 -0
  29. package/dist/shared/debug/pause.d.ts +1 -2
  30. package/dist/shared/debug/pause.js +13 -36
  31. package/dist/shared/ipc/child-process-transport.d.ts +7 -0
  32. package/dist/shared/ipc/child-process-transport.js +60 -0
  33. package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
  34. package/dist/shared/ipc/child-process-transport.spec.js +68 -0
  35. package/dist/shared/ipc/ipc.d.ts +46 -0
  36. package/dist/shared/ipc/ipc.js +165 -0
  37. package/dist/shared/ipc/ipc.spec.d.ts +2 -0
  38. package/dist/shared/ipc/ipc.spec.js +114 -0
  39. package/dist/shared/ipc/socket-transport.d.ts +9 -0
  40. package/dist/shared/ipc/socket-transport.js +143 -0
  41. package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
  42. package/dist/shared/ipc/socket-transport.spec.js +117 -0
  43. package/dist/shared/package-manager.d.ts +7 -0
  44. package/dist/shared/package-manager.js +60 -0
  45. package/dist/shared/paths/paths.d.ts +1 -8
  46. package/dist/shared/paths/paths.js +1 -49
  47. package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
  48. package/dist/shared/snapshot/capture-snapshot.js +463 -0
  49. package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
  50. package/dist/shared/snapshot/diff-snapshots.js +358 -0
  51. package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
  52. package/dist/shared/snapshot/render-snapshot.js +651 -0
  53. package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
  54. package/dist/shared/snapshot/snapshot.spec.js +333 -0
  55. package/dist/shared/snapshot/types.d.ts +40 -0
  56. package/dist/shared/snapshot/types.js +0 -0
  57. package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
  58. package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
  59. package/dist/shared/state/session-state.d.ts +1 -0
  60. package/dist/shared/state/session-state.js +1 -0
  61. package/docs/experiments.md +67 -0
  62. package/package.json +4 -2
  63. package/skills/libretto/SKILL.md +3 -1
  64. package/skills/libretto-readonly/SKILL.md +1 -1
  65. package/src/cli/AGENTS.md +7 -0
  66. package/src/cli/cli.ts +4 -3
  67. package/src/cli/commands/ai.ts +3 -2
  68. package/src/cli/commands/browser.ts +13 -11
  69. package/src/cli/commands/execution.ts +303 -271
  70. package/src/cli/commands/experiments.ts +120 -0
  71. package/src/cli/commands/setup.ts +18 -36
  72. package/src/cli/commands/shared.ts +20 -0
  73. package/src/cli/commands/snapshot.ts +99 -11
  74. package/src/cli/commands/status.ts +5 -4
  75. package/src/cli/core/ai-model.ts +6 -3
  76. package/src/cli/core/browser.ts +369 -147
  77. package/src/cli/core/config.ts +3 -1
  78. package/src/cli/core/context.ts +4 -0
  79. package/src/cli/core/daemon/config.ts +35 -19
  80. package/src/cli/core/daemon/daemon.ts +686 -106
  81. package/src/cli/core/daemon/ipc.ts +330 -214
  82. package/src/cli/core/daemon/snapshot.ts +106 -8
  83. package/src/cli/core/experiments.ts +85 -0
  84. package/src/cli/core/session.ts +5 -4
  85. package/src/cli/core/skill-version.ts +2 -1
  86. package/src/cli/core/snapshot-analyzer.ts +4 -3
  87. package/src/cli/core/workflow-runner/runner.ts +237 -0
  88. package/src/cli/core/workflow-runtime.ts +85 -0
  89. package/src/cli/router.ts +4 -1
  90. package/src/shared/debug/pause-handler.ts +20 -0
  91. package/src/shared/debug/pause.ts +14 -48
  92. package/src/shared/ipc/AGENTS.md +24 -0
  93. package/src/shared/ipc/child-process-transport.spec.ts +86 -0
  94. package/src/shared/ipc/child-process-transport.ts +96 -0
  95. package/src/shared/ipc/ipc.spec.ts +161 -0
  96. package/src/shared/ipc/ipc.ts +288 -0
  97. package/src/shared/ipc/socket-transport.spec.ts +141 -0
  98. package/src/shared/ipc/socket-transport.ts +189 -0
  99. package/src/shared/package-manager.ts +76 -0
  100. package/src/shared/paths/paths.ts +0 -72
  101. package/src/shared/snapshot/capture-snapshot.ts +615 -0
  102. package/src/shared/snapshot/diff-snapshots.ts +579 -0
  103. package/src/shared/snapshot/render-snapshot.ts +962 -0
  104. package/src/shared/snapshot/snapshot.spec.ts +388 -0
  105. package/src/shared/snapshot/types.ts +43 -0
  106. package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
  107. package/src/shared/state/session-state.ts +1 -0
  108. package/dist/cli/core/daemon/index.js +0 -16
  109. package/dist/cli/core/daemon/spawn.js +0 -90
  110. package/dist/cli/core/pause-signals.js +0 -29
  111. package/dist/cli/workers/run-integration-runtime.js +0 -235
  112. package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
  113. package/dist/cli/workers/run-integration-worker.js +0 -64
  114. package/src/cli/core/daemon/index.ts +0 -24
  115. package/src/cli/core/daemon/spawn.ts +0 -171
  116. package/src/cli/core/pause-signals.ts +0 -35
  117. package/src/cli/workers/run-integration-runtime.ts +0 -326
  118. package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
  119. package/src/cli/workers/run-integration-worker.ts +0 -72
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ import { librettoCommand } from "../../shared/package-manager.js";
3
+ import {
4
+ EXPERIMENTS,
5
+ isExperimentName,
6
+ resolveExperiments,
7
+ setExperimentEnabled,
8
+ type ExperimentName,
9
+ type Experiments,
10
+ } from "../core/experiments.js";
11
+ import { SimpleCLI } from "../framework/simple-cli.js";
12
+
13
+ const experimentNames = Object.keys(EXPERIMENTS) as ExperimentName[];
14
+
15
+ const experimentsUsage = [
16
+ "Usage:",
17
+ ` ${librettoCommand("experiments")}`,
18
+ ` ${librettoCommand("experiments describe <experiment>")}`,
19
+ ` ${librettoCommand("experiments enable <experiment>")}`,
20
+ ` ${librettoCommand("experiments disable <experiment>")}`,
21
+ ].join("\n");
22
+
23
+ export const experimentsInput = SimpleCLI.input({
24
+ positionals: [
25
+ SimpleCLI.positional("action", z.string().optional(), {
26
+ help: "Action to apply",
27
+ }),
28
+ SimpleCLI.positional("experiment", z.string().optional(), {
29
+ help: "Experiment name",
30
+ }),
31
+ ],
32
+ named: {},
33
+ });
34
+
35
+ function formatAvailableExperiments(): string {
36
+ return [
37
+ "Available experiments:",
38
+ ...experimentNames.map((name) => ` ${name}`),
39
+ ].join("\n");
40
+ }
41
+
42
+ function experimentUsageError(message: string): Error {
43
+ return new Error(
44
+ [message, "", experimentsUsage, "", formatAvailableExperiments()].join(
45
+ "\n",
46
+ ),
47
+ );
48
+ }
49
+
50
+ function printExperiments(experiments: Experiments): void {
51
+ console.log("Libretto experiments:");
52
+ for (const name of experimentNames) {
53
+ const metadata = EXPERIMENTS[name];
54
+ console.log(
55
+ `- ${name}: ${experiments[name] ? "enabled" : "disabled"} — ${metadata.title}`,
56
+ );
57
+ console.log(` ${metadata.oneSentenceDescription}`);
58
+ }
59
+ }
60
+
61
+ function printExperimentDescription(
62
+ name: ExperimentName,
63
+ experiments: Experiments,
64
+ ): void {
65
+ const metadata = EXPERIMENTS[name];
66
+ console.log(`${metadata.title} (${name})`);
67
+ console.log(`Status: ${experiments[name] ? "enabled" : "disabled"}`);
68
+ console.log("");
69
+ if (experiments[name]) {
70
+ console.log(
71
+ "Since this experiment is enabled, Libretto’s expected usage deviates from the skill. Use these instructions where they differ:",
72
+ );
73
+ console.log("");
74
+ }
75
+ console.log(metadata.docs ?? metadata.oneSentenceDescription);
76
+ }
77
+
78
+ export const experimentsCommand = SimpleCLI.command({
79
+ description: "List or update Libretto experiment flags",
80
+ })
81
+ .input(experimentsInput)
82
+ .handle(async ({ input }) => {
83
+ if (!input.action) {
84
+ printExperiments(resolveExperiments());
85
+ return;
86
+ }
87
+
88
+ if (
89
+ input.action !== "describe" &&
90
+ input.action !== "enable" &&
91
+ input.action !== "disable"
92
+ ) {
93
+ throw experimentUsageError(`Unknown experiments action "${input.action}".`);
94
+ }
95
+
96
+ if (!input.experiment) {
97
+ throw experimentUsageError(
98
+ `Missing experiment name for ${input.action}.`,
99
+ );
100
+ }
101
+
102
+ if (!isExperimentName(input.experiment)) {
103
+ throw experimentUsageError(`Unknown experiment "${input.experiment}".`);
104
+ }
105
+
106
+ if (input.action === "describe") {
107
+ printExperimentDescription(input.experiment, resolveExperiments());
108
+ return;
109
+ }
110
+
111
+ const experiments = setExperimentEnabled(
112
+ input.experiment,
113
+ input.action === "enable",
114
+ );
115
+ console.log(`Experiment "${input.experiment}" ${input.action}d.`);
116
+ if (input.action === "enable") {
117
+ console.log("");
118
+ printExperimentDescription(input.experiment, experiments);
119
+ }
120
+ });
@@ -1,10 +1,5 @@
1
1
  import { createInterface } from "node:readline";
2
- import {
3
- cpSync,
4
- existsSync,
5
- readdirSync,
6
- rmSync,
7
- } from "node:fs";
2
+ import { cpSync, existsSync, readdirSync, rmSync } from "node:fs";
8
3
  import { spawnSync } from "node:child_process";
9
4
  import { basename, dirname, join } from "node:path";
10
5
  import { fileURLToPath } from "node:url";
@@ -19,6 +14,11 @@ import {
19
14
  DEFAULT_SNAPSHOT_MODELS,
20
15
  resolveAiSetupStatus,
21
16
  } from "../core/ai-model.js";
17
+ import {
18
+ detectProjectPackageManager,
19
+ installCommand,
20
+ librettoCommand,
21
+ } from "../../shared/package-manager.js";
22
22
  import type { Provider } from "../core/resolve-model.js";
23
23
  import { SimpleCLI } from "../framework/simple-cli.js";
24
24
 
@@ -30,26 +30,6 @@ const PROVIDER_SDK_PACKAGES: Record<Provider, string> = {
30
30
  openrouter: "@ai-sdk/openai",
31
31
  };
32
32
 
33
- function detectPackageManager(): string {
34
- if (existsSync(join(REPO_ROOT, "pnpm-lock.yaml"))) return "pnpm";
35
- if (existsSync(join(REPO_ROOT, "yarn.lock"))) return "yarn";
36
- if (existsSync(join(REPO_ROOT, "bun.lockb"))) return "bun";
37
- return "npm";
38
- }
39
-
40
- function installCommand(pkgManager: string): string {
41
- switch (pkgManager) {
42
- case "yarn":
43
- return "yarn add";
44
- case "bun":
45
- return "bun add";
46
- case "pnpm":
47
- return "pnpm add";
48
- default:
49
- return "npm install";
50
- }
51
- }
52
-
53
33
  function isSdkInstalled(sdkPackage: string): boolean {
54
34
  try {
55
35
  const result = spawnSync("node", ["-e", `require.resolve("${sdkPackage}")`], {
@@ -66,7 +46,7 @@ function installSdkIfNeeded(provider: Provider): void {
66
46
  const sdkPackage = PROVIDER_SDK_PACKAGES[provider];
67
47
  if (isSdkInstalled(sdkPackage)) return;
68
48
 
69
- const pkgManager = detectPackageManager();
49
+ const pkgManager = detectProjectPackageManager();
70
50
  const cmd = installCommand(pkgManager);
71
51
  console.log(`\nInstalling ${sdkPackage}...`);
72
52
  const result = spawnSync(cmd, [sdkPackage], {
@@ -177,7 +157,7 @@ function printHealthySummary(status: AiSetupStatus & { kind: "ready" }): void {
177
157
  console.log(`✓ Using ${providerLabel(status.provider)} (${status.model}).`);
178
158
  }
179
159
  console.log(
180
- "To change: npx libretto ai configure openai | anthropic | gemini | vertex | openrouter",
160
+ `To change: ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
181
161
  );
182
162
  }
183
163
 
@@ -254,14 +234,16 @@ function printSnapshotApiStatus(): boolean {
254
234
  console.log();
255
235
  console.log(formatMissingCredentialsMessage(plan));
256
236
  console.log(
257
- ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`,
237
+ ` To fix: add ${plan.envVar} to .env, or run \`${librettoCommand("setup")}\` interactively to repair.`,
258
238
  );
259
239
  return false;
260
240
  }
261
241
 
262
242
  if (plan.kind === "repair-invalid-config") {
263
243
  printInvalidAiConfigWarning(status);
264
- console.log(" Run `npx libretto setup` interactively to reconfigure.");
244
+ console.log(
245
+ ` Run \`${librettoCommand("setup")}\` interactively to reconfigure.`,
246
+ );
265
247
  return false;
266
248
  }
267
249
 
@@ -275,10 +257,10 @@ function printSnapshotApiStatus(): boolean {
275
257
  " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex",
276
258
  );
277
259
  console.log(
278
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model.",
260
+ ` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
279
261
  );
280
262
  console.log(
281
- " Run `npx libretto setup` interactively to set up credentials.",
263
+ ` Run \`${librettoCommand("setup")}\` interactively to set up credentials.`,
282
264
  );
283
265
  return false;
284
266
  }
@@ -324,14 +306,14 @@ async function promptProviderSelection(
324
306
 
325
307
  function printSkipMessage(): void {
326
308
  console.log(
327
- "\nSkipped. You can set up API credentials later by rerunning `npx libretto setup`.",
309
+ `\nSkipped. You can set up API credentials later by rerunning \`${librettoCommand("setup")}\`.`,
328
310
  );
329
311
  console.log("Or add credentials directly to your .env file:");
330
312
  console.log(" OPENAI_API_KEY=...");
331
313
  console.log(" ANTHROPIC_API_KEY=...");
332
314
  console.log(" GEMINI_API_KEY=...");
333
315
  console.log(
334
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex | openrouter` to set a specific model.",
316
+ ` Or run \`${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}\` to set a specific model.`,
335
317
  );
336
318
  }
337
319
 
@@ -442,7 +424,7 @@ function copySkills(): void {
442
424
  "\n⚠️ No .agents/ or .claude/ directory found. Libretto skills were not installed.",
443
425
  );
444
426
  console.log(
445
- " Create one of these directories in your repo root and rerun `npx libretto setup` to install skills:",
427
+ ` Create one of these directories in your repo root and rerun \`${librettoCommand("setup")}\` to install skills:`,
446
428
  );
447
429
  console.log(` mkdir ${join(REPO_ROOT, ".claude")}`);
448
430
  return;
@@ -510,7 +492,7 @@ export const setupCommand = SimpleCLI.command({
510
492
  const ready = printSnapshotApiStatus();
511
493
  if (!ready) {
512
494
  console.log(
513
- "\nIf you're an agent, request the user to run `npx libretto setup`.",
495
+ `\nIf you're an agent, request the user to run \`${librettoCommand("setup")}\`.`,
514
496
  );
515
497
  }
516
498
  }
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { LoggerApi } from "../../shared/logger/index.js";
3
+ import type { Experiments } from "../core/experiments.js";
4
+ import { resolveExperiments } from "../core/experiments.js";
3
5
  import { createLoggerForSession } from "../core/context.js";
4
6
  import {
5
7
  generateSessionName,
@@ -9,6 +11,8 @@ import {
9
11
  } from "../core/session.js";
10
12
  import {
11
13
  SimpleCLI,
14
+ type SimpleCLIMiddlewareArgs,
15
+ type SimpleCLIContext,
12
16
  type SimpleCLIMiddleware,
13
17
  } from "../framework/simple-cli.js";
14
18
 
@@ -33,6 +37,22 @@ export type SessionStateContext = SessionContext & {
33
37
  sessionState: SessionState;
34
38
  };
35
39
 
40
+ export type ExperimentsContext = {
41
+ experiments: Experiments;
42
+ };
43
+
44
+ export function withExperiments() {
45
+ return async <TInput, TContext extends SimpleCLIContext>({
46
+ ctx,
47
+ }: SimpleCLIMiddlewareArgs<
48
+ TInput,
49
+ TContext
50
+ >): Promise<TContext & ExperimentsContext> => ({
51
+ ...ctx,
52
+ experiments: resolveExperiments(),
53
+ });
54
+ }
55
+
36
56
  export function withRequiredSession(): SimpleCLIMiddleware<
37
57
  { session?: string },
38
58
  {},
@@ -8,11 +8,18 @@ import {
8
8
  type ScreenshotPair,
9
9
  } from "../core/snapshot-analyzer.js";
10
10
  import { SimpleCLI } from "../framework/simple-cli.js";
11
- import { pageOption, sessionOption, withRequiredSession } from "./shared.js";
11
+ import {
12
+ pageOption,
13
+ sessionOption,
14
+ withExperiments,
15
+ withRequiredSession,
16
+ } from "./shared.js";
12
17
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
13
18
  import { readSnapshotModel } from "../core/config.js";
14
19
  import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
15
- import { DaemonClient } from "../core/daemon/index.js";
20
+ import { DaemonClient } from "../core/daemon/ipc.js";
21
+ import { librettoCommand } from "../../shared/package-manager.js";
22
+ import { renderSnapshot } from "../../shared/snapshot/render-snapshot.js";
16
23
 
17
24
  export const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 } as const;
18
25
 
@@ -116,9 +123,17 @@ async function captureSnapshot(
116
123
  pageId?: string,
117
124
  ): Promise<ScreenshotPair> {
118
125
  logger.info("snapshot-via-daemon", { session, pageId });
119
- const client = new DaemonClient(daemonSocketPath);
120
- const { pngPath, htmlPath, snapshotRunId, pageUrl, title } =
121
- await client.snapshot({ pageId });
126
+ const client = await DaemonClient.connect(daemonSocketPath);
127
+ let snapshotResult: Awaited<ReturnType<DaemonClient["snapshot"]>>;
128
+ try {
129
+ snapshotResult = await client.snapshot({ pageId });
130
+ } finally {
131
+ client.destroy();
132
+ }
133
+ if (!("htmlPath" in snapshotResult)) {
134
+ throw new Error("Daemon returned a compact snapshot for a legacy request.");
135
+ }
136
+ const { pngPath, htmlPath, snapshotRunId, pageUrl, title } = snapshotResult;
122
137
 
123
138
  // condenseDom runs in the CLI process, not the daemon.
124
139
  const htmlContent = readFileSync(htmlPath, "utf8");
@@ -148,9 +163,16 @@ async function runSnapshot(
148
163
  session: string,
149
164
  logger: LoggerApi,
150
165
  pageId: string | undefined,
151
- objective: string,
152
- context: string,
166
+ objective: string | undefined,
167
+ context: string | undefined,
153
168
  ): Promise<void> {
169
+ if (objective === undefined) {
170
+ throw new Error("Missing required option --objective.");
171
+ }
172
+ if (context === undefined) {
173
+ throw new Error("Missing required option --context.");
174
+ }
175
+
154
176
  const normalizedObjective = objective.trim();
155
177
  const normalizedContext = context.trim();
156
178
 
@@ -161,7 +183,7 @@ async function runSnapshot(
161
183
  if (!state?.daemonSocketPath) {
162
184
  throw new Error(
163
185
  `Session "${session}" has no daemon socket. The browser daemon may have crashed. ` +
164
- `Close and reopen the session: libretto close --session ${session}`,
186
+ `Close and reopen the session: ${librettoCommand(`close --session ${session}`)}`,
165
187
  );
166
188
  }
167
189
 
@@ -189,13 +211,61 @@ async function runSnapshot(
189
211
  await runApiInterpret(interpretArgs, logger, snapshotModel);
190
212
  }
191
213
 
214
+ async function runCompactSnapshot(
215
+ args: {
216
+ session: string;
217
+ daemonSocketPath?: string;
218
+ logger: LoggerApi;
219
+ pageId?: string;
220
+ ref?: string;
221
+ },
222
+ ): Promise<void> {
223
+ if (!args.daemonSocketPath) {
224
+ throw new Error(
225
+ `Session "${args.session}" has no daemon socket. The browser daemon may have crashed. ` +
226
+ `Close and reopen the session: ${librettoCommand(`close --session ${args.session}`)}`,
227
+ );
228
+ }
229
+
230
+ args.logger.info("compact-snapshot-via-daemon", {
231
+ session: args.session,
232
+ pageId: args.pageId,
233
+ ref: args.ref,
234
+ });
235
+
236
+ const client = await DaemonClient.connect(args.daemonSocketPath);
237
+ let result: Awaited<ReturnType<DaemonClient["snapshot"]>>;
238
+ try {
239
+ result = await client.snapshot({
240
+ mode: "compact",
241
+ pageId: args.pageId,
242
+ useCachedSnapshot: args.ref !== undefined,
243
+ });
244
+ } finally {
245
+ client.destroy();
246
+ }
247
+ if (!("mode" in result) || result.mode !== "compact") {
248
+ throw new Error("Daemon returned a legacy snapshot for a compact request.");
249
+ }
250
+
251
+ console.log(`Screenshot at ${result.pngPath}`);
252
+ console.log(renderSnapshot(result.snapshot, args.ref));
253
+ console.log(
254
+ `Hint: Use ${librettoCommand(`snapshot <ref> --session ${args.session}`)} to inspect a subtree.`,
255
+ );
256
+ }
257
+
192
258
  export const snapshotInput = SimpleCLI.input({
193
- positionals: [],
259
+ positionals: [
260
+ SimpleCLI.positional("ref", z.string().optional(), {
261
+ help: "Optional element ref to scope output to that subtree (for example, l16 or e16)",
262
+ }),
263
+ ],
194
264
  named: {
195
265
  session: sessionOption(),
196
266
  page: pageOption(),
197
- objective: SimpleCLI.option(z.string()),
198
- context: SimpleCLI.option(z.string()),
267
+ objective: SimpleCLI.option(z.string().optional()),
268
+ context: SimpleCLI.option(z.string().optional()),
199
269
  },
200
270
  });
201
271
 
@@ -204,7 +274,25 @@ export const snapshotCommand = SimpleCLI.command({
204
274
  })
205
275
  .input(snapshotInput)
206
276
  .use(withRequiredSession())
277
+ .use(withExperiments())
207
278
  .handle(async ({ input, ctx }) => {
279
+ if (ctx.experiments["compact-snapshot-format"]) {
280
+ await runCompactSnapshot({
281
+ session: ctx.session,
282
+ daemonSocketPath: ctx.sessionState.daemonSocketPath,
283
+ logger: ctx.logger,
284
+ pageId: input.page,
285
+ ref: input.ref,
286
+ });
287
+ return;
288
+ }
289
+
290
+ if (input.ref) {
291
+ throw new Error(
292
+ `Snapshot refs require the compact-snapshot-format experiment. Enable it with ${librettoCommand("experiments enable compact-snapshot-format")}.`,
293
+ );
294
+ }
295
+
208
296
  await runSnapshot(
209
297
  ctx.session,
210
298
  ctx.logger,
@@ -1,5 +1,6 @@
1
1
  import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
2
2
  import { type AiSetupStatus, resolveAiSetupStatus } from "../core/ai-model.js";
3
+ import { librettoCommand } from "../../shared/package-manager.js";
3
4
  import { listRunningSessions, type SessionState } from "../core/session.js";
4
5
  import { SimpleCLI } from "../framework/simple-cli.js";
5
6
 
@@ -17,7 +18,7 @@ function printAiStatus(status: AiSetupStatus): void {
17
18
  console.log(` Source: ${status.source}`);
18
19
  }
19
20
  console.log(
20
- " To change: npx libretto ai configure openai | anthropic | gemini | vertex | openrouter",
21
+ ` To change: ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
21
22
  );
22
23
  break;
23
24
 
@@ -25,7 +26,7 @@ function printAiStatus(status: AiSetupStatus): void {
25
26
  console.log(
26
27
  ` ✗ ${status.provider} is configured (model: ${status.model}), but credentials are missing.`,
27
28
  );
28
- console.log(" Run `npx libretto setup` to repair.");
29
+ console.log(` Run \`${librettoCommand("setup")}\` to repair.`);
29
30
  break;
30
31
 
31
32
  case "invalid-config":
@@ -33,13 +34,13 @@ function printAiStatus(status: AiSetupStatus): void {
33
34
  for (const line of status.message.split("\n")) {
34
35
  console.log(` ${line}`);
35
36
  }
36
- console.log(" Run `npx libretto setup` to reconfigure.");
37
+ console.log(` Run \`${librettoCommand("setup")}\` to reconfigure.`);
37
38
  break;
38
39
 
39
40
  case "unconfigured":
40
41
  console.log(" ✗ No AI model configured.");
41
42
  console.log(
42
- " Run `npx libretto setup` or `npx libretto ai configure` to set up.",
43
+ ` Run \`${librettoCommand("setup")}\` or \`${librettoCommand("ai configure")}\` to set up.`,
43
44
  );
44
45
  break;
45
46
  }
@@ -1,5 +1,6 @@
1
1
  import { readSnapshotModel } from "./config.js";
2
2
  import { LIBRETTO_CONFIG_PATH } from "./context.js";
3
+ import { librettoCommand } from "../../shared/package-manager.js";
3
4
  import {
4
5
  hasProviderCredentials,
5
6
  parseModel,
@@ -80,7 +81,9 @@ function providerSetupSentence(provider: Provider): string {
80
81
  }
81
82
 
82
83
  function defaultModelCommandLine(): string {
83
- return "npx libretto ai configure openai | anthropic | gemini | vertex | openrouter";
84
+ return librettoCommand(
85
+ "ai configure openai | anthropic | gemini | vertex | openrouter",
86
+ );
84
87
  }
85
88
 
86
89
  function providerMissingCredentialSummary(provider: Provider): string {
@@ -102,7 +105,7 @@ function noSnapshotApiConfiguredMessage(): string {
102
105
  return [
103
106
  "Failed to analyze snapshot because no snapshot analyzer is configured.",
104
107
  `Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, GOOGLE_CLOUD_PROJECT, or OPENROUTER_API_KEY to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
105
- "For more info, run `npx libretto setup`.",
108
+ `For more info, run \`${librettoCommand("setup")}\`.`,
106
109
  ].join(" ");
107
110
  }
108
111
 
@@ -116,7 +119,7 @@ function missingProviderSnapshotMessage(
116
119
  return [
117
120
  `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
118
121
  providerSetupSentence(selection.provider),
119
- "For more info, run `npx libretto setup`.",
122
+ `For more info, run \`${librettoCommand("setup")}\`.`,
120
123
  ].join(" ");
121
124
  }
122
125