libretto 0.4.2 → 0.5.0

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 (161) hide show
  1. package/dist/cli/cli.js +20 -19
  2. package/dist/cli/commands/ai.js +1 -1
  3. package/dist/cli/commands/browser.js +3 -3
  4. package/dist/cli/commands/execution.js +3 -3
  5. package/dist/cli/commands/init.js +30 -21
  6. package/dist/cli/commands/logs.js +1 -1
  7. package/dist/cli/commands/snapshot.js +6 -1
  8. package/dist/cli/core/ai-config.js +62 -12
  9. package/dist/cli/core/browser.js +11 -6
  10. package/dist/cli/core/context.js +4 -18
  11. package/dist/cli/core/session.js +2 -2
  12. package/dist/cli/core/snapshot-analyzer.js +2 -2
  13. package/dist/cli/core/snapshot-api-config.js +46 -5
  14. package/dist/cli/router.js +1 -1
  15. package/dist/cli/workers/run-integration-runtime.js +2 -4
  16. package/dist/shared/debug/index.d.ts +1 -1
  17. package/dist/shared/debug/index.js +2 -3
  18. package/dist/shared/debug/pause.d.ts +2 -6
  19. package/dist/shared/debug/pause.js +27 -20
  20. package/dist/shared/llm/client.js +5 -5
  21. package/dist/shared/paths/paths.js +2 -1
  22. package/dist/shared/paths/repo-root.d.ts +3 -0
  23. package/dist/shared/paths/repo-root.js +24 -0
  24. package/package.json +8 -8
  25. package/scripts/postinstall.mjs +50 -0
  26. package/skills/libretto/SKILL.md +93 -404
  27. package/skills/libretto/references/auth-profiles.md +30 -0
  28. package/skills/libretto/references/pages-and-page-targeting.md +29 -0
  29. package/skills/libretto/references/reverse-engineering-network-requests.md +39 -0
  30. package/skills/libretto/references/user-action-log.md +31 -0
  31. package/src/cli/cli.ts +173 -0
  32. package/src/cli/commands/ai.ts +35 -0
  33. package/src/cli/commands/browser.ts +165 -0
  34. package/src/cli/commands/execution.ts +691 -0
  35. package/src/cli/commands/init.ts +327 -0
  36. package/src/cli/commands/logs.ts +128 -0
  37. package/src/cli/commands/shared.ts +70 -0
  38. package/src/cli/commands/snapshot.ts +327 -0
  39. package/src/cli/core/ai-config.ts +255 -0
  40. package/src/cli/core/api-snapshot-analyzer.ts +97 -0
  41. package/src/cli/core/browser.ts +839 -0
  42. package/src/cli/core/context.ts +122 -0
  43. package/src/cli/core/pause-signals.ts +35 -0
  44. package/src/cli/core/session-telemetry.ts +553 -0
  45. package/src/cli/core/session.ts +209 -0
  46. package/src/cli/core/snapshot-analyzer.ts +875 -0
  47. package/src/cli/core/snapshot-api-config.ts +236 -0
  48. package/src/cli/core/telemetry.ts +446 -0
  49. package/src/cli/framework/simple-cli.ts +1273 -0
  50. package/src/cli/index.ts +13 -0
  51. package/src/cli/router.ts +28 -0
  52. package/src/cli/workers/run-integration-runtime.ts +311 -0
  53. package/src/cli/workers/run-integration-worker-protocol.ts +14 -0
  54. package/src/cli/workers/run-integration-worker.ts +75 -0
  55. package/src/index.ts +120 -0
  56. package/src/runtime/download/download.ts +100 -0
  57. package/src/runtime/download/index.ts +7 -0
  58. package/src/runtime/extract/extract.ts +92 -0
  59. package/src/runtime/extract/index.ts +1 -0
  60. package/src/runtime/network/index.ts +5 -0
  61. package/src/runtime/network/network.ts +113 -0
  62. package/src/runtime/recovery/agent.ts +256 -0
  63. package/src/runtime/recovery/errors.ts +152 -0
  64. package/src/runtime/recovery/index.ts +7 -0
  65. package/src/runtime/recovery/recovery.ts +50 -0
  66. package/{dist/shared/condense-dom/condense-dom.cjs → src/shared/condense-dom/condense-dom.ts} +243 -115
  67. package/src/shared/config/config.ts +22 -0
  68. package/src/shared/config/index.ts +5 -0
  69. package/src/shared/debug/index.ts +1 -0
  70. package/src/shared/debug/pause.ts +85 -0
  71. package/src/shared/instrumentation/errors.ts +82 -0
  72. package/src/shared/instrumentation/index.ts +9 -0
  73. package/src/shared/instrumentation/instrument.ts +276 -0
  74. package/src/shared/llm/ai-sdk-adapter.ts +78 -0
  75. package/src/shared/llm/client.ts +217 -0
  76. package/src/shared/llm/index.ts +3 -0
  77. package/src/shared/llm/types.ts +63 -0
  78. package/src/shared/logger/index.ts +6 -0
  79. package/src/shared/logger/logger.ts +352 -0
  80. package/src/shared/logger/sinks.ts +144 -0
  81. package/src/shared/paths/paths.ts +109 -0
  82. package/src/shared/paths/repo-root.ts +27 -0
  83. package/src/shared/run/api.ts +2 -0
  84. package/src/shared/run/browser.ts +98 -0
  85. package/src/shared/state/index.ts +11 -0
  86. package/src/shared/state/session-state.ts +74 -0
  87. package/src/shared/visualization/ghost-cursor.ts +200 -0
  88. package/src/shared/visualization/highlight.ts +146 -0
  89. package/src/shared/visualization/index.ts +18 -0
  90. package/src/shared/workflow/workflow.ts +42 -0
  91. package/dist/index.cjs +0 -144
  92. package/dist/index.d.cts +0 -21
  93. package/dist/runtime/download/download.cjs +0 -70
  94. package/dist/runtime/download/download.d.cts +0 -35
  95. package/dist/runtime/download/index.cjs +0 -30
  96. package/dist/runtime/download/index.d.cts +0 -3
  97. package/dist/runtime/extract/extract.cjs +0 -88
  98. package/dist/runtime/extract/extract.d.cts +0 -23
  99. package/dist/runtime/extract/index.cjs +0 -28
  100. package/dist/runtime/extract/index.d.cts +0 -5
  101. package/dist/runtime/network/index.cjs +0 -28
  102. package/dist/runtime/network/index.d.cts +0 -4
  103. package/dist/runtime/network/network.cjs +0 -91
  104. package/dist/runtime/network/network.d.cts +0 -28
  105. package/dist/runtime/recovery/agent.cjs +0 -223
  106. package/dist/runtime/recovery/agent.d.cts +0 -13
  107. package/dist/runtime/recovery/errors.cjs +0 -124
  108. package/dist/runtime/recovery/errors.d.cts +0 -31
  109. package/dist/runtime/recovery/index.cjs +0 -34
  110. package/dist/runtime/recovery/index.d.cts +0 -7
  111. package/dist/runtime/recovery/recovery.cjs +0 -55
  112. package/dist/runtime/recovery/recovery.d.cts +0 -12
  113. package/dist/shared/condense-dom/condense-dom.d.cts +0 -34
  114. package/dist/shared/config/config.cjs +0 -44
  115. package/dist/shared/config/config.d.cts +0 -10
  116. package/dist/shared/config/index.cjs +0 -32
  117. package/dist/shared/config/index.d.cts +0 -1
  118. package/dist/shared/debug/index.cjs +0 -30
  119. package/dist/shared/debug/index.d.cts +0 -1
  120. package/dist/shared/debug/pause.cjs +0 -90
  121. package/dist/shared/debug/pause.d.cts +0 -16
  122. package/dist/shared/instrumentation/errors.cjs +0 -81
  123. package/dist/shared/instrumentation/errors.d.cts +0 -12
  124. package/dist/shared/instrumentation/index.cjs +0 -35
  125. package/dist/shared/instrumentation/index.d.cts +0 -6
  126. package/dist/shared/instrumentation/instrument.cjs +0 -206
  127. package/dist/shared/instrumentation/instrument.d.cts +0 -32
  128. package/dist/shared/llm/ai-sdk-adapter.cjs +0 -71
  129. package/dist/shared/llm/ai-sdk-adapter.d.cts +0 -22
  130. package/dist/shared/llm/client.cjs +0 -218
  131. package/dist/shared/llm/client.d.cts +0 -13
  132. package/dist/shared/llm/index.cjs +0 -31
  133. package/dist/shared/llm/index.d.cts +0 -5
  134. package/dist/shared/llm/types.cjs +0 -16
  135. package/dist/shared/llm/types.d.cts +0 -67
  136. package/dist/shared/logger/index.cjs +0 -37
  137. package/dist/shared/logger/index.d.cts +0 -2
  138. package/dist/shared/logger/logger.cjs +0 -232
  139. package/dist/shared/logger/logger.d.cts +0 -86
  140. package/dist/shared/logger/sinks.cjs +0 -160
  141. package/dist/shared/logger/sinks.d.cts +0 -9
  142. package/dist/shared/paths/paths.cjs +0 -104
  143. package/dist/shared/paths/paths.d.cts +0 -10
  144. package/dist/shared/run/api.cjs +0 -28
  145. package/dist/shared/run/api.d.cts +0 -2
  146. package/dist/shared/run/browser.cjs +0 -98
  147. package/dist/shared/run/browser.d.cts +0 -22
  148. package/dist/shared/state/index.cjs +0 -38
  149. package/dist/shared/state/index.d.cts +0 -2
  150. package/dist/shared/state/session-state.cjs +0 -92
  151. package/dist/shared/state/session-state.d.cts +0 -40
  152. package/dist/shared/visualization/ghost-cursor.cjs +0 -174
  153. package/dist/shared/visualization/ghost-cursor.d.cts +0 -37
  154. package/dist/shared/visualization/highlight.cjs +0 -134
  155. package/dist/shared/visualization/highlight.d.cts +0 -22
  156. package/dist/shared/visualization/index.cjs +0 -45
  157. package/dist/shared/visualization/index.d.cts +0 -3
  158. package/dist/shared/workflow/workflow.cjs +0 -47
  159. package/dist/shared/workflow/workflow.d.cts +0 -21
  160. package/skills/libretto/code-generation-rules.md +0 -223
  161. package/skills/libretto/integration-approach-selection.md +0 -174
