libretto 0.6.13 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cli/commands/auth.js +24 -33
  2. package/dist/cli/commands/billing.js +3 -5
  3. package/dist/cli/commands/browser.js +3 -6
  4. package/dist/cli/commands/deploy.js +54 -45
  5. package/dist/cli/commands/execution.js +6 -3
  6. package/dist/cli/commands/experiments.js +1 -1
  7. package/dist/cli/commands/setup.js +1 -1
  8. package/dist/cli/commands/shared.js +1 -1
  9. package/dist/cli/commands/snapshot.js +1 -1
  10. package/dist/cli/commands/status.js +1 -1
  11. package/dist/cli/core/auth-fetch.js +11 -6
  12. package/dist/cli/core/browser.js +10 -5
  13. package/dist/cli/core/daemon/daemon.js +63 -10
  14. package/dist/cli/core/daemon/exec-repl.js +133 -0
  15. package/dist/cli/core/daemon/exec.js +6 -21
  16. package/dist/cli/core/daemon/ipc.js +47 -4
  17. package/dist/cli/core/daemon/ipc.spec.js +21 -0
  18. package/dist/cli/core/exec-compiler.js +8 -3
  19. package/dist/cli/core/providers/index.js +13 -4
  20. package/dist/cli/core/providers/libretto-cloud.js +178 -26
  21. package/dist/cli/router.js +9 -4
  22. package/dist/shared/ipc/socket-transport.d.ts +2 -1
  23. package/dist/shared/ipc/socket-transport.js +16 -5
  24. package/dist/shared/ipc/socket-transport.spec.js +5 -0
  25. package/package.json +2 -2
  26. package/skills/libretto/SKILL.md +33 -29
  27. package/skills/libretto/references/code-generation-rules.md +6 -0
  28. package/skills/libretto/references/configuration-file-reference.md +8 -0
  29. package/skills/libretto/references/site-security-review.md +6 -6
  30. package/skills/libretto-readonly/SKILL.md +1 -1
  31. package/src/cli/commands/auth.ts +24 -33
  32. package/src/cli/commands/billing.ts +3 -5
  33. package/src/cli/commands/browser.ts +5 -9
  34. package/src/cli/commands/deploy.ts +55 -49
  35. package/src/cli/commands/execution.ts +6 -3
  36. package/src/cli/commands/experiments.ts +1 -1
  37. package/src/cli/commands/setup.ts +1 -1
  38. package/src/cli/commands/shared.ts +1 -1
  39. package/src/cli/commands/snapshot.ts +1 -1
  40. package/src/cli/commands/status.ts +1 -1
  41. package/src/cli/core/auth-fetch.ts +9 -4
  42. package/src/cli/core/browser.ts +12 -5
  43. package/src/cli/core/daemon/daemon.ts +81 -9
  44. package/src/cli/core/daemon/exec-repl.ts +189 -0
  45. package/src/cli/core/daemon/exec.ts +8 -43
  46. package/src/cli/core/daemon/ipc.spec.ts +27 -0
  47. package/src/cli/core/daemon/ipc.ts +76 -7
  48. package/src/cli/core/exec-compiler.ts +8 -3
  49. package/src/cli/core/providers/index.ts +17 -4
  50. package/src/cli/core/providers/libretto-cloud.ts +224 -36
  51. package/src/cli/router.ts +9 -4
  52. package/src/shared/ipc/socket-transport.spec.ts +6 -0
  53. package/src/shared/ipc/socket-transport.ts +20 -5
  54. package/dist/cli/framework/simple-cli.js +0 -880
  55. package/src/cli/framework/simple-cli.ts +0 -1459
