libretto 0.6.28 → 0.6.29

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.
@@ -6,6 +6,7 @@ import {
6
6
  connect,
7
7
  disconnectBrowser,
8
8
  runClose,
9
+ resolveWindowPosition,
9
10
  resolveViewport
10
11
  } from "../core/browser.js";
11
12
  import { parseViewportArg } from "./browser.js";
@@ -20,7 +21,9 @@ import {
20
21
  writeSessionState
21
22
  } from "../core/session.js";
22
23
  import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
23
- import { readLibrettoConfig } from "../core/config.js";
24
+ import {
25
+ readLibrettoConfig
26
+ } from "../core/config.js";
24
27
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
25
28
  import {
26
29
  getProviderStartupTimeoutMs,
@@ -51,6 +54,20 @@ import {
51
54
  withRequiredSession
52
55
  } from "./shared.js";
53
56
  const require2 = moduleBuiltin.createRequire(import.meta.url);
57
+ function createRunBrowserConfig(args) {
58
+ if (args.providerName) {
59
+ return {
60
+ kind: "provider",
61
+ providerName: args.providerName
62
+ };
63
+ }
64
+ return {
65
+ kind: "launch",
66
+ headed: !args.headless,
67
+ viewport: args.viewport ?? { width: 1366, height: 768 },
68
+ ...!args.headless && args.windowPosition ? { windowPosition: args.windowPosition } : {}
69
+ };
70
+ }
54
71
  function writeDaemonExecOutput(output) {
55
72
  if (output?.stdout) {
56
73
  process.stdout.write(output.stdout);
@@ -427,14 +444,7 @@ async function runIntegrationFromFile(args, logger) {
427
444
  config: {
428
445
  session: args.session,
429
446
  experiments: args.experiments,
430
- browser: args.providerName ? {
431
- kind: "provider",
432
- providerName: args.providerName
433
- } : {
434
- kind: "launch",
435
- headed: !args.headless,
436
- viewport: args.viewport ?? { width: 1366, height: 768 }
437
- },
447
+ browser: createRunBrowserConfig(args),
438
448
  workflow: {
439
449
  integrationPath: absoluteIntegrationPath,
440
450
  params: args.params,
@@ -690,15 +700,18 @@ const runCommand = SimpleCLI.command({
690
700
  });
691
701
  console.log(`Connecting to ${providerName} browser...`);
692
702
  }
703
+ const headless = daemonProviderName ? true : headlessMode ?? false;
704
+ const windowPosition = headless ? void 0 : resolveWindowPosition(ctx.logger);
693
705
  await runIntegrationFromFile(
694
706
  {
695
707
  integrationPath: input.integrationFile,
696
708
  session: ctx.session,
697
709
  params,
698
710
  tsconfigPath: input.tsconfig,
699
- headless: daemonProviderName ? true : headlessMode ?? false,
711
+ headless,
700
712
  visualize,
701
713
  viewport,
714
+ windowPosition,
702
715
  accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : readLibrettoConfig().sessionMode ?? "write-access",
703
716
  providerName: daemonProviderName,
704
717
  stayOpenOnSuccess: input.stayOpenOnSuccess,
@@ -725,6 +738,7 @@ const executionCommands = {
725
738
  resume: resumeCommand
726
739
  };
727
740
  export {
741
+ createRunBrowserConfig,
728
742
  execCommand,
729
743
  execInput,
730
744
  executionCommands,
@@ -1027,6 +1027,7 @@ export {
1027
1027
  normalizeUrl,
1028
1028
  resolvePath,
1029
1029
  resolveViewport,
1030
+ resolveWindowPosition,
1030
1031
  runClose,
1031
1032
  runCloseAll,
1032
1033
  runConnect,
@@ -49,6 +49,7 @@ import {
49
49
  import { WorkflowController } from "../workflow-runner/runner.js";
50
50
  import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
51
51
  import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
52
+ import { applyWindowPosition } from "../../../shared/run/window-position.js";
52
53
  function isOperationalPage(page) {
53
54
  const url = page.url();
54
55
  return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
@@ -266,6 +267,7 @@ class BrowserDaemon {
266
267
  userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
267
268
  });
268
269
  const page = await context.newPage();
270
+ await applyWindowPosition(browser, context, page, config.windowPosition);
269
271
  page.setDefaultTimeout(3e4);
270
272
  page.setDefaultNavigationTimeout(45e3);
271
273
  const daemon = await BrowserDaemon.initialize({
@@ -9,6 +9,9 @@ import {
9
9
  SessionStateFileSchema
10
10
  } from "../state/session-state.js";
11
11
  import { readLibrettoConfig } from "../../cli/core/config.js";
12
+ import {
13
+ applyWindowPosition
14
+ } from "./window-position.js";
12
15
  async function pickFreePort() {
13
16
  return await new Promise((resolve, reject) => {
14
17
  const server = createServer();
@@ -27,38 +30,6 @@ async function pickFreePort() {
27
30
  function resolveWindowPosition() {
28
31
  return readLibrettoConfig().windowPosition;
29
32
  }
30
- async function applyWindowPosition(browser, context, page, windowPosition) {
31
- if (!windowPosition) {
32
- return;
33
- }
34
- const requestedBounds = {
35
- left: windowPosition.x,
36
- top: windowPosition.y,
37
- windowState: "normal"
38
- };
39
- const pageCdp = await context.newCDPSession(page);
40
- let browserCdp;
41
- try {
42
- const targetInfo = await pageCdp.send("Target.getTargetInfo");
43
- const targetId = targetInfo.targetInfo?.targetId;
44
- browserCdp = await browser.newBrowserCDPSession();
45
- const windowResult = await browserCdp.send(
46
- "Browser.getWindowForTarget",
47
- targetId ? { targetId } : {}
48
- );
49
- await browserCdp.send("Browser.setWindowBounds", {
50
- windowId: windowResult.windowId,
51
- bounds: requestedBounds
52
- });
53
- await new Promise((resolve) => setTimeout(resolve, 250));
54
- } catch {
55
- } finally {
56
- await pageCdp.detach().catch(() => {
57
- });
58
- await browserCdp?.detach().catch(() => {
59
- });
60
- }
61
- }
62
33
  async function launchBrowser({
63
34
  sessionName,
64
35
  headless = false,
@@ -0,0 +1,9 @@
1
+ import { Browser, BrowserContext, Page } from 'playwright';
2
+
3
+ type WindowPosition = {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ declare function applyWindowPosition(browser: Browser, context: BrowserContext, page: Page, windowPosition: WindowPosition | undefined): Promise<void>;
8
+
9
+ export { type WindowPosition, applyWindowPosition };
@@ -0,0 +1,36 @@
1
+ async function applyWindowPosition(browser, context, page, windowPosition) {
2
+ if (!windowPosition) {
3
+ return;
4
+ }
5
+ const requestedBounds = {
6
+ left: windowPosition.x,
7
+ top: windowPosition.y,
8
+ windowState: "normal"
9
+ };
10
+ let pageCdp;
11
+ let browserCdp;
12
+ try {
13
+ pageCdp = await context.newCDPSession(page);
14
+ const targetInfo = await pageCdp.send("Target.getTargetInfo");
15
+ const targetId = targetInfo.targetInfo?.targetId;
16
+ browserCdp = await browser.newBrowserCDPSession();
17
+ const windowResult = await browserCdp.send(
18
+ "Browser.getWindowForTarget",
19
+ targetId ? { targetId } : {}
20
+ );
21
+ await browserCdp.send("Browser.setWindowBounds", {
22
+ windowId: windowResult.windowId,
23
+ bounds: requestedBounds
24
+ });
25
+ await new Promise((resolve) => setTimeout(resolve, 250));
26
+ } catch {
27
+ } finally {
28
+ await pageCdp?.detach().catch(() => {
29
+ });
30
+ await browserCdp?.detach().catch(() => {
31
+ });
32
+ }
33
+ }
34
+ export {
35
+ applyWindowPosition
36
+ };
@@ -5,8 +5,8 @@ import '../../runtime/recovery/page-fallbacks.js';
5
5
  import 'ai';
6
6
 
7
7
  type LibrettoAuthenticateOptions = {
8
- validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
9
- fallback: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
8
+ isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
9
+ signIn: (ctx: LibrettoWorkflowContext, credentials: Record<string, string>) => Promise<void> | void;
10
10
  credentials?: Record<string, unknown>;
11
11
  envPrefix?: string;
12
12
  };
@@ -1,13 +1,13 @@
1
1
  async function librettoAuthenticate(ctx, options) {
2
- if (await options.validate(ctx)) {
2
+ if (await options.isSignedIn(ctx)) {
3
3
  return { usedProfile: true };
4
4
  }
5
5
  const credentials = normalizeCredentials(
6
6
  options.credentials ?? readCredentialsFromEnv(options.envPrefix)
7
7
  );
8
- await options.fallback(ctx, credentials);
9
- if (!await options.validate(ctx)) {
10
- throw new Error("Authentication fallback completed, but validation still failed.");
8
+ await options.signIn(ctx, credentials);
9
+ if (!await options.isSignedIn(ctx)) {
10
+ throw new Error("Sign-in completed, but the session is still not signed in.");
11
11
  }
12
12
  return { usedProfile: false };
13
13
  }
package/docs/releasing.md CHANGED
@@ -66,7 +66,7 @@ The root `scripts/prepare-release.sh` script does the following:
66
66
  1. Checks that the working tree is clean.
67
67
  2. Updates local `main` from `origin/main`.
68
68
  3. Runs `pnpm install --frozen-lockfile`, `pnpm --filter libretto type-check`, and `pnpm --filter libretto test`.
69
- 4. Checks whether `packages/affordance` changed since the current Libretto release tag. If it changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
69
+ 4. Checks whether `packages/affordance` changed since the current Libretto release tag, excluding `packages/affordance/package.json` version-only changes. If source/package contents changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
70
70
  5. Bumps the version in `packages/libretto/package.json`.
71
71
  6. Creates a release branch.
72
72
  7. Commits the version bump.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.28"
7
+ version: "0.6.29"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -51,7 +51,11 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
51
51
  - Do not treat visibility as interactivity. If an element will not act, inspect blockers before retrying.
52
52
  - Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
53
53
  - Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
54
- - Validation requires a successful clean `run` with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
54
+ - For authenticated workflows, manual login is discovery only. After the user logs in, read only the sign-in action logs and identify the required credentials; if credentials are unclear, ask before writing code.
55
+ - Add missing blank `LIBRETTO_CLOUD_<secret_name>=` entries without overwriting populated values. If any required credential is blank, stop and ask the user to fill it; until then, do not inspect logged-in pages, read authenticated network bodies, write workflow code, open validation sessions, or continue discovery.
56
+ - Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
57
+ - Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
58
+ - Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
55
59
  - After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
56
60
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
57
61
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
@@ -232,13 +236,15 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
232
236
  [Context: The user wants to build a new browser workflow and does not yet know the page structure]
233
237
  Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
234
238
  Assistant: [Runs `npx libretto open https://target.example.com --headed`]
239
+ Assistant: [If sign-in is required, follows the authenticated workflow rules before continuing]
235
240
  Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
236
241
  Assistant: [Runs `npx libretto snapshot --session <session>`]
237
242
  Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
238
243
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
239
244
  Assistant: I found the working path. I'll now create the workflow file and verify it.
240
245
  Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
241
- Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
246
+ Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` on a fresh, unauthenticated session to verify both sign-in and workflow behavior]
247
+ Assistant: [If sign-in validation passes and a reusable session is useful, adds an auth profile and reruns validation]
242
248
  Assistant: Validation passed. Here are the results:
243
249
  [Shows the output/results from the validation run]
244
250
  To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
@@ -270,7 +276,7 @@ To run it again, use: npx libretto run ./integration.ts
270
276
  - Read `references/configuration-file-reference.md` when you need to inspect or change `.libretto/config.json` for viewport or session defaults.
271
277
  - Read `references/site-security-review.md` before reviewing the site's security posture and deciding whether to lead with network requests, passive interception, or Playwright DOM automation on a new site.
272
278
  - Read `references/code-generation-rules.md` before writing or editing production workflow files.
273
- - Read `references/auth-profiles.md` when auth-profile behavior is relevant.
279
+ - Read `references/website-authentication.md` when website sign-in implementation or auth-profile behavior is relevant.
274
280
  - Read `references/pages-and-page-targeting.md` when a session has multiple open pages or you need `--page`.
275
281
  - Read `references/action-logs.md` for full action log field descriptions and user-vs-agent event semantics.
276
282
  - If the workflow code is deployed to the Libretto Cloud platform and you need to reference its API docs, fetch [https://libretto.sh/docs/llms.txt](https://libretto.sh/docs/llms.txt) and follow the relevant page links.
@@ -49,7 +49,7 @@ Key points:
49
49
  - After validation is complete and the workflow is confirmed working end to end, remove all `pause()` calls and pause-only workflow params unless the user explicitly says to keep them.
50
50
  - The browser is launched and closed automatically by the CLI. Do not launch or close it in the handler.
51
51
 
52
- ## Workflow Credentials And Auth Profiles
52
+ ## Workflow Credentials And Authentication
53
53
 
54
54
  Declare `credentials` for runtime secrets instead of putting them in the Zod input schema or `--params`. Only declared names are injected into `input.credentials`; local runs read matching `LIBRETTO_CLOUD_` variables from `.env`, and hosted runs read matching Libretto Cloud credentials. For example, `LIBRETTO_CLOUD_OPENAI_API_KEY` becomes `input.credentials.openai_api_key`.
55
55
 
@@ -70,14 +70,7 @@ export default workflow("sentimentWorkflow", {
70
70
  });
71
71
  ```
72
72
 
73
- For logged-in website workflows, unless the user says otherwise, generate both:
74
-
75
- - `authProfile: { name, refresh: true }`
76
- - `librettoAuthenticate(...)` fallback login logic using declared credentials such as `credentials: ["username", "password"]`
77
-
78
- After generating or validating a credentialed workflow, include the local `.env` variable names the user must set, and never print secret values.
79
-
80
- The fallback should validate the signed-in state, log in when validation fails, and validate again before continuing. Follow `references/auth-profiles.md` for profile naming, refresh behavior, hosted profile behavior, and the fallback-login pattern.
73
+ For website workflows that require signing in, build working sign-in logic with `librettoAuthenticate` and verify it from a clean signed-out browser before adding an auth profile. Follow `references/website-authentication.md`.
81
74
 
82
75
  ## Workflow Error Handling
83
76
 
@@ -43,7 +43,7 @@ General guidance: determine whether the site has bot protection and roughly how
43
43
 
44
44
  ### Probe 2: Fetch and XHR Interception
45
45
 
46
- Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. If it has, making your own fetch calls from `page.evaluate()` is risky because the site can inspect call stacks and detect calls that do not originate from its own code.
46
+ Check whether the site has monkey-patched `window.fetch` or `XMLHttpRequest`. Patching is a caution signal, not an automatic blocker. Browser-context network requests are usually fine when the target endpoint is already called by the site with fetch/XHR and there is no strong bot-protection evidence.
47
47
 
48
48
  ```js
49
49
  window.fetch.toString()
@@ -52,7 +52,7 @@ Object.getOwnPropertyDescriptor(window, 'fetch')
52
52
  window.fetch.hasOwnProperty('prototype')
53
53
  ```
54
54
 
55
- Important: some sites use `Proxy` to wrap fetch, which makes `toString()` still return `"[native code]"`. The prototype check is a heuristic, not definitive. If you see any sign of fetch interception, treat it as patched.
55
+ Important: ordinary app instrumentation and Libretto's own page-stability tracking can also wrap fetch/XHR. Treat browser fetch as risky only when the wrapper appears security-related, obfuscated, tied to bot-protection scripts, or likely to inspect call stacks. Some `Proxy` wrappers still stringify as `"[native code]"`, so these checks are only heuristics.
56
56
 
57
57
  ## Choosing a Data Capture Strategy
58
58
 
@@ -66,7 +66,7 @@ When to prioritize this:
66
66
 
67
67
  - The target endpoint is normally called by the site with fetch/XHR
68
68
  - No enterprise bot protection is detected
69
- - `fetch` is not monkey-patched
69
+ - No security-related fetch/XHR interception is detected, or the observed wrapper appears to be ordinary app instrumentation
70
70
  - The API responses are parseable and useful
71
71
  - You need data that requires many API calls (deep pagination, bulk queries) where driving the UI would be slow
72
72
 
@@ -83,7 +83,7 @@ Listen to network responses that the browser naturally makes as you navigate. Yo
83
83
  When to prioritize this:
84
84
 
85
85
  - Enterprise bot protection is detected
86
- - `fetch` is monkey-patched
86
+ - Security-related fetch/XHR interception is detected
87
87
  - The site's normal UI flow triggers API calls that return the data you need
88
88
  - You want to minimize detection risk as much as possible
89
89
 
@@ -112,8 +112,9 @@ Trade-off: it is slower, more fragile against DOM changes, and you only get data
112
112
 
113
113
  | Site Profile | Primary Strategy | Supplement With |
114
114
  | --- | --- | --- |
115
- | No bot protection, fetch/XHR endpoint, fetch not patched | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
116
- | No bot protection, fetch is patched or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
115
+ | No bot protection, fetch/XHR endpoint, no security-related interception | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
116
+ | No bot protection, harmless app instrumentation, fetch/XHR endpoint | A (`page.evaluate(fetch)`) | B (`page.on('response', ...)`) if requests fail |
117
+ | No bot protection, security-related interception or endpoint is not fetch/XHR | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
117
118
  | Bot protection detected | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
118
119
  | Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
119
120
 
@@ -0,0 +1,73 @@
1
+ # Website Authentication
2
+
3
+ Use this reference when a workflow needs a logged-in website session. The Working Rules in `../SKILL.md` define the required auth workflow; this file explains how to implement sign-in logic with `librettoAuthenticate`, and how auth profiles save signed-in state for later runs.
4
+
5
+ Build and verify working sign-in logic first. The sign-in code takes priority; an auth profile is added only after the sign-in logic is verified, never as a substitute for it.
6
+
7
+ ## Sign-In Logic
8
+
9
+ Use `librettoAuthenticate` so the workflow can sign in from a fresh browser. Declare each required secret in the workflow credentials array and use those credentials inside `signIn`.
10
+
11
+ ```typescript
12
+ import { librettoAuthenticate, workflow } from "libretto";
13
+
14
+ export default workflow("accountWorkflow", {
15
+ credentials: ["portal_username", "portal_password"],
16
+ async handler(ctx, input) {
17
+ const { page } = ctx;
18
+
19
+ await page.goto("https://app.example.com/dashboard");
20
+
21
+ // Sign in when the session is not already authenticated.
22
+ await librettoAuthenticate(ctx, {
23
+ credentials: input.credentials,
24
+ isSignedIn: async () =>
25
+ await page
26
+ .getByRole("heading", { name: "Dashboard" })
27
+ .isVisible()
28
+ .catch(() => false),
29
+ signIn: async (_ctx, credentials) => {
30
+ await page.goto("https://app.example.com/login");
31
+ await page.getByLabel("Email").fill(credentials.portal_username);
32
+ await page.getByLabel("Password").fill(credentials.portal_password);
33
+ await page.getByRole("button", { name: "Sign in" }).click();
34
+ await page.getByRole("heading", { name: "Dashboard" }).waitFor();
35
+ },
36
+ });
37
+
38
+ // Continue with the signed-in workflow steps.
39
+ },
40
+ });
41
+ ```
42
+
43
+ ## Auth Profiles
44
+
45
+ Auth profiles save the signed-in browser state (cookies, localStorage, IndexedDB) so later runs can reuse a logged-in session instead of signing in from scratch. The sign-in logic still takes priority: do not add a profile until the `librettoAuthenticate` sign-in step has been verified from a signed-out browser with no profile present. If you add a profile first, validation passes on the saved session while the untested sign-in logic fails the first time that session expires.
46
+
47
+ A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one. Local runs load `.libretto/profiles/<name>.json`; hosted runs use the provider-native profile with the same name.
48
+
49
+ Add the profile to the workflow you already verified:
50
+
51
+ ```typescript
52
+ export default workflow("accountWorkflow", {
53
+ // Added only after the signIn step above is verified standalone.
54
+ authProfile: { name: "example-account", refresh: true },
55
+ credentials: ["portal_username", "portal_password"],
56
+ // ...same handler and librettoAuthenticate call as above.
57
+ });
58
+ ```
59
+
60
+ ## Commands
61
+
62
+ ```bash
63
+ # Save the current signed-in session as a named, site-scoped profile.
64
+ npx libretto save example-app --session login --sites app.example.com,auth.example.com
65
+
66
+ # List or delete hosted auth profile names.
67
+ npx libretto cloud profiles list
68
+ npx libretto cloud profiles delete example-app
69
+ ```
70
+
71
+ `save` captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
72
+
73
+ To reuse an existing signed-in Chrome profile instead of signing in, use `npx libretto import-chrome-profiles`. Get the user's consent first, since attaching can close or relaunch their Chrome window.
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.28"
7
+ version: "0.6.29"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
@@ -7,6 +7,7 @@ import {
7
7
  connect,
8
8
  disconnectBrowser,
9
9
  runClose,
10
+ resolveWindowPosition,
10
11
  resolveViewport,
11
12
  } from "../core/browser.js";
12
13
  import { parseViewportArg } from "./browser.js";
@@ -22,7 +23,10 @@ import {
22
23
  type SessionState,
23
24
  } from "../core/session.js";
24
25
  import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
25
- import { readLibrettoConfig } from "../core/config.js";
26
+ import {
27
+ readLibrettoConfig,
28
+ type WindowPositionConfig,
29
+ } from "../core/config.js";
26
30
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
27
31
  import {
28
32
  getProviderStartupTimeoutMs,
@@ -41,6 +45,7 @@ import {
41
45
  type DaemonExecResult,
42
46
  type DaemonToCliApi,
43
47
  } from "../core/daemon/ipc.js";
48
+ import type { DaemonConfig } from "../core/daemon/config.js";
44
49
  import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
45
50
  import {
46
51
  readActionLog,
@@ -66,6 +71,7 @@ type RunIntegrationCommandRequest = {
66
71
  headless: boolean;
67
72
  visualize: boolean;
68
73
  viewport?: { width: number; height: number };
74
+ windowPosition?: WindowPositionConfig;
69
75
  accessMode: SessionAccessMode;
70
76
  providerName?: string;
71
77
  stayOpenOnSuccess: boolean;
@@ -76,6 +82,29 @@ type ExecMode = "exec" | "readonly-exec";
76
82
 
77
83
  const require = moduleBuiltin.createRequire(import.meta.url);
78
84
 
85
+ export function createRunBrowserConfig(args: {
86
+ providerName?: string;
87
+ headless: boolean;
88
+ viewport?: { width: number; height: number };
89
+ windowPosition?: WindowPositionConfig;
90
+ }): DaemonConfig["browser"] {
91
+ if (args.providerName) {
92
+ return {
93
+ kind: "provider",
94
+ providerName: args.providerName,
95
+ };
96
+ }
97
+
98
+ return {
99
+ kind: "launch",
100
+ headed: !args.headless,
101
+ viewport: args.viewport ?? { width: 1366, height: 768 },
102
+ ...(!args.headless && args.windowPosition
103
+ ? { windowPosition: args.windowPosition }
104
+ : {}),
105
+ };
106
+ }
107
+
79
108
  function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
80
109
  if (output?.stdout) {
81
110
  process.stdout.write(output.stdout);
@@ -575,16 +604,7 @@ async function runIntegrationFromFile(
575
604
  config: {
576
605
  session: args.session,
577
606
  experiments: args.experiments,
578
- browser: args.providerName
579
- ? {
580
- kind: "provider",
581
- providerName: args.providerName,
582
- }
583
- : {
584
- kind: "launch",
585
- headed: !args.headless,
586
- viewport: args.viewport ?? { width: 1366, height: 768 },
587
- },
607
+ browser: createRunBrowserConfig(args),
588
608
  workflow: {
589
609
  integrationPath: absoluteIntegrationPath,
590
610
  params: args.params,
@@ -875,15 +895,21 @@ export const runCommand = SimpleCLI.command({
875
895
  console.log(`Connecting to ${providerName} browser...`);
876
896
  }
877
897
 
898
+ const headless = daemonProviderName ? true : (headlessMode ?? false);
899
+ const windowPosition = headless
900
+ ? undefined
901
+ : resolveWindowPosition(ctx.logger);
902
+
878
903
  await runIntegrationFromFile(
879
904
  {
880
905
  integrationPath: input.integrationFile!,
881
906
  session: ctx.session,
882
907
  params,
883
908
  tsconfigPath: input.tsconfig,
884
- headless: daemonProviderName ? true : (headlessMode ?? false),
909
+ headless,
885
910
  visualize,
886
911
  viewport,
912
+ windowPosition,
887
913
  accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : (readLibrettoConfig().sessionMode ?? "write-access"),
888
914
  providerName: daemonProviderName,
889
915
  stayOpenOnSuccess: input.stayOpenOnSuccess,
@@ -388,7 +388,7 @@ export function resolveViewport(
388
388
  return DEFAULT_VIEWPORT;
389
389
  }
390
390
 
391
- function resolveWindowPosition(
391
+ export function resolveWindowPosition(
392
392
  logger: LoggerApi,
393
393
  ): { x: number; y: number } | undefined {
394
394
  const config = readLibrettoConfig();
@@ -91,6 +91,7 @@ import {
91
91
  import { WorkflowController } from "../workflow-runner/runner.js";
92
92
  import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
93
93
  import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
94
+ import { applyWindowPosition } from "../../../shared/run/window-position.js";
94
95
 
95
96
  function isOperationalPage(page: Page): boolean {
96
97
  const url = page.url();
@@ -397,6 +398,7 @@ class BrowserDaemon {
397
398
  });
398
399
 
399
400
  const page = await context.newPage();
401
+ await applyWindowPosition(browser, context, page, config.windowPosition);
400
402
  page.setDefaultTimeout(30000);
401
403
  page.setDefaultNavigationTimeout(45000);
402
404
 
@@ -13,6 +13,10 @@ import {
13
13
  SessionStateFileSchema,
14
14
  } from "../state/session-state.js";
15
15
  import { readLibrettoConfig } from "../../cli/core/config.js";
16
+ import {
17
+ applyWindowPosition,
18
+ type WindowPosition,
19
+ } from "./window-position.js";
16
20
 
17
21
  async function pickFreePort(): Promise<number> {
18
22
  return await new Promise((resolve, reject) => {
@@ -49,53 +53,10 @@ export type BrowserSession = {
49
53
  close: () => Promise<void>;
50
54
  };
51
55
 
52
- function resolveWindowPosition(): { x: number; y: number } | undefined {
56
+ function resolveWindowPosition(): WindowPosition | undefined {
53
57
  return readLibrettoConfig().windowPosition;
54
58
  }
55
59
 
56
- async function applyWindowPosition(
57
- browser: Browser,
58
- context: BrowserContext,
59
- page: Page,
60
- windowPosition: { x: number; y: number } | undefined,
61
- ): Promise<void> {
62
- if (!windowPosition) {
63
- return;
64
- }
65
-
66
- const requestedBounds = {
67
- left: windowPosition.x,
68
- top: windowPosition.y,
69
- windowState: "normal" as const,
70
- };
71
-
72
- const pageCdp = await context.newCDPSession(page);
73
- let browserCdp:
74
- | Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
75
- | undefined;
76
- try {
77
- const targetInfo = await pageCdp.send("Target.getTargetInfo");
78
- const targetId = (
79
- targetInfo as { targetInfo?: { targetId?: string } }
80
- ).targetInfo?.targetId;
81
- browserCdp = await browser.newBrowserCDPSession();
82
- const windowResult = await browserCdp.send(
83
- "Browser.getWindowForTarget",
84
- targetId ? { targetId } : {},
85
- );
86
- await browserCdp.send("Browser.setWindowBounds", {
87
- windowId: windowResult.windowId,
88
- bounds: requestedBounds,
89
- });
90
- await new Promise((resolve) => setTimeout(resolve, 250));
91
- } catch {
92
- // Best-effort: window positioning should not prevent browser launch.
93
- } finally {
94
- await pageCdp.detach().catch(() => {});
95
- await browserCdp?.detach().catch(() => {});
96
- }
97
- }
98
-
99
60
  export async function launchBrowser({
100
61
  sessionName,
101
62
  headless = false,
@@ -0,0 +1,49 @@
1
+ import type { Browser, BrowserContext, Page } from "playwright";
2
+
3
+ export type WindowPosition = { x: number; y: number };
4
+
5
+ export async function applyWindowPosition(
6
+ browser: Browser,
7
+ context: BrowserContext,
8
+ page: Page,
9
+ windowPosition: WindowPosition | undefined,
10
+ ): Promise<void> {
11
+ if (!windowPosition) {
12
+ return;
13
+ }
14
+
15
+ const requestedBounds = {
16
+ left: windowPosition.x,
17
+ top: windowPosition.y,
18
+ windowState: "normal" as const,
19
+ };
20
+
21
+ let pageCdp:
22
+ | Awaited<ReturnType<BrowserContext["newCDPSession"]>>
23
+ | undefined;
24
+ let browserCdp:
25
+ | Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
26
+ | undefined;
27
+ try {
28
+ pageCdp = await context.newCDPSession(page);
29
+ const targetInfo = await pageCdp.send("Target.getTargetInfo");
30
+ const targetId = (
31
+ targetInfo as { targetInfo?: { targetId?: string } }
32
+ ).targetInfo?.targetId;
33
+ browserCdp = await browser.newBrowserCDPSession();
34
+ const windowResult = await browserCdp.send(
35
+ "Browser.getWindowForTarget",
36
+ targetId ? { targetId } : {},
37
+ );
38
+ await browserCdp.send("Browser.setWindowBounds", {
39
+ windowId: windowResult.windowId,
40
+ bounds: requestedBounds,
41
+ });
42
+ await new Promise((resolve) => setTimeout(resolve, 250));
43
+ } catch {
44
+ // Best-effort: window positioning should not prevent browser launch.
45
+ } finally {
46
+ await pageCdp?.detach().catch(() => {});
47
+ await browserCdp?.detach().catch(() => {});
48
+ }
49
+ }
@@ -1,8 +1,8 @@
1
1
  import type { LibrettoWorkflowContext } from "./workflow.js";
2
2
 
3
3
  export type LibrettoAuthenticateOptions = {
4
- validate: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
5
- fallback: (
4
+ isSignedIn: (ctx: LibrettoWorkflowContext) => Promise<boolean> | boolean;
5
+ signIn: (
6
6
  ctx: LibrettoWorkflowContext,
7
7
  credentials: Record<string, string>,
8
8
  ) => Promise<void> | void;
@@ -14,17 +14,17 @@ export async function librettoAuthenticate(
14
14
  ctx: LibrettoWorkflowContext,
15
15
  options: LibrettoAuthenticateOptions,
16
16
  ): Promise<{ usedProfile: boolean }> {
17
- if (await options.validate(ctx)) {
17
+ if (await options.isSignedIn(ctx)) {
18
18
  return { usedProfile: true };
19
19
  }
20
20
 
21
21
  const credentials = normalizeCredentials(
22
22
  options.credentials ?? readCredentialsFromEnv(options.envPrefix),
23
23
  );
24
- await options.fallback(ctx, credentials);
24
+ await options.signIn(ctx, credentials);
25
25
 
26
- if (!(await options.validate(ctx))) {
27
- throw new Error("Authentication fallback completed, but validation still failed.");
26
+ if (!(await options.isSignedIn(ctx))) {
27
+ throw new Error("Sign-in completed, but the session is still not signed in.");
28
28
  }
29
29
 
30
30
  return { usedProfile: false };
@@ -1,79 +0,0 @@
1
- # Auth Profiles
2
-
3
- Use this reference when generating or maintaining workflows that need a logged-in website session.
4
-
5
- ## When to Use This
6
-
7
- - The user wants to persist authentication across runs.
8
- - The workflow should recover when saved login state is stale.
9
-
10
- ## Workflow
11
-
12
- - Open the site in headed mode.
13
- - Ask the user to log in manually.
14
- - Save the current session as a named, site-scoped profile.
15
- - Run a workflow that declares the profile and includes fallback login logic.
16
-
17
- ## Commands
18
-
19
- ```bash
20
- # Save scoped auth state from the current Libretto session.
21
- npx libretto save example-app --session login --sites app.example.com,auth.example.com
22
-
23
- # List or delete hosted auth profile names.
24
- npx libretto cloud profiles list
25
- npx libretto cloud profiles delete example-app
26
- ```
27
-
28
- ## Workflow Definition
29
-
30
- Use `authProfile` to reuse a named login profile: local runs load
31
- `.libretto/profiles/<name>.json`, while hosted runs use provider-native profiles
32
- that `libretto cloud deploy` registers by name without uploading local files.
33
- Use `{ name, refresh: true }` when successful runs should persist updated
34
- browser state back to the profile. Pair profile use with `librettoAuthenticate`
35
- so stale local or hosted sessions can fall back to login with declared
36
- credentials before the workflow continues.
37
-
38
- ```typescript
39
- import { librettoAuthenticate, workflow } from "libretto";
40
-
41
- export default workflow("accountWorkflow", {
42
- authProfile: {
43
- name: "example-account",
44
- refresh: true,
45
- },
46
- credentials: ["username", "password"],
47
- async handler(ctx, input) {
48
- const { page } = ctx;
49
-
50
- await page.goto("https://app.example.com/dashboard");
51
-
52
- await librettoAuthenticate(ctx, {
53
- credentials: input.credentials,
54
- validate: async ({ page }) =>
55
- await page.getByRole("heading", { name: "Dashboard" })
56
- .isVisible()
57
- .catch(() => false),
58
- fallback: async ({ page }, credentials) => {
59
- await page.goto("https://app.example.com/login");
60
- await page.getByLabel("Email").fill(credentials.username);
61
- await page.getByLabel("Password").fill(credentials.password);
62
- await page.getByRole("button", { name: "Sign in" }).click();
63
- await page.getByRole("heading", { name: "Dashboard" }).waitFor();
64
- },
65
- });
66
-
67
- // Continue with the signed-in workflow steps.
68
- },
69
- });
70
- ```
71
-
72
- ## Notes
73
-
74
- - Saving a profile captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
75
- - If the user explicitly wants to import from Chrome, ask which Chrome/profile
76
- to launch or attach to and get consent before attaching because disconnecting
77
- can close or relaunch that Chrome window. Chrome may require copying the
78
- selected profile to a temporary user-data directory before running
79
- `npx libretto import-chrome-profiles example-app --cdp-url http://127.0.0.1:9222 --sites app.example.com`.