libretto 0.6.16 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/cli/cli.js +32 -13
  2. package/dist/cli/commands/browser.js +2 -2
  3. package/dist/cli/commands/execution.js +1 -1
  4. package/dist/cli/commands/search.js +69 -0
  5. package/dist/cli/commands/update.js +122 -0
  6. package/dist/cli/core/context.js +4 -0
  7. package/dist/cli/core/daemon/daemon.js +3 -0
  8. package/dist/cli/core/experiments.js +14 -1
  9. package/dist/cli/core/providers/index.js +5 -1
  10. package/dist/cli/core/providers/steel.js +56 -0
  11. package/dist/cli/core/session-telemetry.js +143 -7
  12. package/dist/cli/core/skill-version.js +1 -0
  13. package/dist/cli/router.js +14 -3
  14. package/dist/shared/html-search/search-html.d.ts +9 -0
  15. package/dist/shared/html-search/search-html.js +46 -0
  16. package/dist/shared/html-search/search-html.spec.d.ts +2 -0
  17. package/dist/shared/html-search/search-html.spec.js +57 -0
  18. package/docs/releasing.md +3 -9
  19. package/package.json +2 -2
  20. package/scripts/generate-changelog.ts +207 -12
  21. package/skills/libretto/SKILL.md +22 -15
  22. package/skills/libretto/references/code-generation-rules.md +2 -2
  23. package/skills/libretto/references/configuration-file-reference.md +3 -2
  24. package/skills/libretto-readonly/SKILL.md +1 -1
  25. package/src/cli/cli.ts +38 -13
  26. package/src/cli/commands/browser.ts +2 -3
  27. package/src/cli/commands/execution.ts +1 -1
  28. package/src/cli/commands/search.ts +74 -0
  29. package/src/cli/commands/update.ts +149 -0
  30. package/src/cli/core/context.ts +4 -0
  31. package/src/cli/core/daemon/daemon.ts +3 -0
  32. package/src/cli/core/experiments.ts +15 -1
  33. package/src/cli/core/providers/index.ts +5 -1
  34. package/src/cli/core/providers/steel.ts +75 -0
  35. package/src/cli/core/session-telemetry.ts +176 -13
  36. package/src/cli/core/skill-version.ts +1 -1
  37. package/src/cli/core/telemetry.ts +19 -3
  38. package/src/cli/router.ts +13 -2
  39. package/src/shared/html-search/search-html.spec.ts +65 -0
  40. package/src/shared/html-search/search-html.ts +75 -0