@@ -1,15 +1,19 @@
1
- import { HOSTED_API_URL } from "../auth-fetch.js";
1
+ import { resolveHostedApiUrl } from "../auth-fetch.js";
2
+ const DEFAULT_POLL_INTERVAL_MS = 2e3;
3
+ const DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS = 3600;
4
+ const QUEUE_WAIT_TIMEOUT_MS = 10 * 6e4;
2
5
  function createLibrettoCloudProvider() {
3
6
  const apiKey = process.env.LIBRETTO_API_KEY;
4
7
  if (!apiKey)
5
8
  throw new Error(
6
9
  "LIBRETTO_API_KEY is required for the Libretto Cloud provider."
7
10
  );
8
- const endpoint = HOSTED_API_URL;
11
+ const endpoint = resolveHostedApiUrl();
9
12
  return {
10
13
  async createSession() {
11
- const timeoutSeconds = Number(
12
- process.env.LIBRETTO_TIMEOUT_SECONDS ?? 7200
14
+ const browserSessionTimeoutSeconds = readPositiveNumberEnv(
15
+ "LIBRETTO_TIMEOUT_SECONDS",
16
+ DEFAULT_BROWSER_SESSION_TIMEOUT_SECONDS
13
17
  );
14
18
  const resp = await fetch(`${endpoint}/v1/sessions/create`, {
15
19
  method: "POST",
@@ -18,42 +22,190 @@ function createLibrettoCloudProvider() {
18
22
  "Content-Type": "application/json"
19
23
  },
20
24
  body: JSON.stringify({
21
- json: { timeout_seconds: timeoutSeconds }
25
+ json: { timeout_seconds: browserSessionTimeoutSeconds }
22
26
  })
23
27
  });
24
28
  if (!resp.ok) {
25
29
  const body = await resp.text();
26
- throw new Error(
27
- `Libretto Cloud API error (${resp.status}): ${body}`
28
- );
30
+ throw new Error(`Libretto Cloud API error (${resp.status}): ${body}`);
29
31
  }
30
32
  const { json } = await resp.json();
33
+ const startupCleanup = createStartupSessionCleanup(
34
+ endpoint,
35
+ apiKey,
36
+ json.session_id
37
+ );
38
+ let readySession;
39
+ try {
40
+ readySession = await waitForCloudSessionReady({
41
+ endpoint,
42
+ apiKey,
43
+ session: json,
44
+ timeoutMs: QUEUE_WAIT_TIMEOUT_MS,
45
+ isCancelled: startupCleanup.isCancelled
46
+ });
47
+ } catch (error) {
48
+ if (startupCleanup.isCancelled()) {
49
+ await startupCleanup.waitForClose();
50
+ } else {
51
+ await closeCloudSession(endpoint, apiKey, json.session_id).catch(
52
+ () => {
53
+ }
54
+ );
55
+ }
56
+ throw error;
57
+ } finally {
58
+ startupCleanup.dispose();
59
+ }
31
60
  return {
32
- sessionId: json.session_id,
33
- cdpEndpoint: json.cdp_url,
34
- liveViewUrl: json.live_view_url ?? void 0
61
+ sessionId: readySession.session_id,
62
+ cdpEndpoint: readySession.cdp_url,
63
+ liveViewUrl: readySession.live_view_url ?? void 0
35
64
  };
36
65
  },
37
66
  async closeSession(sessionId) {
38
- const resp = await fetch(`${endpoint}/v1/sessions/close`, {
39
- method: "POST",
40
- headers: {
41
- "x-api-key": apiKey,
42
- "Content-Type": "application/json"
43
- },
44
- body: JSON.stringify({ json: { session_id: sessionId } })
45
- });
46
- if (!resp.ok) {
47
- const body = await resp.text();
48
- throw new Error(
49
- `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`
50
- );
51
- }
52
- const { json } = await resp.json();
67
+ const json = await closeCloudSession(endpoint, apiKey, sessionId);
53
68
  return { replayUrl: json.replay_url ?? void 0 };
54
69
  }
55
70
  };
56
71
  }
72
+ async function waitForCloudSessionReady(args) {
73
+ let session = args.session;
74
+ if (args.isCancelled?.()) {
75
+ throw new Error(
76
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
77
+ );
78
+ }
79
+ if (session.cdp_url) {
80
+ return { ...session, cdp_url: session.cdp_url };
81
+ }
82
+ sendStartupStatus(
83
+ `Libretto Cloud browser session queued (session: ${session.session_id}). Waiting for browser capacity...`
84
+ );
85
+ const pollIntervalMs = readPositiveNumberEnv(
86
+ "LIBRETTO_CLOUD_SESSION_POLL_INTERVAL_MS",
87
+ DEFAULT_POLL_INTERVAL_MS
88
+ );
89
+ const deadline = Date.now() + args.timeoutMs;
90
+ while (Date.now() < deadline) {
91
+ if (args.isCancelled?.()) {
92
+ throw new Error(
93
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
94
+ );
95
+ }
96
+ await sleep(pollIntervalMs);
97
+ if (args.isCancelled?.()) {
98
+ throw new Error(
99
+ `Libretto Cloud session ${session.session_id} was cancelled before browser capacity was available.`
100
+ );
101
+ }
102
+ session = await getCloudSession(
103
+ args.endpoint,
104
+ args.apiKey,
105
+ session.session_id
106
+ );
107
+ if (session.cdp_url) {
108
+ sendStartupStatus(
109
+ `Libretto Cloud browser capacity available (session: ${session.session_id}). Connecting...`
110
+ );
111
+ return { ...session, cdp_url: session.cdp_url };
112
+ }
113
+ if (!["queued", "starting"].includes(session.status)) {
114
+ throw new Error(
115
+ `Libretto Cloud session ${session.session_id} entered status "${session.status}" before a CDP URL was available.`
116
+ );
117
+ }
118
+ }
119
+ throw new Error(
120
+ `Timed out waiting for Libretto Cloud browser capacity after ${Math.ceil(args.timeoutMs / 1e3)}s (session: ${session.session_id}).`
121
+ );
122
+ }
123
+ async function getCloudSession(endpoint, apiKey, sessionId) {
124
+ const resp = await fetch(`${endpoint}/v1/sessions/get`, {
125
+ method: "POST",
126
+ headers: {
127
+ "x-api-key": apiKey,
128
+ "Content-Type": "application/json"
129
+ },
130
+ body: JSON.stringify({ json: { session_id: sessionId } })
131
+ });
132
+ if (!resp.ok) {
133
+ const body = await resp.text();
134
+ throw new Error(
135
+ `Libretto Cloud API error reading session ${sessionId} (${resp.status}): ${body}`
136
+ );
137
+ }
138
+ const { json } = await resp.json();
139
+ return json;
140
+ }
141
+ async function closeCloudSession(endpoint, apiKey, sessionId) {
142
+ const resp = await fetch(`${endpoint}/v1/sessions/close`, {
143
+ method: "POST",
144
+ headers: {
145
+ "x-api-key": apiKey,
146
+ "Content-Type": "application/json"
147
+ },
148
+ body: JSON.stringify({ json: { session_id: sessionId } })
149
+ });
150
+ if (!resp.ok) {
151
+ const body = await resp.text();
152
+ throw new Error(
153
+ `Libretto Cloud API error closing session ${sessionId} (${resp.status}): ${body}`
154
+ );
155
+ }
156
+ const { json } = await resp.json();
157
+ return json;
158
+ }
159
+ function createStartupSessionCleanup(endpoint, apiKey, sessionId) {
160
+ let cancelled = false;
161
+ let closePromise = null;
162
+ const requestClose = (reason) => {
163
+ if (cancelled) return;
164
+ cancelled = true;
165
+ sendStartupStatus(
166
+ `Libretto Cloud browser session cancelled (${reason}). Cleaning up queued session...`
167
+ );
168
+ closePromise = closeCloudSession(endpoint, apiKey, sessionId).then(
169
+ () => {
170
+ },
171
+ () => {
172
+ }
173
+ );
174
+ };
175
+ const onDisconnect = () => requestClose("parent command disconnected");
176
+ const onSigint = () => requestClose("received SIGINT");
177
+ const onSigterm = () => requestClose("received SIGTERM");
178
+ if (typeof process.send === "function") {
179
+ process.once("disconnect", onDisconnect);
180
+ }
181
+ process.once("SIGINT", onSigint);
182
+ process.once("SIGTERM", onSigterm);
183
+ return {
184
+ isCancelled: () => cancelled,
185
+ waitForClose: async () => {
186
+ await closePromise;
187
+ },
188
+ dispose: () => {
189
+ process.off("disconnect", onDisconnect);
190
+ process.off("SIGINT", onSigint);
191
+ process.off("SIGTERM", onSigterm);
192
+ }
193
+ };
194
+ }
195
+ function readPositiveNumberEnv(name, fallback) {
196
+ const raw = process.env[name];
197
+ if (!raw) return fallback;
198
+ const parsed = Number(raw);
199
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
200
+ }
201
+ function sendStartupStatus(message) {
202
+ if (typeof process.send === "function") {
203
+ process.send({ type: "startup-status", message });
204
+ }
205
+ }
206
+ function sleep(ms) {
207
+ return new Promise((resolve) => setTimeout(resolve, ms));
208
+ }
57
209
  export {
58
210
  createLibrettoCloudProvider
59
211
  };
@@ -8,14 +8,19 @@ import { setupCommand } from "./commands/setup.js";
8
8
  import { statusCommand } from "./commands/status.js";
9
9
  import { snapshotCommand } from "./commands/snapshot.js";
10
10
  import { librettoCommand } from "../shared/package-manager.js";
11
- import { SimpleCLI } from "./framework/simple-cli.js";
11
+ import { SimpleCLI } from "affordance";
12
12
  const cliRoutes = {
13
13
  ...browserCommands,
14
- deploy: deployCommand,
14
+ cloud: SimpleCLI.group({
15
+ description: "Libretto Cloud commands",
16
+ routes: {
17
+ deploy: deployCommand,
18
+ auth: authCommands,
19
+ billing: billingCommands
20
+ }
21
+ }),
15
22
  experiments: experimentsCommand,
16
23
  ...executionCommands,
17
- auth: authCommands,
18
- billing: billingCommands,
19
24
  setup: setupCommand,
20
25
  status: statusCommand,
21
26
  snapshot: snapshotCommand
@@ -5,5 +5,6 @@ declare function connectToIpcSocket(socketPath: string): Promise<IpcTransport<Ip
5
5
  declare function createIpcSocketServer(onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Server;
6
6
  declare function listenForIpcConnections(socketPath: string, onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Promise<Server>;
7
7
  declare function listenOnIpcSocket(server: Server, socketPath: string): Promise<void>;
8
+ declare function isWindowsNamedPipePath(socketPath: string): boolean;
8
9
 
9
- export { connectToIpcSocket, createIpcSocketServer, listenForIpcConnections, listenOnIpcSocket };
10
+ export { connectToIpcSocket, createIpcSocketServer, isWindowsNamedPipePath, listenForIpcConnections, listenOnIpcSocket };
@@ -1,10 +1,9 @@
1
- import { rm } from "node:fs/promises";
1
+ import { mkdir, rm } from "node:fs/promises";
2
2
  import {
3
3
  createServer,
4
4
  createConnection
5
5
  } from "node:net";
6
6
  import { dirname } from "node:path";
7
- import { mkdir } from "node:fs/promises";
8
7
  function createJsonSocketTransport(socket) {
9
8
  socket.setEncoding("utf8");
10
9
  return {
@@ -82,12 +81,11 @@ async function listenForIpcConnections(socketPath, onConnection) {
82
81
  return server;
83
82
  }
84
83
  async function listenOnIpcSocket(server, socketPath) {
85
- await mkdir(dirname(socketPath), { recursive: true });
86
- await rm(socketPath, { force: true });
84
+ await prepareIpcSocketPath(socketPath);
87
85
  const originalClose = server.close.bind(server);
88
86
  server.close = ((callback) => {
89
87
  return originalClose((error) => {
90
- void rm(socketPath, { force: true }).finally(() => callback?.(error));
88
+ void removeStaleSocketFile(socketPath).finally(() => callback?.(error));
91
89
  });
92
90
  });
93
91
  await new Promise((resolve, reject) => {
@@ -104,6 +102,18 @@ async function listenOnIpcSocket(server, socketPath) {
104
102
  server.listen(socketPath);
105
103
  });
106
104
  }
105
+ function isWindowsNamedPipePath(socketPath) {
106
+ return socketPath.startsWith("\\\\.\\pipe\\");
107
+ }
108
+ async function prepareIpcSocketPath(socketPath) {
109
+ if (isWindowsNamedPipePath(socketPath)) return;
110
+ await mkdir(dirname(socketPath), { recursive: true });
111
+ await removeStaleSocketFile(socketPath);
112
+ }
113
+ async function removeStaleSocketFile(socketPath) {
114
+ if (isWindowsNamedPipePath(socketPath)) return;
115
+ await rm(socketPath, { force: true });
116
+ }
107
117
  async function connectSocket(socketPath) {
108
118
  const socket = createConnection(socketPath);
109
119
  return new Promise((resolve, reject) => {
@@ -138,6 +148,7 @@ function isRecord(value) {
138
148
  export {
139
149
  connectToIpcSocket,
140
150
  createIpcSocketServer,
151
+ isWindowsNamedPipePath,
141
152
  listenForIpcConnections,
142
153
  listenOnIpcSocket
143
154
  };
@@ -6,6 +6,7 @@ import { expect, test as base } from "vitest";
6
6
  import { createIpcPeer } from "./ipc.js";
7
7
  import {
8
8
  connectToIpcSocket,
9
+ isWindowsNamedPipePath,
9
10
  listenForIpcConnections
10
11
  } from "./socket-transport.js";
11
12
  const test = base.extend({
@@ -47,6 +48,10 @@ test("sends concurrent calls over one socket", async ({ socketPath }) => {
47
48
  });
48
49
  await expect(stat(socketPath)).rejects.toThrow();
49
50
  });
51
+ test("recognizes Windows named pipe paths", () => {
52
+ expect(isWindowsNamedPipePath("\\\\.\\pipe\\libretto-abc123")).toBe(true);
53
+ expect(isWindowsNamedPipePath("/tmp/libretto-501-abc123.sock")).toBe(false);
54
+ });
50
55
  test("rejects pending calls when the socket closes", async ({ socketPath }) => {
51
56
  const serverPeers = [];
52
57
  const server = await listenForIpcConnections(socketPath, (transport) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -9,7 +9,6 @@
9
9
  "url": "https://github.com/saffron-health/libretto"
10
10
  },
11
11
  "type": "module",
12
- "packageManager": "pnpm@10.33.0",
13
12
  "publishConfig": {
14
13
  "access": "public"
15
14
  },
@@ -86,6 +85,7 @@
86
85
  "vitest": "^4.1.5"
87
86
  },
88
87
  "dependencies": {
88
+ "affordance": "^0.1.0",
89
89
  "ai": "^6.0.116",
90
90
  "esbuild": "^0.27.0",
91
91
  "playwright": "^1.58.2",
@@ -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.13"
7
+ version: "0.6.14"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -19,18 +19,20 @@ The npm package includes `src/` (full TypeScript source) and `docs/` for deeper
19
19
 
20
20
  Full documentation is published at [libretto.sh](https://libretto.sh). Available pages:
21
21
 
22
- - Get started: [introduction](https://libretto.sh/get-started/introduction), [installation](https://libretto.sh/get-started/installation), [configuration](https://libretto.sh/get-started/configuration)
23
- - Fundamentals: [core concepts](https://libretto.sh/fundamentals/core-concepts), [how workflow generation works](https://libretto.sh/fundamentals/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/fundamentals/automation-and-bot-detection), [website authentication](https://libretto.sh/fundamentals/website-authentication)
24
- - Workflow guides: [one-shot generation](https://libretto.sh/workflow-guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/workflow-guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/workflow-guides/debugging-workflows), [convert to network requests](https://libretto.sh/workflow-guides/convert-to-network-requests)
25
- - CLI reference: [open and connect](https://libretto.sh/cli-reference/open-and-connect), [sessions](https://libretto.sh/cli-reference/sessions), [profiles](https://libretto.sh/cli-reference/profiles), [snapshot](https://libretto.sh/cli-reference/snapshot), [exec](https://libretto.sh/cli-reference/exec), [run and resume](https://libretto.sh/cli-reference/run-and-resume), [session logs](https://libretto.sh/cli-reference/session-logs), [pages](https://libretto.sh/cli-reference/pages)
26
- - Library API: [workflow](https://libretto.sh/library-api/workflow), [AI extraction](https://libretto.sh/library-api/ai-extraction), [network requests](https://libretto.sh/library-api/network-requests), [file downloads](https://libretto.sh/library-api/file-downloads)
27
- - Hosting: [introduction](https://libretto.sh/hosting/introduction), [GCP](https://libretto.sh/hosting/gcp), [AWS](https://libretto.sh/hosting/aws)
22
+ - Get started: [quickstart](https://libretto.sh/docs/get-started/quickstart), [first workflow](https://libretto.sh/docs/get-started/first-workflow), [deploying](https://libretto.sh/docs/get-started/deploying)
23
+ - Fundamentals: [core concepts](https://libretto.sh/docs/understand-libretto/core-concepts), [how workflow generation works](https://libretto.sh/docs/understand-libretto/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/docs/understand-libretto/automation-and-bot-detection), [website authentication](https://libretto.sh/docs/understand-libretto/website-authentication)
24
+ - Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
25
+ - CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
26
+ - Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [AI extraction](https://libretto.sh/docs/reference/runtime/ai-extraction), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
27
+ - Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments)
28
+ - Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
28
29
 
29
30
  ## Default Integration Approach
30
31
 
31
- - Prefer network requests first for new integrations unless the user explicitly asks for Playwright or UI automation, then do not use the site's internal API.
32
- - Read `references/site-security-review.md` before committing to a network-first approach on a new site.
33
- - Fall back to passive interception or Playwright-driven UI automation when the security review rules network requests out, the request path is not workable, or the user explicitly asks for Playwright.
32
+ - Use Playwright for navigation and other non-fetch browser behavior, including document and asset loads.
33
+ - Prefer browser-context `fetch()` for data extraction and form submission when the target is a real site fetch/XHR endpoint and `references/site-security-review.md` says the path is safe and workable.
34
+ - Use passive interception when the UI already triggers useful fetch/XHR requests or active fetch is risky.
35
+ - Fall back to Playwright UI automation when fetch is ruled out, the request path is not workable, or the user explicitly asks for Playwright/UI automation.
34
36
 
35
37
  ## Setup
36
38
 
@@ -48,8 +50,8 @@ Full documentation is published at [libretto.sh](https://libretto.sh). Available
48
50
  - Do not treat visibility as interactivity. If an element will not act, inspect blockers before retrying.
49
51
  - Defer repo/code review until you begin generating code, unless the user explicitly asks for it earlier.
50
52
  - Read and follow guidelines in `references/code-generation-rules.md` before generating or editing production workflow code.
51
- - Validation requires a successful clean `run --headless` with confirmation of the actual returned output, not just process success. If the user wants to watch the finished workflow, do a final headed `run` after headless validation succeeds.
52
- - After validation, always show the user: (1) the output/results from the headless validation run, and (2) a headed version of the same command so they can re-run it themselves and watch the browser (e.g. replace `--headless` with `--headed`). Include any `--params` or `--auth-profile` flags the workflow needs.
53
+ - 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
+ - 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`, `--headless`, or `--auth-profile` flags the workflow needs.
53
55
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
54
56
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
55
57
  - Never run multiple `exec` commands at the same time.
@@ -113,17 +115,18 @@ npx libretto snapshot --session debug-example --page <page-id>
113
115
  - Use `exec` for focused inspection and short-lived interaction experiments.
114
116
  - Use `exec` to validate selectors, inspect data, or prototype a step before you encode it in the workflow file.
115
117
  - Use `exec -` to run multi-line scripts from stdin, especially when the code is too long or complex for a command line argument.
116
- - Available globals: `page`, `context`, `browser`, `state`, `fetch`, `Buffer`.
118
+ - The `exec` REPL is persistent for each browser session. Define helper functions once and reuse them in later `exec` calls.
119
+ - Available globals: `page`, `frame`, `context`, `browser`, `fetch`, `Buffer`.
117
120
  - Let failures throw. Do not hide `exec` failures with `try/catch` or `.catch()`.
118
121
  - Do not run multiple `exec` commands in parallel.
119
122
  - Do not use `exec` in read-only diagnosis flows. Use `readonly-exec` from the `libretto-readonly` skill for those sessions.
120
123
  - After successful mutations, `exec` prints page-change diffs from compact snapshots.
121
124
 
122
125
  ```bash
123
- npx libretto exec "return await page.url()"
124
- npx libretto exec "return await page.locator('button').count()"
126
+ npx libretto exec "await page.url()"
125
127
  npx libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
126
- echo "return await page.url()" | npx libretto exec - --session debug-example
128
+ echo "async function textOf(selector) { return await page.locator(selector).textContent(); }" | npx libretto exec - --session debug-example
129
+ npx libretto exec --session debug-example "await textOf('h1')"
127
130
  ```
128
131
 
129
132
  ### `pages`
@@ -133,13 +136,13 @@ echo "return await page.url()" | npx libretto exec - --session debug-example
133
136
 
134
137
  ```bash
135
138
  npx libretto pages --session debug-example
136
- npx libretto exec --session debug-example --page <page-id> "return await page.url()"
139
+ npx libretto exec --session debug-example --page <page-id> "await page.url()"
137
140
  ```
138
141
 
139
142
  ### `run`
140
143
 
141
- - Use `run` to verify a workflow file after creating it or editing it, preferring `run --headless` for the normal fix/verify loop.
142
- - Plain `run` defaults to headed mode.
144
+ - Use `run` to verify a workflow file after creating it or editing it. Use the same headed or headless mode for validation that the workflow run is already using.
145
+ - Plain `run` defaults to headed mode. Do not use `--headless` unless the user asks for headless mode or the existing workflow run already uses it.
143
146
  - Successful runs close the browser by default. Pass `--stay-open-on-success` when you need to inspect the completed state with `pages`, `snapshot`, or `exec`.
144
147
  - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
145
148
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
@@ -148,9 +151,9 @@ npx libretto exec --session debug-example --page <page-id> "return await page.ur
148
151
  - Re-run the same workflow after each fix to verify the browser behavior end to end.
149
152
 
150
153
  ```bash
151
- npx libretto run ./integration.ts --headless --params '{"status":"open"}'
152
- npx libretto run ./integration.ts --headless --read-only
153
- npx libretto run ./integration.ts --headless --stay-open-on-success
154
+ npx libretto run ./integration.ts --params '{"status":"open"}'
155
+ npx libretto run ./integration.ts --read-only
156
+ npx libretto run ./integration.ts --stay-open-on-success
154
157
  npx libretto run ./integration.ts --auth-profile app.example.com
155
158
  ```
156
159
 
@@ -230,10 +233,10 @@ Assistant: [Uses `snapshot` and `exec` as needed to understand the site and deci
230
233
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
231
234
  Assistant: I found the working path. I'll now create the workflow file and verify it.
232
235
  Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
233
- Assistant: [Runs `npx libretto run ./target-workflow.ts --headless --params '{"status":"open"}'` to validate]
236
+ Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
234
237
  Assistant: Validation passed. Here are the results:
235
- [Shows the output/results from the headless run]
236
- To watch it yourself in a browser, run: npx libretto run ./target-workflow.ts --headed --params '{"status":"open"}'
238
+ [Shows the output/results from the validation run]
239
+ To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
237
240
  </example>
238
241
  ```
239
242
 
@@ -243,17 +246,17 @@ To watch it yourself in a browser, run: npx libretto run ./target-workflow.ts --
243
246
  <example>
244
247
  [Context: The user has an existing Libretto workflow that is failing]
245
248
  Assistant: I'll reproduce the failure first so we can inspect the exact browser state it leaves behind.
246
- Assistant: [Runs `npx libretto run ./integration.ts --session debug-flow --headed`]
249
+ Assistant: [Runs `npx libretto run ./integration.ts --session debug-flow`]
247
250
  Assistant: The workflow failed and Libretto kept the browser open. I'll inspect the page state before changing code.
248
251
  Assistant: [Runs `npx libretto snapshot --session debug-flow`]
249
252
  Assistant: [Runs `npx libretto exec --session debug-flow "...focused inspection or prototype..."`]
250
253
  Assistant: [Reads `references/code-generation-rules.md` before patching the workflow file]
251
254
  Assistant: I found the issue. I'll patch the workflow code and verify.
252
255
  Assistant: [Edits `integration.ts` following `references/code-generation-rules.md`]
253
- Assistant: [Runs `npx libretto run ./integration.ts --headless` to validate the fix]
256
+ Assistant: [Runs `npx libretto run ./integration.ts` to validate the fix]
254
257
  Assistant: Fix verified. Here are the results:
255
- [Shows the output/results from the headless run]
256
- To watch it yourself in a browser, run: npx libretto run ./integration.ts --headed
258
+ [Shows the output/results from the validation run]
259
+ To run it again, use: npx libretto run ./integration.ts
257
260
  </example>
258
261
  ```
259
262
 
@@ -265,3 +268,4 @@ To watch it yourself in a browser, run: npx libretto run ./integration.ts --head
265
268
  - Read `references/auth-profiles.md` when auth-profile behavior is relevant.
266
269
  - Read `references/pages-and-page-targeting.md` when a session has multiple open pages or you need `--page`.
267
270
  - Read `references/action-logs.md` for full action log field descriptions and user-vs-agent event semantics.
271
+ - 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.
@@ -117,6 +117,12 @@ Do not rely on broad DOM querying inside `page.evaluate()` for production flows
117
117
 
118
118
  ## Network Request Methods
119
119
 
120
+ Network request methods are for active fetch/XHR endpoints the site already uses. Prefer them for data extraction or form submissions when the security review shows the path is safe and workable.
121
+
122
+ Before codifying a network request, confirm that the browser primitive matches how the site normally makes that request. Use `page.goto()` or link clicks for document navigation. Use `page.evaluate(fetch)` only for endpoints the site calls with fetch/XHR. Let the DOM load scripts, images, stylesheets, and iframes naturally, or create the corresponding DOM element if you truly need that request type.
123
+
124
+ Do not use `fetch()` to avoid UI navigation for page HTML or asset URLs. The request still comes from the browser, but the browser marks it as fetch/XHR with different request-context headers than a navigation, script, image, stylesheet, or iframe load. Do not try to fix that by copying headers, because the browser controls the request context. Prefer passive network interception when the site's own UI already triggers the useful request.
125
+
120
126
  When codifying network-based data extraction or form submissions, wrap `page.evaluate(() => fetch(...))` calls in typed methods on a shared API client class:
121
127
 
122
128
  ```typescript
@@ -6,6 +6,7 @@ Use this reference when you need to inspect or change workspace configuration fo
6
6
 
7
7
  - You want to understand where Libretto stores workspace-level settings.
8
8
  - You want a persistent default viewport for `open` or `run`.
9
+ - You want a persistent default browser provider, such as Kernel or Browserbase.
9
10
 
10
11
  ## File Location
11
12
 
@@ -17,6 +18,9 @@ Libretto reads workspace config from `.libretto/config.json`.
17
18
 
18
19
  ## Supported Settings
19
20
 
21
+ - `provider` is an optional top-level setting used by `open` and `run` when you do not pass `--provider` and do not set `LIBRETTO_PROVIDER`. Must be `"local"`, `"kernel"`, `"browserbase"`, or `"libretto-cloud"`.
22
+ - Provider precedence is: CLI `--provider`, then `LIBRETTO_PROVIDER`, then `.libretto/config.json`, then `"local"`.
23
+ - Provider credentials belong in the repo root `.env` file, which Libretto loads automatically before running CLI commands.
20
24
  - `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
21
25
  - Viewport precedence is: CLI `--viewport`, then `.libretto/config.json`, then the default `1366x768`.
22
26
  - `sessionMode` sets the default session access mode for new sessions created by `open`, `connect`, and `run`. Must be `"read-only"` or `"write-access"`. When omitted, defaults to `"write-access"`. Pass `--read-only` or `--write-access` to `open`, `connect`, or `run` to override when creating a session.
@@ -26,6 +30,7 @@ Example:
26
30
  ```json
27
31
  {
28
32
  "version": 1,
33
+ "provider": "kernel",
29
34
  "viewport": {
30
35
  "width": 1280,
31
36
  "height": 800
@@ -39,11 +44,14 @@ Example:
39
44
  ```bash
40
45
  npx libretto setup # first-time onboarding
41
46
  npx libretto status # inspect open sessions
47
+ npx libretto open https://example.com --provider kernel
48
+ npx libretto run ./integration.ts --provider browserbase
42
49
  npx libretto open https://example.com --viewport 1440x900
43
50
  npx libretto run ./integration.ts --viewport 1440x900
44
51
  ```
45
52
 
46
53
  ## Notes
47
54
 
55
+ - If you want a persistent default provider for the workspace, add `provider` to `.libretto/config.json` instead of repeating `--provider` on every command.
48
56
  - If you want a persistent default viewport for the workspace, add `viewport` to `.libretto/config.json` instead of repeating `--viewport` on every command.
49
57
  - Run `npx libretto status` at any time to check open sessions.
@@ -60,10 +60,11 @@ Use the review above to decide what is safe to prioritize. Every integration use
60
60
 
61
61
  ### Strategy A: Prioritize `page.evaluate(fetch(...))`
62
62
 
63
- Make fetch calls directly from within the browser's JavaScript context. The requests share the browser's TLS fingerprint, cookies, and origin. They look identical to requests the site's own JS would make.
63
+ Make fetch calls directly from within the browser's JavaScript context. Use this only for endpoints the site already calls with fetch/XHR, not for page navigation or asset loads.
64
64
 
65
65
  When to prioritize this:
66
66
 
67
+ - The target endpoint is normally called by the site with fetch/XHR
67
68
  - No enterprise bot protection is detected
68
69
  - `fetch` is not monkey-patched
69
70
  - The API responses are parseable and useful
@@ -71,7 +72,7 @@ When to prioritize this:
71
72
 
72
73
  Why: maximum control and efficiency. You call exactly the endpoints you want with the parameters you want, skip UI rendering, and get structured JSON back. On sites without aggressive detection, this is the fastest and cleanest approach.
73
74
 
74
- Risk: if the site monitors fetch call stacks, your calls may be flagged because they do not originate from the site's bundled code. This is uncommon but exists on high-security sites.
75
+ Risk: fetch is the wrong primitive for page HTML and asset URLs; use Playwright navigation or DOM-driven loads for those. Sites can also monitor fetch call stacks and flag calls that do not originate from the site's bundled code.
75
76
 
76
77
  You will still use Playwright for initial navigation, login/auth flows, cookie consent, and any UI interactions needed to establish session state before making fetch calls.
77
78
 
@@ -111,10 +112,9 @@ Trade-off: it is slower, more fragile against DOM changes, and you only get data
111
112
 
112
113
  | Site Profile | Primary Strategy | Supplement With |
113
114
  | --- | --- | --- |
114
- | No bot protection, fetch not patched | A (`page.evaluate(fetch)`) | Playwright for navigation/auth |
115
- | No bot protection, fetch is patched | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
116
- | Bot protection detected, fetch not patched | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
117
- | Bot protection detected, fetch is patched | B (`page.on('response', ...)`) | Playwright for navigation; DOM extraction as fallback |
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 |
117
+ | Bot protection detected | B (`page.on('response', ...)`) | Playwright for navigation; cautious use of `page.evaluate(fetch)` only if needed |
118
118
  | Server-rendered content (no API calls) | C (DOM extraction) | Playwright for all interaction |
119
119
 
120
120
  ## Output: Site Assessment Summary
@@ -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.13"
7
+ version: "0.6.14"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works