libretto 0.6.12 → 0.6.14

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 (82) 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/auth.js +24 -33
  5. package/dist/cli/commands/billing.js +3 -5
  6. package/dist/cli/commands/browser.js +4 -13
  7. package/dist/cli/commands/deploy.js +54 -45
  8. package/dist/cli/commands/execution.js +6 -3
  9. package/dist/cli/commands/experiments.js +1 -1
  10. package/dist/cli/commands/setup.js +2 -295
  11. package/dist/cli/commands/shared.js +1 -1
  12. package/dist/cli/commands/snapshot.js +10 -100
  13. package/dist/cli/commands/status.js +2 -42
  14. package/dist/cli/core/auth-fetch.js +11 -6
  15. package/dist/cli/core/browser.js +13 -8
  16. package/dist/cli/core/config.js +3 -6
  17. package/dist/cli/core/daemon/daemon.js +88 -74
  18. package/dist/cli/core/daemon/exec-repl.js +133 -0
  19. package/dist/cli/core/daemon/exec.js +6 -21
  20. package/dist/cli/core/daemon/ipc.js +47 -4
  21. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  22. package/dist/cli/core/daemon/snapshot.js +2 -29
  23. package/dist/cli/core/exec-compiler.js +8 -3
  24. package/dist/cli/core/experiments.js +1 -28
  25. package/dist/cli/core/providers/index.js +13 -4
  26. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  27. package/dist/cli/index.js +0 -2
  28. package/dist/cli/router.js +9 -6
  29. package/dist/shared/instrumentation/instrument.js +4 -4
  30. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  31. package/dist/shared/ipc/socket-transport.js +16 -5
  32. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  33. package/docs/releasing.md +8 -6
  34. package/package.json +3 -2
  35. package/skills/libretto/SKILL.md +49 -47
  36. package/skills/libretto/references/code-generation-rules.md +6 -0
  37. package/skills/libretto/references/configuration-file-reference.md +14 -12
  38. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  39. package/skills/libretto/references/site-security-review.md +6 -6
  40. package/skills/libretto-readonly/SKILL.md +2 -9
  41. package/src/cli/cli.ts +0 -24
  42. package/src/cli/commands/auth.ts +24 -33
  43. package/src/cli/commands/billing.ts +3 -5
  44. package/src/cli/commands/browser.ts +6 -16
  45. package/src/cli/commands/deploy.ts +55 -49
  46. package/src/cli/commands/execution.ts +6 -3
  47. package/src/cli/commands/experiments.ts +1 -1
  48. package/src/cli/commands/setup.ts +2 -381
  49. package/src/cli/commands/shared.ts +1 -1
  50. package/src/cli/commands/snapshot.ts +9 -137
  51. package/src/cli/commands/status.ts +2 -50
  52. package/src/cli/core/auth-fetch.ts +9 -4
  53. package/src/cli/core/browser.ts +15 -8
  54. package/src/cli/core/config.ts +3 -6
  55. package/src/cli/core/daemon/daemon.ts +106 -76
  56. package/src/cli/core/daemon/exec-repl.ts +189 -0
  57. package/src/cli/core/daemon/exec.ts +8 -43
  58. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  59. package/src/cli/core/daemon/ipc.ts +81 -23
  60. package/src/cli/core/daemon/snapshot.ts +1 -43
  61. package/src/cli/core/exec-compiler.ts +8 -3
  62. package/src/cli/core/experiments.ts +9 -38
  63. package/src/cli/core/providers/index.ts +17 -4
  64. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  65. package/src/cli/core/resolve-model.ts +5 -0
  66. package/src/cli/core/workflow-runtime.ts +1 -0
  67. package/src/cli/index.ts +0 -1
  68. package/src/cli/router.ts +9 -6
  69. package/src/shared/instrumentation/instrument.ts +4 -4
  70. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  71. package/src/shared/ipc/socket-transport.ts +20 -5
  72. package/dist/cli/commands/ai.js +0 -110
  73. package/dist/cli/core/ai-model.js +0 -195
  74. package/dist/cli/core/api-snapshot-analyzer.js +0 -86
  75. package/dist/cli/core/snapshot-analyzer.js +0 -667
  76. package/dist/cli/framework/simple-cli.js +0 -880
  77. package/scripts/summarize-evals.mjs +0 -135
  78. package/src/cli/commands/ai.ts +0 -144
  79. package/src/cli/core/ai-model.ts +0 -301
  80. package/src/cli/core/api-snapshot-analyzer.ts +0 -110
  81. package/src/cli/core/snapshot-analyzer.ts +0 -856
  82. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -1,22 +1,12 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