package/dist/cli/cli.js CHANGED
@@ -16,30 +16,30 @@ Options:
16
16
  Built-in sessions: default, dev-server, browser-agent
17
17
 
18
18
  Examples:
19
- libretto-cli open https://linkedin.com
19
+ libretto open https://linkedin.com
20
20
 
21
21
  # ... manually log in ...
22
- libretto-cli save linkedin.com
22
+ libretto save linkedin.com
23
23
  # Next time you open linkedin.com, you'll be logged in automatically
24
24
 
25
- libretto-cli exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
26
- libretto-cli exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
27
- libretto-cli ai configure openai
28
- libretto-cli ai configure anthropic
29
- libretto-cli ai configure gemini
30
- libretto-cli ai configure vertex
31
- libretto-cli ai configure openai/gpt-4o
32
- libretto-cli snapshot
33
- libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
34
- libretto-cli resume --session default
35
- libretto-cli close
36
- libretto-cli close --all
37
- libretto-cli close --all --force
25
+ libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
26
+ libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
27
+ libretto ai configure openai
28
+ libretto ai configure anthropic
29
+ libretto ai configure gemini
30
+ libretto ai configure vertex
31
+ libretto ai configure openai/gpt-4o
32
+ libretto snapshot
33
+ libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
34
+ libretto resume --session default
35
+ libretto close
36
+ libretto close --all
37
+ libretto close --all --force
38
38
 
