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.
Files changed (45) hide show
  1. package/README.md +10 -5
  2. package/dist/cli/commands/execution.js +38 -12
  3. package/dist/cli/commands/init.js +4 -21
  4. package/dist/cli/core/ai-config.js +12 -2
  5. package/dist/cli/core/browser.js +75 -8
  6. package/dist/cli/core/session-telemetry.js +429 -172
  7. package/dist/cli/core/telemetry.js +10 -2
  8. package/dist/cli/framework/simple-cli.js +4 -0
  9. package/dist/cli/workers/run-integration-runtime.js +18 -41
  10. package/dist/cli/workers/run-integration-worker-protocol.js +2 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +6 -0
  13. package/dist/shared/condense-dom/condense-dom.js +11 -56
  14. package/dist/shared/dom-semantics.d.ts +8 -0
  15. package/dist/shared/dom-semantics.js +69 -0
  16. package/dist/shared/run/browser.js +40 -1
  17. package/dist/shared/visualization/ghost-cursor.js +17 -4
  18. package/dist/shared/workflow/workflow.d.ts +14 -3
  19. package/dist/shared/workflow/workflow.js +50 -3
  20. package/package.json +7 -4
  21. package/scripts/check-skills-sync.mjs +1 -1
  22. package/scripts/generate-changelog.ts +132 -0
  23. package/scripts/skills-libretto.mjs +1 -1
  24. package/scripts/sync-skills.mjs +1 -1
  25. package/skills/libretto/SKILL.md +54 -38
  26. package/skills/libretto/references/action-logs.md +101 -0
  27. package/skills/libretto/references/auth-profiles.md +1 -2
  28. package/skills/libretto/references/code-generation-rules.md +10 -6
  29. package/skills/libretto/references/pages-and-page-targeting.md +1 -1
  30. package/src/cli/commands/execution.ts +39 -11
  31. package/src/cli/commands/init.ts +5 -24
  32. package/src/cli/core/ai-config.ts +12 -1
  33. package/src/cli/core/browser.ts +82 -8
  34. package/src/cli/core/session-telemetry.ts +431 -190
  35. package/src/cli/core/telemetry.ts +23 -1
  36. package/src/cli/framework/simple-cli.ts +5 -0
  37. package/src/cli/workers/run-integration-runtime.ts +24 -52
  38. package/src/cli/workers/run-integration-worker-protocol.ts +2 -1
  39. package/src/index.ts +4 -0
  40. package/src/shared/condense-dom/condense-dom.ts +12 -64
  41. package/src/shared/dom-semantics.ts +68 -0
  42. package/src/shared/run/browser.ts +53 -0
  43. package/src/shared/visualization/ghost-cursor.ts +22 -4
  44. package/src/shared/workflow/workflow.ts +88 -2
  45. 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 loadWorkflowExport(absolutePath, exportName) {
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 targetExport = loadedModule[exportName];
109
- if (!targetExport) {
110
- const availableExports = Object.keys(loadedModule);
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
- return targetExport;
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 loadWorkflowExport(absolutePath, args.exportName);
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 integration "${args.exportName}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
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
- integrationExport: args.exportName,
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
- exportName: z.string().min(1),
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
- const TEST_ATTRS = /* @__PURE__ */ new Set(["data-testid", "data-test", "data-qa", "data-cy"]);
2
- const TRUSTED_ATTRS = /* @__PURE__ */ new Set([
3
- "id",
4
- "name",
5
- "for",
6
- "tabindex",
7
- "contenteditable",
8
- "role",
9
- "title",
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 = /* @__PURE__ */ new Set([
59
- "a",
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: 20,
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 svg = buildCursorSvg(opts.style, opts.color, opts.size);
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(svg)};
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
- declare function workflow<Input = unknown, Output = unknown, S = {}>(handler: LibrettoWorkflowHandler<Input, Output, S>): LibrettoWorkflow<Input, Output, S>;
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 workflow(handler) {
13
- return new LibrettoWorkflow(handler);
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.1",
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
- "prepare-release": "bash ./scripts/prepare-release.sh",
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) {