libretto 0.6.13 → 0.6.15
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.
- package/dist/cli/commands/auth.js +43 -33
- package/dist/cli/commands/billing.js +3 -5
- package/dist/cli/commands/browser.js +3 -6
- package/dist/cli/commands/deploy.js +54 -45
- package/dist/cli/commands/execution.js +7 -4
- package/dist/cli/commands/experiments.js +1 -1
- package/dist/cli/commands/setup.js +1 -1
- package/dist/cli/commands/shared.js +1 -1
- package/dist/cli/commands/snapshot.js +1 -1
- package/dist/cli/commands/status.js +1 -1
- package/dist/cli/core/auth-fetch.js +11 -6
- package/dist/cli/core/browser.js +10 -5
- package/dist/cli/core/daemon/daemon.js +63 -10
- package/dist/cli/core/daemon/exec-repl.js +133 -0
- package/dist/cli/core/daemon/exec.js +6 -21
- package/dist/cli/core/daemon/ipc.js +47 -4
- package/dist/cli/core/daemon/ipc.spec.js +21 -0
- package/dist/cli/core/exec-compiler.js +8 -3
- package/dist/cli/core/providers/index.js +13 -4
- package/dist/cli/core/providers/kernel.js +3 -3
- package/dist/cli/core/providers/libretto-cloud.js +178 -26
- package/dist/cli/router.js +9 -4
- package/dist/shared/ipc/socket-transport.d.ts +2 -1
- package/dist/shared/ipc/socket-transport.js +16 -5
- package/dist/shared/ipc/socket-transport.spec.js +5 -0
- package/package.json +2 -2
- package/skills/libretto/SKILL.md +33 -29
- package/skills/libretto/references/code-generation-rules.md +6 -0
- package/skills/libretto/references/configuration-file-reference.md +8 -0
- package/skills/libretto/references/site-security-review.md +6 -6
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/auth.ts +46 -33
- package/src/cli/commands/billing.ts +3 -5
- package/src/cli/commands/browser.ts +5 -9
- package/src/cli/commands/deploy.ts +55 -49
- package/src/cli/commands/execution.ts +7 -4
- package/src/cli/commands/experiments.ts +1 -1
- package/src/cli/commands/setup.ts +1 -1
- package/src/cli/commands/shared.ts +1 -1
- package/src/cli/commands/snapshot.ts +1 -1
- package/src/cli/commands/status.ts +1 -1
- package/src/cli/core/auth-fetch.ts +9 -4
- package/src/cli/core/browser.ts +12 -5
- package/src/cli/core/daemon/daemon.ts +81 -9
- package/src/cli/core/daemon/exec-repl.ts +189 -0
- package/src/cli/core/daemon/exec.ts +8 -43
- package/src/cli/core/daemon/ipc.spec.ts +27 -0
- package/src/cli/core/daemon/ipc.ts +76 -7
- package/src/cli/core/exec-compiler.ts +8 -3
- package/src/cli/core/providers/index.ts +17 -4
- package/src/cli/core/providers/kernel.ts +4 -3
- package/src/cli/core/providers/libretto-cloud.ts +224 -36
- package/src/cli/router.ts +9 -4
- package/src/shared/ipc/socket-transport.spec.ts +6 -0
- package/src/shared/ipc/socket-transport.ts +20 -5
- package/dist/cli/framework/simple-cli.js +0 -880
- package/src/cli/framework/simple-cli.ts +0 -1459
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
11
|
+
const endpoint = resolveHostedApiUrl();
|
|
9
12
|
return {
|
|
10
13
|
async createSession() {
|
|
11
|
-
const
|
|
12
|
-
|
|
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:
|
|
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:
|
|
33
|
-
cdpEndpoint:
|
|
34
|
-
liveViewUrl:
|
|
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
|
|
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
|
};
|
package/dist/cli/router.js
CHANGED
|
@@ -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 "
|
|
11
|
+
import { SimpleCLI } from "affordance";
|
|
12
12
|
const cliRoutes = {
|
|
13
13
|
...browserCommands,
|
|
14
|
-
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.6.15",
|
|
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",
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -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.
|
|
7
|
+
version: "0.6.15"
|
|
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: [
|
|
23
|
-
- Fundamentals: [core concepts](https://libretto.sh/
|
|
24
|
-
- Workflow guides: [one-shot generation](https://libretto.sh/
|
|
25
|
-
- CLI reference: [open and connect](https://libretto.sh/
|
|
26
|
-
- Library API: [workflow](https://libretto.sh/
|
|
27
|
-
- Hosting: [
|
|
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
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
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
|
|
52
|
-
- After validation, always show the user: (1) the output/results from the
|
|
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
|
-
-
|
|
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 "
|
|
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.
|
|
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> "
|
|
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
|
|
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 --
|
|
152
|
-
npx libretto run ./integration.ts --
|
|
153
|
-
npx libretto run ./integration.ts --
|
|
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 --
|
|
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
|
|
236
|
-
To
|
|
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
|
|
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
|
|
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
|
|
256
|
-
To
|
|
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.
|
|
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:
|
|
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
|
|
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
|