39
39
  # Multiple sessions
40
- libretto-cli open https://site1.com --session test1
41
- libretto-cli open https://site2.com --session test2
42
- libretto-cli exec "return await page.title()" --session test1
40
+ libretto open https://site1.com --session test1
41
+ libretto open https://site2.com --session test2
42
+ libretto exec "return await page.title()" --session test1
43
43
 
44
44
  Available in exec:
45
45
  page, context, state, browser, networkLog, actionLog
@@ -93,7 +93,8 @@ function validateLegacySessionArg(rawArgs) {
93
93
  if (value === void 0) return;
94
94
  if (value === null) {
95
95
  throw new Error(
96
- "Usage: libretto-cli <command> [--session <name>]\nMissing or invalid --session value."
96
+ `Usage: libretto <command> [--session <name>]
97
+ Missing or invalid --session value.`
97
98
  );
98
99
  }
99
100
  validateSessionName(value);
@@ -23,7 +23,7 @@ const aiCommands = SimpleCLI.group({
23
23
  preset: input.preset
24
24
  },
25
25
  {
26
- configureCommandName: "libretto-cli ai configure"
26
+ configureCommandName: `libretto ai configure`
27
27
  }
28
28
  );
29
29
  })
@@ -47,7 +47,7 @@ const openInput = SimpleCLI.input({
47
47
  }
48
48
  }).refine(
49
49
  (input) => Boolean(input.url),
50
- "Usage: libretto-cli open <url> [--headless] [--viewport WxH] [--session <name>]"
50
+ `Usage: libretto open <url> [--headless] [--viewport WxH] [--session <name>]`
51
51
  ).refine(
52
52
  (input) => !(input.headed && input.headless),
53
53
  "Cannot pass both --headed and --headless."
@@ -73,7 +73,7 @@ const saveInput = SimpleCLI.input({
73
73
  }
74
74
  }).refine(
75
75
  (input) => Boolean(input.urlOrDomain),
76
- "Usage: libretto-cli save <url|domain> [--session <name>]"
76
+ `Usage: libretto save <url|domain> [--session <name>]`
77
77
  );
