libretto 0.5.6 → 0.6.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 (35) hide show
  1. package/dist/cli/commands/browser.js +31 -6
  2. package/dist/cli/commands/execution.js +54 -15
  3. package/dist/cli/commands/setup.js +78 -64
  4. package/dist/cli/commands/status.js +1 -1
  5. package/dist/cli/core/browser.js +163 -10
  6. package/dist/cli/core/config.js +1 -0
  7. package/dist/cli/core/providers/browserbase.js +53 -0
  8. package/dist/cli/core/providers/index.js +48 -0
  9. package/dist/cli/core/providers/kernel.js +46 -0
  10. package/dist/cli/core/providers/libretto-cloud.js +58 -0
  11. package/dist/cli/core/providers/types.js +0 -0
  12. package/dist/cli/core/session.js +9 -0
  13. package/dist/cli/workers/run-integration-runtime.js +3 -1
  14. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  15. package/dist/shared/run/browser.d.ts +6 -1
  16. package/dist/shared/run/browser.js +39 -1
  17. package/dist/shared/state/session-state.d.ts +11 -1
  18. package/dist/shared/state/session-state.js +9 -2
  19. package/package.json +1 -1
  20. package/src/cli/commands/browser.ts +35 -7
  21. package/src/cli/commands/execution.ts +54 -14
  22. package/src/cli/commands/setup.ts +81 -64
  23. package/src/cli/commands/status.ts +3 -1
  24. package/src/cli/core/browser.ts +197 -9
  25. package/src/cli/core/config.ts +1 -0
  26. package/src/cli/core/providers/browserbase.ts +57 -0
  27. package/src/cli/core/providers/index.ts +62 -0
  28. package/src/cli/core/providers/kernel.ts +49 -0
  29. package/src/cli/core/providers/libretto-cloud.ts +61 -0
  30. package/src/cli/core/providers/types.ts +9 -0
  31. package/src/cli/core/session.ts +13 -0
  32. package/src/cli/workers/run-integration-runtime.ts +2 -0
  33. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  34. package/src/shared/run/browser.ts +45 -0
  35. package/src/shared/state/session-state.ts +7 -0
@@ -5,9 +5,14 @@ import {
5
5
  runCloseAll as runCloseAllWithLogger,
6
6
  runConnect as runConnectWithLogger,
7
7
  runOpen,
8
+ runOpenWithProvider,
8
9
  runPages,
9
10
  runSave
10
11
  } from "../core/browser.js";
12
+ import {
13
+ resolveProviderName,
14
+ getCloudProviderApi
15
+ } from "../core/providers/index.js";
11
16
  import { readLibrettoConfig } from "../core/config.js";
12
17
  import { createLoggerForSession, withSessionLogger } from "../core/context.js";
13
18
  import {
@@ -65,6 +70,10 @@ const openInput = SimpleCLI.input({
65
70
  }),
66
71
  viewport: SimpleCLI.option(z.string().optional(), {
67
72
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
73
+ }),
74
+ provider: SimpleCLI.option(z.string().optional(), {
75
+ help: "Browser provider (local, kernel, browserbase)",
76
+ aliases: ["-p"]
68
77
  })
69
78
  }
