libretto 0.6.12 → 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.
- package/README.md +3 -8
- package/README.template.md +3 -8
- package/dist/cli/cli.js +0 -23
- package/dist/cli/commands/auth.js +24 -33
- package/dist/cli/commands/billing.js +3 -5
- package/dist/cli/commands/browser.js +4 -13
- package/dist/cli/commands/deploy.js +54 -45
- package/dist/cli/commands/execution.js +6 -3
- package/dist/cli/commands/experiments.js +1 -1
- package/dist/cli/commands/setup.js +2 -295
- package/dist/cli/commands/shared.js +1 -1
- package/dist/cli/commands/snapshot.js +10 -100
- package/dist/cli/commands/status.js +2 -42
- package/dist/cli/core/auth-fetch.js +11 -6
- package/dist/cli/core/browser.js +13 -8
- package/dist/cli/core/config.js +3 -6
- package/dist/cli/core/daemon/daemon.js +88 -74
- 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/daemon/snapshot.js +2 -29
- package/dist/cli/core/exec-compiler.js +8 -3
- package/dist/cli/core/experiments.js +1 -28
- package/dist/cli/core/providers/index.js +13 -4
- package/dist/cli/core/providers/libretto-cloud.js +178 -26
- package/dist/cli/index.js +0 -2
- package/dist/cli/router.js +9 -6
- package/dist/shared/instrumentation/instrument.js +4 -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/docs/releasing.md +8 -6
- package/package.json +3 -2
- package/skills/libretto/SKILL.md +49 -47
- package/skills/libretto/references/code-generation-rules.md +6 -0
- package/skills/libretto/references/configuration-file-reference.md +14 -12
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +6 -6
- package/skills/libretto-readonly/SKILL.md +2 -9
- package/src/cli/cli.ts +0 -24
- package/src/cli/commands/auth.ts +24 -33
- package/src/cli/commands/billing.ts +3 -5
- package/src/cli/commands/browser.ts +6 -16
- package/src/cli/commands/deploy.ts +55 -49
- package/src/cli/commands/execution.ts +6 -3
- package/src/cli/commands/experiments.ts +1 -1
- package/src/cli/commands/setup.ts +2 -381
- package/src/cli/commands/shared.ts +1 -1
- package/src/cli/commands/snapshot.ts +9 -137
- package/src/cli/commands/status.ts +2 -50
- package/src/cli/core/auth-fetch.ts +9 -4
- package/src/cli/core/browser.ts +15 -8
- package/src/cli/core/config.ts +3 -6
- package/src/cli/core/daemon/daemon.ts +106 -76
- 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 +81 -23
- package/src/cli/core/daemon/snapshot.ts +1 -43
- package/src/cli/core/exec-compiler.ts +8 -3
- package/src/cli/core/experiments.ts +9 -38
- package/src/cli/core/providers/index.ts +17 -4
- package/src/cli/core/providers/libretto-cloud.ts +224 -36
- package/src/cli/core/resolve-model.ts +5 -0
- package/src/cli/core/workflow-runtime.ts +1 -0
- package/src/cli/index.ts +0 -1
- package/src/cli/router.ts +9 -6
- package/src/shared/instrumentation/instrument.ts +4 -4
- package/src/shared/ipc/socket-transport.spec.ts +6 -0
- package/src/shared/ipc/socket-transport.ts +20 -5
- package/dist/cli/commands/ai.js +0 -110
- package/dist/cli/core/ai-model.js +0 -195
- package/dist/cli/core/api-snapshot-analyzer.js +0 -86
- package/dist/cli/core/snapshot-analyzer.js +0 -667
- package/dist/cli/framework/simple-cli.js +0 -880
- package/scripts/summarize-evals.mjs +0 -135
- package/src/cli/commands/ai.ts +0 -144
- package/src/cli/core/ai-model.ts +0 -301
- package/src/cli/core/api-snapshot-analyzer.ts +0 -110
- package/src/cli/core/snapshot-analyzer.ts +0 -856
- package/src/cli/framework/simple-cli.ts +0 -1459
|
@@ -2,34 +2,7 @@ import {
|
|
|
2
2
|
readLibrettoConfig,
|
|
3
3
|
writeLibrettoConfig
|
|
4
4
|
} from "./config.js";
|
|
5
|
-
const EXPERIMENTS = {
|
|
6
|
-
"compact-snapshot-format": {
|
|
7
|
-
title: "Compact snapshot format",
|
|
8
|
-
oneSentenceDescription: "Use compact accessibility snapshots and exec page-change diffs without an AI sub-agent.",
|
|
9
|
-
docs: [
|
|
10
|
-
"Compact snapshot format changes how agents should use snapshot and exec after the experiment is enabled.",
|
|
11
|
-
"",
|
|
12
|
-
"Compared with the skill's documented behavior:",
|
|
13
|
-
" - Run libretto snapshot --session <name> without --objective or --context.",
|
|
14
|
-
" - Snapshot output is a screenshot path plus a compact accessibility tree; it does not use the PNG + HTML + AI analysis path.",
|
|
15
|
-
" - Run libretto snapshot <ref> --session <name> to inspect a subtree from the latest full compact snapshot.",
|
|
16
|
-
" - Run libretto exec normally; after successful mutations, Libretto prints page-change diffs from compact snapshots without AI analysis.",
|
|
17
|
-
" - If a session was already open before enabling the experiment, close and reopen it before relying on this behavior.",
|
|
18
|
-
"",
|
|
19
|
-
"Full compact snapshot:",
|
|
20
|
-
" libretto snapshot --session <name>",
|
|
21
|
-
"",
|
|
22
|
-
"Cached subtree snapshot:",
|
|
23
|
-
" libretto snapshot <ref> --session <name>",
|
|
24
|
-
"",
|
|
25
|
-
"Run an unscoped snapshot before using refs. Subtree snapshots capture a fresh screenshot but reuse the latest cached tree.",
|
|
26
|
-
"",
|
|
27
|
-
"Notes:",
|
|
28
|
-
" - Use ref forms printed in the tree, such as l16. Numeric-suffix aliases such as e16 also match l16."
|
|
29
|
-
].join("\n"),
|
|
30
|
-
defaultValue: false
|
|
31
|
-
}
|
|
32
|
-
};
|
|
5
|
+
const EXPERIMENTS = {};
|
|
33
6
|
function isExperimentName(name) {
|
|
34
7
|
return Object.hasOwn(EXPERIMENTS, name);
|
|
35
8
|
}
|
|
@@ -2,7 +2,14 @@ import { readLibrettoConfig } from "../config.js";
|
|
|
2
2
|
import { createBrowserbaseProvider } from "./browserbase.js";
|
|
3
3
|
import { createKernelProvider } from "./kernel.js";
|
|
4
4
|
import { createLibrettoCloudProvider } from "./libretto-cloud.js";
|
|
5
|
-
const VALID_PROVIDERS = /* @__PURE__ */ new Set([
|
|
5
|
+
const VALID_PROVIDERS = /* @__PURE__ */ new Set([
|
|
6
|
+
"local",
|
|
7
|
+
"kernel",
|
|
8
|
+
"browserbase",
|
|
9
|
+
"libretto-cloud"
|
|
10
|
+
]);
|
|
11
|
+
const DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS = 6e4;
|
|
12
|
+
const LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS = 10 * 6e4;
|
|
6
13
|
function assertValidProviderName(value, source) {
|
|
7
14
|
if (!VALID_PROVIDERS.has(value)) {
|
|
8
15
|
throw new Error(
|
|
@@ -32,9 +39,7 @@ function getCloudProviderApi(name) {
|
|
|
32
39
|
case "browserbase":
|
|
33
40
|
return createBrowserbaseProvider();
|
|
34
41
|
case "libretto-cloud":
|
|
35
|
-
console.warn(
|
|
36
|
-
"Note: The libretto-cloud provider is in alpha."
|
|
37
|
-
);
|
|
42
|
+
console.warn("Note: The libretto-cloud provider is in alpha.");
|
|
38
43
|
return createLibrettoCloudProvider();
|
|
39
44
|
default:
|
|
40
45
|
throw new Error(
|
|
@@ -42,7 +47,11 @@ function getCloudProviderApi(name) {
|
|
|
42
47
|
);
|
|
43
48
|
}
|
|
44
49
|
}
|
|
50
|
+
function getProviderStartupTimeoutMs(providerName) {
|
|
51
|
+
return providerName === "libretto-cloud" ? LIBRETTO_CLOUD_STARTUP_TIMEOUT_MS : DEFAULT_PROVIDER_STARTUP_TIMEOUT_MS;
|
|
52
|
+
}
|
|
45
53
|
export {
|
|
46
54
|
getCloudProviderApi,
|
|
55
|
+
getProviderStartupTimeoutMs,
|
|
47
56
|
resolveProviderName
|
|
48
57
|
};
|
|
@@ -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/index.js
CHANGED
package/dist/cli/router.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { aiCommands } from "./commands/ai.js";
|
|
2
1
|
import { authCommands } from "./commands/auth.js";
|
|
3
2
|
import { billingCommands } from "./commands/billing.js";
|
|
4
3
|
import { browserCommands } from "./commands/browser.js";
|
|
@@ -9,15 +8,19 @@ import { setupCommand } from "./commands/setup.js";
|
|
|
9
8
|
import { statusCommand } from "./commands/status.js";
|
|
10
9
|
import { snapshotCommand } from "./commands/snapshot.js";
|
|
11
10
|
import { librettoCommand } from "../shared/package-manager.js";
|
|
12
|
-
import { SimpleCLI } from "
|
|
11
|
+
import { SimpleCLI } from "affordance";
|
|
13
12
|
const cliRoutes = {
|
|
14
13
|
...browserCommands,
|
|
15
|
-
|
|
14
|
+
cloud: SimpleCLI.group({
|
|
15
|
+
description: "Libretto Cloud commands",
|
|
16
|
+
routes: {
|
|
17
|
+
deploy: deployCommand,
|
|
18
|
+
auth: authCommands,
|
|
19
|
+
billing: billingCommands
|
|
20
|
+
}
|
|
21
|
+
}),
|
|
16
22
|
experiments: experimentsCommand,
|
|
17
23
|
...executionCommands,
|
|
18
|
-
ai: aiCommands,
|
|
19
|
-
auth: authCommands,
|
|
20
|
-
billing: billingCommands,
|
|
21
24
|
setup: setupCommand,
|
|
22
25
|
status: statusCommand,
|
|
23
26
|
snapshot: snapshotCommand
|
|
@@ -82,12 +82,12 @@ function wrapLocatorActions(locator, page, opts) {
|
|
|
82
82
|
try {
|
|
83
83
|
const result = await orig(...args);
|
|
84
84
|
if (opts.visualize) {
|
|
85
|
-
enqueue(page, () => visualizeAfterAction(page));
|
|
85
|
+
void enqueue(page, () => visualizeAfterAction(page));
|
|
86
86
|
}
|
|
87
87
|
return result;
|
|
88
88
|
} catch (err) {
|
|
89
89
|
if (opts.visualize) {
|
|
90
|
-
enqueue(page, () => visualizeAfterAction(page));
|
|
90
|
+
void enqueue(page, () => visualizeAfterAction(page));
|
|
91
91
|
}
|
|
92
92
|
if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
|
|
93
93
|
await enrichTimeoutError(err, locator, page);
|
|
@@ -226,12 +226,12 @@ async function installInstrumentation(page, options) {
|
|
|
226
226
|
try {
|
|
227
227
|
const result = await orig(...args);
|
|
228
228
|
if (visualize) {
|
|
229
|
-
enqueue(page, () => visualizeAfterAction(page));
|
|
229
|
+
void enqueue(page, () => visualizeAfterAction(page));
|
|
230
230
|
}
|
|
231
231
|
return result;
|
|
232
232
|
} catch (err) {
|
|
233
233
|
if (visualize) {
|
|
234
|
-
enqueue(page, () => visualizeAfterAction(page));
|
|
234
|
+
void enqueue(page, () => visualizeAfterAction(page));
|
|
235
235
|
}
|
|
236
236
|
if (POINTER_ACTIONS.has(method) && isTimeoutError(err) && typeof args[0] === "string") {
|
|
237
237
|
await enrichTimeoutError(err, page.locator(args[0]), page);
|
|
@@ -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/docs/releasing.md
CHANGED
|
@@ -24,7 +24,7 @@ This repo does not publish from local machines and does not push directly to `ma
|
|
|
24
24
|
|
|
25
25
|
GitHub Actions needs these repository secrets:
|
|
26
26
|
|
|
27
|
-
- `OPENAI_API_KEY`: used by the existing test suite during the release workflow.
|
|
27
|
+
- `OPENAI_API_KEY`: used by the existing test suite during the release workflow and by the eval workflow's default `openai/gpt-5.5` model.
|
|
28
28
|
|
|
29
29
|
The release workflow uses a GitHub Actions environment named `release`. Create that environment in the repository settings (no required reviewers — access is controlled by branch protection on `main` instead).
|
|
30
30
|
|
|
@@ -69,7 +69,7 @@ The root `scripts/prepare-release.sh` script does the following:
|
|
|
69
69
|
6. Commits the version bump.
|
|
70
70
|
7. Pushes the branch and opens a PR to `main` with the `release` label.
|
|
71
71
|
|
|
72
|
-
Release PRs also run the eval workflow. That workflow
|
|
72
|
+
Release PRs also run the eval workflow. That workflow records score, duration, token, cost, and tool-call metrics for review. Scores are informational: low scores do not fail the workflow, but setup/runtime failures and zero completed records do.
|
|
73
73
|
|
|
74
74
|
## Merge behavior
|
|
75
75
|
|
|
@@ -89,11 +89,13 @@ This makes the workflow safe to re-run after partial failures. For example, if n
|
|
|
89
89
|
|
|
90
90
|
`.github/workflows/evals.yml` now runs automatically for release PRs and for qualifying pushes to `main`.
|
|
91
91
|
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
92
|
+
- It runs `pnpm evals --no-auth --output <runner-temp>/eval-run` so CI only runs cases that do not require local auth profiles.
|
|
93
|
+
- It validates and renders the CI report with `pnpm evals summary <runner-temp>/eval-run`.
|
|
94
|
+
- It writes a GitHub step summary and a sticky PR comment with aggregate score, duration, token, cost, and tool-call metrics.
|
|
95
|
+
- It uploads summary files and per-case result records from the run output directory. Raw transcripts and local profile files are not uploaded.
|
|
96
|
+
- It fails when the eval runner crashes, required setup is missing, result records are malformed, or zero completed records are produced.
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
There is no baseline comparison gate yet. Add one only after the eval records and metrics are stable enough to compare reliably.
|
|
97
99
|
|
|
98
100
|
## Changelog behavior
|
|
99
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
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
|
},
|
|
@@ -37,6 +36,7 @@
|
|
|
37
36
|
"sync-skills": "pnpm run sync:mirrors",
|
|
38
37
|
"check:skills": "pnpm run check:mirrors",
|
|
39
38
|
"build": "tsup --config tsup.config.ts",
|
|
39
|
+
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
40
40
|
"type-check": "tsc --noEmit",
|
|
41
41
|
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
42
42
|
"test:vitest": "vitest run",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"vitest": "^4.1.5"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
+
"affordance": "^0.1.0",
|
|
88
89
|
"ai": "^6.0.116",
|
|
89
90
|
"esbuild": "^0.27.0",
|
|
90
91
|
"playwright": "^1.58.2",
|