78
78
  function createSaveCommand(logger) {
79
79
  return SimpleCLI.command({
@@ -108,7 +108,7 @@ function createCloseCommand(logger) {
108
108
  description: "Close the browser"
109
109
  }).input(closeInput).use(resolveSessionMiddleware).handle(async ({ input, ctx }) => {
110
110
  if (input.force && !input.all) {
111
- throw new Error("Usage: libretto-cli close --all [--force]");
111
+ throw new Error(`Usage: libretto close --all [--force]`);
112
112
  }
113
113
  if (input.all) {
114
114
  await runCloseAllWithLogger(logger, { force: input.force });
@@ -292,7 +292,7 @@ async function runResume(session, logger, sessionState) {
292
292
  } = getPauseSignalPaths(session);
293
293
  if (!existsSync(pausedSignalPath)) {
294
294
  throw new Error(
295
- `Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
295
+ `Session "${session}" is not paused. Run "libretto run ... --session ${session}" and call pause("${session}") first.`
296
296
  );
297
297
  }
298
298
  if (!isProcessRunning(sessionState.pid)) {
@@ -414,7 +414,7 @@ const execInput = SimpleCLI.input({
414
414
  }
415
415
  }).refine(
416
416
  (input) => input.codeParts.length > 0,
417
- "Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
417
+ `Usage: libretto exec <code> [--session <name>] [--visualize]`
418
418
  );
419
419
  function createExecCommand(logger) {
420
420
  return SimpleCLI.command({
@@ -429,7 +429,7 @@ function createExecCommand(logger) {
429
429
  );
430
430
  });
431
431
  }
432
- const runUsage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
432
+ const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]`;
433
433
  const runInput = SimpleCLI.input({
434
434
  positionals: [
435
435
  SimpleCLI.positional("integrationFile", z.string().optional(), {
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { spawnSync } from "node:child_process";
4
- import { dirname, join } from "node:path";
4
+ import { basename, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { readAiConfig } from "../core/ai-config.js";
7
7
  import { REPO_ROOT } from "../core/context.js";
@@ -60,6 +60,17 @@ function safeReadAiConfig() {
60
60
  return null;
61
61
  }
62
62
  }
63
+ function printInvalidAiConfigWarning() {
64
+ try {
65
+ readAiConfig();
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ console.log(" ! Existing AI config is invalid:");
69
+ for (const line of message.split("\n")) {
70
+ console.log(` ${line}`);
71
+ }
72
+ }
73
+ }
63
74
  function printSnapshotApiStatus() {
64
75
  const config = safeReadAiConfig();
65
76
  const selection = resolveSnapshotApiModel(config);
@@ -69,6 +80,7 @@ function printSnapshotApiStatus() {
69
80
  " Libretto uses direct API calls for snapshot analysis when supported credentials are available."
70
81
  );
71
82
  console.log(` Credentials are loaded from process env and ${envPath}.`);
83
+ printInvalidAiConfigWarning();
72
84
  if (selection && hasProviderCredentials(selection.provider)) {
73
85
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
74
86
  console.log(" Snapshot objectives will use the API analyzer by default.");
@@ -84,7 +96,7 @@ function printSnapshotApiStatus() {
84
96
  " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
85
97
  );
86
98
  console.log(
87
- " Or run `npx libretto ai configure <provider>` to set a specific model."
99
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
88
100
  );
89
101
  console.log(" Run `npx libretto init` interactively to set up credentials.");
90
102
  }
@@ -95,6 +107,7 @@ async function runInteractiveApiSetup() {
95
107
  console.log("\nSnapshot analysis setup:");
96
108
  console.log(" Libretto uses direct API calls for snapshot analysis.");
97
109
  console.log(` Credentials are loaded from process env and ${envPath}.`);
110
+ printInvalidAiConfigWarning();
98
111
  if (selection && hasProviderCredentials(selection.provider)) {
99
112
  console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
100
113
  console.log(" Snapshot objectives will use the API analyzer by default.");
@@ -119,7 +132,7 @@ async function runInteractiveApiSetup() {
119
132
  console.log(" ANTHROPIC_API_KEY=...");
120
133
  console.log(" GEMINI_API_KEY=...");
121
134
  console.log(
122
- " Or run `npx libretto ai configure <provider>` to set a specific model."
135
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
123
136
  );
124
137
  return;
125
138
  }
@@ -196,27 +209,21 @@ function getPackageSkillsDir() {
196
209
  }
197
210
  throw new Error("Could not locate libretto skill files in package");
198
211
  }
212
+ function detectAgentDirs(root) {
213
+ const dirs = [];
214
+ if (existsSync(join(root, ".agents"))) dirs.push(join(root, ".agents"));
215
+ if (existsSync(join(root, ".claude"))) dirs.push(join(root, ".claude"));
216
+ return dirs;
217
+ }
199
218
  async function copySkills() {
200
- const cwd = process.cwd();
201
- const agentDirs = [];
202
- if (existsSync(join(cwd, ".agents"))) {
203
- agentDirs.push({
204
- name: ".agents",
205
- skillDest: join(cwd, ".agents", "skills", "libretto")
206
- });
207
- }
208
- if (existsSync(join(cwd, ".claude"))) {
209
- agentDirs.push({
210
- name: ".claude",
211
- skillDest: join(cwd, ".claude", "skills", "libretto")
212
- });
213
- }
219
+ const agentDirs = detectAgentDirs(REPO_ROOT);
214
220
  if (agentDirs.length === 0) {
215
- console.log("\nSkills: No .agents/ or .claude/ directory found \u2014 skipping skill copy.");
221
+ console.log("\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping.");
216
222
  return;
217
223
  }
218
- const dirNames = agentDirs.map((d) => d.name).join(" and ");
219
- const existing = agentDirs.filter((d) => existsSync(d.skillDest));
224
+ const destinations = agentDirs.map((d) => join(d, "skills", "libretto"));
225
+ const dirNames = agentDirs.map((d) => basename(d)).join(" and ");
226
+ const existing = destinations.filter((d) => existsSync(d));
220
227
  const verb = existing.length > 0 ? "Overwrite" : "Install";
221
228
  const proceed = await askYesNo(`
222
229
  ${verb} libretto skills in ${dirNames}?`);
@@ -231,7 +238,9 @@ ${verb} libretto skills in ${dirNames}?`);
231
238
  console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
232
239
  return;
233
240
  }
234
- for (const { name, skillDest } of agentDirs) {
241
+ for (let i = 0; i < agentDirs.length; i++) {
242
+ const skillDest = destinations[i];
243
+ const name = basename(agentDirs[i]);
235
244
  if (existsSync(skillDest)) {
236
245
  rmSync(skillDest, { recursive: true });
237
246
  }
@@ -26,7 +26,7 @@ async function resolvePageId(session, pageId) {
26
26
  const foundPage = pages.find((page) => page.id === pageId);
27
27
  if (!foundPage) {
28
28
  throw new Error(
29
- `Page "${pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
29
+ `Page "${pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
30
30
  );
31
31
  }
32
32
  return pageId;
@@ -13,6 +13,7 @@ import {
13
13
  } from "./shared.js";
14
14
  import { runApiInterpret } from "../core/api-snapshot-analyzer.js";
15
15
  import { readAiConfig } from "../core/ai-config.js";
16
+ import { resolveSnapshotApiModelOrThrow } from "../core/snapshot-api-config.js";
16
17
  const DEFAULT_SNAPSHOT_CONTEXT = "No additional user context provided.";
17
18
  const FALLBACK_SNAPSHOT_VIEWPORT = { width: 1280, height: 800 };
18
19
  function generateSnapshotRunId() {
@@ -191,6 +192,10 @@ async function runSnapshot(session, logger, pageId, objective, context) {
191
192
  "Couldn't run analysis: --objective is required when providing --context."
192
193
  );
193
194
  }
195
+ const configuredAi = normalizedObjective ? readAiConfig() : null;
196
+ if (normalizedObjective) {
197
+ resolveSnapshotApiModelOrThrow(configuredAi);
198
+ }
194
199
  const { pngPath, htmlPath, condensedHtmlPath } = await captureScreenshot(
195
200
  session,
196
201
  logger,
@@ -212,7 +217,7 @@ async function runSnapshot(session, logger, pageId, objective, context) {
212
217
  htmlPath,
213
218
  condensedHtmlPath
214
219
  };
215
- await runApiInterpret(interpretArgs, logger, readAiConfig());
220
+ await runApiInterpret(interpretArgs, logger, configuredAi);
216
221
  }
217
222
  const snapshotInput = SimpleCLI.input({
218
223
  positionals: [],
@@ -6,7 +6,7 @@ const CURRENT_CONFIG_VERSION = 1;
6
6
  const AiConfigSchema = z.object({
7
7
  model: z.string().min(1),
8
8
  updatedAt: z.string()
9
- }).strict();
9
+ });
10
10
  const ViewportConfigSchema = z.object({
11
11
  width: z.number().int().min(1),
12
12
  height: z.number().int().min(1)
@@ -19,22 +19,68 @@ const LibrettoConfigSchema = z.object({
19
19
  const DEFAULT_MODELS = {
20
20
  openai: "openai/gpt-5.4",
21
21
  anthropic: "anthropic/claude-sonnet-4-6",
22
- gemini: "google/gemini-2.5-flash",
23
- google: "google/gemini-2.5-flash",
22
+ gemini: "google/gemini-3-flash-preview",
24
23
  vertex: "vertex/gemini-2.5-pro"
25
24
  };
26
- const CONFIGURE_PROVIDERS = Object.keys(DEFAULT_MODELS);
27
- function invalidConfigError(configPath) {
25
+ const PROVIDER_ALIASES = {
26
+ claude: DEFAULT_MODELS.anthropic,
27
+ google: DEFAULT_MODELS.gemini
28
+ };
29
+ const CONFIGURE_PROVIDERS = ["openai", "anthropic", "gemini", "vertex"];
30
+ function formatConfigureProviders(separator = " | ") {
31
+ return CONFIGURE_PROVIDERS.join(separator);
32
+ }
33
+ function formatConfigIssues(error) {
34
+ return error.issues.map((issue) => ` - ${issue.path.join(".") || "root"}: ${issue.message}`).join("\n");
35
+ }
36
+ function formatExpectedConfigExample() {
37
+ return JSON.stringify(
38
+ {
39
+ version: CURRENT_CONFIG_VERSION,
40
+ ai: {
41
+ model: "openai/gpt-5.4",
42
+ updatedAt: "2026-01-01T00:00:00.000Z"
43
+ },
44
+ viewport: {
45
+ width: 1280,
46
+ height: 800
47
+ }
48
+ },
49
+ null,
50
+ 2
51
+ );
52
+ }
53
+ function invalidConfigError(configPath, detail) {
28
54
  return new Error(
29
- `AI config is invalid at ${configPath}. Fix the file to match the expected schema or delete it.`
55
+ [
56
+ `AI config is invalid at ${configPath}.`,
57
+ detail ? `Problems:
58
+ ${detail}` : null,
59
+ "Expected config example:",
60
+ formatExpectedConfigExample(),
61
+ "Notes:",
62
+ ' - "ai" and "viewport" are optional.',
63
+ ' - "ai.model" must be a provider/model string like "openai/gpt-5.4" or "anthropic/claude-sonnet-4-6".',
64
+ "Fix the file to match this shape, or delete it and rerun:",
65
+ ` npx libretto ai configure ${formatConfigureProviders()}`
66
+ ].filter(Boolean).join("\n")
30
67
  );
31
68
  }
32
69
  function parseConfig(raw, configPath) {
70
+ let parsedJson;
33
71
  try {
34
- return LibrettoConfigSchema.parse(JSON.parse(raw));
35
- } catch {
36
- throw invalidConfigError(configPath);
72
+ parsedJson = JSON.parse(raw);
73
+ } catch (error) {
74
+ throw invalidConfigError(
75
+ configPath,
76
+ ` - root: Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
77
+ );
78
+ }
79
+ const parsed = LibrettoConfigSchema.safeParse(parsedJson);
80
+ if (!parsed.success) {
81
+ throw invalidConfigError(configPath, formatConfigIssues(parsed.error));
37
82
  }
83
+ return parsed.data;
38
84
  }
39
85
  function readLibrettoConfig(configPath = LIBRETTO_CONFIG_PATH) {
40
86
  if (!existsSync(configPath)) {
@@ -88,7 +134,8 @@ function resolveModelFromInput(input) {
88
134
  const trimmed = input.trim();
89
135
  if (!trimmed) return null;
90
136
  if (trimmed.includes("/")) return trimmed;
91
- return DEFAULT_MODELS[trimmed.toLowerCase()] ?? null;
137
+ const normalized = trimmed.toLowerCase();
138
+ return DEFAULT_MODELS[normalized] ?? PROVIDER_ALIASES[normalized] ?? null;
92
139
  }
93
140
  function runAiConfigure(input, options = {}) {
94
141
  const configureCommandName = options.configureCommandName ?? "npx libretto ai configure";
@@ -97,7 +144,10 @@ function runAiConfigure(input, options = {}) {
97
144
  if (!presetArg && !input.clear) {
98
145
  const config2 = readAiConfig(configPath);
99
146
  if (!config2) {
100
- console.log(`No AI config set. Run '${configureCommandName} openai' to set one.`);
147
+ console.log(
148
+ `No AI config set. Choose a default model: ${configureCommandName} ${formatConfigureProviders()}`
149
+ );
150
+ console.log("Provider credentials still come from your shell or .env file.");
101
151
  return;
102
152
  }
103
153
  printAiConfig(config2, configPath);
@@ -120,7 +170,7 @@ function runAiConfigure(input, options = {}) {
120
170
  ${configureCommandName} --clear`
121
171
  );
122
172
  throw new Error(
123
- `Invalid provider or model. Use one of: ${CONFIGURE_PROVIDERS.join(", ")}, or a full model string like "openai/gpt-4o".`
173
+ `Invalid provider or model. Use one of: ${formatConfigureProviders()}, or a full model string like "openai/gpt-4o".`
124
174
  );
125
175
  }
126
176
  const config = writeAiConfig(model, configPath);
@@ -142,9 +142,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
142
142
  port: state.port,
143
143
  pid: state.pid
144
144
  });
145
- clearSessionState(session, logger);
145
+ if (!isPidRunning(state.pid)) {
146
+ clearSessionState(session, logger);
147
+ throw new Error(
148
+ `No browser running for session "${session}". Run 'libretto open <url> --session ${session}' first.`
149
+ );
150
+ }
146
151
  throw new Error(
147
- `No browser running for session "${session}". Run 'libretto-cli open <url> --session ${session}' first.`
152
+ `Could not connect to the browser for session "${session}" at http://127.0.0.1:${state.port}, but the session process (pid ${state.pid}) is still running. Try the command again, or close and reopen the session if it stays stuck.`
148
153
  );
149
154
  }
150
155
  const contexts = browser.contexts();
@@ -170,14 +175,14 @@ async function connect(session, logger, timeoutMs = 1e4, options) {
170
175
  }
171
176
  if (options?.requireSinglePage && !options.pageId && pages.length > 1) {
172
177
  throw new Error(
173
- `Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto-cli pages --session ${session}" to list ids).`
178
+ `Multiple pages are open in session "${session}". Pass --page <id> to target a page (run "libretto pages --session ${session}" to list ids).`
174
179
  );
175
180
  }
176
181
  const pageRefs = await resolvePageReferences(pages);
177
182
  const pageRef = options?.pageId ? pageRefs.find((ref) => ref.id === options.pageId) ?? null : pageRefs[pageRefs.length - 1];
178
183
  if (!pageRef) {
179
184
  throw new Error(
180
- `Page "${options?.pageId}" was not found in session "${session}". Run "libretto-cli pages --session ${session}" to list ids.`
185
+ `Page "${options?.pageId}" was not found in session "${session}". Run "libretto pages --session ${session}" to list ids.`
181
186
  );
182
187
  }
183
188
  const page = pageRef.page;
@@ -293,7 +298,7 @@ function childLog(level, event, data = {}) {
293
298
  timestamp: new Date().toISOString(),
294
299
  id: Math.random().toString(36).slice(2, 10),
295
300
  level,
296
- scope: 'libretto-cli.child',
301
+ scope: 'libretto.child',
297
302
  event,
298
303
  data,
299
304
  });
@@ -606,7 +611,7 @@ async function runCloseAll(logger, options) {
606
611
  [
607
612
  `Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
608
613
  `Closed ${closed} session(s).`,
609
- "Retry with: libretto-cli close --all --force"
614
+ `Retry with: libretto close --all --force`
610
615
  ].join("\n")
611
616
  );
612
617
  }
@@ -1,23 +1,9 @@
1
1
  import { Logger, createFileLogSink } from "../../shared/logger/index.js";
2
- import { spawnSync } from "node:child_process";
3
- import { cwd } from "node:process";
4
2
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
5
- import { join, resolve } from "node:path";
3
+ import { join } from "node:path";
4
+ import { resolveLibrettoRepoRoot } from "../../shared/paths/repo-root.js";
6
5
  import { validateSessionName } from "./session.js";
7
- function getRepoRoot() {
8
- const override = process.env.LIBRETTO_REPO_ROOT?.trim();
9
- if (override) {
10
- return resolve(override);
11
- }
12
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
13
- encoding: "utf-8"
14
- });
15
- if (result.status === 0 && result.stdout) {
16
- return result.stdout.trim();
17
- }
18
- return cwd();
19
- }
20
- const REPO_ROOT = getRepoRoot();
6
+ const REPO_ROOT = resolveLibrettoRepoRoot();
21
7
  const LIBRETTO_CONFIG_DIR = join(REPO_ROOT, ".libretto");
22
8
  const LIBRETTO_CONFIG_PATH = join(LIBRETTO_CONFIG_DIR, "config.json");
23
9
  const PROFILES_DIR = join(LIBRETTO_CONFIG_DIR, "profiles");
@@ -63,7 +49,7 @@ function createLoggerForSession(session) {
63
49
  const sessionDir = getSessionDir(session);
64
50
  mkdirSync(sessionDir, { recursive: true });
65
51
  const logFilePath = getSessionLogsPath(session);
66
- return new Logger(["libretto-cli"], [createFileLogSink({ filePath: logFilePath })]);
52
+ return new Logger(["libretto"], [createFileLogSink({ filePath: logFilePath })]);
67
53
  }
68
54
  async function closeLogger(logger) {
69
55
  if (!logger) return;
@@ -93,7 +93,7 @@ function throwSessionNotFoundError(session) {
93
93
  }
94
94
  lines.push("");
95
95
  lines.push("Start one with:");
96
- lines.push(` libretto-cli open <url> --session ${session}`);
96
+ lines.push(` libretto open <url> --session ${session}`);
97
97
  throw new Error(lines.join("\n"));
98
98
  }
99
99
  function assertSessionStateExistsOrThrow(session) {
@@ -161,7 +161,7 @@ function assertSessionAvailableForStart(session, logger) {
161
161
  }
162
162
  const endpoint = `http://127.0.0.1:${existingState.port}`;
163
163
  throw new Error(
164
- `Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto-cli close --session ${session}`
164
+ `Session "${session}" is already open and connected to ${endpoint} (pid ${existingState.pid}). Create a new session or close the current one with: libretto close --session ${session}`
165
165
  );
166
166
  }
167
167
  export {
@@ -76,7 +76,7 @@ ${stripAnsi(result.stderr).trim() || stripAnsi(result.stdout).trim() || "No erro
76
76
  }
77
77
  class CodexUserCodingAgent extends UserCodingAgent {
78
78
  async analyzeSnapshot(prompt, pngPath, logger) {
79
- const tempDir = mkdtempSync(join(tmpdir(), "libretto-cli-analyzer-"));
79
+ const tempDir = mkdtempSync(join(tmpdir(), "libretto-analyzer-"));
80
80
  const outputPath = join(
81
81
  tempDir,
82
82
  `snapshot-analyzer-${Date.now()}-${Math.random().toString(36).slice(2)}.json`
@@ -171,7 +171,7 @@ async function runExternalCommand(command, args, logger, stdinText) {
171
171
  if (error.code === "ENOENT") {
172
172
  reject(
173
173
  new Error(
174
- `Command not found: ${command}. Configure AI with 'libretto-cli ai configure'.`
174
+ `Command not found: ${command}. Configure AI with 'libretto ai configure'.`
175
175
  )
176
176
  );
177
177
  return;
@@ -3,16 +3,15 @@ import { dirname, join, resolve } from "node:path";
3
3
  import {
4
4
  readAiConfig
5
5
  } from "./ai-config.js";
6
- import { REPO_ROOT } from "./context.js";
6
+ import { LIBRETTO_CONFIG_PATH, REPO_ROOT } from "./context.js";
7
7
  import {
8
8
  hasProviderCredentials,
9
- missingProviderCredentialsMessage,
10
9
  parseModel
11
10
  } from "../../shared/llm/client.js";
12
11
  const DEFAULT_SNAPSHOT_MODELS = {
13
12
  openai: "openai/gpt-5.4",
14
13
  anthropic: "anthropic/claude-sonnet-4-6",
15
- google: "google/gemini-2.5-flash",
14
+ google: "google/gemini-3-flash-preview",
16
15
  vertex: "vertex/gemini-2.5-pro"
17
16
  };
18
17
  class SnapshotApiUnavailableError extends Error {
@@ -21,6 +20,48 @@ class SnapshotApiUnavailableError extends Error {
21
20
  this.name = "SnapshotApiUnavailableError";
22
21
  }
23
22
  }
23
+ function providerSetupSentence(provider) {
24
+ switch (provider) {
25
+ case "openai":
26
+ return "Add OPENAI_API_KEY to .env or as a shell environment variable.";
27
+ case "anthropic":
28
+ return "Add ANTHROPIC_API_KEY to .env or as a shell environment variable.";
29
+ case "google":
30
+ return "Add GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY to .env or as a shell environment variable.";
31
+ case "vertex":
32
+ return "Add GOOGLE_CLOUD_PROJECT or GCLOUD_PROJECT to .env or as a shell environment variable, and make sure application default credentials are configured.";
33
+ }
34
+ }
35
+ function defaultModelCommandLine() {
36
+ return "npx libretto ai configure openai | anthropic | gemini | vertex";
37
+ }
38
+ function providerMissingCredentialSummary(provider) {
39
+ switch (provider) {
40
+ case "openai":
41
+ return "OPENAI_API_KEY is missing";
42
+ case "anthropic":
43
+ return "ANTHROPIC_API_KEY is missing";
44
+ case "google":
45
+ return "GEMINI_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are missing";
46
+ case "vertex":
47
+ return "GOOGLE_CLOUD_PROJECT and GCLOUD_PROJECT are missing";
48
+ }
49
+ }
50
+ function noSnapshotApiConfiguredMessage() {
51
+ return [
52
+ "Failed to analyze snapshot because no snapshot analyzer is configured.",
53
+ `Add OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT to .env or as a shell environment variable, or choose a default model with \`${defaultModelCommandLine()}\`.`,
54
+ "For more info, run `npx libretto init`."
55
+ ].join(" ");
56
+ }
57
+ function missingProviderSnapshotMessage(selection) {
58
+ const configuredSource = selection.source === "config" ? ` in ${LIBRETTO_CONFIG_PATH}` : " from process env or .env";
59
+ return [
60
+ `Failed to analyze snapshot because ${selection.provider} is configured${configuredSource}, but ${providerMissingCredentialSummary(selection.provider)}.`,
61
+ providerSetupSentence(selection.provider),
62
+ "For more info, run `npx libretto init`."
63
+ ].join(" ");
64
+ }
24
65
  function readWorktreeEnvPath() {
25
66
  const gitPath = join(REPO_ROOT, ".git");
26
67
  if (!existsSync(gitPath)) return null;
@@ -114,12 +155,12 @@ function resolveSnapshotApiModelOrThrow(config = readAiConfig()) {
114
155
  const selection = resolveSnapshotApiModel(config);
115
156
  if (!selection) {
116
157
  throw new SnapshotApiUnavailableError(
117
- "No API snapshot analyzer is available. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY/GOOGLE_GENERATIVE_AI_API_KEY, or GOOGLE_CLOUD_PROJECT, or run `npx libretto ai configure <provider>` to set a default model."
158
+ noSnapshotApiConfiguredMessage()
118
159
  );
119
160
  }
120
161
  if (!hasProviderCredentials(selection.provider)) {
121
162
  throw new SnapshotApiUnavailableError(
122
- missingProviderCredentialsMessage(selection.provider)
163
+ missingProviderSnapshotMessage(selection)
123
164
  );
124
165
  }
125
166
  return selection;
@@ -17,7 +17,7 @@ function buildCLIRoutes(logger) {
17
17
  };
18
18
  }
19
19
  function createCLIApp(logger) {
20
- return SimpleCLI.define("libretto-cli", buildCLIRoutes(logger), {
20
+ return SimpleCLI.define("libretto", buildCLIRoutes(logger), {
21
21
  globalNamed: {
22
22
  session: sessionOption()
23
23
  }
@@ -6,7 +6,6 @@ import { pathToFileURL } from "node:url";
6
6
  import {
7
7
  launchBrowser
8
8
  } from "../../index.js";
9
- import { setSessionForPause } from "../../shared/debug/pause.js";
10
9
  import { parseSessionStateContent } from "../../shared/state/index.js";
11
10
  import { getProfilePath, normalizeDomain } from "../core/browser.js";
12
11
  import {
@@ -78,9 +77,9 @@ function getMissingLocalAuthProfileError(args) {
78
77
  `Local auth profile not found for domain "${normalizedDomain}".`,
79
78
  `Expected profile file: ${args.profilePath}`,
80
79
  "To create it:",
81
- ` 1. libretto-cli open https://${normalizedDomain} --headed --session ${args.session}`,
80
+ ` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
82
81
  " 2. Log in manually in the browser window.",
83
- ` 3. libretto-cli save ${normalizedDomain} --session ${args.session}`
82
+ ` 3. libretto save ${normalizedDomain} --session ${args.session}`
84
83
  ].join("\n");
85
84
  }
86
85
  function getAbsoluteIntegrationPath(integrationPath) {
@@ -181,7 +180,6 @@ async function runIntegrationInternal(args, options) {
181
180
  appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
182
181
  }
183
182
  });
184
- setSessionForPause(args.session);
185
183
  const workflowContext = {
186
184
  logger: integrationLogger,
187
185
  page: browserSession.page,
@@ -1 +1 @@
1
- export { pause, setSessionForPause } from './pause.js';
1
+ export { pause } from './pause.js';
@@ -1,5 +1,4 @@
1
- import { pause, setSessionForPause } from "./pause.js";
1
+ import { pause } from "./pause.js";
2
2
  export {
3
- pause,
4
- setSessionForPause
3
+ pause
5
4
  };