libretto 0.6.16 → 0.6.18
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 +32 -13
- package/dist/cli/commands/browser.js +2 -2
- package/dist/cli/commands/execution.js +1 -1
- package/dist/cli/commands/search.js +69 -0
- package/dist/cli/commands/update.js +122 -0
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/daemon.js +3 -0
- package/dist/cli/core/experiments.js +14 -1
- package/dist/cli/core/providers/index.js +5 -1
- package/dist/cli/core/providers/steel.js +56 -0
- package/dist/cli/core/session-telemetry.js +143 -7
- package/dist/cli/core/skill-version.js +1 -0
- package/dist/cli/router.js +14 -3
- package/dist/shared/html-search/search-html.d.ts +9 -0
- package/dist/shared/html-search/search-html.js +46 -0
- package/dist/shared/html-search/search-html.spec.d.ts +2 -0
- package/dist/shared/html-search/search-html.spec.js +57 -0
- package/docs/releasing.md +3 -9
- package/package.json +18 -19
- package/scripts/generate-changelog.ts +207 -12
- package/skills/libretto/SKILL.md +22 -15
- package/skills/libretto/references/code-generation-rules.md +2 -2
- package/skills/libretto/references/configuration-file-reference.md +3 -2
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +38 -13
- package/src/cli/commands/browser.ts +2 -3
- package/src/cli/commands/execution.ts +1 -1
- package/src/cli/commands/search.ts +74 -0
- package/src/cli/commands/update.ts +149 -0
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/daemon.ts +3 -0
- package/src/cli/core/experiments.ts +15 -1
- package/src/cli/core/providers/index.ts +5 -1
- package/src/cli/core/providers/steel.ts +75 -0
- package/src/cli/core/session-telemetry.ts +176 -13
- package/src/cli/core/skill-version.ts +1 -1
- package/src/cli/core/telemetry.ts +19 -3
- package/src/cli/router.ts +13 -2
- package/src/shared/html-search/search-html.spec.ts +65 -0
- 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,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
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://libretto.sh",
|
|
@@ -30,21 +30,6 @@
|
|
|
30
30
|
"default": "./dist/index.js"
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
|
-
"scripts": {
|
|
34
|
-
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
35
|
-
"check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
|
|
36
|
-
"sync-skills": "pnpm run sync:mirrors",
|
|
37
|
-
"check:skills": "pnpm run check:mirrors",
|
|
38
|
-
"build": "tsup --config tsup.config.ts",
|
|
39
|
-
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
40
|
-
"type-check": "tsc --noEmit",
|
|
41
|
-
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
42
|
-
"test:vitest": "vitest run",
|
|
43
|
-
"test:watch": "vitest",
|
|
44
|
-
"cli": "node dist/index.js",
|
|
45
|
-
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
46
|
-
"prepack": "pnpm run build"
|
|
47
|
-
},
|
|
48
33
|
"peerDependencies": {
|
|
49
34
|
"@ai-sdk/anthropic": "^3.0.58",
|
|
50
35
|
"@ai-sdk/google": "^3.0.51",
|
|
@@ -85,11 +70,25 @@
|
|
|
85
70
|
"vitest": "^4.1.5"
|
|
86
71
|
},
|
|
87
72
|
"dependencies": {
|
|
88
|
-
"affordance": "^0.1.0",
|
|
89
73
|
"ai": "^6.0.116",
|
|
90
74
|
"esbuild": "^0.27.0",
|
|
91
75
|
"playwright": "^1.58.2",
|
|
92
76
|
"tsx": "^4.21.0",
|
|
93
|
-
"zod": "^4.3.6"
|
|
77
|
+
"zod": "^4.3.6",
|
|
78
|
+
"affordance": "^0.2.0"
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
82
|
+
"check:mirrors": "node ../dev-tools/scripts/check-mirrors-sync.mjs",
|
|
83
|
+
"sync-skills": "pnpm run sync:mirrors",
|
|
84
|
+
"check:skills": "pnpm run check:mirrors",
|
|
85
|
+
"build": "tsup --config tsup.config.ts",
|
|
86
|
+
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
87
|
+
"type-check": "tsc --noEmit",
|
|
88
|
+
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
89
|
+
"test:vitest": "vitest run",
|
|
90
|
+
"test:watch": "vitest",
|
|
91
|
+
"cli": "node dist/index.js",
|
|
92
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts"
|
|
94
93
|
}
|
|
95
|
-
}
|
|
94
|
+
}
|
|
@@ -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"
|
|
19
|
-
const ALLOWED_ACTIONS = new Set(["
|
|
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:
|
|
218
|
+
"Examples:",
|
|
32
219
|
"'pr view 128 --json title,body,files', 'pr diff 128'.",
|
|
33
|
-
"Only
|
|
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 =
|
|
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
|
-
|
|
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
|
"",
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: saffron-health
|
|
7
|
-
version: "0.6.
|
|
7
|
+
version: "0.6.18"
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
71
|
-
libretto open https://example.com --
|
|
72
|
-
libretto open https://example.com --
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
-
- `
|
|
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
|
|
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
|
```
|