libretto 0.6.2 → 0.6.3
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/cli.js +28 -49
- package/dist/cli/commands/browser.js +6 -2
- package/dist/cli/commands/deploy.js +16 -3
- package/dist/cli/framework/simple-cli.js +6 -3
- package/package.json +2 -1
- package/src/cli/cli.ts +29 -48
- package/src/cli/commands/browser.ts +6 -2
- package/src/cli/commands/deploy.ts +18 -3
- package/src/cli/framework/simple-cli.ts +6 -3
package/dist/cli/cli.js
CHANGED
|
@@ -1,62 +1,40 @@
|
|
|
1
|
+
import { resolveAiSetupStatus } from "./core/ai-model.js";
|
|
1
2
|
import { ensureLibrettoSetup } from "./core/context.js";
|
|
2
3
|
import { createCLIApp } from "./router.js";
|
|
4
|
+
import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
|
|
3
5
|
function renderUsage(app) {
|
|
4
6
|
return `${app.renderHelp()}
|
|
5
7
|
|
|
6
8
|
Options:
|
|
7
9
|
--session <name> Use a named session (auto-generated for open/run if omitted)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
libretto open https://linkedin.com
|
|
11
|
-
|
|
12
|
-
# ... manually log in ...
|
|
13
|
-
libretto save linkedin.com
|
|
14
|
-
# Next time you open linkedin.com, you'll be logged in automatically
|
|
15
|
-
|
|
16
|
-
libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
17
|
-
libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
18
|
-
libretto readonly-exec "return await page.title()" --session test1
|
|
19
|
-
libretto connect http://127.0.0.1:9222 --read-only --session test1
|
|
20
|
-
libretto run ./integration.ts --read-only --session test1
|
|
21
|
-
libretto status
|
|
22
|
-
libretto ai configure openai
|
|
23
|
-
libretto ai configure anthropic
|
|
24
|
-
libretto ai configure gemini
|
|
25
|
-
libretto ai configure vertex
|
|
26
|
-
libretto ai configure openai/gpt-4o
|
|
27
|
-
libretto snapshot
|
|
28
|
-
libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
29
|
-
libretto resume --session my-session
|
|
30
|
-
libretto close
|
|
31
|
-
libretto close --all
|
|
32
|
-
libretto close --all --force
|
|
33
|
-
|
|
34
|
-
# Multiple sessions
|
|
35
|
-
libretto open https://site1.com --session test1
|
|
36
|
-
libretto open https://site2.com --session test2
|
|
37
|
-
libretto exec "return await page.title()" --session test1
|
|
38
|
-
|
|
39
|
-
Available in exec:
|
|
40
|
-
page, context, state, browser, networkLog, actionLog
|
|
41
|
-
|
|
42
|
-
Available in readonly-exec:
|
|
43
|
-
page, state, snapshot, scrollBy, get
|
|
44
|
-
|
|
45
|
-
Profiles:
|
|
46
|
-
Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
|
|
47
|
-
They persist cookies, localStorage, and session data across browser launches.
|
|
48
|
-
Local profiles are machine-local and are not shared with other users/environments.
|
|
49
|
-
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
50
|
-
|
|
51
|
-
Sessions:
|
|
52
|
-
Session state is stored in .libretto/sessions/<session>/state.json
|
|
53
|
-
CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
|
|
54
|
-
Each session runs an isolated browser instance on a dynamic port.
|
|
55
|
-
Session mode is stored per session as read-only or write-access.
|
|
56
|
-
Use --read-only on open, connect, or run to create a read-only session.
|
|
57
|
-
Session mode is enforced by Libretto commands, not by raw CDP clients outside Libretto.
|
|
11
|
+
Docs (agent-friendly): https://libretto.sh/docs
|
|
58
12
|
`;
|
|
59
13
|
}
|
|
14
|
+
function printSetupAudit() {
|
|
15
|
+
warnIfInstalledSkillOutOfDate();
|
|
16
|
+
const status = resolveAiSetupStatus();
|
|
17
|
+
switch (status.kind) {
|
|
18
|
+
case "ready":
|
|
19
|
+
console.log(`\u2713 AI model: ${status.model}`);
|
|
20
|
+
break;
|
|
21
|
+
case "configured-missing-credentials":
|
|
22
|
+
console.log(
|
|
23
|
+
`\u2717 ${status.provider} configured (model: ${status.model}), but credentials are missing. Run \`npx libretto setup\` to repair.`
|
|
24
|
+
);
|
|
25
|
+
break;
|
|
26
|
+
case "invalid-config":
|
|
27
|
+
console.log(
|
|
28
|
+
`\u2717 AI config is invalid. Run \`npx libretto setup\` to reconfigure.`
|
|
29
|
+
);
|
|
30
|
+
break;
|
|
31
|
+
case "unconfigured":
|
|
32
|
+
console.log(
|
|
33
|
+
`\u2717 No AI model configured. Run \`npx libretto setup\` or \`npx libretto ai configure\` to set up.`
|
|
34
|
+
);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
60
38
|
function isRootHelpRequest(rawArgs) {
|
|
61
39
|
if (rawArgs.length === 0) return true;
|
|
62
40
|
if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
|
|
@@ -70,6 +48,7 @@ async function runLibrettoCLI() {
|
|
|
70
48
|
try {
|
|
71
49
|
if (isRootHelpRequest(rawArgs)) {
|
|
72
50
|
console.log(renderUsage(app));
|
|
51
|
+
printSetupAudit();
|
|
73
52
|
return;
|
|
74
53
|
}
|
|
75
54
|
const result = await app.run(rawArgs);
|
|
@@ -199,7 +199,11 @@ const sessionModeCommand = SimpleCLI.command({
|
|
|
199
199
|
console.log(`Session "${ctx.session}" mode set to ${nextState.mode}.`);
|
|
200
200
|
});
|
|
201
201
|
const closeInput = SimpleCLI.input({
|
|
202
|
-
positionals: [
|
|
202
|
+
positionals: [
|
|
203
|
+
SimpleCLI.positional("session", z.string().optional(), {
|
|
204
|
+
help: "Session name to close"
|
|
205
|
+
})
|
|
206
|
+
],
|
|
203
207
|
named: {
|
|
204
208
|
session: sessionOption(),
|
|
205
209
|
all: SimpleCLI.flag({
|
|
@@ -211,7 +215,7 @@ const closeInput = SimpleCLI.input({
|
|
|
211
215
|
}
|
|
212
216
|
}).refine(
|
|
213
217
|
(input) => input.all || input.session,
|
|
214
|
-
`Usage: libretto close
|
|
218
|
+
`Usage: libretto close <session>
|
|
215
219
|
Usage: libretto close --all [--force]`
|
|
216
220
|
);
|
|
217
221
|
const closeCommand = SimpleCLI.command({
|
|
@@ -32,27 +32,40 @@ async function postJson(apiUrl, apiKey, path, input = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
|
|
34
34
|
const start = Date.now();
|
|
35
|
+
const workflowWaitMs = 6e4;
|
|
35
36
|
let status = "building";
|
|
37
|
+
let workflows = null;
|
|
38
|
+
let readyAt = null;
|
|
36
39
|
let deployment;
|
|
37
|
-
while (
|
|
40
|
+
while (Date.now() - start < maxWaitMs) {
|
|
41
|
+
if (status !== "building" && status !== "ready") break;
|
|
42
|
+
if (status === "ready" && workflows?.length) break;
|
|
43
|
+
if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
|
|
38
44
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
39
|
-
const res = await postJson(apiUrl, apiKey, "/v1/deployments/
|
|
45
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
|
|
40
46
|
id: deploymentId
|
|
41
47
|
});
|
|
42
48
|
const body = await res.json();
|
|
43
49
|
if (res.status !== 200) {
|
|
44
50
|
throw new Error(
|
|
45
|
-
`Failed to
|
|
51
|
+
`Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`
|
|
46
52
|
);
|
|
47
53
|
}
|
|
48
54
|
status = body.json.status;
|
|
55
|
+
workflows = body.json.workflows;
|
|
49
56
|
deployment = body.json;
|
|
57
|
+
if (status === "ready" && readyAt === null) readyAt = Date.now();
|
|
50
58
|
process.stdout.write(".");
|
|
51
59
|
}
|
|
52
60
|
console.log();
|
|
53
61
|
if (!deployment) {
|
|
54
62
|
throw new Error("Deployment timed out before receiving a status update.");
|
|
55
63
|
}
|
|
64
|
+
if (status === "ready" && !workflows?.length) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Build completed but workflow discovery failed due to a server-side error. Please redeploy."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
56
69
|
return deployment;
|
|
57
70
|
}
|
|
58
71
|
const deployInput = SimpleCLI.input({
|
|
@@ -762,14 +762,17 @@ function buildInputNormalizer(definition) {
|
|
|
762
762
|
toKebabCase(key),
|
|
763
763
|
key
|
|
764
764
|
].filter((candidate) => candidate.length > 0);
|
|
765
|
-
let
|
|
765
|
+
let found = false;
|
|
766
766
|
for (const candidate of normalizedCandidates) {
|
|
767
767
|
if (Object.prototype.hasOwnProperty.call(named, candidate)) {
|
|
768
|
-
|
|
768
|
+
output[key] = named[candidate];
|
|
769
|
+
found = true;
|
|
769
770
|
break;
|
|
770
771
|
}
|
|
771
772
|
}
|
|
772
|
-
output
|
|
773
|
+
if (!found && !Object.prototype.hasOwnProperty.call(output, key)) {
|
|
774
|
+
output[key] = void 0;
|
|
775
|
+
}
|
|
773
776
|
}
|
|
774
777
|
return output;
|
|
775
778
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"homepage": "https://libretto.sh",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "https://github.com/saffron-health/libretto"
|
package/src/cli/cli.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { resolveAiSetupStatus } from "./core/ai-model.js";
|
|
1
2
|
import { ensureLibrettoSetup } from "./core/context.js";
|
|
2
3
|
import { createCLIApp } from "./router.js";
|
|
4
|
+
import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
|
|
3
5
|
|
|
4
6
|
function renderUsage(app: ReturnType<typeof createCLIApp>): string {
|
|
5
7
|
return `${app.renderHelp()}
|
|
@@ -7,56 +9,34 @@ function renderUsage(app: ReturnType<typeof createCLIApp>): string {
|
|
|
7
9
|
Options:
|
|
8
10
|
--session <name> Use a named session (auto-generated for open/run if omitted)
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# ... manually log in ...
|
|
14
|
-
libretto save linkedin.com
|
|
15
|
-
# Next time you open linkedin.com, you'll be logged in automatically
|
|
16
|
-
|
|
17
|
-
libretto exec "await page.locator('button:has-text(\\"Sign in\\")').click()"
|
|
18
|
-
libretto exec "await page.fill('input[name=\\"email\\"]', 'test@example.com')"
|
|
19
|
-
libretto readonly-exec "return await page.title()" --session test1
|
|
20
|
-
libretto connect http://127.0.0.1:9222 --read-only --session test1
|
|
21
|
-
libretto run ./integration.ts --read-only --session test1
|
|
22
|
-
libretto status
|
|
23
|
-
libretto ai configure openai
|
|
24
|
-
libretto ai configure anthropic
|
|
25
|
-
libretto ai configure gemini
|
|
26
|
-
libretto ai configure vertex
|
|
27
|
-
libretto ai configure openai/gpt-4o
|
|
28
|
-
libretto snapshot
|
|
29
|
-
libretto snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
30
|
-
libretto resume --session my-session
|
|
31
|
-
libretto close
|
|
32
|
-
libretto close --all
|
|
33
|
-
libretto close --all --force
|
|
34
|
-
|
|
35
|
-
# Multiple sessions
|
|
36
|
-
libretto open https://site1.com --session test1
|
|
37
|
-
libretto open https://site2.com --session test2
|
|
38
|
-
libretto exec "return await page.title()" --session test1
|
|
39
|
-
|
|
40
|
-
Available in exec:
|
|
41
|
-
page, context, state, browser, networkLog, actionLog
|
|
42
|
-
|
|
43
|
-
Available in readonly-exec:
|
|
44
|
-
page, state, snapshot, scrollBy, get
|
|
12
|
+
Docs (agent-friendly): https://libretto.sh/docs
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
45
15
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
They persist cookies, localStorage, and session data across browser launches.
|
|
49
|
-
Local profiles are machine-local and are not shared with other users/environments.
|
|
50
|
-
Sessions can expire; if run fails auth, log in again and re-save the profile.
|
|
16
|
+
function printSetupAudit(): void {
|
|
17
|
+
warnIfInstalledSkillOutOfDate();
|
|
51
18
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
19
|
+
const status = resolveAiSetupStatus();
|
|
20
|
+
switch (status.kind) {
|
|
21
|
+
case "ready":
|
|
22
|
+
console.log(`✓ AI model: ${status.model}`);
|
|
23
|
+
break;
|
|
24
|
+
case "configured-missing-credentials":
|
|
25
|
+
console.log(
|
|
26
|
+
`✗ ${status.provider} configured (model: ${status.model}), but credentials are missing. Run \`npx libretto setup\` to repair.`,
|
|
27
|
+
);
|
|
28
|
+
break;
|
|
29
|
+
case "invalid-config":
|
|
30
|
+
console.log(
|
|
31
|
+
`✗ AI config is invalid. Run \`npx libretto setup\` to reconfigure.`,
|
|
32
|
+
);
|
|
33
|
+
break;
|
|
34
|
+
case "unconfigured":
|
|
35
|
+
console.log(
|
|
36
|
+
`✗ No AI model configured. Run \`npx libretto setup\` or \`npx libretto ai configure\` to set up.`,
|
|
37
|
+
);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
function isRootHelpRequest(rawArgs: readonly string[]): boolean {
|
|
@@ -74,6 +54,7 @@ export async function runLibrettoCLI(): Promise<void> {
|
|
|
74
54
|
try {
|
|
75
55
|
if (isRootHelpRequest(rawArgs)) {
|
|
76
56
|
console.log(renderUsage(app));
|
|
57
|
+
printSetupAudit();
|
|
77
58
|
return;
|
|
78
59
|
}
|
|
79
60
|
|
|
@@ -242,7 +242,11 @@ export const sessionModeCommand = SimpleCLI.command({
|
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
export const closeInput = SimpleCLI.input({
|
|
245
|
-
positionals: [
|
|
245
|
+
positionals: [
|
|
246
|
+
SimpleCLI.positional("session", z.string().optional(), {
|
|
247
|
+
help: "Session name to close",
|
|
248
|
+
}),
|
|
249
|
+
],
|
|
246
250
|
named: {
|
|
247
251
|
session: sessionOption(),
|
|
248
252
|
all: SimpleCLI.flag({
|
|
@@ -254,7 +258,7 @@ export const closeInput = SimpleCLI.input({
|
|
|
254
258
|
},
|
|
255
259
|
}).refine(
|
|
256
260
|
(input) => input.all || input.session,
|
|
257
|
-
`Usage: libretto close
|
|
261
|
+
`Usage: libretto close <session>\nUsage: libretto close --all [--force]`,
|
|
258
262
|
);
|
|
259
263
|
|
|
260
264
|
export const closeCommand = SimpleCLI.command({
|
|
@@ -60,23 +60,32 @@ async function pollDeployment(
|
|
|
60
60
|
maxWaitMs: number,
|
|
61
61
|
): Promise<DeploymentResponse["json"]> {
|
|
62
62
|
const start = Date.now();
|
|
63
|
+
const workflowWaitMs = 60_000;
|
|
63
64
|
let status: DeploymentStatus = "building";
|
|
65
|
+
let workflows: string[] | null | undefined = null;
|
|
66
|
+
let readyAt: number | null = null;
|
|
64
67
|
let deployment: DeploymentResponse["json"] | undefined;
|
|
65
68
|
|
|
66
|
-
while (
|
|
69
|
+
while (Date.now() - start < maxWaitMs) {
|
|
70
|
+
if (status !== "building" && status !== "ready") break;
|
|
71
|
+
if (status === "ready" && workflows?.length) break;
|
|
72
|
+
if (status === "ready" && readyAt && Date.now() - readyAt > workflowWaitMs) break;
|
|
73
|
+
|
|
67
74
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
68
75
|
|
|
69
|
-
const res = await postJson(apiUrl, apiKey, "/v1/deployments/
|
|
76
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/sync", {
|
|
70
77
|
id: deploymentId,
|
|
71
78
|
});
|
|
72
79
|
const body = (await res.json()) as DeploymentResponse;
|
|
73
80
|
if (res.status !== 200) {
|
|
74
81
|
throw new Error(
|
|
75
|
-
`Failed to
|
|
82
|
+
`Failed to sync deployment status (${res.status}): ${JSON.stringify(body)}`,
|
|
76
83
|
);
|
|
77
84
|
}
|
|
78
85
|
status = body.json.status;
|
|
86
|
+
workflows = body.json.workflows;
|
|
79
87
|
deployment = body.json;
|
|
88
|
+
if (status === "ready" && readyAt === null) readyAt = Date.now();
|
|
80
89
|
process.stdout.write(".");
|
|
81
90
|
}
|
|
82
91
|
console.log();
|
|
@@ -85,6 +94,12 @@ async function pollDeployment(
|
|
|
85
94
|
throw new Error("Deployment timed out before receiving a status update.");
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
if (status === "ready" && !workflows?.length) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Build completed but workflow discovery failed due to a server-side error. Please redeploy.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
88
103
|
return deployment;
|
|
89
104
|
}
|
|
90
105
|
|
|
@@ -1265,14 +1265,17 @@ function buildInputNormalizer<
|
|
|
1265
1265
|
key,
|
|
1266
1266
|
].filter((candidate) => candidate.length > 0);
|
|
1267
1267
|
|
|
1268
|
-
let
|
|
1268
|
+
let found = false;
|
|
1269
1269
|
for (const candidate of normalizedCandidates) {
|
|
1270
1270
|
if (Object.prototype.hasOwnProperty.call(named, candidate)) {
|
|
1271
|
-
|
|
1271
|
+
output[key] = named[candidate];
|
|
1272
|
+
found = true;
|
|
1272
1273
|
break;
|
|
1273
1274
|
}
|
|
1274
1275
|
}
|
|
1275
|
-
output
|
|
1276
|
+
if (!found && !Object.prototype.hasOwnProperty.call(output, key)) {
|
|
1277
|
+
output[key] = undefined;
|
|
1278
|
+
}
|
|
1276
1279
|
}
|
|
1277
1280
|
|
|
1278
1281
|
return output as InputObjectFor<TPositionals, TNamed>;
|