libretto 0.6.19 → 0.6.20

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
@@ -28,17 +28,14 @@ https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
28
28
  ## Installation
29
29
 
30
30
  ```bash
31
- # Install the Libretto command once. Requires Node.js and npm.
32
- curl -fsSL https://libretto.sh/install.sh | bash
33
-
34
- # Add Libretto to your project
31
+ # Add Libretto to your project. Requires Node.js and npm.
35
32
  npm install libretto
36
33
 
37
34
  # First-time onboarding: install skills and download Chromium
38
- libretto setup
35
+ npx libretto setup
39
36
 
40
37
  # Check workspace readiness at any time
41
- libretto status
38
+ npx libretto status
42
39
  ```
43
40
 
44
41
  `setup` creates the `.libretto/` directory, installs agent skills, and downloads Chromium unless you pass `--skip-browsers`.
@@ -76,17 +73,17 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
76
73
  You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
77
74
 
78
75
  ```bash
79
- libretto open <url> # launch browser and open a URL
80
- libretto run ./integration.ts --headless # run a workflow and close on success
81
- libretto run ./integration.ts --headless --stay-open-on-success # keep a successful run inspectable
82
- libretto snapshot --session <name> # capture a screenshot and compact accessibility tree
83
- libretto exec "<code>" # execute Playwright TypeScript against the open page
84
- libretto close # close the browser
76
+ npx libretto open <url> # launch browser and open a URL
77
+ npx libretto run ./integration.ts --headless # run a workflow and close on success
78
+ npx libretto run ./integration.ts --headless --stay-open-on-success # keep a successful run inspectable
79
+ npx libretto snapshot --session <name> # capture a screenshot and compact accessibility tree
80
+ npx libretto exec "<code>" # execute Playwright TypeScript against the open page
81
+ npx libretto close # close the browser
85
82
  ```
86
83
 
87
84
  `run` sessions are inspectable through the same daemon-backed commands as `open` sessions. Successful runs close the browser by default; pass `--stay-open-on-success` to keep the browser open for `pages`, `snapshot`, and `exec`. Failed or paused workflows keep the browser open so you can inspect the exact page state before fixing or resuming the workflow.
88
85
 
89
- Run `libretto help` for the full list of commands.
86
+ Run `npx libretto help` for the full list of commands.
90
87
 
91
88
  ## Configuration
92
89
 
@@ -26,17 +26,14 @@ https://github.com/user-attachments/assets/9b9a0ab3-5133-4b20-b3be-459943349d18
26
26
  ## Installation
27
27
 
28
28
  ```bash
29
- # Install the Libretto command once. Requires Node.js and npm.
30
- curl -fsSL https://libretto.sh/install.sh | bash
31
-
32
- # Add Libretto to your project
29
+ # Add Libretto to your project. Requires Node.js and npm.
33
30
  npm install libretto
34
31
 
35
32
  # First-time onboarding: install skills and download Chromium
36
- libretto setup
33
+ npx libretto setup
37
34
 
38
35
  # Check workspace readiness at any time
39
- libretto status
36
+ npx libretto status
40
37
  ```
41
38
 
42
39
  `setup` creates the `.libretto/` directory, installs agent skills, and downloads Chromium unless you pass `--skip-browsers`.
@@ -74,17 +71,17 @@ Agents can use Libretto to reproduce the failure, pause the workflow at any poin
74
71
  You can also use Libretto directly from the command line. All commands accept `--session <name>` to target a specific session.
75
72
 
76
73
  ```bash
77
- libretto open <url> # launch browser and open a URL
78
- libretto run ./integration.ts --headless # run a workflow and close on success
79
- libretto run ./integration.ts --headless --stay-open-on-success # keep a successful run inspectable
80
- libretto snapshot --session <name> # capture a screenshot and compact accessibility tree
81
- libretto exec "<code>" # execute Playwright TypeScript against the open page
82
- libretto close # close the browser
74
+ npx libretto open <url> # launch browser and open a URL
75
+ npx libretto run ./integration.ts --headless # run a workflow and close on success
76
+ npx libretto run ./integration.ts --headless --stay-open-on-success # keep a successful run inspectable
77
+ npx libretto snapshot --session <name> # capture a screenshot and compact accessibility tree
78
+ npx libretto exec "<code>" # execute Playwright TypeScript against the open page
79
+ npx libretto close # close the browser
83
80
  ```
84
81
 
85
82
  `run` sessions are inspectable through the same daemon-backed commands as `open` sessions. Successful runs close the browser by default; pass `--stay-open-on-success` to keep the browser open for `pages`, `snapshot`, and `exec`. Failed or paused workflows keep the browser open so you can inspect the exact page state before fixing or resuming the workflow.
86
83
 
87
- Run `libretto help` for the full list of commands.
84
+ Run `npx libretto help` for the full list of commands.
88
85
 
89
86
  ## Configuration
90
87
 