70
79
  }).refine(
@@ -82,12 +91,28 @@ const openCommand = SimpleCLI.command({
82
91
  }).input(openInput).use(withAutoSession()).handle(async ({ input, ctx }) => {
83
92
  warnIfInstalledSkillOutOfDate();
84
93
  assertSessionAvailableForStart(ctx.session, ctx.logger);
85
- const headed = input.headed || !input.headless;
86
- const viewport = parseViewportArg(input.viewport);
87
- await runOpen(input.url, headed, ctx.session, ctx.logger, {
88
- viewport,
89
- accessMode: resolveRequestedSessionMode(input.readOnly, input.writeAccess)
90
- });
94
+ const providerName = resolveProviderName(input.provider);
95
+ if (providerName === "local") {
96
+ const headed = input.headed || !input.headless;
97
+ const viewport = parseViewportArg(input.viewport);
98
+ await runOpen(input.url, headed, ctx.session, ctx.logger, {
99
+ viewport,
100
+ accessMode: resolveRequestedSessionMode(
101
+ input.readOnly,
102
+ input.writeAccess
103
+ )
104
+ });
105
+ } else {
106
+ const provider = getCloudProviderApi(providerName);
107
+ await runOpenWithProvider(
108
+ input.url,
109
+ providerName,
110
+ provider,
111
+ ctx.session,
112
+ ctx.logger,
113
+ resolveRequestedSessionMode(input.readOnly, input.writeAccess)
114
+ );
115
+ }
91
116
  });
92
117
  const connectInput = SimpleCLI.input({
93
118
  positionals: [
@@ -25,6 +25,7 @@ import {
25
25
  wrapPageForActionLogging
26
26
  } from "../core/telemetry.js";
27
27
  import { readLibrettoConfig } from "../core/config.js";
28
+ import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
28
29
  import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
29
30
  import { SimpleCLI } from "../framework/simple-cli.js";
30
31
  import {
@@ -462,7 +463,9 @@ async function runIntegrationFromFile(args, logger) {
462
463
  visualize: args.visualize,
463
464
  authProfileDomain: args.authProfileDomain,
464
465
  viewport: args.viewport,
465
- accessMode: args.accessMode
466
+ accessMode: args.accessMode,
467
+ cdpEndpoint: args.cdpEndpoint,
468
+ provider: args.provider
466
469
  });
467
470
  const worker = spawn(
468
471
  process.execPath,
@@ -625,6 +628,10 @@ const runInput = SimpleCLI.input({
625
628
  }),
626
629
  viewport: SimpleCLI.option(z.string().optional(), {
627
630
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)"
631
+ }),
632
+ provider: SimpleCLI.option(z.string().optional(), {
633
+ help: "Browser provider (local, kernel, browserbase)",
634
+ aliases: ["-p"]
628
635
  })
629
636
  }
630
637
  }).refine(
@@ -670,20 +677,52 @@ const runCommand = SimpleCLI.command({
670
677
  parseViewportArg(input.viewport),
671
678
  ctx.logger
672
679
  );
673
- await runIntegrationFromFile(
674
- {
675
- integrationPath: input.integrationFile,
676
- session: ctx.session,
677
- params,
678
- tsconfigPath: input.tsconfig,
679
- headless: headlessMode ?? false,
680
- visualize,
681
- authProfileDomain: input.authProfile,
682
- viewport,
683
- accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access"
684
- },
685
- ctx.logger
686
- );
680
+ const providerName = resolveProviderName(input.provider);
681
+ let cdpEndpoint;
682
+ let providerInfo;
683
+ let provider;
684
+ if (providerName !== "local") {
685
+ provider = getCloudProviderApi(providerName);
686
+ console.log(
687
+ `Creating ${providerName} browser session (session: ${ctx.session})...`
688
+ );
689
+ const providerSession = await provider.createSession();
690
+ console.log(`Connecting to ${providerName} browser...`);
691
+ cdpEndpoint = providerSession.cdpEndpoint;
692
+ providerInfo = {
693
+ name: providerName,
694
+ sessionId: providerSession.sessionId
695
+ };
696
+ }
697
+ try {
698
+ await runIntegrationFromFile(
699
+ {
700
+ integrationPath: input.integrationFile,
701
+ session: ctx.session,
702
+ params,
703
+ tsconfigPath: input.tsconfig,
704
+ headless: cdpEndpoint ? true : headlessMode ?? false,
705
+ visualize,
706
+ authProfileDomain: input.authProfile,
707
+ viewport,
708
+ accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
709
+ cdpEndpoint,
710
+ provider: providerInfo
711
+ },
712
+ ctx.logger
713
+ );
714
+ } finally {
715
+ if (provider && providerInfo) {
716
+ try {
717
+ await provider.closeSession(providerInfo.sessionId);
718
+ } catch (cleanupErr) {
719
+ console.error(
720
+ `Failed to clean up ${providerInfo.name} session ${providerInfo.sessionId}:`,
721
+ cleanupErr instanceof Error ? cleanupErr.message : cleanupErr
722
+ );
723
+ }
724
+ }
725
+ }
687
726
  });
688
727
  const resumeInput = SimpleCLI.input({
689
728
  positionals: [],
@@ -60,6 +60,14 @@ function promptUser(rl, question) {
60
60
  });
61
61
  });
