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 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
- Examples:
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 --session <name>
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 (status === "building" && Date.now() - start < maxWaitMs) {
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/get", {
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 get deployment status (${res.status}): ${JSON.stringify(body)}`
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 value = void 0;
765
+ let found = false;
766
766
  for (const candidate of normalizedCandidates) {
767
767
  if (Object.prototype.hasOwnProperty.call(named, candidate)) {
768
- value = named[candidate];
768
+ output[key] = named[candidate];
769
+ found = true;
769
770
  break;
770
771
  }
771
772
  }
772
- output[key] = value;
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.2",
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
- Examples:
11
- libretto open https://linkedin.com
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
- Profiles:
47
- Profiles are saved to .libretto/profiles/<domain>.json (git-ignored)
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
- Sessions:
53
- Session state is stored in .libretto/sessions/<session>/state.json
54
- CLI logs are stored in .libretto/sessions/<session>/logs.jsonl
55
- Each session runs an isolated browser instance on a dynamic port.
56
- Session mode is stored per session as read-only or write-access.
57
- Use --read-only on open, connect, or run to create a read-only session.
58
- Session mode is enforced by Libretto commands, not by raw CDP clients outside Libretto.
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 --session <name>\nUsage: libretto close --all [--force]`,
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 (status === "building" && Date.now() - start < maxWaitMs) {
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/get", {
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 get deployment status (${res.status}): ${JSON.stringify(body)}`,
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 value: unknown = undefined;
1268
+ let found = false;
1269
1269
  for (const candidate of normalizedCandidates) {
1270
1270
  if (Object.prototype.hasOwnProperty.call(named, candidate)) {
1271
- value = named[candidate];
1271
+ output[key] = named[candidate];
1272
+ found = true;
1272
1273
  break;
1273
1274
  }
1274
1275
  }
1275
- output[key] = value;
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>;