libretto 0.5.2 → 0.5.3-experimental.1

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 CHANGED
@@ -63,8 +63,9 @@ You can also use Libretto directly from the command line. All commands accept `-
63
63
  npx libretto init # interactive; run yourself, not through an agent
64
64
  npx libretto open <url> # launch browser and open a URL (headed by default)
65
65
  npx libretto snapshot --objective "..." --context "..." # capture PNG + HTML and analyze with an LLM
66
- npx libretto exec "<code>" # execute Playwright TypeScript against the open page
67
- npx libretto run <file> <export> # run an exported workflow from a file
66
+ npx libretto exec "<code>" # execute Playwright TypeScript against the open page (single quoted argument)
67
+ echo "<code>" | npx libretto exec - # intentionally read Playwright TypeScript from stdin
68
+ npx libretto run <file> <workflowName> # run an exported workflow from a file
68
69
  npx libretto resume # resume a paused workflow
69
70
  npx libretto network # view captured network requests
70
71
  npx libretto actions # view captured user/agent actions
@@ -0,0 +1,162 @@
1
+ import { execSync } from "node:child_process";
2
+ import {
3
+ cpSync,
4
+ mkdirSync,
5
+ readFileSync
6
+ } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join, resolve } from "node:path";
9
+ import { z } from "zod";
10
+ import { SimpleCLI } from "../framework/simple-cli.js";
11
+ function getConfig() {
12
+ const apiUrl = process.env.LIBRETTO_API_URL;
13
+ const apiKey = process.env.LIBRETTO_API_KEY;
14
+ if (!apiUrl) {
15
+ throw new Error(
16
+ "LIBRETTO_API_URL environment variable is required."
17
+ );
18
+ }
19
+ if (!apiKey) {
20
+ throw new Error(
21
+ "LIBRETTO_API_KEY environment variable is required."
22
+ );
23
+ }
24
+ return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
25
+ }
26
+ async function postJson(apiUrl, apiKey, path, input = {}) {
27
+ return fetch(`${apiUrl}${path}`, {
28
+ method: "POST",
29
+ headers: {
30
+ "x-api-key": apiKey,
31
+ "Content-Type": "application/json"
32
+ },
33
+ body: JSON.stringify({ json: input })
34
+ });
35
+ }
36
+ function buildSourceTarball(sourceDir) {
37
+ const absSourceDir = resolve(sourceDir);
38
+ const pkgJsonPath = join(absSourceDir, "package.json");
39
+ try {
40
+ readFileSync(pkgJsonPath, "utf8");
41
+ } catch {
42
+ throw new Error(
43
+ `No package.json found in ${absSourceDir}. Deploy source must contain a package.json.`
44
+ );
45
+ }
46
+ const dir = join(tmpdir(), `libretto-deploy-${Date.now()}`);
47
+ mkdirSync(dir, { recursive: true });
48
+ cpSync(absSourceDir, dir, { recursive: true });
49
+ const tarPath = join(dir, "source.tar.gz");
50
+ execSync(
51
+ `tar czf "${tarPath}" --exclude=source.tar.gz --exclude=node_modules --exclude=.git -C "${dir}" .`
52
+ );
53
+ return readFileSync(tarPath).toString("base64");
54
+ }
55
+ async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
56
+ const start = Date.now();
57
+ let status = "building";
58
+ let deployment;
59
+ while (status === "building" && Date.now() - start < maxWaitMs) {
60
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
61
+ const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
62
+ id: deploymentId
63
+ });
64
+ const body = await res.json();
65
+ if (res.status !== 200) {
66
+ throw new Error(
67
+ `Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`
68
+ );
69
+ }
70
+ status = body.json.status;
71
+ deployment = body.json;
72
+ process.stdout.write(".");
73
+ }
74
+ console.log();
75
+ if (!deployment) {
76
+ throw new Error("Deployment timed out before receiving a status update.");
77
+ }
78
+ return deployment;
79
+ }
80
+ const deployInput = SimpleCLI.input({
81
+ positionals: [
82
+ SimpleCLI.positional("sourceDir", z.string().default("."), {
83
+ help: "Path to source directory (default: current directory)"
84
+ })
85
+ ],
86
+ named: {
87
+ name: SimpleCLI.option(z.string(), {
88
+ help: "Deployment name"
89
+ }),
90
+ description: SimpleCLI.option(z.string().optional(), {
91
+ help: "Deployment description"
92
+ }),
93
+ entryPoint: SimpleCLI.option(z.string().optional(), {
94
+ name: "entry-point",
95
+ help: "Entry point file (default: index.ts)"
96
+ })
97
+ }
98
+ });
99
+ const deployCommand = SimpleCLI.command({
100
+ description: "[experimental] Deploy workflows to the hosted platform",
101
+ experimental: true
102
+ }).input(deployInput).handle(async ({ input }) => {
103
+ const { apiUrl, apiKey } = getConfig();
104
+ console.log(`Packaging source from ${resolve(input.sourceDir)}...`);
105
+ const source = buildSourceTarball(input.sourceDir);
106
+ const createPayload = {
107
+ name: input.name,
108
+ source
109
+ };
110
+ if (input.description) createPayload.description = input.description;
111
+ if (input.entryPoint) createPayload.entry_point = input.entryPoint;
112
+ console.log("Uploading deployment...");
113
+ const res = await postJson(
114
+ apiUrl,
115
+ apiKey,
116
+ "/v1/deployments/create",
117
+ createPayload
118
+ );
119
+ const body = await res.json();
120
+ if (res.status !== 200) {
121
+ throw new Error(
122
+ `Failed to create deployment (${res.status}): ${JSON.stringify(body)}`
123
+ );
124
+ }
125
+ const { deployment_id, name, version, status } = body.json;
126
+ console.log(
127
+ `Deployment created: ${name} v${version} (${deployment_id})`
128
+ );
129
+ console.log(`Status: ${status}`);
130
+ if (status === "building") {
131
+ process.stdout.write("Waiting for build");
132
+ const deployment = await pollDeployment(
133
+ apiUrl,
134
+ apiKey,
135
+ deployment_id,
136
+ 1e4,
137
+ 5 * 60 * 1e3
138
+ );
139
+ if (deployment.status === "failed") {
140
+ throw new Error(
141
+ `Build failed: ${deployment.build_error ?? "unknown error"}`
142
+ );
143
+ }
144
+ if (deployment.status === "ready") {
145
+ console.log(`Build complete.`);
146
+ if (deployment.workflows?.length) {
147
+ console.log(
148
+ `Workflows: ${deployment.workflows.join(", ")}`
149
+ );
150
+ }
151
+ } else {
152
+ console.log(
153
+ `Build still in progress (timed out waiting). Check status with deployment ID: ${deployment_id}`
154
+ );
155
+ }
156
+ }
157
+ return deployment_id;
158
+ });
159
+ export {
160
+ deployCommand,
161
+ deployInput
162
+ };
@@ -445,9 +445,10 @@ async function runIntegrationFromFile(args, logger) {
445
445
  );
446
446
  const payload = JSON.stringify({
447
447
  integrationPath: args.integrationPath,
448
- exportName: args.exportName,
448
+ workflowName: args.workflowName,
449
449
  session: args.session,
450
450
  params: args.params,
451
+ credentials: args.credentials,
451
452
  headless: args.headless,
452
453
  visualize: args.visualize,
453
454
  authProfileDomain: args.authProfileDomain,
@@ -496,11 +497,19 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
496
497
  setSessionStatus(args.session, "completed", logger);
497
498
  console.log("Integration completed.");
498
499
  }
500
+ function readStdinSync() {
501
+ if (process.stdin.isTTY === true) return null;
502
+ try {
503
+ const content = readFileSync(0, "utf8");
504
+ return content.trim().length > 0 ? content : null;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
499
509
  const execInput = SimpleCLI.input({
500
510
  positionals: [
501
- SimpleCLI.positional("codeParts", z.array(z.string()).default([]), {
502
- help: "Playwright TypeScript code to execute",
503
- variadic: true
511
+ SimpleCLI.positional("code", z.string().optional(), {
512
+ help: "Playwright TypeScript code to execute"
504
513
  })
505
514
  ],
506
515
  named: {
@@ -511,28 +520,36 @@ const execInput = SimpleCLI.input({
511
520
  page: pageOption()
512
521
  }
513
522
  }).refine(
514
- (input) => input.codeParts.length > 0,
515
- `Usage: libretto exec <code> [--session <name>] [--visualize]`
523
+ (input) => input.code !== void 0,
524
+ `Usage: libretto exec <code|-> [--session <name>] [--visualize]
525
+ echo '<code>' | libretto exec - [--session <name>] [--visualize]`
516
526
  );
517
527
  const execCommand = SimpleCLI.command({
518
528
  description: "Execute Playwright TypeScript code"
519
529
  }).input(execInput).use(withRequiredSession()).handle(async ({ input, ctx }) => {
530
+ const code = input.code;
531
+ const codeFromArgsOrStdin = code === "-" ? readStdinSync() : code;
532
+ if (codeFromArgsOrStdin === null) {
533
+ throw new Error(
534
+ "Missing stdin input for `exec -`. Pipe Playwright code into stdin."
535
+ );
536
+ }
520
537
  await runExec(
521
- input.codeParts.join(" "),
538
+ codeFromArgsOrStdin,
522
539
  ctx.session,
523
540
  ctx.logger,
524
541
  input.visualize,
525
542
  input.page
526
543
  );
527
544
  });
528
- const runUsage = `Usage: libretto run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
545
+ const runUsage = `Usage: libretto run <integrationFile> <workflowName> [--params <json> | --params-file <path>] [--credentials <json>] [--tsconfig <path>] [--headed|--headless] [--no-visualize] [--viewport WxH]`;
529
546
  const runInput = SimpleCLI.input({
530
547
  positionals: [
531
548
  SimpleCLI.positional("integrationFile", z.string().optional(), {
532
549
  help: "Path to the integration file"
533
550
  }),
534
- SimpleCLI.positional("integrationExport", z.string().optional(), {
535
- help: "Named workflow export to run"
551
+ SimpleCLI.positional("workflowName", z.string().optional(), {
552
+ help: "Workflow name to run (from workflow(name, handler))"
536
553
  })
537
554
  ],
538
555
  named: {
@@ -544,6 +561,9 @@ const runInput = SimpleCLI.input({
544
561
  name: "params-file",
545
562
  help: "Path to a JSON params file"
546
563
  }),
564
+ credentials: SimpleCLI.option(z.string().optional(), {
565
+ help: "Inline JSON credentials passed to ctx.credentials"
566
+ }),
547
567
  tsconfig: SimpleCLI.option(z.string().optional(), {
548
568
  help: "Path to a tsconfig used for workflow module resolution"
549
569
  }),
@@ -562,7 +582,7 @@ const runInput = SimpleCLI.input({
562
582
  })
563
583
  }
564
584
  }).refine(
565
- (input) => Boolean(input.integrationFile && input.integrationExport),
585
+ (input) => Boolean(input.integrationFile && input.workflowName),
566
586
  runUsage
567
587
  ).refine(
568
588
  (input) => !(input.params && input.paramsFile),
@@ -594,6 +614,11 @@ const runCommand = SimpleCLI.command({
594
614
  await stopExistingFailedRunSession(ctx.session, ctx.logger);
595
615
  assertSessionAvailableForStart(ctx.session, ctx.logger);
596
616
  const params = resolveRunParams(input.params, input.paramsFile);
617
+ const rawCredentials = input.credentials ? parseJsonArg("--credentials", input.credentials) : void 0;
618
+ if (rawCredentials !== void 0 && (typeof rawCredentials !== "object" || rawCredentials === null || Array.isArray(rawCredentials))) {
619
+ throw new Error(`--credentials must be a JSON object (e.g., '{"key": "value"}').`);
620
+ }
621
+ const credentials = rawCredentials;
597
622
  const headlessMode = input.headed ? false : input.headless ? true : void 0;
598
623
  const visualize = !input.noVisualize;
599
624
  const viewport = resolveViewport(
@@ -603,9 +628,10 @@ const runCommand = SimpleCLI.command({
603
628
  await runIntegrationFromFile(
604
629
  {
605
630
  integrationPath: input.integrationFile,
606
- exportName: input.integrationExport,
631
+ workflowName: input.workflowName,
607
632
  session: ctx.session,
608
633
  params,
634
+ credentials,
609
635
  tsconfigPath: input.tsconfig,
610
636
  headless: headlessMode ?? false,
611
637
  visualize,
@@ -270,6 +270,10 @@ class SimpleCLIApp {
270
270
  named["--"] = args.slice(index + 1);
271
271
  break;
272
272
  }
273
+ if (arg === "-") {
274
+ positionals.push(arg);
275
+ continue;
276
+ }
273
277
  if (arg.startsWith("--")) {
274
278
  const [rawName, inlineValue] = splitNamedArg(arg.slice(2));
275
279
  const namedEntry = namedSpecs.get(rawName);
@@ -489,6 +493,7 @@ class SimpleCLIApp {
489
493
  continue;
490
494
  }
491
495
  const command2 = this.findCommandByPath(routeEntry.path);
496
+ if (command2?.experimental) continue;
492
497
  entries.push({
493
498
  label: token,
494
499
  description: command2?.description
@@ -633,6 +638,7 @@ function resolveRouteTree(routes, parentPath = [], parentMiddlewares = []) {
633
638
  routeKey: pathToRouteKey(path),
634
639
  path,
635
640
  description: command2.config.description,
641
+ experimental: command2.config.experimental,
636
642
  input: command2.input,
637
643
  middlewares: mergeInheritedMiddlewares(
638
644
  parentMiddlewares,
@@ -1,5 +1,6 @@
1
1
  import { aiCommands } from "./commands/ai.js";
2
2
  import { browserCommands } from "./commands/browser.js";
3
+ import { deployCommand } from "./commands/deploy.js";
3
4
  import { executionCommands } from "./commands/execution.js";
4
5
  import { initCommand } from "./commands/init.js";
5
6
  import { logCommands } from "./commands/logs.js";
@@ -11,7 +12,8 @@ const cliRoutes = {
11
12
  ...logCommands,
12
13
  ai: aiCommands,
13
14
  init: initCommand,
14
- snapshot: snapshotCommand
15
+ snapshot: snapshotCommand,
16
+ deploy: deployCommand
15
17
  };
16
18
  function createCLIApp() {
17
19
  return SimpleCLI.define("libretto", cliRoutes);
@@ -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,
@@ -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.2",
3
+ "version": "0.5.3-experimental.1",
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) {