62
62
  }
63
+ function providerLabel(provider) {
64
+ const choice = PROVIDER_CHOICES.find((c) => c.provider === provider);
65
+ return choice?.label ?? provider;
66
+ }
67
+ function sourceEnvVar(source) {
68
+ if (source.startsWith("env:")) return source.slice(4);
69
+ return null;
70
+ }
63
71
  function ensurePinnedDefaultModel(status) {
64
72
  if (status.source !== "config") {
65
73
  writeAiConfig(status.model);
@@ -68,17 +76,23 @@ function ensurePinnedDefaultModel(status) {
68
76
  return status;
69
77
  }
70
78
  function printHealthySummary(status) {
71
- console.log(` \u2713 Model: ${status.model}`);
72
- console.log(` Config: ${LIBRETTO_CONFIG_PATH}`);
79
+ const envVar = sourceEnvVar(status.source);
80
+ if (envVar) {
81
+ console.log(
82
+ `\u2713 Detected ${envVar}. Using ${providerLabel(status.provider)}.`
83
+ );
84
+ } else {
85
+ console.log(`\u2713 Using ${providerLabel(status.provider)} (${status.model}).`);
86
+ }
73
87
  console.log(
74
- " To change: npx libretto ai configure openai | anthropic | gemini | vertex"
88
+ "To change: npx libretto ai configure openai | anthropic | gemini | vertex"
75
89
  );
76
90
  }
77
91
  function printInvalidAiConfigWarning(status) {
78
92
  if (status.kind !== "invalid-config") return;
79
- console.log(" ! Existing AI config is invalid:");
93
+ console.log("! Existing AI config is invalid:");
80
94
  for (const line of status.message.split("\n")) {
81
- console.log(` ${line}`);
95
+ console.log(` ${line}`);
82
96
  }
83
97
  }
84
98
  function buildRepairPlan(status) {
@@ -98,49 +112,47 @@ function buildRepairPlan(status) {
98
112
  return { kind: "no-repair-needed" };
99
113
  }
100
114
  function formatMissingCredentialsMessage(plan) {
101
- return [
102
- ` \u2717 ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`
103
- ].join("\n");
115
+ return `\u2717 ${plan.provider} is configured (model: ${plan.model}), but ${plan.envVar} is not set.`;
104
116
  }
105
117
  function printSnapshotApiStatus() {
106
118
  const status = resolveAiSetupStatus();
107
- const envPath = join(REPO_ROOT, ".env");
108
- console.log("\nSnapshot analysis:");
109
119
  console.log(
110
- " Libretto uses direct API calls for snapshot analysis when supported credentials are available."
120
+ "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
111
121
  );
112
- console.log(` Credentials are loaded from process env and ${envPath}.`);
113
122
  if (status.kind === "ready") {
114
- const pinned = ensurePinnedDefaultModel(status);
115
- printHealthySummary(pinned);
123
+ console.log();
124
+ printHealthySummary(status);
125
+ ensurePinnedDefaultModel(status);
116
126
  return true;
117
127
  }
118
128
  const plan = buildRepairPlan(status);
119
129
  if (plan.kind === "repair-missing-credentials") {
130
+ console.log();
120
131
  console.log(formatMissingCredentialsMessage(plan));
121
132
  console.log(
122
- ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`
133
+ ` To fix: add ${plan.envVar} to .env, or run \`npx libretto setup\` interactively to repair.`
123
134
  );
124
135
  return false;
125
136
  }
126
137
  if (plan.kind === "repair-invalid-config") {
127
138
  printInvalidAiConfigWarning(status);
128
- console.log(" Run `npx libretto setup` interactively to reconfigure.");
139
+ console.log(" Run `npx libretto setup` interactively to reconfigure.");
129
140
  return false;
130
141
  }
131
- console.log(" \u2717 No snapshot API credentials detected.");
132
- console.log(" Add one provider to .env:");
133
- console.log(" OPENAI_API_KEY=...");
134
- console.log(" ANTHROPIC_API_KEY=...");
135
- console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
142
+ console.log();
143
+ console.log("\u2717 No snapshot API credentials detected.");
144
+ console.log(" Add one provider to .env:");
145
+ console.log(" OPENAI_API_KEY=...");
146
+ console.log(" ANTHROPIC_API_KEY=...");
147
+ console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
136
148
  console.log(
137
- " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
149
+ " GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
138
150
  );
139
151
  console.log(
140
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
152
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
141
153
  );
142
154
  console.log(
143
- " Run `npx libretto setup` interactively to set up credentials."
155
+ " Run `npx libretto setup` interactively to set up credentials."
144
156
  );
145
157
  return false;
146
158
  }
@@ -157,42 +169,45 @@ function writeEnvVar(envVar, value, envPath) {
157
169
  );
158
170
  writeFileSync(envPath, updated);
159
171
  console.log(`
160
- \u2713 Updated ${envVar} in ${envPath}`);
172
+ \u2713 Updated ${envVar} in ${envPath}`);
161
173
  } else {
162
174
  const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
163
175
  appendFileSync(envPath, `${separator}${envLine}
164
176
  `);
165
177
  console.log(`
166
- \u2713 Added ${envVar} to ${envPath}`);
178
+ \u2713 Added ${envVar} to ${envPath}`);
167
179
  }
168
180
  process.env[envVar] = value;
169
181
  }
