libretto 0.6.18 → 0.6.19

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/dist/cli/cli.js CHANGED
@@ -2,14 +2,14 @@ import { ensureLibrettoSetup } from "./core/context.js";
2
2
  import { createCLIApp } from "./router.js";
3
3
  import {
4
4
  readCurrentCliVersion,
5
- warnIfInstalledSkillOutOfDate
5
+ warnIfLibrettoVersionsDiffer
6
6
  } from "./core/skill-version.js";
7
7
  import { loadEnv } from "../shared/env/load-env.js";
8
8
  function renderVersion() {
9
9
  return readCurrentCliVersion();
10
10
  }
11
11
  function printSetupAudit() {
12
- warnIfInstalledSkillOutOfDate();
12
+ warnIfLibrettoVersionsDiffer();
13
13
  }
14
14
  function isPackageManagerExec(env = process.env) {
15
15
  return env.npm_command === "exec";
@@ -17,7 +17,7 @@ import {
17
17
  setSessionMode,
18
18
  validateSessionName
19
19
  } from "../core/session.js";
20
- import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
20
+ import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
21
21
  import { SimpleCLI } from "affordance";
22
22
  import {
23
23
  sessionOption,
@@ -88,7 +88,7 @@ const openInput = SimpleCLI.input({
88
88
  const openCommand = SimpleCLI.command({
89
89
  description: "Launch browser and open URL"
90
90
  }).input(openInput).use(withAutoSession()).use(withExperiments()).handle(async ({ input, ctx }) => {
91
- warnIfInstalledSkillOutOfDate();
91
+ warnIfLibrettoVersionsDiffer();
92
92
  assertSessionAvailableForStart(ctx.session, ctx.logger);
93
93
  const providerName = resolveProviderName(input.provider);
94
94
  if (providerName === "local") {
@@ -141,7 +141,7 @@ const connectInput = SimpleCLI.input({
141
141
  const connectCommand = SimpleCLI.command({
142
142
  description: "Connect to an existing Chrome DevTools Protocol (CDP) endpoint"
143
143
  }).input(connectInput).use(withAutoSession()).use(withExperiments()).handle(async ({ input, ctx }) => {
144
- warnIfInstalledSkillOutOfDate();
144
+ warnIfLibrettoVersionsDiffer();
145
145
  await runConnectWithLogger(
146
146
  input.cdpUrl,
147
147
  ctx.session,
@@ -23,7 +23,7 @@ import {
23
23
  setSessionStatus,
24
24
  writeSessionState
25
25
  } from "../core/session.js";
26
- import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
26
+ import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
27
27
  import { readLibrettoConfig } from "../core/config.js";
28
28
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
29
29
  import {
@@ -682,7 +682,7 @@ function resolveRunParams(rawInlineParams, paramsFile) {
682
682
  const runCommand = SimpleCLI.command({
683
683
  description: "Run the default-exported Libretto workflow from a file"
684
684
  }).input(runInput).use(withAutoSession()).use(withExperiments()).handle(async ({ input, ctx }) => {
685
- warnIfInstalledSkillOutOfDate();
685
+ warnIfLibrettoVersionsDiffer();
686
686
  await stopExistingFailedRunSession(ctx.session, ctx.logger);
687
687
  assertSessionAvailableForStart(ctx.session, ctx.logger);
688
688
  const params = resolveRunParams(input.params, input.paramsFile);
@@ -7,6 +7,15 @@ const INSTALLED_SKILL_PATHS = [
7
7
  [".claude", "skills", "libretto", "SKILL.md"]
8
8
  ];
9
9
  let cachedCliVersion = null;
10
+ function readPackageVersion(packageJsonPath) {
11
+ if (!existsSync(packageJsonPath)) {
12
+ return null;
13
+ }
14
+ const manifest = JSON.parse(
15
+ readFileSync(packageJsonPath, "utf8")
16
+ );
17
+ return manifest.version?.trim() || null;
18
+ }
10
19
  function readCurrentCliVersion() {
11
20
  if (cachedCliVersion) {
12
21
  return cachedCliVersion;
@@ -14,17 +23,20 @@ function readCurrentCliVersion() {
14
23
  const packageJsonPath = fileURLToPath(
15
24
  new URL("../../../package.json", import.meta.url)
16
25
  );
17
- const manifest = JSON.parse(
18
- readFileSync(packageJsonPath, "utf8")
19
- );
20
- if (!manifest.version) {
26
+ const version = readPackageVersion(packageJsonPath);
27
+ if (!version) {
21
28
  throw new Error(
22
29
  `Unable to determine current libretto version from ${packageJsonPath}.`
23
30
  );
24
31
  }
25
- cachedCliVersion = manifest.version;
32
+ cachedCliVersion = version;
26
33
  return cachedCliVersion;
27
34
  }
35
+ function readLocalPackageVersion() {
36
+ return readPackageVersion(
37
+ join(REPO_ROOT, "node_modules", "libretto", "package.json")
38
+ );
39
+ }
28
40
  function readInstalledSkillVersion(skillPath) {
29
41
  if (!existsSync(skillPath)) {
30
42
  return null;
@@ -45,30 +57,139 @@ function readInstalledSkillVersion(skillPath) {
45
57
  );
46
58
  return versionMatch?.[1]?.trim() ?? null;
47
59
  }
48
- function findInstalledSkillVersionMismatch() {
49
- const cliVersion = readCurrentCliVersion();
60
+ function readInstalledSkillVersions() {
61
+ const versions = /* @__PURE__ */ new Set();
50
62
  for (const relativePathParts of INSTALLED_SKILL_PATHS) {
51
63
  const skillPath = join(REPO_ROOT, ...relativePathParts);
52
64
  const installedVersion = readInstalledSkillVersion(skillPath);
53
- if (installedVersion && installedVersion !== cliVersion) {
54
- return { installedVersion, cliVersion };
65
+ if (installedVersion) {
66
+ versions.add(installedVersion);
67
+ }
68
+ }
69
+ return [...versions];
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;
55
93
  }
56
94
  }
57
- return null;
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) {
120
+ if (versions.length === 0) {
121
+ return "not installed";
122
+ }
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;
143
+ }
144
+ function formatVersionWarning(components) {
145
+ const targetVersion = selectTargetVersion([
146
+ components.cliVersion,
147
+ ...components.localPackageVersion ? [components.localPackageVersion] : [],
148
+ ...components.skillVersions
149
+ ]);
150
+ const skillLabel = components.skillVersions.length > 1 ? "agent skills" : "agent skill";
151
+ const updateInstructions = formatUpdateInstructions({
152
+ ...components,
153
+ targetVersion
154
+ });
155
+ 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
167
+ ].join("\n");
58
168
  }
59
- function warnIfInstalledSkillOutOfDate() {
169
+ function warnIfLibrettoVersionsDiffer() {
60
170
  try {
61
- const mismatch = findInstalledSkillVersionMismatch();
62
- if (!mismatch) {
171
+ const cliVersion = readCurrentCliVersion();
172
+ const localPackageVersion = readLocalPackageVersion();
173
+ const skillVersions = readInstalledSkillVersions();
174
+ const observedVersions = /* @__PURE__ */ new Set([
175
+ cliVersion,
176
+ ...localPackageVersion ? [localPackageVersion] : [],
177
+ ...skillVersions
178
+ ]);
179
+ if (observedVersions.size <= 1) {
63
180
  return;
64
181
  }
65
182
  console.error(
66
- `Warning: Your agent skill (${mismatch.installedVersion}) is out of date with your Libretto CLI (${mismatch.cliVersion}). Please run \`libretto setup\` to update your skills to the correct version.`
183
+ formatVersionWarning({
184
+ cliVersion,
185
+ localPackageVersion,
186
+ skillVersions
187
+ })
67
188
  );
68
189
  } catch {
69
190
  }
70
191
  }
71
192
  export {
72
193
  readCurrentCliVersion,
73
- warnIfInstalledSkillOutOfDate
194
+ warnIfLibrettoVersionsDiffer
74
195
  };
package/docs/releasing.md CHANGED
@@ -28,17 +28,19 @@ GitHub Actions needs these repository secrets:
28
28
 
29
29
  The release workflow uses a GitHub Actions environment named `release`. Create that environment in the repository settings (no required reviewers — access is controlled by branch protection on `main` instead).
30
30
 
31
- On npm, configure `libretto` to trust this repository and workflow for publishing. The trusted publisher fields should match:
31
+ On npm, configure each published package to trust this repository and workflow for publishing:
32
32
 
33
33
  - Organization or user: `saffron-health`
34
34
  - Repository: `libretto`
35
35
  - Workflow filename: `release.yml`
36
36
  - Environment name: `release`
37
37
 
38
- If you prefer the CLI, the setup command is:
38
+ If you prefer the CLI, run one setup command per package with npm 11.10.0 or newer:
39
39
 
40
40
  ```bash
41
- npm trust github libretto --repo saffron-health/libretto --file release.yml --env release
41
+ npx --yes npm@11.13.0 trust github libretto --repo saffron-health/libretto --file release.yml --env release --yes
42
+ npx --yes npm@11.13.0 trust github affordance --repo saffron-health/libretto --file release.yml --env release --yes
43
+ npx --yes npm@11.13.0 trust github create-libretto --repo saffron-health/libretto --file release.yml --env release --yes
42
44
  ```
43
45
 
44
46
  Trusted publishing only works on supported cloud-hosted runners. This workflow uses `ubuntu-latest`, which satisfies that requirement. npm also requires a recent toolchain for trusted publishing, so the publish job runs on Node 24.
@@ -80,7 +82,7 @@ The workflow:
80
82
  1. Reads the version from `packages/libretto/package.json`.
81
83
  2. Checks whether that version already exists on npm and in GitHub Releases.
82
84
  3. Runs install, type-check, and tests for the `libretto` package in a verification job.
83
- 4. Publishes `libretto@X.Y.Z` to npm from `packages/libretto` with trusted publishing if it is not already published.
85
+ 4. Publishes `affordance` first, then `libretto@X.Y.Z`, then `create-libretto@X.Y.Z` with trusted publishing.
84
86
  5. Creates GitHub release `vX.Y.Z` with generated release notes if it does not already exist.
85
87
 
86
88
  This makes the workflow safe to re-run after partial failures. For example, if npm publish succeeds but GitHub release creation fails, a re-run will skip npm and only create the missing release.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.18",
3
+ "version": "0.6.19",
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.18"
7
+ version: "0.6.19"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -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.18"
7
+ version: "0.6.19"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
package/src/cli/cli.ts CHANGED
@@ -2,7 +2,7 @@ import { ensureLibrettoSetup } from "./core/context.js";
2
2
  import { createCLIApp } from "./router.js";
3
3
  import {
4
4
  readCurrentCliVersion,
5
- warnIfInstalledSkillOutOfDate,
5
+ warnIfLibrettoVersionsDiffer,
6
6
  } from "./core/skill-version.js";
7
7
  import { loadEnv } from "../shared/env/load-env.js";
8
8
 
@@ -11,7 +11,7 @@ function renderVersion(): string {
11
11
  }
12
12
 
13
13
  function printSetupAudit(): void {
14
- warnIfInstalledSkillOutOfDate();
14
+ warnIfLibrettoVersionsDiffer();
15
15
  }
16
16
 
17
17
  function isPackageManagerExec(env: NodeJS.ProcessEnv = process.env): boolean {
@@ -18,7 +18,7 @@ import {
18
18
  setSessionMode,
19
19
  validateSessionName,
20
20
  } from "../core/session.js";
21
- import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
21
+ import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
22
22
  import { SimpleCLI } from "affordance";
23
23
  import {
24
24
  sessionOption,
@@ -107,7 +107,7 @@ export const openCommand = SimpleCLI.command({
107
107
  .use(withAutoSession())
108
108
  .use(withExperiments())
109
109
  .handle(async ({ input, ctx }) => {
110
- warnIfInstalledSkillOutOfDate();
110
+ warnIfLibrettoVersionsDiffer();
111
111
  assertSessionAvailableForStart(ctx.session, ctx.logger);
112
112
  const providerName = resolveProviderName(input.provider);
113
113
  if (providerName === "local") {
@@ -168,7 +168,7 @@ export const connectCommand = SimpleCLI.command({
168
168
  .use(withAutoSession())
169
169
  .use(withExperiments())
170
170
  .handle(async ({ input, ctx }) => {
171
- warnIfInstalledSkillOutOfDate();
171
+ warnIfLibrettoVersionsDiffer();
172
172
  await runConnectWithLogger(
173
173
  input.cdpUrl!,
174
174
  ctx.session,
@@ -25,7 +25,7 @@ import {
25
25
  writeSessionState,
26
26
  type SessionState,
27
27
  } from "../core/session.js";
28
- import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
28
+ import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
29
29
  import { readLibrettoConfig } from "../core/config.js";
30
30
  import { renderSnapshotDiff } from "../../shared/snapshot/diff-snapshots.js";
31
31
  import {
@@ -860,7 +860,7 @@ export const runCommand = SimpleCLI.command({
860
860
  .use(withAutoSession())
861
861
  .use(withExperiments())
862
862
  .handle(async ({ input, ctx }) => {
863
- warnIfInstalledSkillOutOfDate();
863
+ warnIfLibrettoVersionsDiffer();
864
864
  await stopExistingFailedRunSession(ctx.session, ctx.logger);
865
865
  assertSessionAvailableForStart(ctx.session, ctx.logger);
866
866
 
@@ -14,6 +14,17 @@ const INSTALLED_SKILL_PATHS = [
14
14
 
15
15
  let cachedCliVersion: string | null = null;
16
16
 
17
+ function readPackageVersion(packageJsonPath: string): string | null {
18
+ if (!existsSync(packageJsonPath)) {
19
+ return null;
20
+ }
21
+
22
+ const manifest = JSON.parse(
23
+ readFileSync(packageJsonPath, "utf8"),
24
+ ) as PackageManifest;
25
+ return manifest.version?.trim() || null;
26
+ }
27
+
17
28
  export function readCurrentCliVersion(): string {
18
29
  if (cachedCliVersion) {
19
30
  return cachedCliVersion;
@@ -22,20 +33,24 @@ export function readCurrentCliVersion(): string {
22
33
  const packageJsonPath = fileURLToPath(
23
34
  new URL("../../../package.json", import.meta.url),
24
35
  );
25
- const manifest = JSON.parse(
26
- readFileSync(packageJsonPath, "utf8"),
27
- ) as PackageManifest;
36
+ const version = readPackageVersion(packageJsonPath);
28
37
 
29
- if (!manifest.version) {
38
+ if (!version) {
30
39
  throw new Error(
31
40
  `Unable to determine current libretto version from ${packageJsonPath}.`,
32
41
  );
33
42
  }
34
43
 
35
- cachedCliVersion = manifest.version;
44
+ cachedCliVersion = version;
36
45
  return cachedCliVersion;
37
46
  }
38
47
 
48
+ function readLocalPackageVersion(): string | null {
49
+ return readPackageVersion(
50
+ join(REPO_ROOT, "node_modules", "libretto", "package.json"),
51
+ );
52
+ }
53
+
39
54
  function readInstalledSkillVersion(skillPath: string): string | null {
40
55
  if (!existsSync(skillPath)) {
41
56
  return null;
@@ -60,32 +75,188 @@ function readInstalledSkillVersion(skillPath: string): string | null {
60
75
  return versionMatch?.[1]?.trim() ?? null;
61
76
  }
62
77
 
63
- function findInstalledSkillVersionMismatch(): {
64
- installedVersion: string;
65
- cliVersion: string;
66
- } | null {
67
- const cliVersion = readCurrentCliVersion();
78
+ function readInstalledSkillVersions(): string[] {
79
+ const versions = new Set<string>();
68
80
 
69
81
  for (const relativePathParts of INSTALLED_SKILL_PATHS) {
70
82
  const skillPath = join(REPO_ROOT, ...relativePathParts);
71
83
  const installedVersion = readInstalledSkillVersion(skillPath);
72
- if (installedVersion && installedVersion !== cliVersion) {
73
- return { installedVersion, cliVersion };
84
+ if (installedVersion) {
85
+ versions.add(installedVersion);
74
86
  }
75
87
  }
76
88
 
77
- return null;
89
+ return [...versions];
78
90
  }
79
91
 
80
- export function warnIfInstalledSkillOutOfDate(): void {
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
+ function formatSkillVersions(
156
+ versions: string[],
157
+ targetVersion: string,
158
+ ): string {
159
+ if (versions.length === 0) {
160
+ return "not installed";
161
+ }
162
+
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;
201
+ }
202
+
203
+ function formatVersionWarning(components: {
204
+ cliVersion: string;
205
+ localPackageVersion: string | null;
206
+ skillVersions: string[];
207
+ }): string {
208
+ const targetVersion = selectTargetVersion([
209
+ components.cliVersion,
210
+ ...(components.localPackageVersion ? [components.localPackageVersion] : []),
211
+ ...components.skillVersions,
212
+ ]);
213
+ const skillLabel =
214
+ components.skillVersions.length > 1 ? "agent skills" : "agent skill";
215
+ const updateInstructions = formatUpdateInstructions({
216
+ ...components,
217
+ targetVersion,
218
+ });
219
+
220
+ return [
221
+ "WARNING: Libretto version mismatch detected.",
222
+ "",
223
+ ` global CLI: ${formatVersion(components.cliVersion, targetVersion)}`,
224
+ ` local package: ${
225
+ components.localPackageVersion
226
+ ? formatVersion(components.localPackageVersion, targetVersion)
227
+ : "not installed"
228
+ }`,
229
+ ` ${skillLabel}: ${formatSkillVersions(
230
+ components.skillVersions,
231
+ targetVersion,
232
+ )}`,
233
+ "",
234
+ "How to update:",
235
+ ...updateInstructions,
236
+ ].join("\n");
237
+ }
238
+
239
+ export function warnIfLibrettoVersionsDiffer(): void {
81
240
  try {
82
- const mismatch = findInstalledSkillVersionMismatch();
83
- if (!mismatch) {
241
+ const cliVersion = readCurrentCliVersion();
242
+ const localPackageVersion = readLocalPackageVersion();
243
+ const skillVersions = readInstalledSkillVersions();
244
+ const observedVersions = new Set([
245
+ cliVersion,
246
+ ...(localPackageVersion ? [localPackageVersion] : []),
247
+ ...skillVersions,
248
+ ]);
249
+
250
+ if (observedVersions.size <= 1) {
84
251
  return;
85
252
  }
86
253
 
87
254
  console.error(
88
- `Warning: Your agent skill (${mismatch.installedVersion}) is out of date with your Libretto CLI (${mismatch.cliVersion}). Please run \`libretto setup\` to update your skills to the correct version.`,
255
+ formatVersionWarning({
256
+ cliVersion,
257
+ localPackageVersion,
258
+ skillVersions,
259
+ }),
89
260
  );
90
261
  } catch {
91
262
  // Never block command execution on a best-effort skill version check.