package/dist/cli/cli.js CHANGED
@@ -11,19 +11,6 @@ function renderVersion() {
11
11
  function printSetupAudit() {
12
12
  warnIfLibrettoVersionsDiffer();
13
13
  }
14
- function isPackageManagerExec(env = process.env) {
15
- return env.npm_command === "exec";
16
- }
17
- function warnIfPackageManagerExec() {
18
- if (!isPackageManagerExec()) return;
19
- console.error(
20
- [
21
- "Warning: running Libretto through a package manager is deprecated and will be removed in a future release.",
22
- "Install the native command instead:",
23
- " curl -fsSL https://libretto.sh/install.sh | bash"
24
- ].join("\n")
25
- );
26
- }
27
14
  function isRootHelpRequest(rawArgs) {
28
15
  if (rawArgs.length === 0) return true;
29
16
  return rawArgs[0] === "help" && rawArgs.length === 1;
@@ -42,7 +29,6 @@ async function runLibrettoCLI() {
42
29
  const rawArgs = process.argv.slice(2);
43
30
  let exitCode = 0;
44
31
  loadEnv();
45
- warnIfPackageManagerExec();
46
32
  ensureLibrettoSetup();
47
33
  const app = createCLIApp();
48
34
  try {
@@ -93,6 +93,10 @@ const deployInput = SimpleCLI.input({
93
93
  name: "entry-point",
94
94
  help: "Entry point file (default: index.ts)"
95
95
  }),
96
+ autoRepair: SimpleCLI.flag({
97
+ name: "auto-repair",
98
+ help: "Route failed jobs for this deployment to autofix"
99
+ }),
96
100
  external: SimpleCLI.option(
97
101
  z.string().optional().transform(
98
102
  (value) => value?.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0) ?? []
@@ -120,6 +124,7 @@ const deployCommand = SimpleCLI.command({
120
124
  entry_point: entryPoint
121
125
  };
122
126
  if (input.description) createPayload.description = input.description;
127
+ if (input.autoRepair) createPayload.auto_repair = true;
123
128
  console.log("Uploading deployment...");
124
129
  const body = await orpcCall({
125
130
  apiUrl,
@@ -1,8 +1,16 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { SimpleCLI } from "affordance";
5
- const UPDATE_COMMAND = "curl -fsSL https://libretto.sh/install.sh | bash";
6
+ import { REPO_ROOT } from "../core/context.js";
7
+ import {
8
+ detectProjectPackageManager,
9
+ installCommand
10
+ } from "../../shared/package-manager.js";
11
+ function packageInstallCommand(packageManager, packageSpec) {
12
+ return `${installCommand(packageManager)} ${packageSpec}`;
13
+ }
6
14
  function readCurrentCliVersion() {
7
15
  const packageJsonPath = fileURLToPath(
8
16
  new URL("../../../package.json", import.meta.url)
@@ -17,6 +25,21 @@ function readCurrentCliVersion() {
17
25
  }
18
26
  return manifest.version;
19
27
  }
28
+ function readPackageVersion(packageJsonPath) {
29
+ try {
30
+ const manifest = JSON.parse(
31
+ readFileSync(packageJsonPath, "utf8")
32
+ );
33
+ return manifest.version?.trim() || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function readLocalPackageVersion() {
39
+ return readPackageVersion(
40
+ join(REPO_ROOT, "node_modules", "libretto", "package.json")
41
+ );
42
+ }
20
43
  function readLatestNpmVersion() {
21
44
  const result = spawnSync("npm", ["view", "libretto@latest", "version"], {
22
45
  encoding: "utf8"
@@ -65,55 +88,61 @@ const updateInput = SimpleCLI.input({
65
88
  })
66
89
  }
67
90
  });
68
- function formatUpdateFailure(status, signal) {
69
- const knownState = status === null ? `installer was interrupted${signal ? ` by ${signal}` : ""}.` : `installer exited with status ${status}.`;
91
+ function formatUpdateFailure(status, signal, updateCommand2) {
92
+ const knownState = status === null ? `package update was interrupted${signal ? ` by ${signal}` : ""}.` : `package update exited with status ${status}.`;
70
93
  return [
71
94
  "Error: failed to update Libretto to the latest version.",
72
95
  `Known state: ${knownState}`,
73
- `Try: ${UPDATE_COMMAND}`,
96
+ `Try: ${updateCommand2}`,
74
97
  "Help: libretto help update"
75
98
  ].join("\n");
76
99
  }
77
100
  const updateCommand = SimpleCLI.command({
78
101
  description: "Update Libretto to the latest version"
79
102
  }).input(updateInput).handle(async ({ input }) => {
103
+ const packageManager = detectProjectPackageManager();
104
+ const updateCommand2 = packageInstallCommand(packageManager, "libretto@latest");
80
105
  if (input.dryRun) {
81
106
  console.log("Update command:");
82
- console.log(` ${UPDATE_COMMAND}`);
107
+ console.log(` ${updateCommand2}`);
83
108
  console.log("No changes made.");
84
109
  return;
85
110
  }
86
111
  const currentVersion = readCurrentCliVersion();
112
+ const localPackageVersion = readLocalPackageVersion();
113
+ const installedVersion = localPackageVersion ?? currentVersion;
87
114
  const latestVersion = readLatestNpmVersion();
88
- console.log(`Current version: ${currentVersion}`);
115
+ console.log(`Current version: ${installedVersion}`);
89
116
  console.log(`Latest version: ${latestVersion}`);
90
- if (currentVersion === latestVersion) {
91
- console.log(`Libretto is already up to date (${currentVersion}).`);
117
+ if (localPackageVersion && installedVersion === latestVersion) {
118
+ console.log(`Libretto is already up to date (${installedVersion}).`);
92
119
  console.log("No further action required.");
93
120
  return;
94
121
  }
95
- console.log("Updating Libretto to latest...");
96
- const result = spawnSync("bash", ["-lc", UPDATE_COMMAND], {
122
+ if (!localPackageVersion) {
123
+ console.log("Local package: not installed");
124
+ }
125
+ console.log("Updating local Libretto package to latest...");
126
+ const result = spawnSync(updateCommand2, {
97
127
  stdio: "inherit",
98
- env: {
99
- ...process.env,
100
- LIBRETTO_VERSION: "latest"
101
- }
128
+ shell: true
102
129
  });
103
130
  if (result.error) {
104
131
  throw new Error(
105
132
  [
106
- "Error: failed to start the Libretto installer.",
133
+ "Error: failed to start the Libretto package update.",
107
134
  `Known state: ${result.error.message}`,
108
- `Try: ${UPDATE_COMMAND}`,
135
+ `Try: ${updateCommand2}`,
109
136
  "Help: libretto help update"
110
137
  ].join("\n")
111
138
  );
112
139
  }
113
140
  if (result.status !== 0) {
114
- throw new Error(formatUpdateFailure(result.status, result.signal));
141
+ throw new Error(
142
+ formatUpdateFailure(result.status, result.signal, updateCommand2)
143
+ );
115
144
  }
116
- console.log("Libretto updated to latest.");
145
+ console.log("Local Libretto package updated to latest.");
117
146
  console.log("No further action required.");
118
147
  });
119
148
  export {
@@ -47,6 +47,7 @@ import {
47
47
  loadDefaultWorkflow
48
48
  } from "../workflow-runtime.js";
49
49
  import { WorkflowController } from "../workflow-runner/runner.js";
50
+ import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
50
51
  function isOperationalPage(page) {
51
52
  const url = page.url();
52
53
  return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
@@ -682,6 +683,7 @@ async function main() {
682
683
  loadedWorkflow = await loadDefaultWorkflow(
683
684
  getAbsoluteIntegrationPath(config.workflow.integrationPath)
684
685
  );
686
+ validateWorkflowInput(loadedWorkflow, config.workflow.params ?? {});
685
687
  } catch (error) {
686
688
  throw new UserFacingStartupError(
687
689
  error instanceof Error ? error.message : String(error)
@@ -68,115 +68,28 @@ function readInstalledSkillVersions() {
68
68
  }
69
69
  return [...versions];
70
70
  }
71
- function parseVersion(version) {
72
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
73
- if (!match) {
74
- return null;
75
- }
76
- return {
77
- major: Number(match[1]),
78
- minor: Number(match[2]),
79
- patch: Number(match[3]),
80
- prerelease: match[4] ?? null
81
- };
82
- }
83
- function compareVersions(left, right) {
84
- const parsedLeft = parseVersion(left);
85
- const parsedRight = parseVersion(right);
86
- if (!parsedLeft || !parsedRight) {
87
- return left.localeCompare(right);
88
- }
89
- for (const key of ["major", "minor", "patch"]) {
90
- const diff = parsedLeft[key] - parsedRight[key];
91
- if (diff !== 0) {
92
- return diff;
93
- }
94
- }
95
- if (parsedLeft.prerelease === parsedRight.prerelease) {
96
- return 0;
97
- }
98
- if (parsedLeft.prerelease === null) {
99
- return 1;
100
- }
101
- if (parsedRight.prerelease === null) {
102
- return -1;
103
- }
104
- return parsedLeft.prerelease.localeCompare(parsedRight.prerelease);
105
- }
106
- function selectTargetVersion(versions) {
107
- const counts = /* @__PURE__ */ new Map();
108
- for (const version of versions) {
109
- counts.set(version, (counts.get(version) ?? 0) + 1);
110
- }
111
- const byCountThenVersion = [...counts.entries()].sort(
112
- ([leftVersion, leftCount], [rightVersion, rightCount]) => rightCount - leftCount || compareVersions(rightVersion, leftVersion)
113
- );
114
- return byCountThenVersion[0]?.[0] ?? versions[0] ?? "latest";
115
- }
116
- function formatVersion(version, targetVersion) {
117
- return version === targetVersion ? version : `${version} (out of date)`;
118
- }
119
- function formatSkillVersions(versions, targetVersion) {
71
+ function formatSkillVersions(versions) {
120
72
  if (versions.length === 0) {
121
73
  return "not installed";
122
74
  }
123
- return versions.map((version) => formatVersion(version, targetVersion)).join(", ");
124
- }
125
- function formatUpdateInstructions(components) {
126
- const instructions = [];
127
- if (components.cliVersion !== components.targetVersion) {
128
- instructions.push(
129
- ` global CLI: curl -fsSL https://libretto.sh/install.sh | LIBRETTO_VERSION=${components.targetVersion} bash`
130
- );
131
- }
132
- if (components.localPackageVersion && components.localPackageVersion !== components.targetVersion) {
133
- instructions.push(
134
- ` local package: npm install libretto@${components.targetVersion}`
135
- );
136
- }
137
- if (components.skillVersions.length > 0 && components.skillVersions.some(
138
- (skillVersion) => skillVersion !== components.targetVersion
139
- )) {
140
- instructions.push(" agent skill: libretto setup");
141
- }
142
- return instructions;
75
+ return versions.join(", ");
143
76
  }
144
77
  function formatVersionWarning(components) {
145
- const targetVersion = selectTargetVersion([
146
- components.cliVersion,
147
- ...components.localPackageVersion ? [components.localPackageVersion] : [],
148
- ...components.skillVersions
149
- ]);
150
78
  const skillLabel = components.skillVersions.length > 1 ? "agent skills" : "agent skill";
151
- const updateInstructions = formatUpdateInstructions({
152
- ...components,
153
- targetVersion
154
- });
155
79
  return [
156
- "WARNING: Libretto version mismatch detected.",
157
- "",
158
- ` global CLI: ${formatVersion(components.cliVersion, targetVersion)}`,
159
- ` local package: ${components.localPackageVersion ? formatVersion(components.localPackageVersion, targetVersion) : "not installed"}`,
160
- ` ${skillLabel}: ${formatSkillVersions(
161
- components.skillVersions,
162
- targetVersion
163
- )}`,
164
- "",
165
- "How to update:",
166
- ...updateInstructions
80
+ "WARNING: Libretto skill version does not match the local package.",
81
+ ` local package: ${components.localPackageVersion ?? `${components.cliVersion} (current command)`}`,
82
+ ` ${skillLabel}: ${formatSkillVersions(components.skillVersions)}`,
83
+ "Fix: run libretto setup"
167
84
  ].join("\n");
168
85
  }
169
86
  function warnIfLibrettoVersionsDiffer() {
170
87
  try {
171
88
  const cliVersion = readCurrentCliVersion();
172
89
  const localPackageVersion = readLocalPackageVersion();
90
+ const packageVersion = localPackageVersion ?? cliVersion;
173
91
  const skillVersions = readInstalledSkillVersions();
174
- const observedVersions = /* @__PURE__ */ new Set([
175
- cliVersion,
176
- ...localPackageVersion ? [localPackageVersion] : [],
177
- ...skillVersions
178
- ]);
179
- if (observedVersions.size <= 1) {
92
+ if (skillVersions.length === 0 || skillVersions.every((skillVersion) => skillVersion === packageVersion)) {
180
93
  return;
181
94
  }
182
95
  console.error(
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { InstrumentationOptions, InstrumentedPage, installInstrumentation, instr
12
12
  export { GhostCursorOptions, ensureGhostCursor, ghostClick, hideGhostCursor, moveGhostCursor } from './shared/visualization/ghost-cursor.js';
13
13
  export { HighlightOptions, clearHighlights, ensureHighlightLayer, showHighlight } from './shared/visualization/highlight.js';
14
14
  export { BrowserSession, LaunchBrowserArgs, launchBrowser } from './shared/run/browser.js';
15
- export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowSchemas, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow } from './shared/workflow/workflow.js';
15
+ export { ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, LibrettoWorkflowContext, LibrettoWorkflowHandler, LibrettoWorkflowInputError, LibrettoWorkflowSchemas, WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow } from './shared/workflow/workflow.js';
16
16
  import 'zod';
17
17
  import 'playwright';
18
18
  import 'ai';
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  LibrettoWorkflow,
60
60
  LibrettoWorkflowInputError,
61
61
  LIBRETTO_WORKFLOW_BRAND,
62
+ validateWorkflowInput,
62
63
  workflow
63
64
  } from "./shared/workflow/workflow.js";
64
65
  const isDirectExecution = () => {
@@ -113,5 +114,6 @@ export {
113
114
  prettyConsoleSink,
114
115
  serializeSessionState,
115
116
  showHighlight,
117
+ validateWorkflowInput,
116
118
  workflow
117
119
  };
@@ -16,6 +16,11 @@ declare class LibrettoWorkflowInputError extends Error {
16
16
  readonly zodError: z.ZodError;
17
17
  constructor(workflowName: string, zodError: z.ZodError);
18
18
  }
19
+ type WorkflowInputValidator = {
20
+ readonly name: string;
21
+ readonly inputSchema?: z.ZodType;
22
+ };
23
+ declare function validateWorkflowInput(workflow: WorkflowInputValidator, input: unknown): void;
19
24
  declare class LibrettoWorkflow<InputSchema extends z.ZodType = z.ZodType<unknown>, OutputSchema extends z.ZodType = z.ZodType<unknown>> {
20
25
  readonly [LIBRETTO_WORKFLOW_BRAND] = true;
21
26
  readonly name: string;
@@ -40,4 +45,4 @@ declare function getWorkflowFromModuleExports(moduleExports: WorkflowModuleExpor
40
45
  declare function workflow<InputSchema extends z.ZodType, OutputSchema extends z.ZodType>(name: string, schemas: LibrettoWorkflowSchemas<InputSchema, OutputSchema>, handler: LibrettoWorkflowHandler<z.infer<InputSchema>, z.infer<OutputSchema>>): LibrettoWorkflow<InputSchema, OutputSchema>;
41
46
  declare function workflow<Input = unknown, Output = unknown>(name: string, handler: LibrettoWorkflowHandler<Input, Output>): LibrettoWorkflow<z.ZodType<Input>, z.ZodType<Output>>;
42
47
 
43
- export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type LibrettoWorkflowSchemas, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, workflow };
48
+ export { type ExportedLibrettoWorkflow, LIBRETTO_WORKFLOW_BRAND, LibrettoWorkflow, type LibrettoWorkflowContext, type LibrettoWorkflowHandler, LibrettoWorkflowInputError, type LibrettoWorkflowSchemas, type WorkflowInputValidator, getDefaultWorkflowFromModuleExports, getWorkflowFromModuleExports, getWorkflowsFromModuleExports, isLibrettoWorkflow, validateWorkflowInput, workflow };
@@ -19,6 +19,17 @@ function formatZodErrorMessage(workflowName, zodError) {
19
19
  ...lines
20
20
  ].join("\n");
21
21
  }
22
+ function parseWorkflowInput(workflowName, inputSchema, input) {
23
+ if (!inputSchema) return input;
24
+ const result = inputSchema.safeParse(input);
25
+ if (!result.success) {
26
+ throw new LibrettoWorkflowInputError(workflowName, result.error);
27
+ }
28
+ return result.data;
29
+ }
30
+ function validateWorkflowInput(workflow2, input) {
31
+ parseWorkflowInput(workflow2.name, workflow2.inputSchema, input);
32
+ }
22
33
  class LibrettoWorkflow {
23
34
  [LIBRETTO_WORKFLOW_BRAND] = true;
24
35
  name;
@@ -38,16 +49,7 @@ class LibrettoWorkflow {
38
49
  this.handler = handler;
39
50
  }
40
51
  async run(ctx, input) {
41
- let parsed;
42
- if (this.inputSchema) {
43
- const result = this.inputSchema.safeParse(input);
44
- if (!result.success) {
45
- throw new LibrettoWorkflowInputError(this.name, result.error);
46
- }
47
- parsed = result.data;
48
- } else {
49
- parsed = input;
50
- }
52
+ const parsed = parseWorkflowInput(this.name, this.inputSchema, input);
51
53
  return this.handler(ctx, parsed);
52
54
  }
53
55
  }
@@ -118,5 +120,6 @@ export {
118
120
  getWorkflowFromModuleExports,
119
121
  getWorkflowsFromModuleExports,
120
122
  isLibrettoWorkflow,
123
+ validateWorkflowInput,
121
124
  workflow
122
125
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.19"
7
+ version: "0.6.20"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -39,12 +39,13 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
39
39
 
40
40
  ## Setup
41
41
 
42
- - Use `libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
43
- - Use `libretto status` to inspect open sessions without triggering setup.
42
+ - Use the package manager convention for the target project. The examples use `npx libretto`; pnpm, yarn, and bun projects should use their equivalent package-manager execution form.
43
+ - Use `npx libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
44
+ - Use `npx libretto status` to inspect open sessions without triggering setup.
44
45
 
45
46
  ## Experiments
46
47
 
47
- - Use `libretto experiments` to list internal feature flags and `libretto experiments describe <name>` for usage notes when an experiment is enabled.
48
+ - Use `npx libretto experiments` to list internal feature flags and `npx libretto experiments describe <name>` for usage notes when an experiment is enabled.
48
49
 
49
50
  ## Working Rules
50
51
 
@@ -83,9 +84,9 @@ npx libretto open https://example.com --session debug-example
83
84
  - Pass `--read-only` if the connected session must stay inspection-only from the start.
84
85
 
85
86
  ```bash
86
- libretto connect http://127.0.0.1:9222 --session my-session
87
- libretto connect http://127.0.0.1:9222 --read-only --session readonly-session
88
- libretto connect http://127.0.0.1:9223 --session another-session
87
+ npx libretto connect http://127.0.0.1:9222 --session my-session
88
+ npx libretto connect http://127.0.0.1:9222 --read-only --session readonly-session
89
+ npx libretto connect http://127.0.0.1:9223 --session another-session
89
90
  ```
90
91
 
91
92
  ### `session-mode`
@@ -96,7 +97,7 @@ libretto connect http://127.0.0.1:9223 --session another-session
96
97
  - Pass `--read-only` or `--write-access` to override the config default for a single command.
97
98
 
98
99
  ```bash
99
- libretto session-mode --session my-session
100
+ npx libretto session-mode --session my-session
100
101
  ```
101
102
 
102
103
  ### `snapshot`
@@ -108,9 +109,9 @@ libretto session-mode --session my-session
108
109
  - Use it before guessing at selectors, after workflow failures, and whenever the visible page state is unclear.
109
110
 
110
111
  ```bash
111
- libretto snapshot --session debug-example
112
- libretto snapshot <ref> --session debug-example
113
- libretto snapshot --session debug-example --page <page-id>
112
+ npx libretto snapshot --session debug-example
113
+ npx libretto snapshot <ref> --session debug-example
114
+ npx libretto snapshot --session debug-example --page <page-id>
114
115
  ```
115
116
 
116
117
  ### `exec`
@@ -126,10 +127,10 @@ libretto snapshot --session debug-example --page <page-id>
126
127
  - After successful mutations, `exec` prints page-change diffs from compact snapshots.
127
128
 
128
129
  ```bash
129
- libretto exec "await page.url()"
130
- libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
131
- echo "async function textOf(selector) { return await page.locator(selector).textContent(); }" | libretto exec - --session debug-example
132
- libretto exec --session debug-example "await textOf('h1')"
130
+ npx libretto exec "await page.url()"
131
+ npx libretto exec "await page.locator('button:has-text(\"Continue\")').click()"
132
+ echo "async function textOf(selector) { return await page.locator(selector).textContent(); }" | npx libretto exec - --session debug-example
133
+ npx libretto exec --session debug-example "await textOf('h1')"
133
134
  ```
134
135
 
135
136
  ### `pages`
@@ -138,8 +139,8 @@ libretto exec --session debug-example "await textOf('h1')"
138
139
  - If `exec` or `snapshot` complains about multiple pages, list page ids first and then pass `--page`.
139
140
 
140
141
  ```bash
141
- libretto pages --session debug-example
142
- libretto exec --session debug-example --page <page-id> "await page.url()"
142
+ npx libretto pages --session debug-example
143
+ npx libretto exec --session debug-example --page <page-id> "await page.url()"
143
144
  ```
144
145
 
145
146
  ### `run`
@@ -150,14 +151,14 @@ libretto exec --session debug-example --page <page-id> "await page.url()"
150
151
  - Pass `--read-only` if the preserved session should come back locked for follow-up terminal inspection after the workflow run.
151
152
  - If the workflow fails, Libretto keeps the browser open. Inspect the failed state with `snapshot` and `exec` before editing code.
152
153
  - Insert `await pause(session)` statements in the workflow file when you need to stop at specific states for interactive debugging, like breakpoints in the browser flow.
153
- - If the workflow pauses, resume it with `libretto resume --session <name>`.
154
+ - If the workflow pauses, resume it with `npx libretto resume --session <name>`.
154
155
  - Re-run the same workflow after each fix to verify the browser behavior end to end.
155
156
 
156
157
  ```bash
157
- libretto run ./integration.ts --params '{"status":"open"}'
158
- libretto run ./integration.ts --read-only
159
- libretto run ./integration.ts --stay-open-on-success
160
- libretto run ./integration.ts --auth-profile app.example.com
158
+ npx libretto run ./integration.ts --params '{"status":"open"}'
159
+ npx libretto run ./integration.ts --read-only
160
+ npx libretto run ./integration.ts --stay-open-on-success
161
+ npx libretto run ./integration.ts --auth-profile app.example.com
161
162
  ```
162
163
 
163
164
  ### `resume`
@@ -168,7 +169,7 @@ libretto run ./integration.ts --auth-profile app.example.com
168
169
  - Keep resuming the same session until the workflow completes or pauses again.
169
170
 
170
171
  ```bash
171
- libretto resume --session debug-example
172
+ npx libretto resume --session debug-example
172
173
  ```
173
174
 
174
175
  ### `save`
@@ -176,7 +177,7 @@ libretto resume --session debug-example
176
177
  - Use `save` only when the user explicitly asks to save or reuse authenticated browser state.
177
178
 
178
179
  ```bash
179
- libretto save app.example.com
180
+ npx libretto save app.example.com
180
181
  ```
181
182
 
182
183
  ### `close`
@@ -185,8 +186,8 @@ libretto save app.example.com
185
186
  - `close --all` is available for workspace cleanup.
186
187
 
187
188
  ```bash
188
- libretto close --session debug-example
189
- libretto close --all
189
+ npx libretto close --session debug-example
190
+ npx libretto close --all
190
191
  ```
191
192
 
192
193
  ## Session Logs
@@ -234,17 +235,17 @@ Key fields: `id` (incrementing request id), `ts` (ISO timestamp), `pageId` (page
234
235
  <example>
235
236
  [Context: The user wants to build a new browser workflow and does not yet know the page structure]
236
237
  Assistant: I'll inspect the real site first if needed, but before I finish I'll create `target-workflow.ts` so the task produces reusable automation code.
237
- Assistant: [Runs `libretto open https://target.example.com --headed`]
238
+ Assistant: [Runs `npx libretto open https://target.example.com --headed`]
238
239
  Assistant: [Reads `references/site-security-review.md` before choosing between passive network inspection, direct browser fetch calls, and Playwright-first automation]
239
- Assistant: [Runs `libretto snapshot --session <session>`]
240
+ Assistant: [Runs `npx libretto snapshot --session <session>`]
240
241
  Assistant: [Uses `snapshot` and `exec` as needed to understand the site and decide the implementation path]
241
242
  Assistant: [Reads `references/code-generation-rules.md` before writing production workflow code]
242
243
  Assistant: I found the working path. I'll now create the workflow file and verify it.
243
244
  Assistant: [Creates or edits `target-workflow.ts` following `references/code-generation-rules.md`]
244
- Assistant: [Runs `libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
245
+ Assistant: [Runs `npx libretto run ./target-workflow.ts --params '{"status":"open"}'` to validate]
245
246
  Assistant: Validation passed. Here are the results:
246
247
  [Shows the output/results from the validation run]
247
- To run it again, use: libretto run ./target-workflow.ts --params '{"status":"open"}'
248
+ To run it again, use: npx libretto run ./target-workflow.ts --params '{"status":"open"}'
248
249
  </example>
249
250
  ```
250
251
 
@@ -254,17 +255,17 @@ To run it again, use: libretto run ./target-workflow.ts --params '{"status":"ope
254
255
  <example>
255
256
  [Context: The user has an existing Libretto workflow that is failing]
256
257
  Assistant: I'll reproduce the failure first so we can inspect the exact browser state it leaves behind.
257
- Assistant: [Runs `libretto run ./integration.ts --session debug-flow`]
258
+ Assistant: [Runs `npx libretto run ./integration.ts --session debug-flow`]
258
259
  Assistant: The workflow failed and Libretto kept the browser open. I'll inspect the page state before changing code.
259
- Assistant: [Runs `libretto snapshot --session debug-flow`]
260
- Assistant: [Runs `libretto exec --session debug-flow "...focused inspection or prototype..."`]
260
+ Assistant: [Runs `npx libretto snapshot --session debug-flow`]
261
+ Assistant: [Runs `npx libretto exec --session debug-flow "...focused inspection or prototype..."`]
261
262
  Assistant: [Reads `references/code-generation-rules.md` before patching the workflow file]
262
263
  Assistant: I found the issue. I'll patch the workflow code and verify.
263
264
  Assistant: [Edits `integration.ts` following `references/code-generation-rules.md`]
264
- Assistant: [Runs `libretto run ./integration.ts` to validate the fix]
265
+ Assistant: [Runs `npx libretto run ./integration.ts` to validate the fix]
265
266
  Assistant: Fix verified. Here are the results:
266
267
  [Shows the output/results from the validation run]
267
- To run it again, use: libretto run ./integration.ts
268
+ To run it again, use: npx libretto run ./integration.ts
268
269
  </example>
269
270
  ```
270
271
 
@@ -17,9 +17,9 @@ Use this reference only when the user explicitly asks to save or reuse local aut
17
17
  ## Commands
18
18
 
19
19
  ```bash
20
- libretto open https://app.example.com --headed
21
- libretto save app.example.com
22
- libretto run ./integration.ts --auth-profile app.example.com
20
+ npx libretto open https://app.example.com --headed
21
+ npx libretto save app.example.com
22
+ npx libretto run ./integration.ts --auth-profile app.example.com
23
23
  ```
24
24
 
25
25
  ## Notes
@@ -6,7 +6,7 @@ Follow the user's existing codebase conventions, abstractions, and patterns when
6
6
 
7
7
  ## Workflow File Structure
8
8
 
9
- Generated files must default-export a `workflow()` instance so they can be run via `libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
9
+ Generated files must default-export a `workflow()` instance so they can be run via `npx libretto run <file>`. Workflows declare their input and output shapes as Zod schemas, which both type the handler and validate runtime input.
10
10
 
11
11
  Add `zod` (`^4.0.0`) to the workflow's `package.json` dependencies. Then import `workflow` from `"libretto"` and `z` from `"zod"`:
12
12
 
@@ -42,7 +42,7 @@ Key points:
42
42
 
43
43
  - `workflow(name, { input, output }, handler)` takes a unique workflow name, a pair of Zod schemas describing input and output, and the async handler. The handler's `input` parameter is inferred from the input schema — do not redeclare it with a separate `type Input = ...`.
44
44
  - At run time, Libretto validates `input` against `inputSchema` before calling the handler. Invalid input throws a clear error listing each failing field; the workflow handler never sees malformed input.
45
- - `libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
45
+ - `npx libretto run ./file.ts` executes the file's default-exported workflow, so always use `export default workflow(...)`.
46
46
  - `ctx` provides `session` and `page`. Use `console.log`/`console.warn`/`console.error` for logging — the runtime wraps these with structured metadata automatically.
47
47
  - `input` comes from `--params '{"query":"foo"}'` or `--params-file params.json` on the CLI, then gets parsed through `inputSchema`.
48
48
  - Use `await pause(ctx.session)` (or `await pause(session)`) to pause the workflow for debugging. It is a no-op in production.
@@ -12,8 +12,8 @@ Use this reference when you need to inspect or change workspace configuration fo
12
12
 
13
13
  Libretto reads workspace config from `.libretto/config.json`.
14
14
 
15
- - The file is created by `libretto setup` during first-time onboarding.
16
- - Use `libretto status` to inspect open sessions without changing anything.
15
+ - The file is created by `npx libretto setup` during first-time onboarding.
16
+ - Use `npx libretto status` to inspect open sessions without changing anything.
17
17
  - For first-time setup instructions, follow the main `SKILL.md` flow instead of expanding this reference.
18
18
 
19
19
  ## Supported Settings
@@ -42,17 +42,17 @@ Example:
42
42
  ## Common Commands
43
43
 
44
44
  ```bash
45
- libretto setup # first-time onboarding
46
- libretto status # inspect open sessions
47
- libretto open https://example.com --provider kernel
48
- libretto run ./integration.ts --provider browserbase
49
- libretto open https://example.com --provider steel
50
- libretto open https://example.com --viewport 1440x900
51
- libretto run ./integration.ts --viewport 1440x900
45
+ npx libretto setup # first-time onboarding
46
+ npx libretto status # inspect open sessions
47
+ npx libretto open https://example.com --provider kernel
48
+ npx libretto run ./integration.ts --provider browserbase
49
+ npx libretto open https://example.com --provider steel
50
+ npx libretto open https://example.com --viewport 1440x900
51
+ npx libretto run ./integration.ts --viewport 1440x900
52
52
  ```
53
53
 
54
54
  ## Notes
55
55
 
56
56
  - If you want a persistent default provider for the workspace, add `provider` to `.libretto/config.json` instead of repeating `--provider` on every command.
57
57
  - If you want a persistent default viewport for the workspace, add `viewport` to `.libretto/config.json` instead of repeating `--viewport` on every command.
58
- - Run `libretto status` at any time to check open sessions.
58
+ - Run `npx libretto status` at any time to check open sessions.
@@ -17,9 +17,9 @@ Use this reference when a Libretto session has multiple open pages and you need
17
17
  ## Commands
18
18
 
19
19
  ```bash
20
- libretto pages --session debug-flow
21
- libretto exec --session debug-flow --page <page-id> "return await page.url()"
22
- libretto snapshot --session debug-flow --page <page-id>
20
+ npx libretto pages --session debug-flow
21
+ npx libretto exec --session debug-flow --page <page-id> "return await page.url()"
22
+ npx libretto snapshot --session debug-flow --page <page-id>
23
23
  ```
24
24
 
25
25
  ## Notes
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.19"
7
+ version: "0.6.20"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
package/src/cli/cli.ts CHANGED
@@ -14,22 +14,6 @@ function printSetupAudit(): void {
14
14
  warnIfLibrettoVersionsDiffer();
15
15
  }
16
16
 
17
- function isPackageManagerExec(env: NodeJS.ProcessEnv = process.env): boolean {
18
- return env.npm_command === "exec";
19
- }
20
-
21
- function warnIfPackageManagerExec(): void {
22
- if (!isPackageManagerExec()) return;
23
-
24
- console.error(
25
- [
26
- "Warning: running Libretto through a package manager is deprecated and will be removed in a future release.",
27
- "Install the native command instead:",
28
- " curl -fsSL https://libretto.sh/install.sh | bash",
29
- ].join("\n"),
30
- );
31
- }
32
-
33
17
  function isRootHelpRequest(rawArgs: readonly string[]): boolean {
34
18
  if (rawArgs.length === 0) return true;
35
19
  return rawArgs[0] === "help" && rawArgs.length === 1;
@@ -55,7 +39,6 @@ export async function runLibrettoCLI(): Promise<void> {
55
39
  const rawArgs = process.argv.slice(2);
56
40
  let exitCode = 0;
57
41
  loadEnv();
58
- warnIfPackageManagerExec();
59
42
  ensureLibrettoSetup();
60
43
  const app = createCLIApp();
61
44
 
@@ -125,6 +125,10 @@ export const deployInput = SimpleCLI.input({
125
125
  name: "entry-point",
126
126
  help: "Entry point file (default: index.ts)",
127
127
  }),
128
+ autoRepair: SimpleCLI.flag({
129
+ name: "auto-repair",
130
+ help: "Route failed jobs for this deployment to autofix",
131
+ }),
128
132
  external: SimpleCLI.option(
129
133
  z
130
134
  .string()
@@ -167,6 +171,7 @@ export const deployCommand = SimpleCLI.command({
167
171
  entry_point: entryPoint,
168
172
  };
169
173
  if (input.description) createPayload.description = input.description;
174
+ if (input.autoRepair) createPayload.auto_repair = true;
170
175
 
171
176
  console.log("Uploading deployment...");
172
177
  const body = await orpcCall<DeploymentResponse["json"]>({
@@ -1,14 +1,26 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { SimpleCLI } from "affordance";
5
-
6
- const UPDATE_COMMAND = "curl -fsSL https://libretto.sh/install.sh | bash";
6
+ import { REPO_ROOT } from "../core/context.js";
7
+ import {
8
+ detectProjectPackageManager,
9
+ installCommand,
10
+ type PackageManager,
11
+ } from "../../shared/package-manager.js";
7
12
 
8
13
  type PackageManifest = {
9
14
  version?: string;
10
15
  };
11
16
 
17
+ function packageInstallCommand(
18
+ packageManager: PackageManager,
19
+ packageSpec: string,
20
+ ): string {
21
+ return `${installCommand(packageManager)} ${packageSpec}`;
22
+ }
23
+
12
24
  function readCurrentCliVersion(): string {
13
25
  const packageJsonPath = fileURLToPath(
14
26
  new URL("../../../package.json", import.meta.url),
@@ -26,6 +38,23 @@ function readCurrentCliVersion(): string {
26
38
  return manifest.version;
27
39
  }
28
40
 
41
+ function readPackageVersion(packageJsonPath: string): string | null {
42
+ try {
43
+ const manifest = JSON.parse(
44
+ readFileSync(packageJsonPath, "utf8"),
45
+ ) as PackageManifest;
46
+ return manifest.version?.trim() || null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function readLocalPackageVersion(): string | null {
53
+ return readPackageVersion(
54
+ join(REPO_ROOT, "node_modules", "libretto", "package.json"),
55
+ );
56
+ }
57
+
29
58
  function readLatestNpmVersion(): string {
30
59
  const result = spawnSync("npm", ["view", "libretto@latest", "version"], {
31
60
  encoding: "utf8",
@@ -83,16 +112,17 @@ export const updateInput = SimpleCLI.input({
83
112
  function formatUpdateFailure(
84
113
  status: number | null,
85
114
  signal: string | null,
115
+ updateCommand: string,
86
116
  ): string {
87
117
  const knownState =
88
118
  status === null
89
- ? `installer was interrupted${signal ? ` by ${signal}` : ""}.`
90
- : `installer exited with status ${status}.`;
119
+ ? `package update was interrupted${signal ? ` by ${signal}` : ""}.`
120
+ : `package update exited with status ${status}.`;
91
121
 
92
122
  return [
93
123
  "Error: failed to update Libretto to the latest version.",
94
124
  `Known state: ${knownState}`,
95
- `Try: ${UPDATE_COMMAND}`,
125
+ `Try: ${updateCommand}`,
96
126
  "Help: libretto help update",
97
127
  ].join("\n");
98
128
  }
@@ -102,48 +132,56 @@ export const updateCommand = SimpleCLI.command({
102
132
  })
103
133
  .input(updateInput)
104
134
  .handle(async ({ input }) => {
135
+ const packageManager = detectProjectPackageManager();
136
+ const updateCommand = packageInstallCommand(packageManager, "libretto@latest");
137
+
105
138
  if (input.dryRun) {
106
139
  console.log("Update command:");
107
- console.log(` ${UPDATE_COMMAND}`);
140
+ console.log(` ${updateCommand}`);
108
141
  console.log("No changes made.");
109
142
  return;
110
143
  }
111
144
 
112
145
  const currentVersion = readCurrentCliVersion();
146
+ const localPackageVersion = readLocalPackageVersion();
147
+ const installedVersion = localPackageVersion ?? currentVersion;
113
148
  const latestVersion = readLatestNpmVersion();
114
- console.log(`Current version: ${currentVersion}`);
149
+ console.log(`Current version: ${installedVersion}`);
115
150
  console.log(`Latest version: ${latestVersion}`);
116
151
 
117
- if (currentVersion === latestVersion) {
118
- console.log(`Libretto is already up to date (${currentVersion}).`);
152
+ if (localPackageVersion && installedVersion === latestVersion) {
153
+ console.log(`Libretto is already up to date (${installedVersion}).`);
119
154
  console.log("No further action required.");
120
155
  return;
121
156
  }
122
157
 
123
- console.log("Updating Libretto to latest...");
124
- const result = spawnSync("bash", ["-lc", UPDATE_COMMAND], {
158
+ if (!localPackageVersion) {
159
+ console.log("Local package: not installed");
160
+ }
161
+
162
+ console.log("Updating local Libretto package to latest...");
163
+ const result = spawnSync(updateCommand, {
125
164
  stdio: "inherit",
126
- env: {
127
- ...process.env,
128
- LIBRETTO_VERSION: "latest",
129
- },
165
+ shell: true,
130
166
  });
131
167
 
132
168
  if (result.error) {
133
169
  throw new Error(
134
170
  [
135
- "Error: failed to start the Libretto installer.",
171
+ "Error: failed to start the Libretto package update.",
136
172
  `Known state: ${result.error.message}`,
137
- `Try: ${UPDATE_COMMAND}`,
173
+ `Try: ${updateCommand}`,
138
174
  "Help: libretto help update",
139
175
  ].join("\n"),
140
176
  );
141
177
  }
142
178
 
143
179
  if (result.status !== 0) {
144
- throw new Error(formatUpdateFailure(result.status, result.signal));
180
+ throw new Error(
181
+ formatUpdateFailure(result.status, result.signal, updateCommand),
182
+ );
145
183
  }
146
184
 
147
- console.log("Libretto updated to latest.");
185
+ console.log("Local Libretto package updated to latest.");
148
186
  console.log("No further action required.");
149
187
  });
@@ -89,6 +89,7 @@ import {
89
89
  loadDefaultWorkflow,
90
90
  } from "../workflow-runtime.js";
91
91
  import { WorkflowController } from "../workflow-runner/runner.js";
92
+ import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
92
93
 
93
94
  function isOperationalPage(page: Page): boolean {
94
95
  const url = page.url();
@@ -941,6 +942,7 @@ async function main(): Promise<void> {
941
942
  loadedWorkflow = await loadDefaultWorkflow(
942
943
  getAbsoluteIntegrationPath(config.workflow.integrationPath),
943
944
  );
945
+ validateWorkflowInput(loadedWorkflow, config.workflow.params ?? {});
944
946
  } catch (error) {
945
947
  throw new UserFacingStartupError(
946
948
  error instanceof Error ? error.message : String(error),
@@ -89,115 +89,14 @@ function readInstalledSkillVersions(): string[] {
89
89
  return [...versions];
90
90
  }
91
91
 
92
- function parseVersion(version: string): {
93
- major: number;
94
- minor: number;
95
- patch: number;
96
- prerelease: string | null;
97
- } | null {
98
- const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
99
- if (!match) {
100
- return null;
101
- }
102
-
103
- return {
104
- major: Number(match[1]),
105
- minor: Number(match[2]),
106
- patch: Number(match[3]),
107
- prerelease: match[4] ?? null,
108
- };
109
- }
110
-
111
- function compareVersions(left: string, right: string): number {
112
- const parsedLeft = parseVersion(left);
113
- const parsedRight = parseVersion(right);
114
- if (!parsedLeft || !parsedRight) {
115
- return left.localeCompare(right);
116
- }
117
-
118
- for (const key of ["major", "minor", "patch"] as const) {
119
- const diff = parsedLeft[key] - parsedRight[key];
120
- if (diff !== 0) {
121
- return diff;
122
- }
123
- }
124
-
125
- if (parsedLeft.prerelease === parsedRight.prerelease) {
126
- return 0;
127
- }
128
- if (parsedLeft.prerelease === null) {
129
- return 1;
130
- }
131
- if (parsedRight.prerelease === null) {
132
- return -1;
133
- }
134
- return parsedLeft.prerelease.localeCompare(parsedRight.prerelease);
135
- }
136
-
137
- function selectTargetVersion(versions: string[]): string {
138
- const counts = new Map<string, number>();
139
- for (const version of versions) {
140
- counts.set(version, (counts.get(version) ?? 0) + 1);
141
- }
142
-
143
- const byCountThenVersion = [...counts.entries()].sort(
144
- ([leftVersion, leftCount], [rightVersion, rightCount]) =>
145
- rightCount - leftCount || compareVersions(rightVersion, leftVersion),
146
- );
147
-
148
- return byCountThenVersion[0]?.[0] ?? versions[0] ?? "latest";
149
- }
150
-
151
- function formatVersion(version: string, targetVersion: string): string {
152
- return version === targetVersion ? version : `${version} (out of date)`;
153
- }
154
-
155
92
  function formatSkillVersions(
156
93
  versions: string[],
157
- targetVersion: string,
158
94
  ): string {
159
95
  if (versions.length === 0) {
160
96
  return "not installed";
161
97
  }
162
98
 
163
- return versions
164
- .map((version) => formatVersion(version, targetVersion))
165
- .join(", ");
166
- }
167
-
168
- function formatUpdateInstructions(components: {
169
- cliVersion: string;
170
- localPackageVersion: string | null;
171
- skillVersions: string[];
172
- targetVersion: string;
173
- }): string[] {
174
- const instructions: string[] = [];
175
-
176
- if (components.cliVersion !== components.targetVersion) {
177
- instructions.push(
178
- ` global CLI: curl -fsSL https://libretto.sh/install.sh | LIBRETTO_VERSION=${components.targetVersion} bash`,
179
- );
180
- }
181
-
182
- if (
183
- components.localPackageVersion &&
184
- components.localPackageVersion !== components.targetVersion
185
- ) {
186
- instructions.push(
187
- ` local package: npm install libretto@${components.targetVersion}`,
188
- );
189
- }
190
-
191
- if (
192
- components.skillVersions.length > 0 &&
193
- components.skillVersions.some(
194
- (skillVersion) => skillVersion !== components.targetVersion,
195
- )
196
- ) {
197
- instructions.push(" agent skill: libretto setup");
198
- }
199
-
200
- return instructions;
99
+ return versions.join(", ");
201
100
  }
202
101
 
203
102
  function formatVersionWarning(components: {
@@ -205,34 +104,16 @@ function formatVersionWarning(components: {
205
104
  localPackageVersion: string | null;
206
105
  skillVersions: string[];
207
106
  }): string {
208
- const targetVersion = selectTargetVersion([
209
- components.cliVersion,
210
- ...(components.localPackageVersion ? [components.localPackageVersion] : []),
211
- ...components.skillVersions,
212
- ]);
213
107
  const skillLabel =
214
108
  components.skillVersions.length > 1 ? "agent skills" : "agent skill";
215
- const updateInstructions = formatUpdateInstructions({
216
- ...components,
217
- targetVersion,
218
- });
219
109
 
220
110
  return [
221
- "WARNING: Libretto version mismatch detected.",
222
- "",
223
- ` global CLI: ${formatVersion(components.cliVersion, targetVersion)}`,
111
+ "WARNING: Libretto skill version does not match the local package.",
224
112
  ` local package: ${
225
- components.localPackageVersion
226
- ? formatVersion(components.localPackageVersion, targetVersion)
227
- : "not installed"
113
+ components.localPackageVersion ?? `${components.cliVersion} (current command)`
228
114
  }`,
229
- ` ${skillLabel}: ${formatSkillVersions(
230
- components.skillVersions,
231
- targetVersion,
232
- )}`,
233
- "",
234
- "How to update:",
235
- ...updateInstructions,
115
+ ` ${skillLabel}: ${formatSkillVersions(components.skillVersions)}`,
116
+ "Fix: run libretto setup",
236
117
  ].join("\n");
237
118
  }
238
119
 
@@ -240,14 +121,12 @@ export function warnIfLibrettoVersionsDiffer(): void {
240
121
  try {
241
122
  const cliVersion = readCurrentCliVersion();
242
123
  const localPackageVersion = readLocalPackageVersion();
124
+ const packageVersion = localPackageVersion ?? cliVersion;
243
125
  const skillVersions = readInstalledSkillVersions();
244
- const observedVersions = new Set([
245
- cliVersion,
246
- ...(localPackageVersion ? [localPackageVersion] : []),
247
- ...skillVersions,
248
- ]);
249
-
250
- if (observedVersions.size <= 1) {
126
+ if (
127
+ skillVersions.length === 0 ||
128
+ skillVersions.every((skillVersion) => skillVersion === packageVersion)
129
+ ) {
251
130
  return;
252
131
  }
253
132
 
package/src/index.ts CHANGED
@@ -100,11 +100,13 @@ export {
100
100
  LibrettoWorkflow,
101
101
  LibrettoWorkflowInputError,
102
102
  LIBRETTO_WORKFLOW_BRAND,
103
+ validateWorkflowInput,
103
104
  workflow,
104
105
  type ExportedLibrettoWorkflow,
105
106
  type LibrettoWorkflowContext,
106
107
  type LibrettoWorkflowHandler,
107
108
  type LibrettoWorkflowSchemas,
109
+ type WorkflowInputValidator,
108
110
  } from "./shared/workflow/workflow.js";
109
111
  const isDirectExecution = (): boolean => {
110
112
  const entryArg = process.argv[1];
@@ -49,6 +49,32 @@ function formatZodErrorMessage(
49
49
  ].join("\n");
50
50
  }
51
51
 
52
+ function parseWorkflowInput<InputSchema extends z.ZodType>(
53
+ workflowName: string,
54
+ inputSchema: InputSchema | undefined,
55
+ input: unknown,
56
+ ): z.infer<InputSchema> {
57
+ if (!inputSchema) return input as z.infer<InputSchema>;
58
+
59
+ const result = inputSchema.safeParse(input);
60
+ if (!result.success) {
61
+ throw new LibrettoWorkflowInputError(workflowName, result.error);
62
+ }
63
+ return result.data;
64
+ }
65
+
66
+ export type WorkflowInputValidator = {
67
+ readonly name: string;
68
+ readonly inputSchema?: z.ZodType;
69
+ };
70
+
71
+ export function validateWorkflowInput(
72
+ workflow: WorkflowInputValidator,
73
+ input: unknown,
74
+ ): void {
75
+ parseWorkflowInput(workflow.name, workflow.inputSchema, input);
76
+ }
77
+
52
78
  export class LibrettoWorkflow<
53
79
  InputSchema extends z.ZodType = z.ZodType<unknown>,
54
80
  OutputSchema extends z.ZodType = z.ZodType<unknown>,
@@ -86,16 +112,7 @@ export class LibrettoWorkflow<
86
112
  ctx: LibrettoWorkflowContext,
87
113
  input: unknown,
88
114
  ): Promise<z.infer<OutputSchema>> {
89
- let parsed: z.infer<InputSchema>;
90
- if (this.inputSchema) {
91
- const result = this.inputSchema.safeParse(input);
92
- if (!result.success) {
93
- throw new LibrettoWorkflowInputError(this.name, result.error);
94
- }
95
- parsed = result.data;
96
- } else {
97
- parsed = input as z.infer<InputSchema>;
98
- }
115
+ const parsed = parseWorkflowInput(this.name, this.inputSchema, input);
99
116
  return this.handler(ctx, parsed);
100
117
  }
101
118
  }