libretto 0.6.32 → 0.6.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.template.md +1 -1
- package/dist/cli/commands/auth.js +119 -268
- package/dist/cli/commands/browser.js +1 -0
- package/dist/cli/commands/cloud-jobs.js +7 -0
- package/dist/cli/commands/execution.js +3 -2
- package/dist/cli/core/browser.js +2 -1
- package/dist/cli/core/daemon/daemon.js +2 -1
- package/dist/cli/core/deploy-artifact.js +41 -16
- package/dist/cli/core/providers/kernel.js +3 -2
- package/dist/cli/core/providers/libretto-cloud.js +2 -1
- package/dist/cli/core/telemetry.js +14 -2
- package/dist/index.d.ts +1 -1
- package/dist/shared/workflow/workflow.d.ts +18 -0
- package/dist/shared/workflow/workflow.js +9 -0
- package/docs/releasing.md +3 -1
- package/package.json +2 -3
- package/scripts/generate-changelog.ts +196 -2
- package/skills/libretto/SKILL.md +17 -2
- package/skills/libretto/references/website-authentication.md +18 -2
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/auth.ts +169 -382
- package/src/cli/commands/browser.ts +1 -0
- package/src/cli/commands/cloud-jobs.ts +8 -0
- package/src/cli/commands/execution.ts +2 -1
- package/src/cli/core/browser.ts +2 -0
- package/src/cli/core/daemon/config.ts +1 -0
- package/src/cli/core/daemon/daemon.ts +1 -0
- package/src/cli/core/deploy-artifact.ts +63 -14
- package/src/cli/core/providers/kernel.ts +3 -2
- package/src/cli/core/providers/libretto-cloud.ts +1 -0
- package/src/cli/core/providers/types.ts +1 -0
- package/src/cli/core/resolve-model.ts +5 -5
- package/src/cli/core/telemetry.ts +15 -1
- package/src/cli/core/workflow-runtime.ts +1 -1
- package/src/index.ts +1 -0
- package/src/shared/workflow/workflow.ts +22 -0
|
@@ -49,7 +49,8 @@ function createKernelProvider(options = {}) {
|
|
|
49
49
|
const enableRecording = options.enableRecording ?? readBooleanEnv("KERNEL_ENABLE_RECORDING", false);
|
|
50
50
|
const replays = /* @__PURE__ */ new Map();
|
|
51
51
|
return {
|
|
52
|
-
async createSession() {
|
|
52
|
+
async createSession(sessionOptions) {
|
|
53
|
+
const sessionHeadless = sessionOptions?.headless ?? headless;
|
|
53
54
|
const json = await kernelFetchJson(
|
|
54
55
|
endpoint,
|
|
55
56
|
apiKey,
|
|
@@ -57,7 +58,7 @@ function createKernelProvider(options = {}) {
|
|
|
57
58
|
{
|
|
58
59
|
method: "POST",
|
|
59
60
|
body: JSON.stringify({
|
|
60
|
-
headless,
|
|
61
|
+
headless: sessionHeadless,
|
|
61
62
|
stealth,
|
|
62
63
|
timeout_seconds: timeoutSeconds
|
|
63
64
|
})
|
|
@@ -25,7 +25,8 @@ function createLibrettoCloudProvider() {
|
|
|
25
25
|
json: {
|
|
26
26
|
timeout_seconds: browserSessionTimeoutSeconds,
|
|
27
27
|
profile_name: options?.authProfileName,
|
|
28
|
-
profile_persist: options?.authProfilePersist
|
|
28
|
+
profile_persist: options?.authProfilePersist,
|
|
29
|
+
headless: options?.headless
|
|
29
30
|
}
|
|
30
31
|
})
|
|
31
32
|
});
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
8
|
+
import { readAuthState } from "./auth-storage.js";
|
|
8
9
|
const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
9
10
|
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
10
11
|
const TELEMETRY_TIMEOUT_MS = 250;
|
|
@@ -66,7 +67,7 @@ function writeTelemetryNotice() {
|
|
|
66
67
|
if (!process.stderr.isTTY) return;
|
|
67
68
|
process.stderr.write(
|
|
68
69
|
[
|
|
69
|
-
"Libretto collects
|
|
70
|
+
"Libretto collects CLI telemetry: install id, timestamp, command event, error status, package version, build channel, and signed-in cloud user id only.",
|
|
70
71
|
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json."
|
|
71
72
|
].join(" ") + "\n"
|
|
72
73
|
);
|
|
@@ -82,15 +83,26 @@ async function recordCliTelemetryEvent(command, error) {
|
|
|
82
83
|
if (isTelemetryDisabled()) return;
|
|
83
84
|
const installId = await readOrCreateInstallId();
|
|
84
85
|
if (!installId) return;
|
|
86
|
+
const cloudUserId = await readConfiguredCloudUserId();
|
|
85
87
|
await sendWithTimeout({
|
|
86
88
|
installId,
|
|
87
89
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
90
|
event: `libretto ${command.path.join(" ")}`,
|
|
89
91
|
error,
|
|
90
92
|
packageVersion,
|
|
91
|
-
buildChannel
|
|
93
|
+
buildChannel,
|
|
94
|
+
...cloudUserId ? { cloudUserId } : {}
|
|
92
95
|
});
|
|
93
96
|
}
|
|
97
|
+
async function readConfiguredCloudUserId() {
|
|
98
|
+
try {
|
|
99
|
+
const state = await readAuthState();
|
|
100
|
+
const cloudUserId = state?.session?.userId;
|
|
101
|
+
return cloudUserId && cloudUserId.length > 0 ? cloudUserId : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
94
106
|
async function sendWithTimeout(payload) {
|
|
95
107
|
const controller = new AbortController();
|
|
96
108
|
const timeout = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, mov
|
|
|
14
14
|
export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
|
|
15
15
|
export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
|
|
16
16
|
export { LibrettoAuthenticateOptions, librettoAuthenticate } from './shared/workflow/authenticate.js';
|
|
17
|
-
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowOptions, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
|
|
17
|
+
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowDefinition, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowOptions, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
|
|
18
18
|
export { AuthProfileStorageState, captureAuthProfileStorageState, normalizeAuthProfileSite, parseAuthProfileSites } from './shared/workflow/auth-profile-state.js';
|
|
19
19
|
import 'zod';
|
|
20
20
|
import 'playwright';
|
|
@@ -3,6 +3,12 @@ import { z } from 'zod';
|
|
|
3
3
|
import { RecoveryAction } from '../../runtime/recovery/page-fallbacks.js';
|
|
4
4
|
import 'ai';
|
|
5
5
|
|
|
6
|
+
declare const ViewportConfigSchema: z.ZodObject<{
|
|
7
|
+
width: z.ZodNumber;
|
|
8
|
+
height: z.ZodNumber;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
type ViewportConfig = z.infer<typeof ViewportConfigSchema>;
|
|
11
|
+
|
|
6
12
|
declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
|
|
7
13
|
type LibrettoWorkflowContext = {
|
|
8
14
|
session: string;
|
|
@@ -18,6 +24,9 @@ type LibrettoWorkflowDefinition<InputSchema extends z.ZodType = z.ZodType<unknow
|
|
|
18
24
|
output?: OutputSchema;
|
|
19
25
|
credentials?: readonly string[];
|
|
20
26
|
authProfile?: LibrettoWorkflowAuthProfile;
|
|
27
|
+
startUrl?: string;
|
|
28
|
+
gpu?: boolean;
|
|
29
|
+
viewport?: ViewportConfig;
|
|
21
30
|
recoveryAction?: RecoveryAction;
|
|
22
31
|
};
|
|
23
32
|
type LibrettoWorkflowOptions<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> = LibrettoWorkflowDefinition<InputSchema, OutputSchema> & {
|
|
@@ -41,6 +50,9 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
|
|
|
41
50
|
readonly credentialNames: readonly string[];
|
|
42
51
|
readonly authProfileName?: string;
|
|
43
52
|
readonly authProfileRefresh?: boolean;
|
|
53
|
+
readonly startUrl?: string;
|
|
54
|
+
readonly gpu?: boolean;
|
|
55
|
+
readonly viewport?: ViewportConfig;
|
|
44
56
|
readonly recoveryAction?: RecoveryAction;
|
|
45
57
|
private readonly handler;
|
|
46
58
|
constructor(name: string, options: {
|
|
@@ -49,6 +61,9 @@ declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown
|
|
|
49
61
|
credentialNames?: readonly string[];
|
|
50
62
|
authProfileName?: string;
|
|
51
63
|
authProfileRefresh?: boolean;
|
|
64
|
+
startUrl?: string;
|
|
65
|
+
gpu?: boolean;
|
|
66
|
+
viewport?: ViewportConfig;
|
|
52
67
|
recoveryAction?: RecoveryAction;
|
|
53
68
|
} | undefined, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>);
|
|
54
69
|
run(ctx: LibrettoWorkflowContext, input: unknown): Promise<z.infer<OutputSchema>>;
|
|
@@ -61,6 +76,9 @@ type ExportedLibrettoWorkflow = {
|
|
|
61
76
|
readonly credentialNames: readonly string[];
|
|
62
77
|
readonly authProfileName?: string;
|
|
63
78
|
readonly authProfileRefresh?: boolean;
|
|
79
|
+
readonly startUrl?: string;
|
|
80
|
+
readonly gpu?: boolean;
|
|
81
|
+
readonly viewport?: ViewportConfig;
|
|
64
82
|
readonly recoveryAction?: RecoveryAction;
|
|
65
83
|
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
66
84
|
};
|
|
@@ -76,6 +76,9 @@ class LibrettoWorkflow {
|
|
|
76
76
|
credentialNames;
|
|
77
77
|
authProfileName;
|
|
78
78
|
authProfileRefresh;
|
|
79
|
+
startUrl;
|
|
80
|
+
gpu;
|
|
81
|
+
viewport;
|
|
79
82
|
recoveryAction;
|
|
80
83
|
handler;
|
|
81
84
|
constructor(name, options, handler) {
|
|
@@ -85,6 +88,9 @@ class LibrettoWorkflow {
|
|
|
85
88
|
this.credentialNames = options?.credentialNames ?? [];
|
|
86
89
|
this.authProfileName = options?.authProfileName;
|
|
87
90
|
this.authProfileRefresh = options?.authProfileRefresh;
|
|
91
|
+
this.startUrl = options?.startUrl;
|
|
92
|
+
this.gpu = options?.gpu;
|
|
93
|
+
this.viewport = options?.viewport;
|
|
88
94
|
this.recoveryAction = options?.recoveryAction;
|
|
89
95
|
this.handler = handler;
|
|
90
96
|
}
|
|
@@ -159,6 +165,9 @@ function getWorkflowConstructorOptions(options) {
|
|
|
159
165
|
credentialNames: normalizeCredentialNames(options.credentials),
|
|
160
166
|
authProfileName: authProfile?.name,
|
|
161
167
|
authProfileRefresh: authProfile?.refresh,
|
|
168
|
+
startUrl: options.startUrl,
|
|
169
|
+
gpu: options.gpu,
|
|
170
|
+
viewport: options.viewport,
|
|
162
171
|
recoveryAction: options.recoveryAction
|
|
163
172
|
};
|
|
164
173
|
}
|
package/docs/releasing.md
CHANGED
|
@@ -49,7 +49,7 @@ The workflow needs `contents: write` to create the GitHub release and tag, and `
|
|
|
49
49
|
|
|
50
50
|
After trusted publishing is working, remove any old npm publish token from the repo secrets. npm recommends restricting token-based publishing after the migration.
|
|
51
51
|
|
|
52
|
-
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.
|
|
52
|
+
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. The release workflow uploads the generated notes and a changelog audit JSON artifact containing the release range, eligible PRs, inspected PR diffs, tool calls, and final model output.
|
|
53
53
|
|
|
54
54
|
## Prepare a release PR
|
|
55
55
|
|
|
@@ -108,6 +108,8 @@ The GitHub Releases page is the changelog for this repo.
|
|
|
108
108
|
|
|
109
109
|
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`.
|
|
110
110
|
|
|
111
|
+
The generator logs the release range and changelog-eligible PRs to stderr. When `CHANGELOG_AUDIT_FILE` is set, it also writes a JSON audit file with the eligible PR list, inspected diff PRs, tool-call summaries, and raw/final release notes. Generation fails if the agent does not inspect every eligible PR diff, so omissions are easier to diagnose before a GitHub release is created.
|
|
112
|
+
|
|
111
113
|
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.
|
|
112
114
|
|
|
113
115
|
## Notes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://libretto.sh",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"playwright": "^1.58.2",
|
|
71
71
|
"tsx": "^4.21.0",
|
|
72
72
|
"zod": "^4.3.6",
|
|
73
|
-
"affordance": "^0.2.
|
|
73
|
+
"affordance": "^0.2.2"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
|
@@ -78,7 +78,6 @@
|
|
|
78
78
|
"sync-skills": "pnpm run sync:mirrors",
|
|
79
79
|
"check:skills": "pnpm run check:mirrors",
|
|
80
80
|
"build": "tsup --config tsup.config.ts",
|
|
81
|
-
"lint": "lintcn lint --tsconfig tsconfig.json",
|
|
82
81
|
"type-check": "tsc --noEmit",
|
|
83
82
|
"test": "turbo run test:vitest --filter=libretto --log-order=grouped",
|
|
84
83
|
"test:vitest": "vitest run",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
4
|
import { Agent, type AgentTool, type AgentEvent } from "@mariozechner/pi-agent-core";
|
|
3
5
|
import { getModel } from "@mariozechner/pi-ai";
|
|
4
6
|
import { Type, type Static } from "@sinclair/typebox";
|
|
@@ -60,6 +62,34 @@ interface ReleaseContext {
|
|
|
60
62
|
pullRequests: PullRequestDetails[];
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
interface ToolCallAudit {
|
|
66
|
+
toolCallId: string;
|
|
67
|
+
toolName: string;
|
|
68
|
+
args: unknown;
|
|
69
|
+
isError?: boolean;
|
|
70
|
+
resultTextBytes?: number;
|
|
71
|
+
resultTextPreview?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ChangelogAudit {
|
|
75
|
+
tag: string;
|
|
76
|
+
generatedAt: string;
|
|
77
|
+
previousTag: string;
|
|
78
|
+
currentRef: string;
|
|
79
|
+
eligiblePullRequests: Array<{
|
|
80
|
+
number: number;
|
|
81
|
+
title: string;
|
|
82
|
+
labels: string[];
|
|
83
|
+
files: string[];
|
|
84
|
+
url: string;
|
|
85
|
+
}>;
|
|
86
|
+
inspectedDiffPullRequests: number[];
|
|
87
|
+
missingDiffPullRequests: number[];
|
|
88
|
+
toolCalls: ToolCallAudit[];
|
|
89
|
+
rawFinalText: string;
|
|
90
|
+
finalText: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
function runGh(args: string[]): string {
|
|
64
94
|
return execFileSync("gh", args, {
|
|
65
95
|
encoding: "utf8",
|
|
@@ -190,6 +220,7 @@ function buildReleaseContext(currentTag: string): ReleaseContext {
|
|
|
190
220
|
|
|
191
221
|
const releaseContext = buildReleaseContext(tag);
|
|
192
222
|
const allowedPullRequestNumbers = new Set(releaseContext.pullRequests.map((pr) => String(pr.number)));
|
|
223
|
+
const auditFilePath = process.env.CHANGELOG_AUDIT_FILE;
|
|
193
224
|
const pullRequestSummary = releaseContext.pullRequests
|
|
194
225
|
.map((pr) => {
|
|
195
226
|
const labels = pr.labels.map((label) => label.name).join(", ") || "none";
|
|
@@ -204,6 +235,13 @@ const pullRequestSummary = releaseContext.pullRequests
|
|
|
204
235
|
})
|
|
205
236
|
.join("\n\n");
|
|
206
237
|
|
|
238
|
+
console.error(`Changelog release range: ${releaseContext.previousTag}...${releaseContext.currentRef}`);
|
|
239
|
+
console.error("Changelog-eligible PRs:");
|
|
240
|
+
for (const pr of releaseContext.pullRequests) {
|
|
241
|
+
const labels = pr.labels.map((label) => label.name).join(", ") || "none";
|
|
242
|
+
console.error(`- #${pr.number} ${pr.title} [${labels}]`);
|
|
243
|
+
}
|
|
244
|
+
|
|
207
245
|
const GhToolParamsSchema = Type.Object({
|
|
208
246
|
args: Type.String({ description: "Arguments to pass to gh (without the leading 'gh')" }),
|
|
209
247
|
});
|
|
@@ -242,6 +280,12 @@ const ghTool: AgentTool = {
|
|
|
242
280
|
`PR #${number ?? "(missing)"} is outside the ${releaseContext.previousTag}...${releaseContext.currentRef} release range.`,
|
|
243
281
|
);
|
|
244
282
|
}
|
|
283
|
+
|
|
284
|
+
// Diff inspection enforcement counts successful 'pr diff NUMBER' calls,
|
|
285
|
+
// so reject flags like --name-only that would return less than the full diff.
|
|
286
|
+
if (action === "diff" && parts.length > 3) {
|
|
287
|
+
throw new Error("'pr diff' accepts only a PR number. Run 'pr diff NUMBER' with no extra arguments.");
|
|
288
|
+
}
|
|
245
289
|
}
|
|
246
290
|
try {
|
|
247
291
|
const output = runGh(parts);
|
|
@@ -277,6 +321,9 @@ const agent = new Agent({
|
|
|
277
321
|
"- Write concise, user-facing release notes in markdown.",
|
|
278
322
|
"- Group changes into sections like Features, Fixes, and Improvements. Only include sections that have entries.",
|
|
279
323
|
"- Focus on what changed from the user's perspective, not internal implementation details.",
|
|
324
|
+
"- For feature and improvement entries, include minimal code blocks when they make the change easier to use.",
|
|
325
|
+
"- Each relevant feature or improvement can have its own snippet.",
|
|
326
|
+
"- Keep each code block to at most 10 lines, and use at most one comment line inside the block if helpful.",
|
|
280
327
|
"- Do NOT include PR numbers or links.",
|
|
281
328
|
"- Skip PRs labeled 'skip-changelog'.",
|
|
282
329
|
"- Your response must contain ONLY the raw markdown release notes. No preamble like 'Here are the release notes'. No commentary or explanation. No '---' separators. The very first character of your response must be '#'. Example format:",
|
|
@@ -291,8 +338,123 @@ const agent = new Agent({
|
|
|
291
338
|
});
|
|
292
339
|
|
|
293
340
|
let finalText = "";
|
|
341
|
+
let rawFinalText = "";
|
|
342
|
+
const toolCalls = new Map<string, ToolCallAudit>();
|
|
343
|
+
|
|
344
|
+
function getObjectProperty(value: unknown, key: string): unknown {
|
|
345
|
+
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return (value as Record<string, unknown>)[key];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getTextContent(value: unknown): string {
|
|
353
|
+
const content = getObjectProperty(value, "content");
|
|
354
|
+
if (!Array.isArray(content)) {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return content
|
|
359
|
+
.map((block) => {
|
|
360
|
+
if (typeof block !== "object" || block === null) {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
const type = getObjectProperty(block, "type");
|
|
364
|
+
const text = getObjectProperty(block, "text");
|
|
365
|
+
return type === "text" && typeof text === "string" ? text : "";
|
|
366
|
+
})
|
|
367
|
+
.filter((text) => text.length > 0)
|
|
368
|
+
.join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function getInspectedDiffPullRequests(): number[] {
|
|
372
|
+
const inspected = new Set<number>();
|
|
373
|
+
for (const toolCall of toolCalls.values()) {
|
|
374
|
+
if (toolCall.isError !== false) {
|
|
375
|
+
// Only count tool calls that completed successfully.
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const args = getObjectProperty(toolCall.args, "args");
|
|
380
|
+
if (typeof args !== "string") {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const parts = args.trim().split(/\s+/);
|
|
385
|
+
if (parts[0] !== "pr" || parts[1] !== "diff") {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const number = Number(parts[2]);
|
|
390
|
+
if (Number.isInteger(number)) {
|
|
391
|
+
inspected.add(number);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return [...inspected].sort((a, b) => a - b);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildAudit(): ChangelogAudit {
|
|
399
|
+
const inspectedDiffPullRequests = getInspectedDiffPullRequests();
|
|
400
|
+
const inspected = new Set(inspectedDiffPullRequests);
|
|
401
|
+
const missingDiffPullRequests = releaseContext.pullRequests
|
|
402
|
+
.map((pr) => pr.number)
|
|
403
|
+
.filter((number) => !inspected.has(number));
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
tag,
|
|
407
|
+
generatedAt: new Date().toISOString(),
|
|
408
|
+
previousTag: releaseContext.previousTag,
|
|
409
|
+
currentRef: releaseContext.currentRef,
|
|
410
|
+
eligiblePullRequests: releaseContext.pullRequests.map((pr) => ({
|
|
411
|
+
number: pr.number,
|
|
412
|
+
title: pr.title,
|
|
413
|
+
labels: pr.labels.map((label) => label.name),
|
|
414
|
+
files: pr.files.map((file) => file.path),
|
|
415
|
+
url: pr.url,
|
|
416
|
+
})),
|
|
417
|
+
inspectedDiffPullRequests,
|
|
418
|
+
missingDiffPullRequests,
|
|
419
|
+
toolCalls: [...toolCalls.values()],
|
|
420
|
+
rawFinalText,
|
|
421
|
+
finalText,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function writeAuditFile(): void {
|
|
426
|
+
if (!auditFilePath) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
mkdirSync(dirname(auditFilePath), { recursive: true });
|
|
431
|
+
writeFileSync(auditFilePath, `${JSON.stringify(buildAudit(), null, 2)}\n`);
|
|
432
|
+
}
|
|
294
433
|
|
|
295
434
|
agent.subscribe((event: AgentEvent) => {
|
|
435
|
+
if (event.type === "tool_execution_start") {
|
|
436
|
+
toolCalls.set(event.toolCallId, {
|
|
437
|
+
toolCallId: event.toolCallId,
|
|
438
|
+
toolName: event.toolName,
|
|
439
|
+
args: event.args,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (event.type === "tool_execution_end") {
|
|
444
|
+
const text = getTextContent(event.result);
|
|
445
|
+
const existing = toolCalls.get(event.toolCallId) ?? {
|
|
446
|
+
toolCallId: event.toolCallId,
|
|
447
|
+
toolName: event.toolName,
|
|
448
|
+
args: undefined,
|
|
449
|
+
};
|
|
450
|
+
toolCalls.set(event.toolCallId, {
|
|
451
|
+
...existing,
|
|
452
|
+
isError: event.isError,
|
|
453
|
+
resultTextBytes: Buffer.byteLength(text, "utf8"),
|
|
454
|
+
resultTextPreview: text.slice(0, 2000),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
296
458
|
if (event.type === "agent_end") {
|
|
297
459
|
const messages = event.messages;
|
|
298
460
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -300,7 +462,8 @@ agent.subscribe((event: AgentEvent) => {
|
|
|
300
462
|
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
301
463
|
for (const block of msg.content) {
|
|
302
464
|
if (typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
|
|
303
|
-
|
|
465
|
+
rawFinalText = block.text as string;
|
|
466
|
+
finalText = rawFinalText;
|
|
304
467
|
return;
|
|
305
468
|
}
|
|
306
469
|
}
|
|
@@ -309,10 +472,18 @@ agent.subscribe((event: AgentEvent) => {
|
|
|
309
472
|
}
|
|
310
473
|
});
|
|
311
474
|
|
|
312
|
-
|
|
475
|
+
try {
|
|
476
|
+
await agent.prompt("Generate the release notes now.");
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("Changelog generation failed: agent prompt threw an error.");
|
|
479
|
+
console.error(error);
|
|
480
|
+
writeAuditFile();
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
313
483
|
|
|
314
484
|
if (!finalText) {
|
|
315
485
|
console.error("Changelog generation failed: no text output from agent.");
|
|
486
|
+
writeAuditFile();
|
|
316
487
|
process.exit(1);
|
|
317
488
|
}
|
|
318
489
|
|
|
@@ -324,7 +495,30 @@ if (headingIndex >= 0) {
|
|
|
324
495
|
// Already starts with a heading, keep as-is.
|
|
325
496
|
} else {
|
|
326
497
|
console.error("Changelog generation failed: output does not contain markdown headings.");
|
|
498
|
+
writeAuditFile();
|
|
327
499
|
process.exit(1);
|
|
328
500
|
}
|
|
329
501
|
|
|
502
|
+
const inspectedDiffPullRequests = getInspectedDiffPullRequests();
|
|
503
|
+
const missingDiffPullRequests = releaseContext.pullRequests
|
|
504
|
+
.map((pr) => pr.number)
|
|
505
|
+
.filter((number) => !inspectedDiffPullRequests.includes(number));
|
|
506
|
+
|
|
507
|
+
console.error(`Inspected PR diffs: ${inspectedDiffPullRequests.map((number) => `#${number}`).join(", ") || "none"}`);
|
|
508
|
+
|
|
509
|
+
if (missingDiffPullRequests.length > 0) {
|
|
510
|
+
console.error(
|
|
511
|
+
`Changelog generation failed: the agent did not inspect diffs for PRs ${missingDiffPullRequests
|
|
512
|
+
.map((number) => `#${number}`)
|
|
513
|
+
.join(", ")}.`,
|
|
514
|
+
);
|
|
515
|
+
writeAuditFile();
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
writeAuditFile();
|
|
520
|
+
if (auditFilePath) {
|
|
521
|
+
console.error(`Wrote changelog audit: ${auditFilePath}`);
|
|
522
|
+
}
|
|
523
|
+
|
|
330
524
|
process.stdout.write(finalText);
|
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.34"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -44,6 +44,18 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
44
44
|
|
|
45
45
|
- Use `npx libretto experiments` to list internal feature flags and `npx libretto experiments describe <name>` for usage notes when an experiment is enabled.
|
|
46
46
|
|
|
47
|
+
## Run Modes
|
|
48
|
+
|
|
49
|
+
There are three ways to run workflows:
|
|
50
|
+
|
|
51
|
+
- Local browser run: `npx libretto run ./workflow.ts` runs workflow code and local Chromium on the current machine. This is easy to watch and private, but most likely to hit CAPTCHAs or anti-bot checks.
|
|
52
|
+
- Hosted browser run: `npx libretto run ./workflow.ts --provider libretto-cloud` runs workflow code locally while the browser runs on provider infrastructure with stealth mechanisms. Use this for anti-bot/CAPTCHA issues, provider-specific behavior, and pre-deploy validation. If the user has not specified a provider, prefer `libretto-cloud` because it includes one free allocated browser-hour and does not require a third-party provider API key.
|
|
53
|
+
- Deployed workflow: Libretto Cloud packages the workflow as an API-backed workflow with the same remote-browser anti-bot mechanisms as hosted provider runs. Deploy only after the workflow passes with the target provider.
|
|
54
|
+
|
|
55
|
+
When editing a deployed workflow, validate changes with `run --provider <deployment-provider>` before redeploying; do not debug by repeatedly deploying and running the deployed job.
|
|
56
|
+
|
|
57
|
+
If the user prefers a provider for all local CLI runs in a workspace or only deploys workflows to that provider, update `.libretto/config.json` with `provider` instead of repeating `--provider`. Read `references/configuration-file-reference.md` first.
|
|
58
|
+
|
|
47
59
|
## Working Rules
|
|
48
60
|
|
|
49
61
|
- Announce which session you are using and what page you are on.
|
|
@@ -56,7 +68,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
56
68
|
- Authenticated workflows must implement `librettoAuthenticate` with declared credentials before validation. Use a reusable `*_totp_secret` credential for authenticator-app MFA, not a one-time `otp_code`; text and email verification codes are not supported for fully automated sign-in.
|
|
57
69
|
- Read `references/website-authentication.md` when you need `librettoAuthenticate` examples or auth-profile details.
|
|
58
70
|
- Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
|
|
59
|
-
- After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
|
|
71
|
+
- After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--provider`, `--headed`, or `--headless` flags the workflow needs. Do not add `--auth-profile` to `run`; workflow `authProfile` metadata controls profile use.
|
|
60
72
|
- Treat exploration sessions as disposable unless the user explicitly wants one kept open.
|
|
61
73
|
- Close disposable sessions before your final response once exploration, debugging, or validation is complete. Open browsers keep consuming local or hosted resources.
|
|
62
74
|
- Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
|
|
@@ -74,6 +86,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
74
86
|
|
|
75
87
|
```bash
|
|
76
88
|
npx libretto open https://example.com
|
|
89
|
+
npx libretto open https://example.com --provider libretto-cloud --session provider-debug
|
|
77
90
|
npx libretto open https://example.com --read-only --session readonly-example
|
|
78
91
|
npx libretto open https://example.com --session debug-example
|
|
79
92
|
```
|
|
@@ -150,6 +163,7 @@ npx libretto exec --session debug-example --page <page-id> "await page.url()"
|
|
|
150
163
|
- 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.
|
|
151
164
|
- 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.
|
|
152
165
|
- 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`.
|
|
166
|
+
- Use `--provider <name>` when validating behavior in that provider's browser runtime.
|
|
153
167
|
- Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
|
|
154
168
|
- If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
|
|
155
169
|
- Insert `await pause(session)` statements in the workflow file when you need to stop at specific states for interactive debugging, like breakpoints in the browser flow.
|
|
@@ -158,6 +172,7 @@ npx libretto exec --session debug-example --page <page-id> "await page.url()"
|
|
|
158
172
|
|
|
159
173
|
```bash
|
|
160
174
|
npx libretto run ./integration.ts --params '{"status":"open"}'
|
|
175
|
+
npx libretto run ./integration.ts --provider libretto-cloud
|
|
161
176
|
npx libretto run ./integration.ts --read-only
|
|
162
177
|
npx libretto run ./integration.ts --stay-open-on-success
|
|
163
178
|
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference when a workflow needs a logged-in website session. The Working Rules in `../SKILL.md` define the required auth workflow; this file explains how to implement sign-in logic with `librettoAuthenticate`, and how auth profiles save signed-in state for later runs.
|
|
4
4
|
|
|
5
|
-
Build and verify working sign-in logic first.
|
|
5
|
+
Build and verify working sign-in logic first. Authenticated workflows must use `librettoAuthenticate`; an auth profile is added only after the sign-in logic is verified, never as a substitute for it.
|
|
6
6
|
|
|
7
7
|
## Sign-In Logic
|
|
8
8
|
|
|
@@ -44,7 +44,15 @@ export default workflow("accountWorkflow", {
|
|
|
44
44
|
|
|
45
45
|
Auth profiles save the signed-in browser state (cookies, localStorage, IndexedDB) so later runs can reuse a logged-in session instead of signing in from scratch. The sign-in logic still takes priority: do not add a profile until the `librettoAuthenticate` sign-in step has been verified from a signed-out browser with no profile present. If you add a profile first, validation passes on the saved session while the untested sign-in logic fails the first time that session expires.
|
|
46
46
|
|
|
47
|
-
A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one.
|
|
47
|
+
A profile only holds whatever a signed-in session wrote into it, so it does nothing until a run has signed in at least once. With `refresh: true`, a successful run writes updated browser state back to the profile, so a fresh sign-in repairs an expired one.
|
|
48
|
+
|
|
49
|
+
Auth profiles are browser-runtime-specific:
|
|
50
|
+
|
|
51
|
+
- Local runs load `.libretto/profiles/<name>.json`.
|
|
52
|
+
- `libretto-cloud` and providers that explicitly support auth profiles use provider-backed profile state; a local profile file is not available to provider browsers.
|
|
53
|
+
- Provider-backed profiles are created or refreshed by workflow-declared `authProfile` metadata. For manual login on a remote provider, run the hosted workflow with a pause/wait, ask the human to sign in through the live URL, use the observed login flow to implement `librettoAuthenticate`, then let the workflow finish so the provider can persist the profile.
|
|
54
|
+
- A plain `open --provider` session does not have workflow auth profile metadata. `libretto save` writes a local profile from an open session; it does not persist provider-backed workflow auth profile state.
|
|
55
|
+
- Revalidate login when switching providers, even if the profile name is the same.
|
|
48
56
|
|
|
49
57
|
Add the profile to the workflow you already verified:
|
|
50
58
|
|
|
@@ -60,9 +68,15 @@ export default workflow("accountWorkflow", {
|
|
|
60
68
|
## Commands
|
|
61
69
|
|
|
62
70
|
```bash
|
|
71
|
+
# Open the site in headed mode and ask the user to log in manually.
|
|
72
|
+
npx libretto open https://app.example.com --headed --session login
|
|
73
|
+
|
|
63
74
|
# Save the current signed-in session as a named, site-scoped profile.
|
|
64
75
|
npx libretto save example-app --session login --sites app.example.com,auth.example.com
|
|
65
76
|
|
|
77
|
+
# You can now reopen the site locally with that profile.
|
|
78
|
+
npx libretto open https://app.example.com --auth-profile example-app
|
|
79
|
+
|
|
66
80
|
# List or delete hosted auth profile names.
|
|
67
81
|
npx libretto cloud profiles list
|
|
68
82
|
npx libretto cloud profiles delete example-app
|
|
@@ -71,3 +85,5 @@ npx libretto cloud profiles delete example-app
|
|
|
71
85
|
`save` captures cookies, localStorage, and IndexedDB only for the comma-separated `--sites` list.
|
|
72
86
|
|
|
73
87
|
To reuse an existing signed-in Chrome profile instead of signing in, use `npx libretto import-chrome-profiles`. Get the user's consent first, since attaching can close or relaunch their Chrome window.
|
|
88
|
+
|
|
89
|
+
When the user wants to deploy an authenticated workflow, trace the login flow before writing the workflow: open the site with the target provider, inspect behavior while the human signs in, implement `librettoAuthenticate`, then validate the workflow with the auth profile in the target browser runtime.
|