2
1
  import { z } from "zod";
3
2
  import type { LoggerApi } from "../../shared/logger/index.js";
4
- import { condenseDom } from "../../shared/condense-dom/condense-dom.js";
5
3
  import { readSessionState } from "../core/session.js";
6
- import {
7
- type InterpretArgs,
8
- type ScreenshotPair,
9
- } from "../core/snapshot-analyzer.js";
10
- import { SimpleCLI } from "../framework/simple-cli.js";
4
+ import { SimpleCLI } from "affordance";
11
5
  import {
12
6
  pageOption,
13
7
  sessionOption,
14
- withExperiments,
15
8
  withRequiredSession,
16
9
  } from "./shared.js";
17
- import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
18
- import { readSnapshotModel } from "../core/config.js";
19
- import { resolveSnapshotApiModelOrThrow } from "../core/ai-model.js";
20
10
  import { DaemonClient } from "../core/daemon/ipc.js";
21
11
  import { librettoCommand } from "../../shared/package-manager.js";
22
12
  import { renderSnapshot } from "../../shared/snapshot/render-snapshot.js";
@@ -116,101 +106,6 @@ export async function forceSnapshotViewport(
116
106
  });
117
107
  }
118
108
 
119
- async function captureSnapshot(
120
- session: string,
121
- logger: LoggerApi,
122
- daemonSocketPath: string,
123
- pageId?: string,
124
- ): Promise<ScreenshotPair> {
125
- logger.info("snapshot-via-daemon", { session, 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;
137
-
138
- // condenseDom runs in the CLI process, not the daemon.
139
- const htmlContent = readFileSync(htmlPath, "utf8");
140
- const condenseResult = condenseDom(htmlContent);
141
- const condensedHtmlPath = htmlPath.replace(/\.html$/, ".condensed.html");
142
- writeFileSync(condensedHtmlPath, condenseResult.html);
143
-
144
- logger.info("snapshot-daemon-success", {
145
- session,
146
- pageUrl,
147
- title,
148
- pngPath,
149
- htmlPath,
150
- condensedHtmlPath,
151
- snapshotRunId,
152
- domCondenseStats: {
153
- originalLength: condenseResult.originalLength,
154
- condensedLength: condenseResult.condensedLength,
155
- reductions: condenseResult.reductions,
156
- },
157
- });
158
-
159
- return { pngPath, htmlPath, condensedHtmlPath, baseName: snapshotRunId };
160
- }
161
-
162
- async function runSnapshot(
163
- session: string,
164
- logger: LoggerApi,
165
- pageId: string | undefined,
166
- objective: string | undefined,
167
- context: string | undefined,
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
-
176
- const normalizedObjective = objective.trim();
177
- const normalizedContext = context.trim();
178
-
179
- const snapshotModel = readSnapshotModel();
180
- resolveSnapshotApiModelOrThrow(snapshotModel);
181
-
182
- const state = readSessionState(session, logger);
183
- if (!state?.daemonSocketPath) {
184
- throw new Error(
185
- `Session "${session}" has no daemon socket. The browser daemon may have crashed. ` +
186
- `Close and reopen the session: ${librettoCommand(`close --session ${session}`)}`,
187
- );
188
- }
189
-
190
- const { pngPath, htmlPath, condensedHtmlPath } =
191
- await captureSnapshot(session, logger, state.daemonSocketPath, pageId);
192
-
193
- console.log("Screenshot saved:");
194
- console.log(` PNG: ${pngPath}`);
195
- console.log(` HTML: ${htmlPath}`);
196
- console.log(` Condensed HTML: ${condensedHtmlPath}`);
197
-
198
- const interpretArgs: InterpretArgs = {
199
- objective: normalizedObjective,
200
- session,
201
- context: normalizedContext,
202
- pngPath,
203
- htmlPath,
204
- condensedHtmlPath,
205
- };
206
-
207
- // Analysis uses direct API calls via the Vercel AI SDK (see api-snapshot-analyzer.ts).
208
- // The legacy CLI-agent path (spawning codex/claude/gemini as a subprocess) is preserved
209
- // in snapshot-analyzer.ts — to switch back, replace this call with:
210
- // await runInterpret(interpretArgs, logger);
211
- await runApiInterpret(interpretArgs, logger, snapshotModel);
212
- }
213
-
214
109
  async function runCompactSnapshot(
215
110
  args: {
216
111
  session: string;
@@ -237,17 +132,12 @@ async function runCompactSnapshot(
237
132
  let result: Awaited<ReturnType<DaemonClient["snapshot"]>>;
238
133
  try {
239
134
  result = await client.snapshot({
240
- mode: "compact",
241
135
  pageId: args.pageId,
242
136
  useCachedSnapshot: args.ref !== undefined,
243
137
  });
244
138
  } finally {
245
139
  client.destroy();
246
140
  }
247
- if (!("mode" in result) || result.mode !== "compact") {
248
- throw new Error("Daemon returned a legacy snapshot for a compact request.");
249
- }
250
-
251
141
  console.log(`Screenshot at ${result.pngPath}`);
252
142
  console.log(renderSnapshot(result.snapshot, args.ref));
253
143
  console.log(
@@ -270,34 +160,16 @@ export const snapshotInput = SimpleCLI.input({
270
160
  });
271
161
 
272
162
  export const snapshotCommand = SimpleCLI.command({
273
- description: "Capture PNG + HTML and analyze with --objective and --context",
163
+ description: "Capture a screenshot and compact accessibility snapshot",
274
164
  })
275
165
  .input(snapshotInput)
276
166
  .use(withRequiredSession())
277
- .use(withExperiments())
278
167
  .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
-
296
- await runSnapshot(
297
- ctx.session,
298
- ctx.logger,
299
- input.page,
300
- input.objective,
301
- input.context,
302
- );
168
+ await runCompactSnapshot({
169
+ session: ctx.session,
170
+ daemonSocketPath: ctx.sessionState.daemonSocketPath,
171
+ logger: ctx.logger,
172
+ pageId: input.page,
173
+ ref: input.ref,
174
+ });
303
175
  });
@@ -1,50 +1,5 @@
1
- import { LIBRETTO_CONFIG_PATH } from "../core/context.js";
2
- import { type AiSetupStatus, resolveAiSetupStatus } from "../core/ai-model.js";
3
- import { librettoCommand } from "../../shared/package-manager.js";
4
1
  import { listRunningSessions, type SessionState } from "../core/session.js";
5
- import { SimpleCLI } from "../framework/simple-cli.js";
6
-
7
- // ── AI status printing ──────────────────────────────────────────────────────
8
-
9
- function printAiStatus(status: AiSetupStatus): void {
10
- console.log("AI configuration:");
11
-
12
- switch (status.kind) {
13
- case "ready":
14
- console.log(` ✓ Snapshot model: ${status.model}`);
15
- if (status.source === "config") {
16
- console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
17
- } else {
18
- console.log(` Source: ${status.source}`);
19
- }
20
- console.log(
21
- ` To change: ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
22
- );
23
- break;
24
-
25
- case "configured-missing-credentials":
26
- console.log(
27
- ` ✗ ${status.provider} is configured (model: ${status.model}), but credentials are missing.`,
28
- );
29
- console.log(` Run \`${librettoCommand("setup")}\` to repair.`);
30
- break;
31
-
32
- case "invalid-config":
33
- console.log(" ✗ Config is invalid:");
34
- for (const line of status.message.split("\n")) {
35
- console.log(` ${line}`);
36
- }
37
- console.log(` Run \`${librettoCommand("setup")}\` to reconfigure.`);
38
- break;
39
-
40
- case "unconfigured":
41
- console.log(" ✗ No AI model configured.");
42
- console.log(
43
- ` Run \`${librettoCommand("setup")}\` or \`${librettoCommand("ai configure")}\` to set up.`,
44
- );
45
- break;
46
- }
47
- }
2
+ import { SimpleCLI } from "affordance";
48
3
 
49
4
  // ── Session status printing ─────────────────────────────────────────────────
50
5
 
@@ -68,13 +23,10 @@ function printOpenSessions(sessions: SessionState[]): void {
68
23
  // ── Command ─────────────────────────────────────────────────────────────────
69
24
 
70
25
  export const statusCommand = SimpleCLI.command({
71
- description: "Show workspace status: AI configuration and open sessions",
26
+ description: "Show workspace status and open sessions",
72
27
  })
73
28
  .input(SimpleCLI.input({ positionals: [], named: {} }))
74
29
  .handle(async () => {
75
- const aiStatus = resolveAiSetupStatus();
76
- printAiStatus(aiStatus);
77
-
78
30
  const sessions = listRunningSessions();
79
31
  printOpenSessions(sessions);
80
32
  });
@@ -11,7 +11,11 @@
11
11
 
12
12
  import { readAuthState, writeAuthState, type AuthState } from "./auth-storage.js";
13
13
 
14
- export const HOSTED_API_URL = "https://api.libretto.sh";
14
+ export const DEFAULT_HOSTED_API_URL = "https://api.libretto.sh";
15
+
16
+ export function resolveHostedApiUrl(): string {
17
+ return process.env.LIBRETTO_API_URL?.trim() || DEFAULT_HOSTED_API_URL;
18
+ }
15
19
 
16
20
  /**
17
21
  * Shared "you have no usable credential" message. Pointed at the two
@@ -19,8 +23,9 @@ export const HOSTED_API_URL = "https://api.libretto.sh";
19
23
  */
20
24
  export const NOT_AUTHENTICATED_MESSAGE = [
21
25
  "Not authenticated.",
22
- " • Cookie expired or never set: run `libretto experimental auth login` to refresh it.",
23
- " • Or set LIBRETTO_API_KEY in your .env (issue one with `libretto experimental auth api-key issue --label <label>` after logging in).",
26
+ " • New account: run `libretto cloud auth signup`.",
27
+ " • Existing account: run `libretto cloud auth login`.",
28
+ " • Automation: set LIBRETTO_API_KEY in your env (issue one with `libretto cloud auth api-key issue --label <label>` after signing in).",
24
29
  ].join("\n");
25
30
 
26
31
  export type CredentialSource = "env-api-key" | "cookie" | "none";
@@ -41,7 +46,7 @@ export function pickCredential(state: AuthState | null): CredentialChoice {
41
46
  }
42
47
 
43
48
  export function resolveApiUrl(_state: AuthState | null): string {
44
- return HOSTED_API_URL;
49
+ return resolveHostedApiUrl();
45
50
  }
46
51
 
47
52
  type FetchOptions = {
@@ -6,15 +6,20 @@ import {
6
6
  type Page,
7
7
  } from "playwright";
8
8
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
9
+ import { mkdir, writeFile } from "node:fs/promises";
9
10
  import { dirname, join } from "node:path";
10
11
  import { createServer } from "node:net";
12
+ import { isWindowsNamedPipePath } from "../../shared/ipc/socket-transport.js";
11
13
  import type { LoggerApi } from "../../shared/logger/index.js";
12
14
  import type { SessionAccessMode } from "../../shared/state/index.js";
13
15
  import type { Experiments } from "./experiments.js";
14
16
  import { getSessionProviderClosePath, PROFILES_DIR } from "./context.js";
15
17
  import { readLibrettoConfig } from "./config.js";
16
18
  import { librettoCommand } from "../../shared/package-manager.js";
17
- import { getCloudProviderApi } from "./providers/index.js";
19
+ import {
20
+ getCloudProviderApi,
21
+ getProviderStartupTimeoutMs,
22
+ } from "./providers/index.js";
18
23
  import {
19
24
  assertSessionAvailableForStart,
20
25
  clearSessionState,
@@ -88,7 +93,8 @@ export function normalizeUrl(url: string): URL {
88
93
  if (
89
94
  parsedUrl.protocol === "http:" ||
90
95
  parsedUrl.protocol === "https:" ||
91
- parsedUrl.protocol === "file:"
96
+ parsedUrl.protocol === "file:" ||
97
+ parsedUrl.href === "about:blank"
92
98
  ) {
93
99
  return parsedUrl;
94
100
  }
@@ -98,7 +104,7 @@ export function normalizeUrl(url: string): URL {
98
104
  }
99
105
 
100
106
  throw new Error(
101
- `Unsupported URL protocol: ${parsedUrl.protocol}. Use http://, https://, or file://.`,
107
+ `Unsupported URL protocol: ${parsedUrl.protocol}. Use http://, https://, file://, or about:blank.`,
102
108
  );
103
109
  }
104
110
 
@@ -547,8 +553,8 @@ export async function runOpenWithProvider(
547
553
  },
548
554
  logger,
549
555
  logPath: runLogPath,
550
- // Remote CDP connection + navigation; must cover both.
551
- startupTimeoutMs: 60_000,
556
+ // Remote provider creation can wait for cloud capacity before CDP exists.
557
+ startupTimeoutMs: getProviderStartupTimeoutMs(providerName),
552
558
  });
553
559
  client.destroy();
554
560
 
@@ -657,9 +663,8 @@ export async function runSave(
657
663
  }
658
664
 
659
665
  const state = { cookies, origins };
660
- const fs = await import("node:fs/promises");
661
- await fs.mkdir(dirname(profilePath), { recursive: true });
662
- await fs.writeFile(profilePath, JSON.stringify(state, null, 2));
666
+ await mkdir(dirname(profilePath), { recursive: true });
667
+ await writeFile(profilePath, JSON.stringify(state, null, 2));
663
668
 
664
669
  logger.info("save-success", {
665
670
  domain,
@@ -974,6 +979,8 @@ function unlinkDaemonSocket(
974
979
  session: string,
975
980
  ): void {
976
981
  if (!socketPath) return;
982
+ if (isWindowsNamedPipePath(socketPath)) return;
983
+
977
984
  try {
978
985
  unlinkSync(socketPath);
979
986
  } catch (err) {
@@ -3,7 +3,6 @@ import { dirname } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { SessionAccessModeSchema } from "../../shared/state/index.js";
5
5
  import { LIBRETTO_CONFIG_PATH } from "./context.js";
6
- import { librettoCommand } from "../../shared/package-manager.js";
7
6
 
8
7
  export const CURRENT_CONFIG_VERSION = 1;
9
8
 
@@ -42,7 +41,6 @@ function formatExpectedConfigExample(): string {
42
41
  return JSON.stringify(
43
42
  {
44
43
  version: CURRENT_CONFIG_VERSION,
45
- snapshotModel: "openai/gpt-5.4",
46
44
  viewport: {
47
45
  width: 1280,
48
46
  height: 800,
@@ -66,10 +64,9 @@ function invalidConfigError(configPath: string, detail?: string): Error {
66
64
  "Expected config example:",
67
65
  formatExpectedConfigExample(),
68
66
  "Notes:",
69
- ' - "snapshotModel", "viewport", "windowPosition", and "sessionMode" are optional.',
70
- ' - "snapshotModel" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
71
- "Fix the file to match this shape, or delete it and rerun:",
72
- ` ${librettoCommand("ai configure openai | anthropic | gemini | vertex | openrouter")}`,
67
+ ' - "viewport", "windowPosition", and "sessionMode" are optional.',
68
+ ' - "snapshotModel" is deprecated and ignored by snapshot.',
69
+ "Fix the file to match this shape, or delete it and rerun setup.",
73
70
  ]
74
71
  .filter(Boolean)
75
72
  .join("\n"),