libretto 0.5.1 → 0.5.3-experimental.0
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 +10 -5
- package/dist/cli/commands/execution.js +38 -12
- package/dist/cli/commands/init.js +4 -21
- package/dist/cli/core/ai-config.js +12 -2
- package/dist/cli/core/browser.js +75 -8
- package/dist/cli/core/session-telemetry.js +429 -172
- package/dist/cli/core/telemetry.js +10 -2
- package/dist/cli/framework/simple-cli.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +18 -41
- package/dist/cli/workers/run-integration-worker-protocol.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -0
- package/dist/shared/condense-dom/condense-dom.js +11 -56
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/run/browser.js +40 -1
- package/dist/shared/visualization/ghost-cursor.js +17 -4
- package/dist/shared/workflow/workflow.d.ts +14 -3
- package/dist/shared/workflow/workflow.js +50 -3
- package/package.json +7 -4
- package/scripts/check-skills-sync.mjs +1 -1
- package/scripts/generate-changelog.ts +132 -0
- package/scripts/skills-libretto.mjs +1 -1
- package/scripts/sync-skills.mjs +1 -1
- package/skills/libretto/SKILL.md +54 -38
- 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 +10 -6
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/src/cli/commands/execution.ts +39 -11
- package/src/cli/commands/init.ts +5 -24
- package/src/cli/core/ai-config.ts +12 -1
- package/src/cli/core/browser.ts +82 -8
- package/src/cli/core/session-telemetry.ts +431 -190
- package/src/cli/core/telemetry.ts +23 -1
- package/src/cli/framework/simple-cli.ts +5 -0
- package/src/cli/workers/run-integration-runtime.ts +24 -52
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
- package/src/index.ts +4 -0
- package/src/shared/condense-dom/condense-dom.ts +12 -64
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/run/browser.ts +53 -0
- package/src/shared/visualization/ghost-cursor.ts +22 -4
- package/src/shared/workflow/workflow.ts +88 -2
- package/scripts/prepare-release.sh +0 -97
|
@@ -4,6 +4,8 @@ import { cwd } from "node:process";
|
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import {
|
|
7
|
+
getWorkflowFromModuleExports,
|
|
8
|
+
getWorkflowsFromModuleExports,
|
|
7
9
|
instrumentContext,
|
|
8
10
|
launchBrowser
|
|
9
11
|
} from "../../index.js";
|
|
@@ -19,7 +21,6 @@ import {
|
|
|
19
21
|
removeSignalIfExists
|
|
20
22
|
} from "../core/pause-signals.js";
|
|
21
23
|
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
22
|
-
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
23
24
|
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
24
25
|
const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
25
26
|
function isTsxCompileError(error) {
|
|
@@ -67,11 +68,6 @@ async function waitForFailureSessionRelease(args) {
|
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
function isLoadedLibrettoWorkflow(value) {
|
|
71
|
-
if (!value || typeof value !== "object") return false;
|
|
72
|
-
const candidate = value;
|
|
73
|
-
return candidate[LIBRETTO_WORKFLOW_BRAND] === true && typeof candidate.run === "function";
|
|
74
|
-
}
|
|
75
71
|
function resolveLocalAuthProfilePath(domain) {
|
|
76
72
|
return getProfilePath(normalizeDomain(domain));
|
|
77
73
|
}
|
|
@@ -93,7 +89,7 @@ function getAbsoluteIntegrationPath(integrationPath) {
|
|
|
93
89
|
}
|
|
94
90
|
return absolutePath;
|
|
95
91
|
}
|
|
96
|
-
async function
|
|
92
|
+
async function loadWorkflowByName(absolutePath, workflowName) {
|
|
97
93
|
let loadedModule;
|
|
98
94
|
try {
|
|
99
95
|
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
@@ -105,37 +101,17 @@ ${TSCONFIG_HINT}` : "";
|
|
|
105
101
|
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
|
|
106
102
|
);
|
|
107
103
|
}
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
const detail = availableExports.length > 0 ? ` Available exports: ${availableExports.join(", ")}` : " The module has no exports.";
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Export "${exportName}" was not found in ${absolutePath}.${detail}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (!isLoadedLibrettoWorkflow(targetExport)) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
[
|
|
119
|
-
`Export "${exportName}" in ${absolutePath} is not a valid Libretto workflow.`,
|
|
120
|
-
"",
|
|
121
|
-
'A workflow must be created using the workflow() function from "libretto":',
|
|
122
|
-
"",
|
|
123
|
-
' import { workflow } from "libretto";',
|
|
124
|
-
"",
|
|
125
|
-
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
126
|
-
" async (ctx, input) => {",
|
|
127
|
-
" // ctx.session \u2014 libretto session name",
|
|
128
|
-
" // ctx.page \u2014 Playwright Page instance",
|
|
129
|
-
" // ctx.logger \u2014 MinimalLogger",
|
|
130
|
-
" // ctx.services \u2014 injected dependencies (generic, default {})",
|
|
131
|
-
" // input \u2014 JSON-serializable input matching InputType",
|
|
132
|
-
" return output; // must match OutputType",
|
|
133
|
-
" },",
|
|
134
|
-
" );"
|
|
135
|
-
].join("\n")
|
|
136
|
-
);
|
|
104
|
+
const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
|
|
105
|
+
if (workflow) {
|
|
106
|
+
return workflow;
|
|
137
107
|
}
|
|
138
|
-
|
|
108
|
+
const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
|
|
109
|
+
(candidate) => candidate.name
|
|
110
|
+
);
|
|
111
|
+
const detail = availableWorkflows.length > 0 ? ` Available workflows: ${availableWorkflows.join(", ")}` : ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Workflow "${workflowName}" not found in ${absolutePath}.${detail}`
|
|
114
|
+
);
|
|
139
115
|
}
|
|
140
116
|
async function installHeadedWorkflowVisualization(args) {
|
|
141
117
|
await (args.instrument ?? instrumentContext)(args.context, {
|
|
@@ -146,7 +122,7 @@ async function installHeadedWorkflowVisualization(args) {
|
|
|
146
122
|
async function runIntegrationInternal(args, options) {
|
|
147
123
|
const { logger } = options;
|
|
148
124
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
149
|
-
const workflow = await
|
|
125
|
+
const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
|
|
150
126
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
151
127
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
152
128
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -154,11 +130,11 @@ async function runIntegrationInternal(args, options) {
|
|
|
154
130
|
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
155
131
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
156
132
|
console.log(
|
|
157
|
-
`Running
|
|
133
|
+
`Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
158
134
|
);
|
|
159
135
|
const integrationLogger = logger.withScope("integration-run", {
|
|
160
136
|
integrationPath: absolutePath,
|
|
161
|
-
|
|
137
|
+
workflowName: args.workflowName,
|
|
162
138
|
session: args.session
|
|
163
139
|
});
|
|
164
140
|
const authProfileDomain = args.authProfileDomain;
|
|
@@ -201,7 +177,8 @@ async function runIntegrationInternal(args, options) {
|
|
|
201
177
|
session: args.session,
|
|
202
178
|
logger: integrationLogger,
|
|
203
179
|
page: browserSession.page,
|
|
204
|
-
services: {}
|
|
180
|
+
services: {},
|
|
181
|
+
credentials: args.credentials
|
|
205
182
|
};
|
|
206
183
|
try {
|
|
207
184
|
try {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const RunIntegrationWorkerRequestSchema = z.object({
|
|
3
3
|
integrationPath: z.string().min(1),
|
|
4
|
-
|
|
4
|
+
workflowName: z.string().min(1),
|
|
5
5
|
session: z.string().min(1),
|
|
6
6
|
params: z.unknown(),
|
|
7
|
+
credentials: z.record(z.string(), z.unknown()).optional(),
|
|
7
8
|
headless: z.boolean(),
|
|
8
9
|
visualize: z.boolean().default(true),
|
|
9
10
|
authProfileDomain: z.string().optional(),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instr
|
|
|
14
14
|
export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
|
|
15
15
|
export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
|
|
16
16
|
export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
|
|
17
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, workflow } from './shared/workflow/workflow.js';
|
|
17
|
+
export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow } from './shared/workflow/workflow.js';
|
|
18
18
|
import 'zod';
|
|
19
19
|
import 'ai';
|
|
20
20
|
import 'playwright';
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,9 @@ import {
|
|
|
54
54
|
launchBrowser
|
|
55
55
|
} from "./shared/run/api.js";
|
|
56
56
|
import {
|
|
57
|
+
getWorkflowFromModuleExports,
|
|
58
|
+
getWorkflowsFromModuleExports,
|
|
59
|
+
isLibrettoWorkflow,
|
|
57
60
|
LibrettoWorkflow,
|
|
58
61
|
LIBRETTO_WORKFLOW_BRAND,
|
|
59
62
|
workflow
|
|
@@ -92,11 +95,14 @@ export {
|
|
|
92
95
|
ensureHighlightLayer,
|
|
93
96
|
executeRecoveryAgent,
|
|
94
97
|
extractFromPage,
|
|
98
|
+
getWorkflowFromModuleExports,
|
|
99
|
+
getWorkflowsFromModuleExports,
|
|
95
100
|
ghostClick,
|
|
96
101
|
hideGhostCursor,
|
|
97
102
|
installInstrumentation,
|
|
98
103
|
instrumentContext,
|
|
99
104
|
instrumentPage,
|
|
105
|
+
isLibrettoWorkflow,
|
|
100
106
|
jsonlConsoleSink,
|
|
101
107
|
launchBrowser,
|
|
102
108
|
moveGhostCursor,
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"alt",
|
|
11
|
-
"type",
|
|
12
|
-
"value",
|
|
13
|
-
"placeholder",
|
|
14
|
-
"autocomplete",
|
|
15
|
-
"href",
|
|
16
|
-
"action",
|
|
17
|
-
"method",
|
|
18
|
-
"src"
|
|
19
|
-
]);
|
|
1
|
+
import {
|
|
2
|
+
filterSemanticClasses,
|
|
3
|
+
INTERACTIVE_ROLE_NAMES,
|
|
4
|
+
INTERACTIVE_TAG_NAMES,
|
|
5
|
+
TEST_ATTRIBUTE_NAMES,
|
|
6
|
+
TRUSTED_ATTRIBUTE_NAMES
|
|
7
|
+
} from "../dom-semantics.js";
|
|
8
|
+
const TEST_ATTRS = new Set(TEST_ATTRIBUTE_NAMES);
|
|
9
|
+
const TRUSTED_ATTRS = new Set(TRUSTED_ATTRIBUTE_NAMES);
|
|
20
10
|
const STATE_ATTRS = /* @__PURE__ */ new Set([
|
|
21
11
|
"disabled",
|
|
22
12
|
"hidden",
|
|
@@ -55,28 +45,8 @@ const SCRIPT_ATTRS = /* @__PURE__ */ new Set([
|
|
|
55
45
|
"referrerpolicy"
|
|
56
46
|
]);
|
|
57
47
|
const STYLE_TAG_ATTRS = /* @__PURE__ */ new Set(["media", "type", "nonce", "title"]);
|
|
58
|
-
const INTERACTIVE_TAGS =
|
|
59
|
-
|
|
60
|
-
"button",
|
|
61
|
-
"input",
|
|
62
|
-
"select",
|
|
63
|
-
"textarea",
|
|
64
|
-
"form",
|
|
65
|
-
"details",
|
|
66
|
-
"dialog",
|
|
67
|
-
"label"
|
|
68
|
-
]);
|
|
69
|
-
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
70
|
-
"button",
|
|
71
|
-
"link",
|
|
72
|
-
"tab",
|
|
73
|
-
"menuitem",
|
|
74
|
-
"checkbox",
|
|
75
|
-
"radio",
|
|
76
|
-
"switch",
|
|
77
|
-
"slider",
|
|
78
|
-
"combobox"
|
|
79
|
-
]);
|
|
48
|
+
const INTERACTIVE_TAGS = new Set(INTERACTIVE_TAG_NAMES);
|
|
49
|
+
const INTERACTIVE_ROLES = new Set(INTERACTIVE_ROLE_NAMES);
|
|
80
50
|
const OPEN_TAG_PATTERN = /<([a-zA-Z][\w:-]*)(\s(?:[^"'<>/]|"[^"]*"|'[^']*')*)?\s*(\/?)>/g;
|
|
81
51
|
function condenseDom(html) {
|
|
82
52
|
const originalLength = html.length;
|
|
@@ -337,21 +307,6 @@ function normalizeUrlValue(value) {
|
|
|
337
307
|
return `${value.slice(0, 96)}[omitted]`;
|
|
338
308
|
}
|
|
339
309
|
}
|
|
340
|
-
function filterSemanticClasses(value) {
|
|
341
|
-
const classes = value.split(/\s+/).filter(Boolean);
|
|
342
|
-
const kept = classes.filter((cls) => !isObfuscatedClass(cls));
|
|
343
|
-
return kept.join(" ");
|
|
344
|
-
}
|
|
345
|
-
function isObfuscatedClass(cls) {
|
|
346
|
-
if (cls.length > 80) return true;
|
|
347
|
-
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
348
|
-
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
349
|
-
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
350
|
-
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
351
|
-
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
352
|
-
if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
310
|
function parseAttributes(rawAttrs) {
|
|
356
311
|
const attrs = [];
|
|
357
312
|
const attrPattern = /([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare const TEST_ATTRIBUTE_NAMES: readonly ["data-testid", "data-test", "data-qa", "data-cy"];
|
|
2
|
+
declare const TRUSTED_ATTRIBUTE_NAMES: readonly ["id", "name", "for", "tabindex", "contenteditable", "role", "title", "alt", "type", "value", "placeholder", "autocomplete", "href", "action", "method", "src"];
|
|
3
|
+
declare const INTERACTIVE_TAG_NAMES: readonly ["a", "button", "input", "select", "textarea", "form", "details", "dialog", "label"];
|
|
4
|
+
declare const INTERACTIVE_ROLE_NAMES: readonly ["button", "link", "tab", "menuitem", "checkbox", "radio", "switch", "slider", "combobox"];
|
|
5
|
+
declare function filterSemanticClasses(value: string): string;
|
|
6
|
+
declare function isObfuscatedClass(cls: string): boolean;
|
|
7
|
+
|
|
8
|
+
export { INTERACTIVE_ROLE_NAMES, INTERACTIVE_TAG_NAMES, TEST_ATTRIBUTE_NAMES, TRUSTED_ATTRIBUTE_NAMES, filterSemanticClasses, isObfuscatedClass };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const TEST_ATTRIBUTE_NAMES = [
|
|
2
|
+
"data-testid",
|
|
3
|
+
"data-test",
|
|
4
|
+
"data-qa",
|
|
5
|
+
"data-cy"
|
|
6
|
+
];
|
|
7
|
+
const TRUSTED_ATTRIBUTE_NAMES = [
|
|
8
|
+
"id",
|
|
9
|
+
"name",
|
|
10
|
+
"for",
|
|
11
|
+
"tabindex",
|
|
12
|
+
"contenteditable",
|
|
13
|
+
"role",
|
|
14
|
+
"title",
|
|
15
|
+
"alt",
|
|
16
|
+
"type",
|
|
17
|
+
"value",
|
|
18
|
+
"placeholder",
|
|
19
|
+
"autocomplete",
|
|
20
|
+
"href",
|
|
21
|
+
"action",
|
|
22
|
+
"method",
|
|
23
|
+
"src"
|
|
24
|
+
];
|
|
25
|
+
const INTERACTIVE_TAG_NAMES = [
|
|
26
|
+
"a",
|
|
27
|
+
"button",
|
|
28
|
+
"input",
|
|
29
|
+
"select",
|
|
30
|
+
"textarea",
|
|
31
|
+
"form",
|
|
32
|
+
"details",
|
|
33
|
+
"dialog",
|
|
34
|
+
"label"
|
|
35
|
+
];
|
|
36
|
+
const INTERACTIVE_ROLE_NAMES = [
|
|
37
|
+
"button",
|
|
38
|
+
"link",
|
|
39
|
+
"tab",
|
|
40
|
+
"menuitem",
|
|
41
|
+
"checkbox",
|
|
42
|
+
"radio",
|
|
43
|
+
"switch",
|
|
44
|
+
"slider",
|
|
45
|
+
"combobox"
|
|
46
|
+
];
|
|
47
|
+
function filterSemanticClasses(value) {
|
|
48
|
+
const classes = value.split(/\s+/).filter(Boolean);
|
|
49
|
+
const kept = classes.filter((cls) => !isObfuscatedClass(cls));
|
|
50
|
+
return kept.join(" ");
|
|
51
|
+
}
|
|
52
|
+
function isObfuscatedClass(cls) {
|
|
53
|
+
if (cls.length > 80) return true;
|
|
54
|
+
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
55
|
+
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
56
|
+
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
57
|
+
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
58
|
+
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
59
|
+
if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
INTERACTIVE_ROLE_NAMES,
|
|
64
|
+
INTERACTIVE_TAG_NAMES,
|
|
65
|
+
TEST_ATTRIBUTE_NAMES,
|
|
66
|
+
TRUSTED_ATTRIBUTE_NAMES,
|
|
67
|
+
filterSemanticClasses,
|
|
68
|
+
isObfuscatedClass
|
|
69
|
+
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
SESSION_STATE_VERSION,
|
|
9
9
|
SessionStateFileSchema
|
|
10
10
|
} from "../state/session-state.js";
|
|
11
|
+
import { readLibrettoConfig } from "../../cli/core/ai-config.js";
|
|
11
12
|
async function pickFreePort() {
|
|
12
13
|
return await new Promise((resolve, reject) => {
|
|
13
14
|
const server = createServer();
|
|
@@ -23,6 +24,41 @@ async function pickFreePort() {
|
|
|
23
24
|
});
|
|
24
25
|
});
|
|
25
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
|
+
}
|
|
26
62
|
async function launchBrowser({
|
|
27
63
|
sessionName,
|
|
28
64
|
headless = false,
|
|
@@ -30,12 +66,14 @@ async function launchBrowser({
|
|
|
30
66
|
storageStatePath
|
|
31
67
|
}) {
|
|
32
68
|
const debugPort = await pickFreePort();
|
|
69
|
+
const windowPosition = headless ? void 0 : resolveWindowPosition();
|
|
33
70
|
const browser = await chromium.launch({
|
|
34
71
|
headless,
|
|
35
72
|
args: [
|
|
36
73
|
"--disable-blink-features=AutomationControlled",
|
|
37
74
|
`--remote-debugging-port=${debugPort}`,
|
|
38
|
-
"--no-focus-on-check"
|
|
75
|
+
"--no-focus-on-check",
|
|
76
|
+
...windowPosition ? [`--window-position=${windowPosition.x},${windowPosition.y}`] : []
|
|
39
77
|
]
|
|
40
78
|
});
|
|
41
79
|
const context = await browser.newContext({
|
|
@@ -43,6 +81,7 @@ async function launchBrowser({
|
|
|
43
81
|
...storageStatePath ? { storageState: storageStatePath } : {}
|
|
44
82
|
});
|
|
45
83
|
const page = await context.newPage();
|
|
84
|
+
await applyWindowPosition(browser, context, page, windowPosition);
|
|
46
85
|
page.setDefaultTimeout(3e4);
|
|
47
86
|
page.setDefaultNavigationTimeout(45e3);
|
|
48
87
|
const metadataPath = ensureLibrettoSessionStatePath(sessionName);
|
|
@@ -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
|
`;
|
|
@@ -7,14 +7,25 @@ type LibrettoWorkflowContext<S = {}> = {
|
|
|
7
7
|
page: Page;
|
|
8
8
|
logger: MinimalLogger;
|
|
9
9
|
services: S;
|
|
10
|
+
credentials?: Record<string, unknown>;
|
|
10
11
|
};
|
|
11
12
|
type LibrettoWorkflowHandler<Input = unknown, Output = unknown, S = {}> = (ctx: LibrettoWorkflowContext<S>, input: Input) => Promise<Output>;
|
|
12
13
|
declare class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
|
|
13
14
|
readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
15
|
+
readonly name: string;
|
|
14
16
|
private readonly handler;
|
|
15
|
-
constructor(handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
17
|
+
constructor(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>);
|
|
16
18
|
run(ctx: LibrettoWorkflowContext<S>, input: Input): Promise<Output>;
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
type ExportedLibrettoWorkflow = {
|
|
21
|
+
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
type WorkflowModuleExports = Record<string, unknown>;
|
|
26
|
+
declare function isLibrettoWorkflow(value: unknown): value is ExportedLibrettoWorkflow;
|
|
27
|
+
declare function getWorkflowsFromModuleExports(moduleExports: WorkflowModuleExports): ExportedLibrettoWorkflow[];
|
|
28
|
+
declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExports, workflowName: string): ExportedLibrettoWorkflow | null;
|
|
29
|
+
declare function workflow<Input = unknown, Output = unknown, S = {}>(name: string, handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
|
|
19
30
|
|
|
20
|
-
export { LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, workflow };
|
|
31
|
+
export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
|
|
@@ -1,19 +1,66 @@
|
|
|
1
1
|
const LIBRETTO_WORKFLOW_BRAND = /* @__PURE__ */ Symbol.for("libretto.workflow");
|
|
2
2
|
class LibrettoWorkflow {
|
|
3
3
|
[LIBRETTO_WORKFLOW_BRAND] = true;
|
|
4
|
+
name;
|
|
4
5
|
handler;
|
|
5
|
-
constructor(handler) {
|
|
6
|
+
constructor(name, handler) {
|
|
7
|
+
this.name = name;
|
|
6
8
|
this.handler = handler;
|
|
7
9
|
}
|
|
8
10
|
async run(ctx, input) {
|
|
9
11
|
return this.handler(ctx, input);
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
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);
|
|
14
58
|
}
|
|
15
59
|
export {
|
|
16
60
|
LIBRETTO_WORKFLOW_BRAND,
|
|
17
61
|
LibrettoWorkflow,
|
|
62
|
+
getWorkflowFromModuleExports,
|
|
63
|
+
getWorkflowsFromModuleExports,
|
|
64
|
+
isLibrettoWorkflow,
|
|
18
65
|
workflow
|
|
19
66
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3-experimental.0",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,11 +36,9 @@
|
|
|
36
36
|
"build": "tsup --config tsup.config.ts",
|
|
37
37
|
"type-check": "tsc --noEmit",
|
|
38
38
|
"test": "pnpm run build && vitest run",
|
|
39
|
-
"eval": "pnpm run build && vitest run --config vitest.evals.config.ts",
|
|
40
|
-
"benchmark": "pnpm run build && tsx benchmarks/run.ts",
|
|
41
39
|
"test:watch": "vitest",
|
|
42
40
|
"cli": "node dist/index.js",
|
|
43
|
-
"
|
|
41
|
+
"generate-changelog": "tsx scripts/generate-changelog.ts",
|
|
44
42
|
"prepack": "pnpm run build"
|
|
45
43
|
},
|
|
46
44
|
"peerDependencies": {
|
|
@@ -69,8 +67,13 @@
|
|
|
69
67
|
"@ai-sdk/google-vertex": "^4.0.80",
|
|
70
68
|
"@ai-sdk/openai": "^3.0.41",
|
|
71
69
|
"@anthropic-ai/claude-agent-sdk": "^0.2.75",
|
|
70
|
+
"@mariozechner/pi-agent-core": "^0.62.0",
|
|
71
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
72
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
73
|
+
"@sinclair/typebox": "^0.34.48",
|
|
72
74
|
"@types/node": "^25.5.0",
|
|
73
75
|
"glimpseui": "^0.5.1",
|
|
76
|
+
"google-auth-library": "^10.6.1",
|
|
74
77
|
"openai": "^6.29.0",
|
|
75
78
|
"tsup": "^8.5.1",
|
|
76
79
|
"typescript": "^5.9.3",
|
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { compareSkillDirs, SKILL_DIRS } from "./skills-libretto.mjs";
|
|
7
7
|
|
|
8
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const repoRoot = join(__dirname, "..");
|
|
9
|
+
const repoRoot = join(__dirname, "..", "..", "..");
|
|
10
10
|
const result = compareSkillDirs(repoRoot);
|
|
11
11
|
|
|
12
12
|
if (result.ok) {
|