170
182
  async function promptForCredential(rl, choice, envPath, modelOverride) {
171
183
  console.log(`
172
- ${choice.label} selected.`);
173
- console.log(` ${choice.envHint}
184
+ ${choice.label} selected.`);
185
+ console.log(`${choice.envHint}
174
186
  `);
175
- const apiKeyValue = await promptUser(rl, ` Enter your ${choice.envVar}: `);
187
+ const apiKeyValue = await promptUser(rl, `Enter your ${choice.envVar}: `);
176
188
  if (!apiKeyValue) {
177
- console.log("\n No value entered. Skipping API key setup.");
189
+ console.log("\nNo value entered. Skipping API key setup.");
178
190
  return false;
179
191
  }
180
192
  writeEnvVar(choice.envVar, apiKeyValue, envPath);
181
193
  loadSnapshotEnv();
182
194
  const model = modelOverride ?? DEFAULT_SNAPSHOT_MODELS[choice.provider];
183
195
  writeAiConfig(model);
184
- console.log(` \u2713 Snapshot API ready: ${model}`);
196
+ console.log(`\u2713 Snapshot API ready: ${model}`);
197
+ console.log(
198
+ "To change: npx libretto ai configure openai | anthropic | gemini | vertex"
199
+ );
185
200
  return true;
186
201
  }