@@ -0,0 +1,46 @@
1
+ import { condenseDom } from "../condense-dom/condense-dom.js";
2
+ const DEFAULT_CONTEXT_LINES = 4;
3
+ const DEFAULT_MATCH_LIMIT = 8;
4
+ function formatHtmlForSearch(html) {
5
+ const condensed = condenseDom(html).html;
6
+ const separated = condensed.replace(/>\s+</g, ">\n<").replace(/(<[^/!][^>]*>)([^<\n][\s\S]*?)(<\/[^>]+>)/g, "$1\n$2\n$3");
7
+ const lines = separated.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
8
+ let indent = 0;
9
+ return lines.map((line) => {
10
+ if (/^<\//.test(line)) indent = Math.max(0, indent - 1);
11
+ const formatted = `${" ".repeat(indent)}${line}`;
12
+ if (isOpeningTag(line)) indent += 1;
13
+ return formatted;
14
+ }).join("\n");
15
+ }
16
+ function searchFormattedHtml(formattedHtml, pattern, contextLines = DEFAULT_CONTEXT_LINES, matchLimit = DEFAULT_MATCH_LIMIT) {
17
+ const regex = new RegExp(pattern);
18
+ const lines = formattedHtml.split("\n");
19
+ const matchingIndexes = lines.map((line, index) => regex.test(line) ? index : -1).filter((index) => index >= 0).slice(0, matchLimit);
20
+ const matches = [];
21
+ for (const matchingIndex of matchingIndexes) {
22
+ const startLine = Math.max(0, matchingIndex - contextLines);
23
+ const endLine = Math.min(lines.length - 1, matchingIndex + contextLines);
24
+ const previous = matches.at(-1);
25
+ if (previous && startLine <= previous.endLine) {
26
+ previous.endLine = Math.max(previous.endLine, endLine + 1);
27
+ previous.lines = lines.slice(previous.startLine - 1, previous.endLine);
28
+ continue;
29
+ }
30
+ matches.push({
31
+ startLine: startLine + 1,
32
+ endLine: endLine + 1,
33
+ lines: lines.slice(startLine, endLine + 1)
34
+ });
35
+ }
36
+ return matches;
37
+ }
38
+ function isOpeningTag(line) {
39
+ return /^<[^/!?][^>]*>$/.test(line) && !/\/>$/.test(line) && !/^<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)\b/i.test(
40
+ line
41
+ ) && !/^<[^>]+>.*<\/[^>]+>$/.test(line);
42
+ }
43
+ export {
44
+ formatHtmlForSearch,
45
+ searchFormattedHtml
46
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { formatHtmlForSearch, searchFormattedHtml } from "./search-html.js";
3
+ describe("HTML search", () => {
4
+ test("formats condensed HTML before searching", () => {
5
+ const formatted = formatHtmlForSearch(
6
+ '<!doctype html><html><body><main><p data-testid="target">Needle</p></main></body></html>'
7
+ );
8
+ expect(formatted).toContain('<p data-testid="target">');
9
+ expect(formatted).toContain("Needle");
10
+ });
11
+ test("returns merged matching regions with context", () => {
12
+ const formatted = [
13
+ "<html>",
14
+ "<body>",
15
+ "<main>",
16
+ "<section>",
17
+ "<h1>Heading</h1>",
18
+ '<p class="target">Needle</p>',
19
+ "<p>More content</p>",
20
+ "</section>",
21
+ "</main>",
22
+ "</body>",
23
+ "</html>"
24
+ ].join("\n");
25
+ const matches = searchFormattedHtml(formatted, "Needle", 2);
26
+ expect(matches).toEqual([
27
+ {
28
+ startLine: 4,
29
+ endLine: 8,
30
+ lines: [
31
+ "<section>",
32
+ "<h1>Heading</h1>",
33
+ '<p class="target">Needle</p>',
34
+ "<p>More content</p>",
35
+ "</section>"
36
+ ]
37
+ }
38
+ ]);
39
+ });
40
+ test("limits matching regions before adding context", () => {
41
+ const formatted = Array.from(
42
+ { length: 12 },
43
+ (_value, index) => `<p>Needle ${index}</p>`
44
+ ).join("\n");
45
+ const matches = searchFormattedHtml(formatted, "Needle", 0, 8);
46
+ expect(matches).toEqual([
47
+ {
48
+ startLine: 1,
49
+ endLine: 8,
50
+ lines: Array.from(
51
+ { length: 8 },
52
+ (_value, index) => `<p>Needle ${index}</p>`
53
+ )
54
+ }
55
+ ]);
56
+ });
57
+ });
package/docs/releasing.md CHANGED
@@ -47,7 +47,7 @@ The workflow needs `contents: write` to create the GitHub release and tag, and `
47
47
 
48
48
  After trusted publishing is working, remove any old npm publish token from the repo secrets. npm recommends restricting token-based publishing after the migration.
49
49
 
50
- GitHub release notes are auto-generated from merged pull requests. The release note categories live in `.github/release.yml`, so PR labels control where entries show up in the changelog.
50
+ GitHub release notes are generated by `packages/libretto/scripts/generate-changelog.ts`. The script finds the previous release, compares that tag to the release commit, and passes only the merged PRs in that range to the release notes agent.
51
51
 
52
52
  ## Prepare a release PR
53
53
 
@@ -101,15 +101,9 @@ There is no baseline comparison gate yet. Add one only after the eval records an
101
101
 
102
102
  The GitHub Releases page is the changelog for this repo.
103
103
 
104
- When the workflow runs `gh release create ... --generate-notes`, GitHub builds the release notes from the merged PRs since the previous release. `.github/release.yml` groups PRs into sections such as Features, Fixes, and Documentation.
104
+ When the workflow runs `pnpm generate-changelog vX.Y.Z`, the script finds the previous GitHub release, compares that tag to the release commit, and extracts PR numbers from the compare commits. The release notes agent can inspect only those PRs with `gh pr view` and `gh pr diff`.
105
105
 
106
- Today the categories map directly to labels that already exist in the repo:
107
-
108
- - `enhancement` -> Features
109
- - `bug` -> Fixes
110
- - `documentation` -> Documentation
111
-
112
- To keep release notes readable, use clear PR titles and apply one of those labels before merging. If a PR should not appear in the changelog, add the `skip-changelog` label.
106
+ To keep release notes readable, use clear PR titles and descriptions before merging. If a PR should not appear in the changelog, add the `skip-changelog` label.
113
107
 
114
108
  ## Notes
115
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -85,7 +85,7 @@
85
85
  "vitest": "^4.1.5"
86
86
  },
87
87
  "dependencies": {
88
- "affordance": "^0.1.0",
88
+ "affordance": "workspace:^",
89
89
  "ai": "^6.0.116",
90
90
  "esbuild": "^0.27.0",
91
91
  "playwright": "^1.58.2",
@@ -15,8 +15,195 @@ if (!process.env.ANTHROPIC_API_KEY) {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
- const ALLOWED_GH_SUBCOMMANDS = new Set(["pr", "release", "repo", "issue"]);
19
- const ALLOWED_ACTIONS = new Set(["list", "view", "diff", "status", "checks"]);
18
+ const ALLOWED_GH_SUBCOMMANDS = new Set(["pr"]);
19
+ const ALLOWED_ACTIONS = new Set(["view", "diff"]);
20
+ const SQUASH_MERGE_PR_NUMBER_PATTERN = /\(#(?<number>\d+)\)\s*$/;
21
+ const MERGE_COMMIT_PR_NUMBER_PATTERN = /^Merge pull request #(?<number>\d+) /;
22
+
23
+ interface GitHubRelease {
24
+ tagName: string;
25
+ isDraft?: boolean;
26
+ publishedAt?: string;
27
+ }
28
+
29
+ interface CompareCommit {
30
+ commit: {
31
+ message: string;
32
+ };
33
+ }
34
+
35
+ interface CompareResponse {
36
+ commits: CompareCommit[];
37
+ }
38
+
39
+ interface PullRequestLabel {
40
+ name: string;
41
+ }
42
+
43
+ interface PullRequestFile {
44
+ path: string;
45
+ }
46
+
47
+ interface PullRequestDetails {
48
+ number: number;
49
+ title: string;
50
+ body?: string | null;
51
+ mergedAt?: string | null;
52
+ url: string;
53
+ labels: PullRequestLabel[];
54
+ files: PullRequestFile[];
55
+ }
56
+
57
+ interface ReleaseContext {
58
+ previousTag: string;
59
+ currentRef: string;
60
+ pullRequests: PullRequestDetails[];
61
+ }
62
+
63
+ function runGh(args: string[]): string {
64
+ return execFileSync("gh", args, {
65
+ encoding: "utf8",
66
+ timeout: 300_000,
67
+ maxBuffer: 1024 * 1024,
68
+ });
69
+ }
70
+
71
+ function runGit(args: string[]): string {
72
+ return execFileSync("git", args, {
73
+ encoding: "utf8",
74
+ timeout: 300_000,
75
+ maxBuffer: 1024 * 1024,
76
+ }).trim();
77
+ }
78
+
79
+ function parseJson<T>(json: string): T {
80
+ return JSON.parse(json) as T;
81
+ }
82
+
83
+ function getRepoNameWithOwner(): string {
84
+ if (process.env.GITHUB_REPOSITORY) {
85
+ return process.env.GITHUB_REPOSITORY;
86
+ }
87
+
88
+ return runGh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
89
+ }
90
+
91
+ function getPreviousReleaseTag(currentTag: string): string {
92
+ const releases = parseJson<GitHubRelease[]>(
93
+ runGh(["release", "list", "--limit", "50", "--json", "tagName,isDraft,publishedAt"]),
94
+ ).filter((release) => !release.isDraft);
95
+
96
+ const currentIndex = releases.findIndex((release) => release.tagName === currentTag);
97
+ if (currentIndex >= 0) {
98
+ const previousRelease = releases[currentIndex + 1];
99
+ if (previousRelease) {
100
+ return previousRelease.tagName;
101
+ }
102
+ }
103
+
104
+ const previousRelease = releases.find((release) => release.tagName !== currentTag);
105
+ if (previousRelease) {
106
+ return previousRelease.tagName;
107
+ }
108
+
109
+ throw new Error(`Could not find a previous GitHub release before ${currentTag}.`);
110
+ }
111
+
112
+ function getCurrentRef(currentTag: string): string {
113
+ try {
114
+ const release = parseJson<{ targetCommitish: string }>(
115
+ runGh(["release", "view", currentTag, "--json", "targetCommitish"]),
116
+ );
117
+ if (release.targetCommitish) {
118
+ return release.targetCommitish;
119
+ }
120
+ } catch {
121
+ // The release does not exist yet when this runs in the release workflow.
122
+ }
123
+
124
+ if (process.env.GITHUB_SHA) {
125
+ return process.env.GITHUB_SHA;
126
+ }
127
+
128
+ return runGit(["rev-parse", "HEAD"]);
129
+ }
130
+
131
+ function collectMergedPullRequestNumbers(repoNameWithOwner: string, previousTag: string, currentRef: string): number[] {
132
+ const compare = parseJson<CompareResponse>(
133
+ runGh(["api", `repos/${repoNameWithOwner}/compare/${previousTag}...${currentRef}`]),
134
+ );
135
+ const numbers: number[] = [];
136
+ const seen = new Set<number>();
137
+
138
+ for (const compareCommit of compare.commits) {
139
+ const firstLine = compareCommit.commit.message.split("\n", 1)[0] ?? "";
140
+ const match = SQUASH_MERGE_PR_NUMBER_PATTERN.exec(firstLine) ?? MERGE_COMMIT_PR_NUMBER_PATTERN.exec(firstLine);
141
+ if (!match?.groups?.number) {
142
+ continue;
143
+ }
144
+
145
+ const number = Number(match.groups.number);
146
+ if (!seen.has(number)) {
147
+ seen.add(number);
148
+ numbers.push(number);
149
+ }
150
+ }
151
+
152
+ return numbers;
153
+ }
154
+
155
+ function getPullRequestDetails(number: number): PullRequestDetails {
156
+ return parseJson<PullRequestDetails>(
157
+ runGh([
158
+ "pr",
159
+ "view",
160
+ String(number),
161
+ "--json",
162
+ "number,title,body,mergedAt,url,labels,files",
163
+ ]),
164
+ );
165
+ }
166
+
167
+ function shouldIncludePullRequest(pr: PullRequestDetails): boolean {
168
+ const labelNames = new Set(pr.labels.map((label) => label.name));
169
+ if (labelNames.has("release") || labelNames.has("skip-changelog")) {
170
+ return false;
171
+ }
172
+
173
+ return !pr.title.toLowerCase().startsWith("release:");
174
+ }
175
+
176
+ function buildReleaseContext(currentTag: string): ReleaseContext {
177
+ const repoNameWithOwner = getRepoNameWithOwner();
178
+ const previousTag = getPreviousReleaseTag(currentTag);
179
+ const currentRef = getCurrentRef(currentTag);
180
+ const pullRequests = collectMergedPullRequestNumbers(repoNameWithOwner, previousTag, currentRef)
181
+ .map((number) => getPullRequestDetails(number))
182
+ .filter(shouldIncludePullRequest);
183
+
184
+ if (pullRequests.length === 0) {
185
+ throw new Error(`No changelog-eligible PRs found in ${previousTag}...${currentRef}.`);
186
+ }
187
+
188
+ return { previousTag, currentRef, pullRequests };
189
+ }
190
+
191
+ const releaseContext = buildReleaseContext(tag);
192
+ const allowedPullRequestNumbers = new Set(releaseContext.pullRequests.map((pr) => String(pr.number)));
193
+ const pullRequestSummary = releaseContext.pullRequests
194
+ .map((pr) => {
195
+ const labels = pr.labels.map((label) => label.name).join(", ") || "none";
196
+ const files = pr.files.map((file) => file.path).join(", ") || "none";
197
+ return [
198
+ `PR #${pr.number}: ${pr.title}`,
199
+ `Merged at: ${pr.mergedAt ?? "unknown"}`,
200
+ `Labels: ${labels}`,
201
+ `Files: ${files}`,
202
+ `Body:\n${pr.body?.trim() || "(empty)"}`,
203
+ ].join("\n");
204
+ })
205
+ .join("\n\n");
206
+
20
207
  const GhToolParamsSchema = Type.Object({
21
208
  args: Type.String({ description: "Arguments to pass to gh (without the leading 'gh')" }),
22
209
  });
