libretto 0.5.3-experimental.5 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -37
- package/README.template.md +160 -0
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/deploy.js +148 -0
- package/dist/cli/commands/execution.js +218 -96
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/deploy-artifact.js +687 -0
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +144 -43
- package/dist/cli/router.js +16 -21
- package/dist/cli/workers/run-integration-runtime.js +25 -45
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -2
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +13 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +18 -10
- package/dist/shared/workflow/workflow.js +50 -5
- package/package.json +14 -6
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/postinstall.mjs +4 -3
- package/scripts/skills-libretto.mjs +2 -88
- package/scripts/summarize-evals.mjs +32 -10
- package/skills/libretto/SKILL.md +132 -62
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +176 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/deploy.ts +198 -0
- package/src/cli/commands/execution.ts +251 -111
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/deploy-artifact.ts +938 -0
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +281 -98
- package/src/cli/router.ts +15 -21
- package/src/cli/workers/run-integration-runtime.ts +35 -57
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +77 -67
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +107 -30
- package/scripts/check-skills-sync.mjs +0 -23
- package/scripts/prepare-release.sh +0 -97
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -75
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chromium
|
|
3
|
+
} from "playwright";
|
|
2
4
|
import { createServer } from "node:net";
|
|
3
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
6
|
import { ensureLibrettoSessionStatePath } from "../paths/paths.js";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
SESSION_STATE_VERSION,
|
|
9
|
+
SessionStateFileSchema
|
|
10
|
+
} from "../state/session-state.js";
|
|
11
|
+
import { readLibrettoConfig } from "../../cli/core/ai-config.js";
|
|
6
12
|
async function pickFreePort() {
|
|
7
13
|
return await new Promise((resolve, reject) => {
|
|
8
14
|
const server = createServer();
|
|
@@ -18,6 +24,41 @@ async function pickFreePort() {
|
|
|
18
24
|
});
|
|
19
25
|
});
|
|
20
26
|
}
|
|
27
|
+
function resolveWindowPosition() {
|
|
28
|
+
return readLibrettoConfig().windowPosition;
|
|
29
|
+
}
|
|
30
|
+
async function applyWindowPosition(browser, context, page, windowPosition) {
|
|
31
|
+
if (!windowPosition) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const requestedBounds = {
|
|
35
|
+
left: windowPosition.x,
|
|
36
|
+
top: windowPosition.y,
|
|
37
|
+
windowState: "normal"
|
|
38
|
+
};
|
|
39
|
+
const pageCdp = await context.newCDPSession(page);
|
|
40
|
+
let browserCdp;
|
|
41
|
+
try {
|
|
42
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
43
|
+
const targetId = targetInfo.targetInfo?.targetId;
|
|
44
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
45
|
+
const windowResult = await browserCdp.send(
|
|
46
|
+
"Browser.getWindowForTarget",
|
|
47
|
+
targetId ? { targetId } : {}
|
|
48
|
+
);
|
|
49
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
50
|
+
windowId: windowResult.windowId,
|
|
51
|
+
bounds: requestedBounds
|
|
52
|
+
});
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
54
|
+
} catch {
|
|
55
|
+
} finally {
|
|
56
|
+
await pageCdp.detach().catch(() => {
|
|
57
|
+
});
|
|
58
|
+
await browserCdp?.detach().catch(() => {
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
21
62
|
async function launchBrowser({
|
|
22
63
|
sessionName,
|
|
23
64
|
headless = false,
|
|
@@ -25,12 +66,14 @@ async function launchBrowser({
|
|
|
25
66
|
storageStatePath
|
|
26
67
|
}) {
|
|
27
68
|
const debugPort = await pickFreePort();
|
|
69
|
+
const windowPosition = headless ? void 0 : resolveWindowPosition();
|
|
28
70
|
const browser = await chromium.launch({
|
|
29
71
|
headless,
|
|
30
72
|
args: [
|
|
31
73
|
"--disable-blink-features=AutomationControlled",
|
|
32
74
|
`--remote-debugging-port=${debugPort}`,
|
|
33
|
-
"--no-focus-on-check"
|
|
75
|
+
"--no-focus-on-check",
|
|
76
|
+
...windowPosition ? [`--window-position=${windowPosition.x},${windowPosition.y}`] : []
|
|
34
77
|
]
|
|
35
78
|
});
|
|
36
79
|
const context = await browser.newContext({
|
|
@@ -38,6 +81,7 @@ async function launchBrowser({
|
|
|
38
81
|
...storageStatePath ? { storageState: storageStatePath } : {}
|
|
39
82
|
});
|
|
40
83
|
const page = await context.newPage();
|
|
84
|
+
await applyWindowPosition(browser, context, page, windowPosition);
|
|
41
85
|
page.setDefaultTimeout(3e4);
|
|
42
86
|
page.setDefaultNavigationTimeout(45e3);
|
|
43
87
|
const metadataPath = ensureLibrettoSessionStatePath(sessionName);
|
|
@@ -15,7 +15,8 @@ declare const SessionViewportSchema: z.ZodObject<{
|
|
|
15
15
|
declare const SessionStateFileSchema: z.ZodObject<{
|
|
16
16
|
version: z.ZodLiteral<1>;
|
|
17
17
|
port: z.ZodNumber;
|
|
18
|
-
pid: z.ZodNumber
|
|
18
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
cdpEndpoint: z.ZodOptional<z.ZodString>;
|
|
19
20
|
session: z.ZodString;
|
|
20
21
|
startedAt: z.ZodString;
|
|
21
22
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -14,7 +14,8 @@ const SessionViewportSchema = z.object({
|
|
|
14
14
|
const SessionStateFileSchema = z.object({
|
|
15
15
|
version: z.literal(SESSION_STATE_VERSION),
|
|
16
16
|
port: z.number().int().min(0).max(65535),
|
|
17
|
-
pid: z.number().int(),
|
|
17
|
+
pid: z.number().int().optional(),
|
|
18
|
+
cdpEndpoint: z.string().url().optional(),
|
|
18
19
|
session: z.string().min(1),
|
|
19
20
|
startedAt: z.string().datetime({ offset: true }),
|
|
20
21
|
status: SessionStatusSchema.optional(),
|
|
@@ -29,7 +30,9 @@ function formatIssues(error) {
|
|
|
29
30
|
function parseSessionStateData(rawState, source) {
|
|
30
31
|
const parsed = SessionStateFileSchema.safeParse(rawState);
|
|
31
32
|
if (!parsed.success) {
|
|
32
|
-
throw new Error(
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Session state at ${source} is invalid: ${formatIssues(parsed.error)}`
|
|
35
|
+
);
|
|
33
36
|
}
|
|
34
37
|
const { version: _version, ...state } = parsed.data;
|
|
35
38
|
return state;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEFAULTS = {
|
|
2
2
|
style: "minimal",
|
|
3
3
|
color: "rgba(255, 70, 70, 0.9)",
|
|
4
|
-
size:
|
|
4
|
+
size: 23,
|
|
5
5
|
zIndex: 2147483646,
|
|
6
6
|
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
7
7
|
minDurationMs: 100,
|
|
@@ -16,19 +16,32 @@ function buildCursorSvg(style, color, size) {
|
|
|
16
16
|
if (style === "screenstudio") {
|
|
17
17
|
return `<div style="width:${size * 1.4}px;height:${size * 1.4}px;border-radius:50%;background:${color};box-shadow:0 0 ${size * 0.6}px ${color};opacity:0.7;"></div>`;
|
|
18
18
|
}
|
|
19
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
19
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 2px 6px rgba(15,23,42,0.22));">
|
|
20
20
|
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="${color}" stroke="rgba(0,0,0,0.3)" stroke-width="1"/>
|
|
21
21
|
</svg>`;
|
|
22
22
|
}
|
|
23
|
+
function buildCursorMarkup(style, color, size) {
|
|
24
|
+
const cursor = buildCursorSvg(style, color, size);
|
|
25
|
+
const badgeHeight = Math.max(12, Math.round(size * 0.54));
|
|
26
|
+
const fontSize = Math.max(8, Math.round(size * 0.28));
|
|
27
|
+
const minWidth = Math.max(28, Math.round(size * 1.28));
|
|
28
|
+
const paddingX = Math.max(5, Math.round(size * 0.2));
|
|
29
|
+
const left = Math.round(size * 0.84);
|
|
30
|
+
const top = Math.round(size * 0.74);
|
|
31
|
+
const width = Math.round(size * 2.4);
|
|
32
|
+
const height = Math.round(size * 1.95);
|
|
33
|
+
const badge = `<div aria-hidden="true" style="position:absolute;left:${left}px;top:${top}px;display:flex;align-items:center;justify-content:center;min-width:${minWidth}px;height:${badgeHeight}px;padding:0 ${paddingX}px;border-radius:${badgeHeight}px;background:${color};color:rgba(255,255,255,0.96);font:700 ${fontSize}px/1 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;letter-spacing:0.02em;white-space:nowrap;border:1px solid rgba(0,0,0,0.16);box-shadow:0 4px 12px rgba(0,0,0,0.14);transform-origin:left center;">Agent</div>`;
|
|
34
|
+
return `<div style="position:relative;width:${width}px;height:${height}px;overflow:visible;">${cursor}${badge}</div>`;
|
|
35
|
+
}
|
|
23
36
|
function buildInitScript(opts) {
|
|
24
|
-
const
|
|
37
|
+
const markup = buildCursorMarkup(opts.style, opts.color, opts.size);
|
|
25
38
|
return `
|
|
26
39
|
(function() {
|
|
27
40
|
if (document.getElementById("${CURSOR_ID}")) return;
|
|
28
41
|
var el = document.createElement("div");
|
|
29
42
|
el.id = "${CURSOR_ID}";
|
|
30
43
|
el.style.cssText = "position:fixed;top:0;left:0;z-index:${opts.zIndex};pointer-events:none;transform:translate3d(-100px,-100px,0);transition:none;will-change:transform,opacity;opacity:0;";
|
|
31
|
-
el.innerHTML = ${JSON.stringify(
|
|
44
|
+
el.innerHTML = ${JSON.stringify(markup)};
|
|
32
45
|
document.documentElement.appendChild(el);
|
|
33
46
|
})();
|
|
34
47
|
`;
|
|
@@ -64,7 +77,13 @@ async function moveGhostCursor(page, target) {
|
|
|
64
77
|
el.style.transition = `transform ${duration}ms ${easing}`;
|
|
65
78
|
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
|
|
66
79
|
},
|
|
67
|
-
{
|
|
80
|
+
{
|
|
81
|
+
id: CURSOR_ID,
|
|
82
|
+
x: target.x,
|
|
83
|
+
y: target.y,
|
|
84
|
+
duration: durationMs,
|
|
85
|
+
easing: opts.easing
|
|
86
|
+
}
|
|
68
87
|
);
|
|
69
88
|
await page.waitForTimeout(durationMs);
|
|
70
89
|
} catch {
|
|
@@ -122,15 +141,18 @@ async function hideGhostCursor(page) {
|
|
|
122
141
|
}
|
|
123
142
|
async function getGhostCursorPosition(page) {
|
|
124
143
|
try {
|
|
125
|
-
return await page.evaluate(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
return await page.evaluate(
|
|
145
|
+
({ id }) => {
|
|
146
|
+
const el = document.getElementById(id);
|
|
147
|
+
if (!el) return null;
|
|
148
|
+
const match = el.style.transform.match(
|
|
149
|
+
/translate3d\(\s*([\d.-]+)px\s*,\s*([\d.-]+)px/
|
|
150
|
+
);
|
|
151
|
+
if (!match) return null;
|
|
152
|
+
return { x: parseFloat(match[1]), y: parseFloat(match[2]) };
|
|
153
|
+
},
|
|
154
|
+
{ id: CURSOR_ID }
|
|
155
|
+
);
|
|
134
156
|
} catch {
|
|
135
157
|
return null;
|
|
136
158
|
}
|
|
@@ -92,12 +92,15 @@ async function showHighlight(page, params) {
|
|
|
92
92
|
}
|
|
93
93
|
async function clearHighlights(page) {
|
|
94
94
|
try {
|
|
95
|
-
await page.evaluate(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
await page.evaluate(
|
|
96
|
+
({ layerId }) => {
|
|
97
|
+
const layer = document.getElementById(layerId);
|
|
98
|
+
if (!layer) return;
|
|
99
|
+
const rects = layer.querySelectorAll(".__libretto_highlight_rect__");
|
|
100
|
+
rects.forEach((r) => r.remove());
|
|
101
|
+
},
|
|
102
|
+
{ layerId: LAYER_ID }
|
|
103
|
+
);
|
|
101
104
|
} catch {
|
|
102
105
|
}
|
|
103
106
|
}
|
|
@@ -2,20 +2,28 @@ import { Page } from 'playwright';
|
|
|
2
2
|
import { MinimalLogger } from '../logger/logger.js';
|
|
3
3
|
|
|
4
4
|
declare const LIBRETTO_WORKFLOW_BRAND: unique symbol;
|
|
5
|
-
type
|
|
6
|
-
|
|
5
|
+
type LibrettoWorkflowContext = {
|
|
6
|
+
session: string;
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
|
-
services: S;
|
|
10
9
|
};
|
|
11
|
-
type LibrettoWorkflowHandler<Input = unknown, Output = unknown
|
|
12
|
-
declare class LibrettoWorkflow<Input = unknown, Output = unknown
|
|
10
|
+
type LibrettoWorkflowHandler<Input = unknown, Output = unknown> = (ctx: LibrettoWorkflowContext, input: Input) => Promise<Output>;
|
|
11
|
+
declare class LibrettoWorkflow<Input = unknown, Output = unknown> {
|
|
13
12
|
readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
14
|
-
readonly
|
|
13
|
+
readonly name: string;
|
|
15
14
|
private readonly handler;
|
|
16
|
-
constructor(
|
|
17
|
-
run(ctx: LibrettoWorkflowContext
|
|
15
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output>);
|
|
16
|
+
run(ctx: LibrettoWorkflowContext, input: Input): Promise<Output>;
|
|
18
17
|
}
|
|
19
|
-
|
|
18
|
+
type ExportedLibrettoWorkflow = {
|
|
19
|
+
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
22
|
+
};
|
|
23
|
+
type WorkflowModuleExports = Record<string, unknown>;
|
|
24
|
+
declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
|
|
25
|
+
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
26
|
+
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
27
|
+
declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<Input, Output>;
|
|
20
28
|
|
|
21
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler,
|
|
29
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
|
|
@@ -1,21 +1,66 @@
|
|
|
1
1
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
2
2
|
class LibrettoWorkflow {
|
|
3
3
|
[LIBRETTO_WORKFLOW_BRAND] = true;
|
|
4
|
-
|
|
4
|
+
name;
|
|
5
5
|
handler;
|
|
6
|
-
constructor(
|
|
7
|
-
this.
|
|
6
|
+
constructor(name, handler) {
|
|
7
|
+
this.name = name;
|
|
8
8
|
this.handler = handler;
|
|
9
9
|
}
|
|
10
10
|
async run(ctx, input) {
|
|
11
11
|
return this.handler(ctx, input);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
14
|
+
function isLibrettoWorkflow(value) {
|
|
15
|
+
if (!value || typeof value !== "object") return false;
|
|
16
|
+
const candidate = value;
|
|
17
|
+
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.name === "string" && typeof candidate.run === "function";
|
|
18
|
+
}
|
|
19
|
+
function addWorkflowOrThrow(workflowsByName, value) {
|
|
20
|
+
if (!isLibrettoWorkflow(value)) return;
|
|
21
|
+
const existing = workflowsByName.get(value.name);
|
|
22
|
+
if (existing && existing !== value) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Duplicate workflow name: "${value.name}". Each workflow() call must use a unique name.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
workflowsByName.set(value.name, value);
|
|
28
|
+
}
|
|
29
|
+
function getWorkflowsFromModuleExports(moduleExports) {
|
|
30
|
+
const workflowsByName = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const [exportName, value] of Object.entries(moduleExports)) {
|
|
32
|
+
if (exportName === "workflows" && value && typeof value === "object") {
|
|
33
|
+
if (isLibrettoWorkflow(value)) {
|
|
34
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
35
|
+
} else {
|
|
36
|
+
for (const nestedValue of Object.values(
|
|
37
|
+
value
|
|
38
|
+
)) {
|
|
39
|
+
addWorkflowOrThrow(workflowsByName, nestedValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
45
|
+
}
|
|
46
|
+
return [...workflowsByName.values()];
|
|
47
|
+
}
|
|
48
|
+
function getWorkflowFromModuleExports(moduleExports, workflowName) {
|
|
49
|
+
for (const workflow2 of getWorkflowsFromModuleExports(moduleExports)) {
|
|
50
|
+
if (workflow2.name === workflowName) {
|
|
51
|
+
return workflow2;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function workflow(name, handler) {
|
|
57
|
+
return new LibrettoWorkflow(name, handler);
|
|
16
58
|
}
|
|
17
59
|
export {
|
|
18
60
|
LIBRETTO_WORKFLOW_BRAND,
|
|
19
61
|
LibrettoWorkflow,
|
|
62
|
+
getWorkflowFromModuleExports,
|
|
63
|
+
getWorkflowsFromModuleExports,
|
|
64
|
+
isLibrettoWorkflow,
|
|
20
65
|
workflow
|
|
21
66
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.5.3
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,15 +31,16 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"postinstall": "node scripts/postinstall.mjs",
|
|
34
|
-
"
|
|
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",
|
|
35
38
|
"build": "tsup --config tsup.config.ts",
|
|
36
39
|
"type-check": "tsc --noEmit",
|
|
37
40
|
"test": "pnpm run build && vitest run",
|
|
38
|
-
"eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
|
|
39
|
-
"benchmark": "pnpm run build && tsx benchmarks/run.ts",
|
|
40
41
|
"test:watch": "vitest",
|
|
41
42
|
"cli": "node dist/index.js",
|
|
42
|
-
"
|
|
43
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
43
44
|
"prepack": "pnpm run build"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
@@ -63,12 +64,18 @@
|
|
|
63
64
|
}
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.75",
|
|
67
67
|
"@ai-sdk/anthropic": "^3.0.58",
|
|
68
68
|
"@ai-sdk/google": "^3.0.51",
|
|
69
69
|
"@ai-sdk/google-vertex": "^4.0.80",
|
|
70
70
|
"@ai-sdk/openai": "^3.0.41",
|
|
71
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.75",
|
|
72
|
+
"@mariozechner/pi-agent-core": "^0.62.0",
|
|
73
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
74
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
75
|
+
"@sinclair/typebox": "^0.34.48",
|
|
71
76
|
"@types/node": "^25.5.0",
|
|
77
|
+
"glimpseui": "^0.5.1",
|
|
78
|
+
"google-auth-library": "^10.6.1",
|
|
72
79
|
"openai": "^6.29.0",
|
|
73
80
|
"tsup": "^8.5.1",
|
|
74
81
|
"typescript": "^5.9.3",
|
|
@@ -76,6 +83,7 @@
|
|
|
76
83
|
},
|
|
77
84
|
"dependencies": {
|
|
78
85
|
"ai": "^6.0.116",
|
|
86
|
+
"esbuild": "^0.27.0",
|
|
79
87
|
"playwright": "^1.58.2",
|
|
80
88
|
"tsx": "^4.21.0",
|
|
81
89
|
"zod": "^4.3.6"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { Agent, type AgentTool, type AgentEvent } from "@mariozechner/pi-agent-core";
|
|
3
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
const tag = process.argv[2];
|
|
7
|
+
if (!tag) {
|
|
8
|
+
console.error("Usage: generate-changelog.ts <tag>");
|
|
9
|
+
console.error("Example: generate-changelog.ts v0.5.2");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
14
|
+
console.error("ANTHROPIC_API_KEY is required.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const ALLOWED_GH_SUBCOMMANDS = new Set(["pr", "release", "repo", "issue"]);
|
|
19
|
+
const ALLOWED_ACTIONS = new Set(["list", "view", "diff", "status", "checks"]);
|
|
20
|
+
|
|
21
|
+
const ghTool: AgentTool = {
|
|
22
|
+
name: "gh",
|
|
23
|
+
label: "GitHub CLI",
|
|
24
|
+
description: [
|
|
25
|
+
"Run a read-only GitHub CLI command. The arguments are passed directly to `gh`.",
|
|
26
|
+
"Examples: 'release list --limit 5', 'pr list --state merged --json number,title',",
|
|
27
|
+
"'pr view 128 --json title,body,files', 'pr diff 128'.",
|
|
28
|
+
"Only read operations are allowed (list, view, diff, etc.). Mutating commands will be rejected.",
|
|
29
|
+
].join(" "),
|
|
30
|
+
parameters: Type.Object({
|
|
31
|
+
args: Type.String({ description: "Arguments to pass to gh (without the leading 'gh')" }),
|
|
32
|
+
}),
|
|
33
|
+
execute: async (_toolCallId, rawParams) => {
|
|
34
|
+
const params = rawParams as { args: string };
|
|
35
|
+
const args = params.args.trim();
|
|
36
|
+
const parts = args.split(/\s+/);
|
|
37
|
+
const subcommand = parts[0];
|
|
38
|
+
|
|
39
|
+
if (!subcommand || !ALLOWED_GH_SUBCOMMANDS.has(subcommand)) {
|
|
40
|
+
throw new Error(`Subcommand '${subcommand}' is not allowed. Allowed: ${[...ALLOWED_GH_SUBCOMMANDS].join(", ")}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const action = parts[1];
|
|
44
|
+
if (!action || !ALLOWED_ACTIONS.has(action)) {
|
|
45
|
+
throw new Error(`Action '${action}' is not allowed. Allowed: ${[...ALLOWED_ACTIONS].join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync("gh", parts, {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
timeout: 300_000,
|
|
52
|
+
maxBuffer: 1024 * 1024,
|
|
53
|
+
});
|
|
54
|
+
return { content: [{ type: "text", text: output }], details: {} };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
throw new Error(`gh command failed: ${message}`);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const agent = new Agent({
|
|
63
|
+
initialState: {
|
|
64
|
+
systemPrompt: [
|
|
65
|
+
`Generate release notes for the ${tag} release of Libretto.`,
|
|
66
|
+
"",
|
|
67
|
+
"Use the gh tool to explore what changed since the previous release.",
|
|
68
|
+
"Useful queries:",
|
|
69
|
+
"- 'release list --limit 5' to find the previous release tag",
|
|
70
|
+
"- 'pr list --state merged --limit 50 --json number,title,body,labels' to find merged PRs",
|
|
71
|
+
"- 'pr diff NUMBER' to see the full diff of a PR (base to head, not individual commits)",
|
|
72
|
+
"- 'pr view NUMBER --json title,body,files' to see PR details",
|
|
73
|
+
"",
|
|
74
|
+
"IMPORTANT: Always read the full PR diff to understand what actually changed.",
|
|
75
|
+
"Do NOT rely solely on PR titles and descriptions — they may be incomplete or misleading.",
|
|
76
|
+
"The diff is the source of truth for what the release note should say.",
|
|
77
|
+
"",
|
|
78
|
+
"Guidelines:",
|
|
79
|
+
"- Write concise, user-facing release notes in markdown.",
|
|
80
|
+
"- Group changes into sections like Features, Fixes, and Improvements. Only include sections that have entries.",
|
|
81
|
+
"- Focus on what changed from the user's perspective, not internal implementation details.",
|
|
82
|
+
"- Do NOT include PR numbers or links.",
|
|
83
|
+
"- Skip PRs labeled 'skip-changelog'.",
|
|
84
|
+
"- 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:",
|
|
85
|
+
"",
|
|
86
|
+
"## Features",
|
|
87
|
+
"",
|
|
88
|
+
"- **Thing**: Description",
|
|
89
|
+
].join("\n"),
|
|
90
|
+
model: getModel("anthropic", "claude-sonnet-4-6"),
|
|
91
|
+
tools: [ghTool],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let finalText = "";
|
|
96
|
+
|
|
97
|
+
agent.subscribe((event: AgentEvent) => {
|
|
98
|
+
if (event.type === "agent_end") {
|
|
99
|
+
const messages = event.messages;
|
|
100
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
101
|
+
const msg = messages[i];
|
|
102
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
103
|
+
for (const block of msg.content) {
|
|
104
|
+
if (typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
|
|
105
|
+
finalText = block.text as string;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await agent.prompt("Generate the release notes now.");
|
|
115
|
+
|
|
116
|
+
if (!finalText) {
|
|
117
|
+
console.error("Changelog generation failed: no text output from agent.");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Strip any preamble before the first markdown heading.
|
|
122
|
+
const headingIndex = finalText.indexOf("\n#");
|
|
123
|
+
if (headingIndex >= 0) {
|
|
124
|
+
finalText = finalText.slice(headingIndex + 1);
|
|
125
|
+
} else if (finalText.startsWith("#")) {
|
|
126
|
+
// Already starts with a heading, keep as-is.
|
|
127
|
+
} else {
|
|
128
|
+
console.error("Changelog generation failed: output does not contain markdown headings.");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.stdout.write(finalText);
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -31,9 +31,10 @@ const gitResult = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
|
31
31
|
encoding: "utf-8",
|
|
32
32
|
stdio: ["pipe", "pipe", "pipe"],
|
|
33
33
|
});
|
|
34
|
-
const repoRoot =
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const repoRoot =
|
|
35
|
+
gitResult.status === 0 && gitResult.stdout
|
|
36
|
+
? gitResult.stdout.trim()
|
|
37
|
+
: installCwd;
|
|
37
38
|
|
|
38
39
|
const sourceDir = join(packageRoot, "skills", "libretto");
|
|
39
40
|
if (!existsSync(sourceDir)) process.exit(0);
|
|
@@ -1,101 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
cpSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
readdirSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import { relative, resolve, join } from "node:path";
|
|
3
|
+
import { cpSync, mkdirSync, rmSync } from "node:fs";
|
|
12
4
|
|
|
13
5
|
export const SKILL_DIRS = [
|
|
14
|
-
"skills/libretto",
|
|
6
|
+
"packages/libretto/skills/libretto",
|
|
15
7
|
".agents/skills/libretto",
|
|
16
8
|
".claude/skills/libretto",
|
|
17
9
|
];
|
|
18
10
|
|
|
19
|
-
function walkFiles(dir, baseDir = dir) {
|
|
20
|
-
const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
21
|
-
a.name.localeCompare(b.name)
|
|
22
|
-
);
|
|
23
|
-
const files = [];
|
|
24
|
-
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const fullPath = join(dir, entry.name);
|
|
27
|
-
if (entry.isDirectory()) {
|
|
28
|
-
files.push(...walkFiles(fullPath, baseDir));
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (entry.isFile()) files.push(relative(baseDir, fullPath));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return files;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
11
|
export function syncSkillDir(sourceDir, destDir) {
|
|
38
12
|
rmSync(destDir, { recursive: true, force: true });
|
|
39
13
|
mkdirSync(destDir, { recursive: true });
|
|
40
14
|
cpSync(sourceDir, destDir, { recursive: true });
|
|
41
15
|
}
|
|
42
|
-
|
|
43
|
-
export function syncRepoSkills(repoRoot) {
|
|
44
|
-
const sourceDir = resolve(repoRoot, "skills/libretto");
|
|
45
|
-
for (const dir of SKILL_DIRS.slice(1)) {
|
|
46
|
-
syncSkillDir(sourceDir, resolve(repoRoot, dir));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function compareSkillDirs(repoRoot) {
|
|
51
|
-
const roots = SKILL_DIRS.map((dir) => ({
|
|
52
|
-
label: dir,
|
|
53
|
-
absPath: resolve(repoRoot, dir),
|
|
54
|
-
}));
|
|
55
|
-
const missing = roots.filter(({ absPath }) => !existsSync(absPath));
|
|
56
|
-
const mismatches = [];
|
|
57
|
-
|
|
58
|
-
if (missing.length > 0) {
|
|
59
|
-
return {
|
|
60
|
-
ok: false,
|
|
61
|
-
issues: missing.map(({ label }) => `missing directory: ${label}`),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const expectedFiles = walkFiles(roots[0].absPath);
|
|
66
|
-
const expectedFileSet = new Set(expectedFiles);
|
|
67
|
-
|
|
68
|
-
for (const root of roots.slice(1)) {
|
|
69
|
-
const actualFiles = walkFiles(root.absPath);
|
|
70
|
-
const actualFileSet = new Set(actualFiles);
|
|
71
|
-
|
|
72
|
-
for (const file of expectedFiles) {
|
|
73
|
-
if (!actualFileSet.has(file)) {
|
|
74
|
-
mismatches.push(`${root.label} is missing file: ${file}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const file of actualFiles) {
|
|
79
|
-
if (!expectedFileSet.has(file)) {
|
|
80
|
-
mismatches.push(`${root.label} has unexpected file: ${file}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const file of expectedFiles) {
|
|
86
|
-
const expectedContent = readFileSync(join(roots[0].absPath, file));
|
|
87
|
-
for (const root of roots.slice(1)) {
|
|
88
|
-
const targetPath = join(root.absPath, file);
|
|
89
|
-
if (!existsSync(targetPath)) continue;
|
|
90
|
-
const actualContent = readFileSync(targetPath);
|
|
91
|
-
if (!expectedContent.equals(actualContent)) {
|
|
92
|
-
mismatches.push(`${root.label} differs from ${roots[0].label}: ${file}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ok: mismatches.length === 0,
|
|
99
|
-
issues: mismatches,
|
|
100
|
-
};
|
|
101
|
-
}
|