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
|
@@ -5,8 +5,11 @@ import { cwd } from "node:process";
|
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import {
|
|
8
|
+
getWorkflowFromModuleExports,
|
|
9
|
+
getWorkflowsFromModuleExports,
|
|
8
10
|
instrumentContext,
|
|
9
11
|
launchBrowser,
|
|
12
|
+
type ExportedLibrettoWorkflow,
|
|
10
13
|
type LibrettoWorkflowContext,
|
|
11
14
|
} from "../../index.js";
|
|
12
15
|
import type { LoggerApi } from "../../shared/logger/index.js";
|
|
@@ -25,9 +28,7 @@ import {
|
|
|
25
28
|
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
26
29
|
import type { RunIntegrationWorkerRequest } from "./run-integration-worker-protocol.js";
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
type LoadedLibrettoWorkflow = {
|
|
31
|
+
type LoadedLibrettoWorkflow = ExportedLibrettoWorkflow & {
|
|
31
32
|
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
32
33
|
};
|
|
33
34
|
|
|
@@ -108,17 +109,6 @@ async function waitForFailureSessionRelease(args: {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function isLoadedLibrettoWorkflow(
|
|
112
|
-
value: unknown,
|
|
113
|
-
): value is LoadedLibrettoWorkflow {
|
|
114
|
-
if (!value || typeof value !== "object") return false;
|
|
115
|
-
const candidate = value as Record<PropertyKey, unknown>;
|
|
116
|
-
return (
|
|
117
|
-
candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
|
|
118
|
-
typeof candidate.run === "function"
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
112
|
function resolveLocalAuthProfilePath(domain: string): string {
|
|
123
113
|
return getProfilePath(normalizeDomain(domain));
|
|
124
114
|
}
|
|
@@ -149,9 +139,9 @@ function getAbsoluteIntegrationPath(integrationPath: string): string {
|
|
|
149
139
|
return absolutePath;
|
|
150
140
|
}
|
|
151
141
|
|
|
152
|
-
async function
|
|
142
|
+
async function loadWorkflowByName(
|
|
153
143
|
absolutePath: string,
|
|
154
|
-
|
|
144
|
+
workflowName: string,
|
|
155
145
|
): Promise<LoadedLibrettoWorkflow> {
|
|
156
146
|
let loadedModule: Record<string, unknown>;
|
|
157
147
|
try {
|
|
@@ -167,42 +157,23 @@ async function loadWorkflowExport(
|
|
|
167
157
|
);
|
|
168
158
|
}
|
|
169
159
|
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
const detail =
|
|
174
|
-
availableExports.length > 0
|
|
175
|
-
? ` Available exports: ${availableExports.join(", ")}`
|
|
176
|
-
: " The module has no exports.";
|
|
177
|
-
throw new Error(
|
|
178
|
-
`Export "${exportName}" was not found in ${absolutePath}.${detail}`,
|
|
179
|
-
);
|
|
160
|
+
const workflow = getWorkflowFromModuleExports(loadedModule, workflowName);
|
|
161
|
+
if (workflow) {
|
|
162
|
+
return workflow as LoadedLibrettoWorkflow;
|
|
180
163
|
}
|
|
181
164
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"",
|
|
191
|
-
` export const ${exportName} = workflow<InputType, OutputType>(`,
|
|
192
|
-
" async (ctx, input) => {",
|
|
193
|
-
" // ctx.session — libretto session name",
|
|
194
|
-
" // ctx.page — Playwright Page instance",
|
|
195
|
-
" // ctx.logger — MinimalLogger",
|
|
196
|
-
" // ctx.services — injected dependencies (generic, default {})",
|
|
197
|
-
" // input — JSON-serializable input matching InputType",
|
|
198
|
-
" return output; // must match OutputType",
|
|
199
|
-
" },",
|
|
200
|
-
" );",
|
|
201
|
-
].join("\n"),
|
|
202
|
-
);
|
|
203
|
-
}
|
|
165
|
+
const availableWorkflows = getWorkflowsFromModuleExports(loadedModule).map(
|
|
166
|
+
(candidate) => candidate.name,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const detail =
|
|
170
|
+
availableWorkflows.length > 0
|
|
171
|
+
? ` Available workflows: ${availableWorkflows.join(", ")}`
|
|
172
|
+
: ' No workflows found in this file. Export a workflow() instance from "libretto" directly or via `export const workflows = { ... }`.';
|
|
204
173
|
|
|
205
|
-
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Workflow "${workflowName}" not found in ${absolutePath}.${detail}`,
|
|
176
|
+
);
|
|
206
177
|
}
|
|
207
178
|
|
|
208
179
|
export async function installHeadedWorkflowVisualization(args: {
|
|
@@ -224,7 +195,7 @@ async function runIntegrationInternal(
|
|
|
224
195
|
): Promise<RunIntegrationOutcome> {
|
|
225
196
|
const { logger } = options;
|
|
226
197
|
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
227
|
-
const workflow = await
|
|
198
|
+
const workflow = await loadWorkflowByName(absolutePath, args.workflowName);
|
|
228
199
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
229
200
|
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
230
201
|
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -233,12 +204,12 @@ async function runIntegrationInternal(
|
|
|
233
204
|
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
234
205
|
|
|
235
206
|
console.log(
|
|
236
|
-
`Running
|
|
207
|
+
`Running workflow "${args.workflowName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`,
|
|
237
208
|
);
|
|
238
209
|
|
|
239
210
|
const integrationLogger = logger.withScope("integration-run", {
|
|
240
211
|
integrationPath: absolutePath,
|
|
241
|
-
|
|
212
|
+
workflowName: args.workflowName,
|
|
242
213
|
session: args.session,
|
|
243
214
|
});
|
|
244
215
|
|
|
@@ -287,6 +258,7 @@ async function runIntegrationInternal(
|
|
|
287
258
|
logger: integrationLogger,
|
|
288
259
|
page: browserSession.page,
|
|
289
260
|
services: {},
|
|
261
|
+
credentials: args.credentials,
|
|
290
262
|
};
|
|
291
263
|
|
|
292
264
|
try {
|
|
@@ -2,9 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
|
|
3
3
|
export const RunIntegrationWorkerRequestSchema = z.object({
|
|
4
4
|
integrationPath: z.string().min(1),
|
|
5
|
-
|
|
5
|
+
workflowName: z.string().min(1),
|
|
6
6
|
session: z.string().min(1),
|
|
7
7
|
params: z.unknown(),
|
|
8
|
+
credentials: z.record(z.string(), z.unknown()).optional(),
|
|
8
9
|
headless: z.boolean(),
|
|
9
10
|
visualize: z.boolean().default(true),
|
|
10
11
|
authProfileDomain: z.string().optional(),
|
package/src/index.ts
CHANGED
|
@@ -102,9 +102,13 @@ export {
|
|
|
102
102
|
|
|
103
103
|
// Workflow helpers
|
|
104
104
|
export {
|
|
105
|
+
getWorkflowFromModuleExports,
|
|
106
|
+
getWorkflowsFromModuleExports,
|
|
107
|
+
isLibrettoWorkflow,
|
|
105
108
|
LibrettoWorkflow,
|
|
106
109
|
LIBRETTO_WORKFLOW_BRAND,
|
|
107
110
|
workflow,
|
|
111
|
+
type ExportedLibrettoWorkflow,
|
|
108
112
|
type LibrettoWorkflowContext,
|
|
109
113
|
type LibrettoWorkflowHandler,
|
|
110
114
|
} from "./shared/workflow/workflow.js";
|
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
* 12. Whitespace — collapse (preserve <pre> content)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import {
|
|
24
|
+
filterSemanticClasses,
|
|
25
|
+
INTERACTIVE_ROLE_NAMES,
|
|
26
|
+
INTERACTIVE_TAG_NAMES,
|
|
27
|
+
TEST_ATTRIBUTE_NAMES,
|
|
28
|
+
TRUSTED_ATTRIBUTE_NAMES,
|
|
29
|
+
} from "../dom-semantics.js";
|
|
30
|
+
|
|
23
31
|
export type CondenseDomResult = {
|
|
24
32
|
/** The condensed HTML string. Valid, parseable HTML. */
|
|
25
33
|
html: string;
|
|
@@ -37,25 +45,8 @@ type ParsedAttribute = {
|
|
|
37
45
|
value: string | null;
|
|
38
46
|
};
|
|
39
47
|
|
|
40
|
-
const TEST_ATTRS = new Set(
|
|
41
|
-
const TRUSTED_ATTRS = new Set(
|
|
42
|
-
"id",
|
|
43
|
-
"name",
|
|
44
|
-
"for",
|
|
45
|
-
"tabindex",
|
|
46
|
-
"contenteditable",
|
|
47
|
-
"role",
|
|
48
|
-
"title",
|
|
49
|
-
"alt",
|
|
50
|
-
"type",
|
|
51
|
-
"value",
|
|
52
|
-
"placeholder",
|
|
53
|
-
"autocomplete",
|
|
54
|
-
"href",
|
|
55
|
-
"action",
|
|
56
|
-
"method",
|
|
57
|
-
"src",
|
|
58
|
-
]);
|
|
48
|
+
const TEST_ATTRS: Set<string> = new Set(TEST_ATTRIBUTE_NAMES);
|
|
49
|
+
const TRUSTED_ATTRS: Set<string> = new Set(TRUSTED_ATTRIBUTE_NAMES);
|
|
59
50
|
const STATE_ATTRS = new Set([
|
|
60
51
|
"disabled",
|
|
61
52
|
"hidden",
|
|
@@ -94,28 +85,8 @@ const SCRIPT_ATTRS = new Set([
|
|
|
94
85
|
"referrerpolicy",
|
|
95
86
|
]);
|
|
96
87
|
const STYLE_TAG_ATTRS = new Set(["media", "type", "nonce", "title"]);
|
|
97
|
-
const INTERACTIVE_TAGS = new Set(
|
|
98
|
-
|
|
99
|
-
"button",
|
|
100
|
-
"input",
|
|
101
|
-
"select",
|
|
102
|
-
"textarea",
|
|
103
|
-
"form",
|
|
104
|
-
"details",
|
|
105
|
-
"dialog",
|
|
106
|
-
"label",
|
|
107
|
-
]);
|
|
108
|
-
const INTERACTIVE_ROLES = new Set([
|
|
109
|
-
"button",
|
|
110
|
-
"link",
|
|
111
|
-
"tab",
|
|
112
|
-
"menuitem",
|
|
113
|
-
"checkbox",
|
|
114
|
-
"radio",
|
|
115
|
-
"switch",
|
|
116
|
-
"slider",
|
|
117
|
-
"combobox",
|
|
118
|
-
]);
|
|
88
|
+
const INTERACTIVE_TAGS: Set<string> = new Set(INTERACTIVE_TAG_NAMES);
|
|
89
|
+
const INTERACTIVE_ROLES: Set<string> = new Set(INTERACTIVE_ROLE_NAMES);
|
|
119
90
|
const OPEN_TAG_PATTERN =
|
|
120
91
|
/<([a-zA-Z][\w:-]*)(\s(?:[^"'<>/]|"[^"]*"|'[^']*')*)?\s*(\/?)>/g;
|
|
121
92
|
|
|
@@ -458,29 +429,6 @@ function normalizeUrlValue(value: string): string {
|
|
|
458
429
|
}
|
|
459
430
|
}
|
|
460
431
|
|
|
461
|
-
function filterSemanticClasses(value: string): string {
|
|
462
|
-
const classes = value.split(/\s+/).filter(Boolean);
|
|
463
|
-
const kept = classes.filter((cls) => !isObfuscatedClass(cls));
|
|
464
|
-
return kept.join(" ");
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* Heuristic: a class name is "obfuscated" if it looks like a hash or random ID
|
|
469
|
-
* rather than a human-readable semantic name.
|
|
470
|
-
*/
|
|
471
|
-
function isObfuscatedClass(cls: string): boolean {
|
|
472
|
-
if (cls.length > 80) return true;
|
|
473
|
-
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
474
|
-
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
475
|
-
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
476
|
-
|
|
477
|
-
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
478
|
-
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
479
|
-
if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
|
|
480
|
-
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
432
|
function parseAttributes(rawAttrs: string): ParsedAttribute[] {
|
|
485
433
|
const attrs: ParsedAttribute[] = [];
|
|
486
434
|
const attrPattern =
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const TEST_ATTRIBUTE_NAMES = [
|
|
2
|
+
"data-testid",
|
|
3
|
+
"data-test",
|
|
4
|
+
"data-qa",
|
|
5
|
+
"data-cy",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
export const TRUSTED_ATTRIBUTE_NAMES = [
|
|
9
|
+
"id",
|
|
10
|
+
"name",
|
|
11
|
+
"for",
|
|
12
|
+
"tabindex",
|
|
13
|
+
"contenteditable",
|
|
14
|
+
"role",
|
|
15
|
+
"title",
|
|
16
|
+
"alt",
|
|
17
|
+
"type",
|
|
18
|
+
"value",
|
|
19
|
+
"placeholder",
|
|
20
|
+
"autocomplete",
|
|
21
|
+
"href",
|
|
22
|
+
"action",
|
|
23
|
+
"method",
|
|
24
|
+
"src",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export const INTERACTIVE_TAG_NAMES = [
|
|
28
|
+
"a",
|
|
29
|
+
"button",
|
|
30
|
+
"input",
|
|
31
|
+
"select",
|
|
32
|
+
"textarea",
|
|
33
|
+
"form",
|
|
34
|
+
"details",
|
|
35
|
+
"dialog",
|
|
36
|
+
"label",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export const INTERACTIVE_ROLE_NAMES = [
|
|
40
|
+
"button",
|
|
41
|
+
"link",
|
|
42
|
+
"tab",
|
|
43
|
+
"menuitem",
|
|
44
|
+
"checkbox",
|
|
45
|
+
"radio",
|
|
46
|
+
"switch",
|
|
47
|
+
"slider",
|
|
48
|
+
"combobox",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
export function filterSemanticClasses(value: string): string {
|
|
52
|
+
const classes = value.split(/\s+/).filter(Boolean);
|
|
53
|
+
const kept = classes.filter((cls) => !isObfuscatedClass(cls));
|
|
54
|
+
return kept.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isObfuscatedClass(cls: string): boolean {
|
|
58
|
+
if (cls.length > 80) return true;
|
|
59
|
+
if (/^_?[0-9a-f]{6,}$/i.test(cls)) return true;
|
|
60
|
+
if (/^[a-z]+_[0-9a-f]{4,}$/i.test(cls)) return true;
|
|
61
|
+
if (/^[a-z]{1,2}[0-9]{2,}$/i.test(cls)) return true;
|
|
62
|
+
|
|
63
|
+
const digits = (cls.match(/[0-9]/g) || []).length;
|
|
64
|
+
const letters = (cls.match(/[a-zA-Z]/g) || []).length;
|
|
65
|
+
if (cls.length >= 6 && digits >= letters * 0.5 && digits >= 2) return true;
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
SESSION_STATE_VERSION,
|
|
12
12
|
SessionStateFileSchema,
|
|
13
13
|
} from "../state/session-state.js";
|
|
14
|
+
import { readLibrettoConfig } from "../../cli/core/ai-config.js";
|
|
14
15
|
|
|
15
16
|
async function pickFreePort(): Promise<number> {
|
|
16
17
|
return await new Promise((resolve, reject) => {
|
|
@@ -44,6 +45,53 @@ export type BrowserSession = {
|
|
|
44
45
|
close: () => Promise<void>;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
function resolveWindowPosition(): { x: number; y: number } | undefined {
|
|
49
|
+
return readLibrettoConfig().windowPosition;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function applyWindowPosition(
|
|
53
|
+
browser: Browser,
|
|
54
|
+
context: BrowserContext,
|
|
55
|
+
page: Page,
|
|
56
|
+
windowPosition: { x: number; y: number } | undefined,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (!windowPosition) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const requestedBounds = {
|
|
63
|
+
left: windowPosition.x,
|
|
64
|
+
top: windowPosition.y,
|
|
65
|
+
windowState: "normal" as const,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const pageCdp = await context.newCDPSession(page);
|
|
69
|
+
let browserCdp:
|
|
70
|
+
| Awaited<ReturnType<Browser["newBrowserCDPSession"]>>
|
|
71
|
+
| undefined;
|
|
72
|
+
try {
|
|
73
|
+
const targetInfo = await pageCdp.send("Target.getTargetInfo");
|
|
74
|
+
const targetId = (
|
|
75
|
+
targetInfo as { targetInfo?: { targetId?: string } }
|
|
76
|
+
).targetInfo?.targetId;
|
|
77
|
+
browserCdp = await browser.newBrowserCDPSession();
|
|
78
|
+
const windowResult = await browserCdp.send(
|
|
79
|
+
"Browser.getWindowForTarget",
|
|
80
|
+
targetId ? { targetId } : {},
|
|
81
|
+
);
|
|
82
|
+
await browserCdp.send("Browser.setWindowBounds", {
|
|
83
|
+
windowId: windowResult.windowId,
|
|
84
|
+
bounds: requestedBounds,
|
|
85
|
+
});
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
87
|
+
} catch {
|
|
88
|
+
// Best-effort: window positioning should not prevent browser launch.
|
|
89
|
+
} finally {
|
|
90
|
+
await pageCdp.detach().catch(() => {});
|
|
91
|
+
await browserCdp?.detach().catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
47
95
|
export async function launchBrowser({
|
|
48
96
|
sessionName,
|
|
49
97
|
headless = false,
|
|
@@ -51,12 +99,16 @@ export async function launchBrowser({
|
|
|
51
99
|
storageStatePath,
|
|
52
100
|
}: LaunchBrowserArgs): Promise<BrowserSession> {
|
|
53
101
|
const debugPort = await pickFreePort();
|
|
102
|
+
const windowPosition = headless ? undefined : resolveWindowPosition();
|
|
54
103
|
const browser = await chromium.launch({
|
|
55
104
|
headless,
|
|
56
105
|
args: [
|
|
57
106
|
"--disable-blink-features=AutomationControlled",
|
|
58
107
|
`--remote-debugging-port=${debugPort}`,
|
|
59
108
|
"--no-focus-on-check",
|
|
109
|
+
...(windowPosition
|
|
110
|
+
? [`--window-position=${windowPosition.x},${windowPosition.y}`]
|
|
111
|
+
: []),
|
|
60
112
|
],
|
|
61
113
|
});
|
|
62
114
|
|
|
@@ -65,6 +117,7 @@ export async function launchBrowser({
|
|
|
65
117
|
...(storageStatePath ? { storageState: storageStatePath } : {}),
|
|
66
118
|
});
|
|
67
119
|
const page = await context.newPage();
|
|
120
|
+
await applyWindowPosition(browser, context, page, windowPosition);
|
|
68
121
|
page.setDefaultTimeout(30_000);
|
|
69
122
|
page.setDefaultNavigationTimeout(45_000);
|
|
70
123
|
|
|
@@ -16,7 +16,7 @@ export type GhostCursorOptions = {
|
|
|
16
16
|
const DEFAULTS: Required<GhostCursorOptions> = {
|
|
17
17
|
style: "minimal",
|
|
18
18
|
color: "rgba(255, 70, 70, 0.9)",
|
|
19
|
-
size:
|
|
19
|
+
size: 23,
|
|
20
20
|
zIndex: 2147483646,
|
|
21
21
|
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
|
|
22
22
|
minDurationMs: 100,
|
|
@@ -38,20 +38,38 @@ function buildCursorSvg(
|
|
|
38
38
|
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>`;
|
|
39
39
|
}
|
|
40
40
|
// minimal: default arrow-like SVG cursor
|
|
41
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
41
|
+
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));">
|
|
42
42
|
<path d="M5 3L19 12L12 13L9 20L5 3Z" fill="${color}" stroke="rgba(0,0,0,0.3)" stroke-width="1"/>
|
|
43
43
|
</svg>`;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function buildCursorMarkup(
|
|
47
|
+
style: GhostCursorStyle,
|
|
48
|
+
color: string,
|
|
49
|
+
size: number,
|
|
50
|
+
): string {
|
|
51
|
+
const cursor = buildCursorSvg(style, color, size);
|
|
52
|
+
const badgeHeight = Math.max(12, Math.round(size * 0.54));
|
|
53
|
+
const fontSize = Math.max(8, Math.round(size * 0.28));
|
|
54
|
+
const minWidth = Math.max(28, Math.round(size * 1.28));
|
|
55
|
+
const paddingX = Math.max(5, Math.round(size * 0.2));
|
|
56
|
+
const left = Math.round(size * 0.84);
|
|
57
|
+
const top = Math.round(size * 0.74);
|
|
58
|
+
const width = Math.round(size * 2.4);
|
|
59
|
+
const height = Math.round(size * 1.95);
|
|
60
|
+
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>`;
|
|
61
|
+
return `<div style="position:relative;width:${width}px;height:${height}px;overflow:visible;">${cursor}${badge}</div>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function buildInitScript(opts: Required<GhostCursorOptions>): string {
|
|
47
|
-
const
|
|
65
|
+
const markup = buildCursorMarkup(opts.style, opts.color, opts.size);
|
|
48
66
|
return `
|
|
49
67
|
(function() {
|
|
50
68
|
if (document.getElementById("${CURSOR_ID}")) return;
|
|
51
69
|
var el = document.createElement("div");
|
|
52
70
|
el.id = "${CURSOR_ID}";
|
|
53
71
|
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;";
|
|
54
|
-
el.innerHTML = ${JSON.stringify(
|
|
72
|
+
el.innerHTML = ${JSON.stringify(markup)};
|
|
55
73
|
document.documentElement.appendChild(el);
|
|
56
74
|
})();
|
|
57
75
|
`;
|
|
@@ -8,6 +8,7 @@ export type LibrettoWorkflowContext<S = {}> = {
|
|
|
8
8
|
page: Page;
|
|
9
9
|
logger: MinimalLogger;
|
|
10
10
|
services: S;
|
|
11
|
+
credentials?: Record<string, unknown>;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export type LibrettoWorkflowHandler<
|
|
@@ -18,9 +19,14 @@ export type LibrettoWorkflowHandler<
|
|
|
18
19
|
|
|
19
20
|
export class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
|
|
20
21
|
public readonly [LIBRETTO_WORKFLOW_BRAND] = true;
|
|
22
|
+
public readonly name: string;
|
|
21
23
|
private readonly handler: LibrettoWorkflowHandler<Input, Output, S>;
|
|
22
24
|
|
|
23
|
-
constructor(
|
|
25
|
+
constructor(
|
|
26
|
+
name: string,
|
|
27
|
+
handler: LibrettoWorkflowHandler<Input, Output, S>,
|
|
28
|
+
) {
|
|
29
|
+
this.name = name;
|
|
24
30
|
this.handler = handler;
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -29,8 +35,88 @@ export class LibrettoWorkflow<Input = unknown, Output = unknown, S = {}> {
|
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
export type ExportedLibrettoWorkflow = {
|
|
39
|
+
readonly [LIBRETTO_WORKFLOW_BRAND]: true;
|
|
40
|
+
readonly name: string;
|
|
41
|
+
run: (ctx: LibrettoWorkflowContext, input: unknown) => Promise<unknown>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type WorkflowModuleExports = Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
// Use the workflow brand instead of `instanceof` so imported workflows are
|
|
47
|
+
// still recognized after loading the integration module dynamically.
|
|
48
|
+
export function isLibrettoWorkflow(
|
|
49
|
+
value: unknown,
|
|
50
|
+
): value is ExportedLibrettoWorkflow {
|
|
51
|
+
if (!value || typeof value !== "object") return false;
|
|
52
|
+
const candidate = value as Record<PropertyKey, unknown>;
|
|
53
|
+
return (
|
|
54
|
+
candidate[LIBRETTO_WORKFLOW_BRAND] === true &&
|
|
55
|
+
typeof candidate.name === "string" &&
|
|
56
|
+
typeof candidate.run === "function"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function addWorkflowOrThrow(
|
|
61
|
+
workflowsByName: Map<string, ExportedLibrettoWorkflow>,
|
|
62
|
+
value: unknown,
|
|
63
|
+
): void {
|
|
64
|
+
if (!isLibrettoWorkflow(value)) return;
|
|
65
|
+
|
|
66
|
+
// Re-exporting the same workflow object is fine, but two distinct workflow
|
|
67
|
+
// instances cannot claim the same runtime name.
|
|
68
|
+
const existing = workflowsByName.get(value.name);
|
|
69
|
+
if (existing && existing !== value) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Duplicate workflow name: "${value.name}". Each workflow() call must use a unique name.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
workflowsByName.set(value.name, value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getWorkflowsFromModuleExports(
|
|
79
|
+
moduleExports: WorkflowModuleExports,
|
|
80
|
+
): ExportedLibrettoWorkflow[] {
|
|
81
|
+
const workflowsByName = new Map<string, ExportedLibrettoWorkflow>();
|
|
82
|
+
|
|
83
|
+
for (const [exportName, value] of Object.entries(moduleExports)) {
|
|
84
|
+
if (exportName === "workflows" && value && typeof value === "object") {
|
|
85
|
+
// Support both `export const workflows = workflow(...)` and
|
|
86
|
+
// `export const workflows = { myWorkflow }`.
|
|
87
|
+
if (isLibrettoWorkflow(value)) {
|
|
88
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
89
|
+
} else {
|
|
90
|
+
for (const nestedValue of Object.values(
|
|
91
|
+
value as Record<string, unknown>,
|
|
92
|
+
)) {
|
|
93
|
+
addWorkflowOrThrow(workflowsByName, nestedValue);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
addWorkflowOrThrow(workflowsByName, value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [...workflowsByName.values()];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getWorkflowFromModuleExports(
|
|
106
|
+
moduleExports: WorkflowModuleExports,
|
|
107
|
+
workflowName: string,
|
|
108
|
+
): ExportedLibrettoWorkflow | null {
|
|
109
|
+
for (const workflow of getWorkflowsFromModuleExports(moduleExports)) {
|
|
110
|
+
if (workflow.name === workflowName) {
|
|
111
|
+
return workflow;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
32
117
|
export function workflow<Input = unknown, Output = unknown, S = {}>(
|
|
118
|
+
name: string,
|
|
33
119
|
handler: LibrettoWorkflowHandler<Input, Output, S>,
|
|
34
120
|
): LibrettoWorkflow<Input, Output, S> {
|
|
35
|
-
return new LibrettoWorkflow(handler);
|
|
121
|
+
return new LibrettoWorkflow(name, handler);
|
|
36
122
|
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
set -euo pipefail
|
|
3
|
-
|
|
4
|
-
usage() {
|
|
5
|
-
cat <<'EOF'
|
|
6
|
-
Usage: scripts/prepare-release.sh [patch|minor|major]
|
|
7
|
-
|
|
8
|
-
Creates a release PR branch from main, bumps package.json, pushes the branch,
|
|
9
|
-
and opens a pull request targeting main.
|
|
10
|
-
EOF
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
bump="${1:-patch}"
|
|
14
|
-
|
|
15
|
-
case "$bump" in
|
|
16
|
-
patch|minor|major)
|
|
17
|
-
;;
|
|
18
|
-
-h|--help|help)
|
|
19
|
-
usage
|
|
20
|
-
exit 0
|
|
21
|
-
;;
|
|
22
|
-
*)
|
|
23
|
-
echo "Invalid bump type: $bump" >&2
|
|
24
|
-
usage >&2
|
|
25
|
-
exit 1
|
|
26
|
-
;;
|
|
27
|
-
esac
|
|
28
|
-
|
|
29
|
-
if ! command -v gh >/dev/null 2>&1; then
|
|
30
|
-
echo "gh CLI is required." >&2
|
|
31
|
-
exit 1
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
if [ -n "$(git status --porcelain)" ]; then
|
|
35
|
-
echo "Working tree must be clean before preparing a release." >&2
|
|
36
|
-
exit 1
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
current_branch="$(git branch --show-current)"
|
|
40
|
-
if [ "$current_branch" != "main" ]; then
|
|
41
|
-
echo "Switching from $current_branch to main."
|
|
42
|
-
fi
|
|
43
|
-
|
|
44
|
-
git fetch origin
|
|
45
|
-
git checkout main
|
|
46
|
-
git pull --ff-only origin main
|
|
47
|
-
|
|
48
|
-
pnpm install --frozen-lockfile
|
|
49
|
-
pnpm type-check
|
|
50
|
-
pnpm test
|
|
51
|
-
|
|
52
|
-
current_version="$(node -p "require('./package.json').version")"
|
|
53
|
-
next_version="$(node -e '
|
|
54
|
-
const [major, minor, patch] = process.argv[1].split(".").map(Number)
|
|
55
|
-
const bump = process.argv[2]
|
|
56
|
-
|
|
57
|
-
let next
|
|
58
|
-
if (bump === "major") next = [major + 1, 0, 0]
|
|
59
|
-
else if (bump === "minor") next = [major, minor + 1, 0]
|
|
60
|
-
else next = [major, minor, patch + 1]
|
|
61
|
-
|
|
62
|
-
process.stdout.write(next.join("."))
|
|
63
|
-
' "$current_version" "$bump")"
|
|
64
|
-
branch_name="tk-release-v${next_version}"
|
|
65
|
-
|
|
66
|
-
if git show-ref --verify --quiet "refs/heads/${branch_name}"; then
|
|
67
|
-
echo "Local branch ${branch_name} already exists." >&2
|
|
68
|
-
exit 1
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
if git ls-remote --exit-code --heads origin "${branch_name}" >/dev/null 2>&1; then
|
|
72
|
-
echo "Remote branch ${branch_name} already exists." >&2
|
|
73
|
-
exit 1
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
npm version "$next_version" --no-git-tag-version >/dev/null
|
|
77
|
-
|
|
78
|
-
git checkout -b "$branch_name"
|
|
79
|
-
git add package.json
|
|
80
|
-
git commit -m "release: v${next_version}"
|
|
81
|
-
git push -u origin "$branch_name"
|
|
82
|
-
|
|
83
|
-
gh pr create \
|
|
84
|
-
--base main \
|
|
85
|
-
--head "$branch_name" \
|
|
86
|
-
--title "release: v${next_version}" \
|
|
87
|
-
--body "$(cat <<EOF
|
|
88
|
-
## Summary
|
|
89
|
-
|
|
90
|
-
- release libretto v${next_version}
|
|
91
|
-
|
|
92
|
-
## Verification
|
|
93
|
-
|
|
94
|
-
- pnpm type-check
|
|
95
|
-
- pnpm test
|
|
96
|
-
EOF
|
|
97
|
-
)"
|