@@ -28,9 +215,9 @@ const ghTool: AgentTool = {
28
215
  label: "GitHub CLI",
29
216
  description: [
30
217
  "Run a read-only GitHub CLI command. The arguments are passed directly to `gh`.",
31
- "Examples: 'release list --limit 5', 'pr list --state merged --json number,title',",
218
+ "Examples:",
32
219
  "'pr view 128 --json title,body,files', 'pr diff 128'.",
33
- "Only read operations are allowed (list, view, diff, etc.). Mutating commands will be rejected.",
220
+ "Only the release PRs precomputed by the changelog harness can be inspected.",
34
221
  ].join(" "),
35
222
  parameters: GhToolParamsSchema,
36
223
  execute: async (_toolCallId: string, rawParams: unknown) => {
@@ -48,12 +235,16 @@ const ghTool: AgentTool = {
48
235
  throw new Error(`Action '${action}' is not allowed. Allowed: ${[...ALLOWED_ACTIONS].join(", ")}`);
49
236
  }
50
237
 
238
+ if (subcommand === "pr" && (action === "view" || action === "diff")) {
239
+ const number = parts[2];
240
+ if (!number || !allowedPullRequestNumbers.has(number)) {
241
+ throw new Error(
242
+ `PR #${number ?? "(missing)"} is outside the ${releaseContext.previousTag}...${releaseContext.currentRef} release range.`,
243
+ );
244
+ }
245
+ }
51
246
  try {
52
- const output = execFileSync("gh", parts, {
53
- encoding: "utf8",
54
- timeout: 300_000,
55
- maxBuffer: 1024 * 1024,
56
- });
247
+ const output = runGh(parts);
57
248
  return { content: [{ type: "text", text: output }], details: {} };
58
249
  } catch (err) {
59
250
  const message = err instanceof Error ? err.message : String(err);
@@ -67,10 +258,14 @@ const agent = new Agent({
67
258
  systemPrompt: [
68
259
  `Generate release notes for the ${tag} release of Libretto.`,
69
260
  "",
70
- "Use the gh tool to explore what changed since the previous release.",
261
+ `The release range is ${releaseContext.previousTag}...${releaseContext.currentRef}.`,
262
+ "The changelog harness has already found the merged PRs in that exact range.",
263
+ "Only write about the PRs listed below. Do not mention open PRs, branches, issues, or other work outside this list.",
264
+ "",
265
+ pullRequestSummary,
266
+ "",
267
+ "Use the gh tool to inspect the listed PRs before writing notes.",
71
268
  "Useful queries:",
72
- "- 'release list --limit 5' to find the previous release tag",
73
- "- 'pr list --state merged --limit 50 --json number,title,body,labels' to find merged PRs",
74
269
  "- 'pr diff NUMBER' to see the full diff of a PR (base to head, not individual commits)",
75
270
  "- 'pr view NUMBER --json title,body,files' to see PR details",
76
271
  "",
@@ -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.16"
7
+ version: "0.6.17"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -25,14 +25,17 @@ Full documentation is published at [libretto.sh](https://libretto.sh). Available
25
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
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
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
+ - 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), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
29
29
 
30
30
  ## Default Integration Approach
31
31
 
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.
32
+ 1. Call the site's fetch/XHR endpoints via browser-context `fetch()`.
33
+ 2. If `references/site-security-review.md` (assess only once per site) rules `fetch()` unsafe, passively capture responses with `page.on('response', ...)`
34
+ 3. Fall back to Playwright UI automation.
35
+
36
+ Mix strategies freely across steps on a site.
37
+
38
+ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first navigation — deep URLs on a cold session are commonly blocked by edge bot protection.
36
39
 
37
40
  ## Setup
38
41
 
@@ -67,9 +70,9 @@ Full documentation is published at [libretto.sh](https://libretto.sh). Available
67
70
  - Pass `--read-only` when you want the session locked for inspection from the moment it is created.
68
71
 
69
72
  ```bash
70
- libretto open https://example.com --headed
71
- libretto open https://example.com --headless --read-only --session readonly-example
72
- libretto open https://example.com --headless --session debug-example
73
+ npx libretto open https://example.com
74
+ npx libretto open https://example.com --read-only --session readonly-example
75
+ npx libretto open https://example.com --session debug-example
73
76
  ```
74
77
 
75
78
  ### `connect`
@@ -141,9 +144,8 @@ libretto exec --session debug-example --page <page-id> "await page.url()"
141
144
 
142
145
  ### `run`
143
146
 
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.
147
+ - 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. Plain `run` defaults to headed mode.
145
148
  - Workflows define their input shape with a Zod schema (see `references/code-generation-rules.md`). `run` validates `--params` against that schema before calling the handler and prints a clear field-by-field error if the input doesn't match.
146
- - 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.
147
149
  - 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`.
148
150
  - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
149
151
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
@@ -195,7 +197,7 @@ Session logs are JSONL files at `.libretto/sessions/<session>/`:
195
197
 
196
198
  - CLI logs are in `.libretto/sessions/<session>/logs.jsonl`.
197
199
  - Action logs are in `.libretto/sessions/<session>/actions.jsonl`.
198
- - Network logs are in `.libretto/sessions/<session>/network.jsonl`.
200
+ - Network logs are in `.libretto/sessions/<session>/network.jsonl` and their corresponding raw request/response bodies are in `.libretto/sessions/<session>/raw-network/`.
199
201
 
200
202
  Use `jq` to query jsonl logs directly — for any filtering, slicing, or inspection task.
201
203
 
@@ -203,8 +205,11 @@ Use `jq` to query jsonl logs directly — for any filtering, slicing, or inspect
203
205
  # Last 20 action entries
204
206
  tail -n 20 .libretto/sessions/<session>/actions.jsonl | jq .
205
207
 
206
- # POST requests only
207
- jq 'select(.method == "POST")' .libretto/sessions/<session>/network.jsonl
208
+ # POST requests with captured response previews
209
+ jq 'select(.method == "POST" and .responseBodyPreview != null) | {id, resourceType, status, contentType, url, requestBodyPreview, responseBodyPreview, responseBodyPath}' .libretto/sessions/<session>/network.jsonl
210
+
211
+ # Read a saved response sidecar
212
+ gunzip -c .libretto/sessions/<session>/raw-network/000001.response.json.gz | jq .
208
213
  ```
209
214
 
210
215
  ### Action log (`actions.jsonl`)
@@ -215,7 +220,9 @@ Read `references/action-logs.md` for full field descriptions and user-vs-agent e
215
220
 
216
221
  ### Network log (`network.jsonl`)
217
222
 
218
- Key fields: `ts` (ISO timestamp), `method` (HTTP method, e.g. `GET`, `POST`), `url` (request URL), `status` (HTTP status code), `contentType` (response content type), `responseBody` (response body string, may be null).
223
+ Libretto logs useful `document`, `xhr`, `fetch`, and non-noisy mutating requests; obvious static/media/tracking noise is skipped.
224
+
225
+ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page target id), `method` (HTTP method, e.g. `GET`, `POST`), `url` (request URL), `resourceType` (browser resource type, e.g. `document`, `xhr`, `fetch`), `status` (HTTP status code, or null for failed requests), `statusText` (HTTP status text), `contentType` (response content type), `requestHeaders` (request headers), `responseHeaders` (response headers), `requestBodyPreview` (inline request body preview), `requestBodyPath` (gzipped full request body sidecar path), `requestBodyBytes` (request body byte size), `requestBodyTruncated` (true when the request body exceeded the save limit), `requestBodyOmittedReason` (why the request body was not captured), `responseBodyPreview` (inline response body preview), `responseBodyPath` (gzipped full response body sidecar path), `responseBodyBytes` (response body byte size), `responseBodyTruncated` (true when the response body exceeded the save limit), `responseBodyOmittedReason` (why the response body was not captured), and `errorText` (request failure or body read error).
219
226
 
220
227
  ## Examples
221
228
 
@@ -6,7 +6,7 @@ Follow the user's existing codebase conventions, abstractions, and patterns when
6
6
 
7
7
  ## Workflow File Structure
8
8
 
9
- Generated files must default-export a `workflow()` instance so they can be run via `npx libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
9
+ Generated files must default-export a `workflow()` instance so they can be run via `libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
10
10
 
11
11
  Add `zod` (`^4.0.0`) to the workflow's `package.json` dependencies. Then import `workflow` from `"libretto"` and `z` from `"zod"`:
12
12
 
@@ -42,7 +42,7 @@ Key points:
42
42
 
43
43
  - `workflow(name, { input, output }, handler)` takes a unique workflow name, a pair of Zod schemas describing input and output, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
44
44
  - At run time, Libretto validates `input` against `inputSchema` before calling the handler. Invalid input throws a clear error listing each failing field; the workflow handler never sees malformed input.
45
- - `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
45
+ - `libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
46
46
  - `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
47
47
  - `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI, then gets parsed through `inputSchema`.
48
48
  - Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
@@ -6,7 +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
+ - You want a persistent default browser provider, such as Kernel, Browserbase, or Steel.
10
10
 
11
11
  ## File Location
12
12
 
@@ -18,7 +18,7 @@ Libretto reads workspace config from `.libretto/config.json`.
18
18
 
19
19
  ## Supported Settings
20
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"`.
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"`, `"steel"`, or `"libretto-cloud"`.
22
22
  - Provider precedence is: CLI `--provider`, then `LIBRETTO_PROVIDER`, then `.libretto/config.json`, then `"local"`.
23
23
  - Provider credentials belong in the repo root `.env` file, which Libretto loads automatically before running CLI commands.
24
24
  - `viewport` is an optional top-level setting used by `open` and `run` when you do not pass `--viewport`.
@@ -46,6 +46,7 @@ libretto setup # first-time onboarding
46
46
  libretto status # inspect open sessions
47
47
  libretto open https://example.com --provider kernel
48
48
  libretto run ./integration.ts --provider browserbase
49
+ libretto open https://example.com --provider steel
49
50
  libretto open https://example.com --viewport 1440x900
50
51
  libretto run ./integration.ts --viewport 1440x900
51
52
  ```
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.16"
7
+ version: "0.6.17"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
package/src/cli/cli.ts CHANGED
@@ -1,16 +1,13 @@
1
1
  import { ensureLibrettoSetup } from "./core/context.js";
2
2
  import { createCLIApp } from "./router.js";
3
- import { warnIfInstalledSkillOutOfDate } from "./core/skill-version.js";
3
+ import {
4
+ readCurrentCliVersion,
5
+ warnIfInstalledSkillOutOfDate,
6
+ } from "./core/skill-version.js";
4
7
  import { loadEnv } from "../shared/env/load-env.js";
5
8
 
6
- function renderUsage(app: ReturnType<typeof createCLIApp>): string {
7
- return `${app.renderHelp()}
8
-
9
- Options:
10
- --session <name> Use a named session (auto-generated for open/run if omitted)
11
-
12
- Docs (agent-friendly): https://libretto.sh/docs
13
- `;
9
+ function renderVersion(): string {
10
+ return readCurrentCliVersion();
14
11
  }
15
12
 
16
13
  function printSetupAudit(): void {
@@ -35,10 +32,25 @@ function warnIfPackageManagerExec(): void {
35
32
 
36
33
  function isRootHelpRequest(rawArgs: readonly string[]): boolean {
37
34
  if (rawArgs.length === 0) return true;
38
- if (rawArgs[0] === "--help" || rawArgs[0] === "-h") return true;
39
35
  return rawArgs[0] === "help" && rawArgs.length === 1;
40
36
  }
41
37
 
38
+ function isVersionRequest(rawArgs: readonly string[]): boolean {
39
+ if (rawArgs.length !== 1) return false;
40
+ return rawArgs[0] === "--version" || rawArgs[0] === "-v";
41
+ }
42
+
43
+ function hasRootHelp(
44
+ message: string,
45
+ app: ReturnType<typeof createCLIApp>,
46
+ ): boolean {
47
+ return message.endsWith(app.renderHelp());
48
+ }
49
+
50
+ function hasScopedHelp(message: string): boolean {
51
+ return message.includes("\nUsage: ");
52
+ }
53
+
42
54
  export async function runLibrettoCLI(): Promise<void> {
43
55
  const rawArgs = process.argv.slice(2);
44
56
  let exitCode = 0;
@@ -48,8 +60,13 @@ export async function runLibrettoCLI(): Promise<void> {
48
60
  const app = createCLIApp();
49
61
 
50
62
  try {
63
+ if (isVersionRequest(rawArgs)) {
64
+ console.log(renderVersion());
65
+ return;
66
+ }
67
+
51
68
  if (isRootHelpRequest(rawArgs)) {
52
- console.log(renderUsage(app));
69
+ console.log(app.renderHelp());
53
70
  printSetupAudit();
54
71
  return;
55
72
  }
@@ -61,8 +78,16 @@ export async function runLibrettoCLI(): Promise<void> {
61
78
  } catch (err) {
62
79
  const message = err instanceof Error ? err.message : String(err);
63
80
  if (message.startsWith("Unknown command: ")) {
64
- console.error(`${message}\n`);
65
- console.log(renderUsage(app));
81
+ if (hasRootHelp(message, app)) {
82
+ const summary = message.split("\n", 1)[0] ?? message;
83
+ console.error(`${summary}\n`);
84
+ console.log(app.renderHelp());
85
+ } else if (hasScopedHelp(message)) {
86
+ console.error(message);
87
+ } else {
88
+ console.error(`${message}\n`);
89
+ console.log(app.renderHelp());
90
+ }
66
91
  } else {
67
92
  console.error(message);
68
93
  }
@@ -86,7 +86,7 @@ export const openInput = SimpleCLI.input({
86
86
  help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
87
87
  }),
88
88
  provider: SimpleCLI.option(z.string().optional(), {
89
- help: "Browser provider (local, kernel, browserbase)",
89
+ help: "Browser provider (local, kernel, browserbase, steel)",
90
90
  aliases: ["-p"],
91
91
  }),
92
92
  },
@@ -101,8 +101,7 @@ export const openInput = SimpleCLI.input({
101
101
  );
102
102
 
103
103
  export const openCommand = SimpleCLI.command({
104
- description:
105
- "Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists.",
104
+ description: "Launch browser and open URL",
106
105
  })
107
106
  .input(openInput)
108
107
  .use(withAutoSession())