187
202
  async function promptProviderSelection(rl, envPath) {
188
203
  console.log(
189
- " Which API provider would you like to use for snapshot analysis?\n"
204
+ "Which model provider would you like to use for snapshot analysis?\n"
190
205
  );
191
206
  for (const choice of PROVIDER_CHOICES) {
192
- console.log(` ${choice.key}) ${choice.label}`);
207
+ console.log(` ${choice.key}) ${choice.label}`);
193
208
  }
194
- console.log(" s) Skip for now\n");
195
- const answer = await promptUser(rl, " Choice: ");
209
+ console.log(" s) Skip for now\n");
210
+ const answer = await promptUser(rl, "Choice: ");
196
211
  if (answer.toLowerCase() === "s" || !answer) {
197
212
  printSkipMessage();
198
213
  return false;
@@ -200,32 +215,33 @@ async function promptProviderSelection(rl, envPath) {
200
215
  const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
201
216
  if (!selected) {
202
217
  console.log(`
203
- Unknown choice "${answer}". Skipping API setup.`);
218
+ Unknown choice "${answer}". Skipping API setup.`);
204
219
  return false;
205
220
  }
206
221
  return promptForCredential(rl, selected, envPath);
207
222
  }
208
223
  function printSkipMessage() {
209
224
  console.log(
210
- "\n Skipped. You can set up API credentials later by rerunning `npx libretto setup`."
225
+ "\nSkipped. You can set up API credentials later by rerunning `npx libretto setup`."
211
226
  );
212
- console.log(" Or add credentials directly to your .env file:");
213
- console.log(" OPENAI_API_KEY=...");
214
- console.log(" ANTHROPIC_API_KEY=...");
215
- console.log(" GEMINI_API_KEY=...");
227
+ console.log("Or add credentials directly to your .env file:");
228
+ console.log(" OPENAI_API_KEY=...");
229
+ console.log(" ANTHROPIC_API_KEY=...");
230
+ console.log(" GEMINI_API_KEY=...");
216
231
  console.log(
217
- " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
232
+ " Or run `npx libretto ai configure openai | anthropic | gemini | vertex` to set a specific model."
218
233
  );
219
234
  }
220
235
  async function runInteractiveApiSetup() {
221
236
  const status = resolveAiSetupStatus();
222
237
  const envPath = join(REPO_ROOT, ".env");
223
- console.log("\nSnapshot analysis setup:");
224
- console.log(" Libretto uses direct API calls for snapshot analysis.");
225
- console.log(` Credentials are loaded from process env and ${envPath}.`);
238
+ console.log(
239
+ "\nLibretto uses a sub-agent to analyze DOM snapshots. The model is determined by environment variables."
240
+ );
226
241
  if (status.kind === "ready") {
227
- const pinned = ensurePinnedDefaultModel(status);
228
- printHealthySummary(pinned);
242
+ console.log();
243
+ printHealthySummary(status);
244
+ ensurePinnedDefaultModel(status);
229
245
  return;
230
246
  }
231
247
  const plan = buildRepairPlan(status);
@@ -237,11 +253,11 @@ async function runInteractiveApiSetup() {
237
253
  if (plan.kind === "repair-missing-credentials") {
238
254
  console.log(formatMissingCredentialsMessage(plan));
239
255
  console.log("");
240
- console.log(" How would you like to fix this?\n");
241
- console.log(` 1) Enter ${plan.envVar}`);
242
- console.log(" 2) Switch to a different provider");
243
- console.log(" s) Skip for now\n");
244
- const answer = await promptUser(rl, " Choice: ");
256
+ console.log("How would you like to fix this?\n");
257
+ console.log(` 1) Enter ${plan.envVar}`);
258
+ console.log(" 2) Switch to a different provider");
259
+ console.log(" s) Skip for now\n");
260
+ const answer = await promptUser(rl, "Choice: ");
245
261
  if (answer === "1") {
246
262
  const matchingChoice = PROVIDER_CHOICES.find(
247
263
  (c) => c.provider === plan.provider
@@ -261,28 +277,28 @@ async function runInteractiveApiSetup() {
261
277
  if (plan.kind === "repair-invalid-config") {
262
278
  printInvalidAiConfigWarning(status);
263
279
  console.log(
264
- "\n Would you like to reconfigure with a fresh provider selection?\n"
280
+ "\nWould you like to reconfigure with a fresh provider selection?\n"
265
281
  );
266
282
  await promptProviderSelection(rl, envPath);
267
283
  return;
268
284
  }
269
- console.log(" \u2717 No snapshot API credentials detected.\n");
285
+ console.log("\u2717 No snapshot API credentials detected.\n");
270
286
  await promptProviderSelection(rl, envPath);
271
287
  } finally {
272
288
  rl.close();
273
289
  }
274
290
  }
275
291
  function installBrowsers() {
276
- console.log("\nInstalling Playwright Chromium...");
292
+ console.log("Installing Playwright Chromium...");
277
293
  const result = spawnSync("npx", ["playwright", "install", "chromium"], {
278
294
  stdio: "inherit",
279
295
  shell: true
280
296
  });
281
297
  if (result.status === 0) {
282
- console.log(" \u2713 Playwright Chromium installed");
298
+ console.log("\u2713 Playwright Chromium installed");
283
299
  } else {
284
300
  console.error(
285
- " \u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
301
+ "\u2717 Failed to install Playwright Chromium. Run manually: npx playwright install chromium"
286
302
  );
287
303
  }
288
304
  }
@@ -306,16 +322,13 @@ function detectAgentDirs(root) {
306
322
  function copySkills() {
307
323
  const agentDirs = detectAgentDirs(REPO_ROOT);
308
324
  if (agentDirs.length === 0) {
309
- console.log(
310
- "\nSkills: No .agents/ or .claude/ directory found in repo root \u2014 skipping."
311
- );
312
325
  return;
313
326
  }
314
327
  let skillsRoot;
315
328
  try {
316
329
  skillsRoot = getPackageSkillsRoot();
317
330
  } catch (e) {
318
- console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
331
+ console.error(`\u2717 ${e instanceof Error ? e.message : String(e)}`);
319
332
  return;
320
333
  }
321
334
  const skillNames = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -330,7 +343,7 @@ function copySkills() {
330
343
  cpSync(sourceDir, skillDest, { recursive: true });
331
344
  const fileCount = readdirSync(skillDest).length;
332
345
  console.log(
333
- ` \u2713 Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`
346
+ `\u2713 Copied ${fileCount} skill files to ${agentName}/skills/${skillName}/`
334
347
  );
335
348
  }
336
349
  }
@@ -347,12 +360,11 @@ const setupInput = SimpleCLI.input({
347
360
  const setupCommand = SimpleCLI.command({
348
361
  description: "Set up libretto in the current project"
349
362
  }).input(setupInput).handle(async ({ input }) => {
350
- console.log("Setting up libretto...\n");
351
363
  ensureLibrettoSetup();
352
364
  if (!input.skipBrowsers) {
353
365
  installBrowsers();
354
366
  } else {
355
- console.log("\nSkipping browser installation (--skip-browsers)");
367
+ console.log("Skipping browser installation (--skip-browsers)");
356
368
  }
357
369
  copySkills();
358
370
  if (process.stdin.isTTY) {
@@ -365,6 +377,8 @@ const setupCommand = SimpleCLI.command({
365
377
  );
366
378
  }
367
379
  }
380
+ console.log(`
381
+ Config set up at ${LIBRETTO_CONFIG_PATH}`);
368
382
  console.log("\n\u2713 libretto setup complete");
369
383
  });
370
384
  export {
@@ -45,7 +45,7 @@ function printOpenSessions(sessions) {
45
45
  }
46
46
  for (const session of sessions) {
47
47
  const statusLabel = session.status ? ` [${session.status}]` : "";
48
- const endpoint = `http://127.0.0.1:${session.port}`;
48
+ const endpoint = session.provider ? `${session.provider.name} (${session.cdpEndpoint})` : `http://127.0.0.1:${session.port}`;
49
49
  console.log(` ${session.session}${statusLabel} \u2014 ${endpoint}`);
50
50
  }
51
51
  }