@cleocode/caamp 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  detectProjectProviders,
17
17
  discoverSkill,
18
18
  discoverSkillsMulti,
19
+ extractVersionTag,
19
20
  formatNetworkError,
20
21
  formatSkillRecommendations,
21
22
  generateInjectionContent,
@@ -27,6 +28,7 @@ import {
27
28
  getRegistryVersion,
28
29
  getSkill,
29
30
  getSkillDir,
31
+ getTrackedMcpServers,
30
32
  getTrackedSkills,
31
33
  groupByInstructFile,
32
34
  injectAll,
@@ -48,6 +50,7 @@ import {
48
50
  readConfig,
49
51
  readLockFile,
50
52
  recommendSkills,
53
+ reconcileCleoLock,
51
54
  recordMcpInstall,
52
55
  recordSkillInstall,
53
56
  removeMcpFromLock,
@@ -71,7 +74,7 @@ import {
71
74
  tokenizeCriteriaValue,
72
75
  updateInstructionsSingleOperation,
73
76
  validateSkill
74
- } from "./chunk-DT22SZ7X.js";
77
+ } from "./chunk-G7UPJOYG.js";
75
78
 
76
79
  // src/cli.ts
77
80
  import { Command } from "commander";
@@ -1171,6 +1174,69 @@ async function checkLockFile() {
1171
1174
  }
1172
1175
  return { name: "Lock File", checks };
1173
1176
  }
1177
+ async function checkMcpLockEntries() {
1178
+ const checks = [];
1179
+ try {
1180
+ const lock = await readLockFile();
1181
+ const lockNames = Object.keys(lock.mcpServers);
1182
+ checks.push({ label: `${lockNames.length} MCP server entries in lock`, status: "pass" });
1183
+ const results = detectAllProviders();
1184
+ const installed = results.filter((r) => r.installed);
1185
+ const liveCleoNames = /* @__PURE__ */ new Set();
1186
+ let untrackedCount = 0;
1187
+ for (const scope of ["project", "global"]) {
1188
+ for (const r of installed) {
1189
+ try {
1190
+ const entries = await listMcpServers(r.provider, scope);
1191
+ for (const entry of entries) {
1192
+ const channel = resolveChannelFromServerName(entry.name);
1193
+ if (!channel) continue;
1194
+ liveCleoNames.add(entry.name);
1195
+ if (!lock.mcpServers[entry.name]) {
1196
+ untrackedCount++;
1197
+ }
1198
+ }
1199
+ } catch {
1200
+ }
1201
+ }
1202
+ }
1203
+ if (untrackedCount === 0) {
1204
+ checks.push({ label: "All CLEO servers tracked in lock", status: "pass" });
1205
+ } else {
1206
+ checks.push({
1207
+ label: `${untrackedCount} untracked CLEO server${untrackedCount !== 1 ? "s" : ""} (in config, not in lock)`,
1208
+ status: "warn",
1209
+ detail: "Run `caamp cleo repair` to backfill lock entries"
1210
+ });
1211
+ }
1212
+ let orphanedCount = 0;
1213
+ const orphanedNames = [];
1214
+ for (const serverName of lockNames) {
1215
+ const channel = resolveChannelFromServerName(serverName);
1216
+ if (!channel) continue;
1217
+ if (!liveCleoNames.has(serverName)) {
1218
+ orphanedCount++;
1219
+ orphanedNames.push(serverName);
1220
+ }
1221
+ }
1222
+ if (orphanedCount === 0) {
1223
+ checks.push({ label: "No orphaned CLEO lock entries", status: "pass" });
1224
+ } else {
1225
+ checks.push({
1226
+ label: `${orphanedCount} orphaned CLEO lock entr${orphanedCount !== 1 ? "ies" : "y"} (in lock, not in any config)`,
1227
+ status: "warn",
1228
+ detail: orphanedNames.join(", ") + " \u2014 Run `caamp cleo repair --prune` to clean up"
1229
+ });
1230
+ }
1231
+ } catch (err) {
1232
+ checks.push({
1233
+ label: "Failed to check MCP lock entries",
1234
+ status: "fail",
1235
+ detail: err instanceof Error ? err.message : String(err)
1236
+ });
1237
+ }
1238
+ return { name: "MCP Lock", checks };
1239
+ }
1174
1240
  async function checkConfigFiles() {
1175
1241
  const checks = [];
1176
1242
  const results = detectAllProviders();
@@ -1239,6 +1305,7 @@ function registerDoctorCommand(program2) {
1239
1305
  sections.push(checkInstalledProviders());
1240
1306
  sections.push(checkSkillSymlinks());
1241
1307
  sections.push(await checkLockFile());
1308
+ sections.push(await checkMcpLockEntries());
1242
1309
  sections.push(await checkConfigFiles());
1243
1310
  let passed = 0;
1244
1311
  let warnings = 0;
@@ -1258,6 +1325,7 @@ function registerDoctorCommand(program2) {
1258
1325
  const detectionResults = detectAllProviders();
1259
1326
  const installedProviders = detectionResults.filter((r) => r.installed);
1260
1327
  const { canonicalCount, brokenCount, staleCount } = countSkillIssues();
1328
+ const { tracked: mcpTracked, untracked: mcpUntracked, orphaned: mcpOrphaned } = countMcpLockIssues(sections);
1261
1329
  const result = {
1262
1330
  environment: {
1263
1331
  node: getNodeVersion(),
@@ -1279,6 +1347,11 @@ function registerDoctorCommand(program2) {
1279
1347
  brokenLinks: brokenCount,
1280
1348
  staleLinks: staleCount
1281
1349
  },
1350
+ mcpServers: {
1351
+ tracked: mcpTracked,
1352
+ untracked: mcpUntracked,
1353
+ orphaned: mcpOrphaned
1354
+ },
1282
1355
  checks: sections.flatMap(
1283
1356
  (s) => s.checks.map((c) => ({
1284
1357
  label: `${s.name}: ${c.label}`,
@@ -1375,6 +1448,26 @@ function countSkillIssues() {
1375
1448
  }
1376
1449
  return { canonicalCount, brokenCount, staleCount };
1377
1450
  }
1451
+ function countMcpLockIssues(sections) {
1452
+ const mcpSection = sections.find((s) => s.name === "MCP Lock");
1453
+ if (!mcpSection) return { tracked: 0, untracked: 0, orphaned: 0 };
1454
+ let tracked = 0;
1455
+ let untracked = 0;
1456
+ let orphaned = 0;
1457
+ for (const check of mcpSection.checks) {
1458
+ const countMatch = check.label.match(/^(\d+)/);
1459
+ if (!countMatch?.[1]) continue;
1460
+ const count = Number.parseInt(countMatch[1], 10);
1461
+ if (check.label.includes("MCP server entries in lock")) {
1462
+ tracked = count;
1463
+ } else if (check.label.includes("untracked")) {
1464
+ untracked = count;
1465
+ } else if (check.label.includes("orphaned")) {
1466
+ orphaned = count;
1467
+ }
1468
+ }
1469
+ return { tracked, untracked, orphaned };
1470
+ }
1378
1471
 
1379
1472
  // src/commands/instructions/inject.ts
1380
1473
  import pc3 from "picocolors";
@@ -1830,7 +1923,8 @@ async function executeCleoInstall(mode, opts, operation) {
1830
1923
  profile.packageSpec ?? resolvedOpts.command ?? "cleo-dev",
1831
1924
  channel === "dev" ? "command" : "package",
1832
1925
  succeeded.map((result) => result.provider.id),
1833
- resolvedOpts.global ?? false
1926
+ resolvedOpts.global ?? false,
1927
+ resolvedOpts.version ?? extractVersionTag(profile.packageSpec)
1834
1928
  );
1835
1929
  }
1836
1930
  if (format === "human") {
@@ -1946,6 +2040,34 @@ async function executeCleoUninstall(opts, operation) {
1946
2040
  });
1947
2041
  }
1948
2042
  }
2043
+ function checkCleoEntryHealth(command, lockTracked) {
2044
+ if (!command) {
2045
+ return {
2046
+ commandReachable: true,
2047
+ commandDetail: "(no command)",
2048
+ configPresent: true,
2049
+ lockTracked,
2050
+ status: lockTracked ? "healthy" : "degraded"
2051
+ };
2052
+ }
2053
+ const reachability = checkCommandReachability(command);
2054
+ if (!reachability.reachable) {
2055
+ return {
2056
+ commandReachable: false,
2057
+ commandDetail: reachability.detail,
2058
+ configPresent: true,
2059
+ lockTracked,
2060
+ status: "broken"
2061
+ };
2062
+ }
2063
+ return {
2064
+ commandReachable: true,
2065
+ commandDetail: reachability.detail,
2066
+ configPresent: true,
2067
+ lockTracked,
2068
+ status: lockTracked ? "healthy" : "degraded"
2069
+ };
2070
+ }
1949
2071
  async function executeCleoShow(opts, operation) {
1950
2072
  const mvi = "standard";
1951
2073
  let format;
@@ -1971,40 +2093,187 @@ async function executeCleoShow(opts, operation) {
1971
2093
  process.exit(1);
1972
2094
  }
1973
2095
  const channelFilter = opts.channel ? normalizeCleoChannel(opts.channel) : null;
1974
- const scope = opts.global ? "global" : "project";
2096
+ const scopes = [];
2097
+ if (opts.global && !opts.project) {
2098
+ scopes.push("global");
2099
+ } else if (opts.project && !opts.global) {
2100
+ scopes.push("project");
2101
+ } else {
2102
+ scopes.push("project", "global");
2103
+ }
2104
+ const lockEntries = await getTrackedMcpServers();
1975
2105
  const entries = [];
1976
- for (const provider of providers) {
1977
- const providerEntries = await listMcpServers(provider, scope);
1978
- for (const entry of providerEntries) {
1979
- const channel = resolveChannelFromServerName(entry.name);
1980
- if (!channel) continue;
1981
- if (channelFilter && channel !== channelFilter) continue;
1982
- entries.push({
1983
- provider: provider.id,
1984
- serverName: entry.name,
1985
- channel,
1986
- command: typeof entry.config.command === "string" ? entry.config.command : void 0,
1987
- args: Array.isArray(entry.config.args) ? entry.config.args.filter((value) => typeof value === "string") : [],
1988
- env: typeof entry.config.env === "object" && entry.config.env !== null ? entry.config.env : {}
1989
- });
2106
+ const warnings = [];
2107
+ for (const scope of scopes) {
2108
+ for (const provider of providers) {
2109
+ const providerEntries = await listMcpServers(provider, scope);
2110
+ for (const entry of providerEntries) {
2111
+ const channel = resolveChannelFromServerName(entry.name);
2112
+ if (!channel) continue;
2113
+ if (channelFilter && channel !== channelFilter) continue;
2114
+ const command = typeof entry.config.command === "string" ? entry.config.command : void 0;
2115
+ const args = Array.isArray(entry.config.args) ? entry.config.args.filter((value) => typeof value === "string") : [];
2116
+ const env = typeof entry.config.env === "object" && entry.config.env !== null ? entry.config.env : {};
2117
+ const lockEntry = lockEntries[entry.name];
2118
+ const lockTracked = lockEntry !== void 0;
2119
+ const health = checkCleoEntryHealth(command, lockTracked);
2120
+ entries.push({
2121
+ provider: provider.id,
2122
+ providerName: provider.toolName,
2123
+ serverName: entry.name,
2124
+ channel,
2125
+ scope,
2126
+ command,
2127
+ args,
2128
+ env,
2129
+ version: lockEntry?.version ?? null,
2130
+ source: lockEntry?.source ?? null,
2131
+ sourceType: lockEntry?.sourceType ?? null,
2132
+ installedAt: lockEntry?.installedAt ?? null,
2133
+ updatedAt: lockEntry?.updatedAt ?? null,
2134
+ health
2135
+ });
2136
+ if (health.status === "broken") {
2137
+ warnings.push({
2138
+ code: "W_COMMAND_UNREACHABLE",
2139
+ message: `${entry.name} command not reachable on ${provider.toolName} (${health.commandDetail})`
2140
+ });
2141
+ } else if (health.status === "degraded") {
2142
+ warnings.push({
2143
+ code: "W_NOT_TRACKED",
2144
+ message: `${entry.name} on ${provider.toolName} is not tracked in lock file`
2145
+ });
2146
+ }
2147
+ }
1990
2148
  }
1991
2149
  }
2150
+ const issueCount = entries.filter((e) => e.health.status !== "healthy").length;
1992
2151
  if (format === "human") {
1993
2152
  if (entries.length === 0) {
1994
- console.log(pc6.dim("No CLEO MCP profiles found."));
2153
+ console.log(pc6.dim("No CLEO channel profiles found."));
1995
2154
  } else {
2155
+ console.log(pc6.bold("CLEO Channel Profiles"));
2156
+ console.log();
2157
+ const header = [
2158
+ "Channel".padEnd(10),
2159
+ "Version".padEnd(10),
2160
+ "Provider".padEnd(22),
2161
+ "Scope".padEnd(9),
2162
+ "Command".padEnd(33),
2163
+ "Status".padEnd(10),
2164
+ "Installed".padEnd(12)
2165
+ ].join("");
2166
+ console.log(` ${pc6.dim(header)}`);
2167
+ console.log(` ${pc6.dim("-".repeat(106))}`);
1996
2168
  for (const entry of entries) {
1997
- console.log(`${pc6.bold(entry.provider.padEnd(22))} ${entry.serverName.padEnd(10)} ${pc6.dim(entry.channel)}`);
2169
+ const commandStr = entry.command ? `${entry.command} ${entry.args.join(" ")}`.slice(0, 31).padEnd(33) : pc6.dim("-").padEnd(33);
2170
+ const versionStr = (entry.version ?? "-").padEnd(10);
2171
+ const installedStr = entry.installedAt ? entry.installedAt.slice(0, 10).padEnd(12) : "-".padEnd(12);
2172
+ let statusStr;
2173
+ if (entry.health.status === "healthy") {
2174
+ statusStr = pc6.green("healthy".padEnd(10));
2175
+ } else if (entry.health.status === "degraded") {
2176
+ statusStr = pc6.yellow("degraded".padEnd(10));
2177
+ } else {
2178
+ statusStr = pc6.red("broken".padEnd(10));
2179
+ }
2180
+ console.log(
2181
+ ` ${entry.channel.padEnd(10)}${versionStr}${entry.providerName.padEnd(22)}${entry.scope.padEnd(9)}${commandStr}${statusStr}${installedStr}`
2182
+ );
2183
+ }
2184
+ console.log();
2185
+ const summary = ` ${entries.length} profile${entries.length !== 1 ? "s" : ""}`;
2186
+ if (issueCount > 0) {
2187
+ console.log(`${summary} | ${pc6.yellow(`${issueCount} issue${issueCount !== 1 ? "s" : ""}`)}`);
2188
+ console.log();
2189
+ console.log(" Issues:");
2190
+ for (const w of warnings) {
2191
+ console.log(` ${pc6.yellow("!")} ${w.message}`);
2192
+ }
2193
+ } else {
2194
+ console.log(summary);
2195
+ }
2196
+ }
2197
+ }
2198
+ if (format === "json") {
2199
+ outputSuccess(
2200
+ operation,
2201
+ mvi,
2202
+ {
2203
+ providers: providers.map((provider) => provider.id),
2204
+ scopes,
2205
+ channel: channelFilter,
2206
+ profiles: entries,
2207
+ count: entries.length
2208
+ },
2209
+ void 0,
2210
+ void 0,
2211
+ warnings.length > 0 ? warnings : void 0
2212
+ );
2213
+ }
2214
+ }
2215
+ async function executeCleoRepair(opts, operation) {
2216
+ const mvi = "standard";
2217
+ let format;
2218
+ try {
2219
+ format = resolveFormat({
2220
+ jsonFlag: opts.json ?? false,
2221
+ humanFlag: (opts.human ?? false) || isHuman(),
2222
+ projectDefault: "json"
2223
+ });
2224
+ } catch (error) {
2225
+ const message = error instanceof Error ? error.message : String(error);
2226
+ emitJsonError(operation, mvi, ErrorCodes.FORMAT_CONFLICT, message, ErrorCategories.VALIDATION);
2227
+ process.exit(1);
2228
+ }
2229
+ const providerIds = opts.provider.length > 0 ? opts.provider : void 0;
2230
+ const result = await reconcileCleoLock({
2231
+ providerIds,
2232
+ all: opts.all,
2233
+ global: opts.global,
2234
+ project: opts.project,
2235
+ prune: opts.prune,
2236
+ dryRun: opts.dryRun
2237
+ });
2238
+ if (format === "human") {
2239
+ const prefix = opts.dryRun ? "CLEO Lock Repair (dry run)" : "CLEO Lock Repair";
2240
+ console.log(pc6.bold(prefix));
2241
+ console.log();
2242
+ if (result.backfilled.length > 0) {
2243
+ for (const entry of result.backfilled) {
2244
+ const agents = entry.agents.join(", ");
2245
+ const versionStr = entry.version ? `(${entry.version})` : "";
2246
+ console.log(
2247
+ ` ${pc6.green("+")} ${entry.serverName.padEnd(12)}${entry.channel.padEnd(10)}${agents.padEnd(22)}${entry.scope.padEnd(10)}${entry.source} ${pc6.dim(versionStr)}`
2248
+ );
2249
+ }
2250
+ }
2251
+ if (result.pruned.length > 0) {
2252
+ for (const name of result.pruned) {
2253
+ console.log(` ${pc6.red("-")} ${name} (removed from lock)`);
2254
+ }
2255
+ }
2256
+ if (result.backfilled.length === 0 && result.pruned.length === 0) {
2257
+ console.log(pc6.dim(" No changes needed. All CLEO entries are tracked."));
2258
+ }
2259
+ console.log();
2260
+ console.log(
2261
+ ` ${result.backfilled.length} backfilled | ${result.pruned.length} pruned | ${result.alreadyTracked} already tracked`
2262
+ );
2263
+ if (result.errors.length > 0) {
2264
+ console.log();
2265
+ for (const err of result.errors) {
2266
+ console.log(` ${pc6.red("!")} ${err.message}`);
1998
2267
  }
1999
2268
  }
2000
2269
  }
2001
2270
  if (format === "json") {
2002
2271
  outputSuccess(operation, mvi, {
2003
- providers: providers.map((provider) => provider.id),
2004
- scope,
2005
- channel: channelFilter,
2006
- profiles: entries,
2007
- count: entries.length
2272
+ backfilled: result.backfilled,
2273
+ pruned: result.pruned,
2274
+ alreadyTracked: result.alreadyTracked,
2275
+ dryRun: opts.dryRun ?? false,
2276
+ errors: result.errors
2008
2277
  });
2009
2278
  }
2010
2279
  }
@@ -2026,9 +2295,12 @@ function registerMcpCleoCommands(parent) {
2026
2295
  cleo.command("uninstall").description("Uninstall CLEO MCP profile for a channel").requiredOption("--channel <channel>", "CLEO channel: stable|beta|dev").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Apply to all detected providers").option("-g, --global", "Use global scope").option("--dry-run", "Preview without writing").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2027
2296
  await executeCleoUninstall(opts, "mcp.cleo.uninstall");
2028
2297
  });
2029
- cleo.command("show").description("Show installed CLEO MCP channel profiles").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Use global scope").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2298
+ cleo.command("show").description("Show installed CLEO MCP channel profiles").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2030
2299
  await executeCleoShow(opts, "mcp.cleo.show");
2031
2300
  });
2301
+ cleo.command("repair").description("Repair lock file by backfilling untracked CLEO entries").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Scan all detected providers").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--prune", "Remove orphaned lock entries not in any config").option("--dry-run", "Preview without writing").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2302
+ await executeCleoRepair(opts, "mcp.cleo.repair");
2303
+ });
2032
2304
  }
2033
2305
  function registerMcpCleoCompatibilityCommands(parent) {
2034
2306
  parent.command("update").description("Update channel-managed MCP profile").argument("<name>", "Managed MCP profile name (cleo)").requiredOption("--channel <channel>", "CLEO channel: stable|beta|dev").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Apply to all detected providers").option("-g, --global", "Use global scope").option("--version <tag>", "Tag/version for stable or beta").option("--command <command>", "Dev channel command").option("--arg <arg>", "Dev command arg (repeatable)", collect, []).option("--env <kv>", "Environment assignment KEY=value (repeatable)", collect, []).option("--cleo-dir <path>", "CLEO_DIR override for dev channel").option("--dry-run", "Preview without writing").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (name, opts) => {
@@ -2045,7 +2317,7 @@ function registerMcpCleoCompatibilityCommands(parent) {
2045
2317
  }
2046
2318
  await executeCleoUninstall(opts, "mcp.uninstall");
2047
2319
  });
2048
- parent.command("show").description("Show channel-managed MCP profile").argument("<name>", "Managed MCP profile name (cleo)").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Use global scope").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (name, opts) => {
2320
+ parent.command("show").description("Show channel-managed MCP profile").argument("<name>", "Managed MCP profile name (cleo)").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (name, opts) => {
2049
2321
  if (name !== "cleo") {
2050
2322
  emitJsonError("mcp.show", "standard", ErrorCodes.INVALID_INPUT, "Only managed profile 'cleo' is supported by mcp show.", ErrorCategories.VALIDATION, { name });
2051
2323
  process.exit(1);
@@ -2090,9 +2362,12 @@ function registerCleoCommands(program2) {
2090
2362
  cleo.command("uninstall").description("Uninstall CLEO profile for a channel").requiredOption("--channel <channel>", "CLEO channel: stable|beta|dev").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Apply to all detected providers").option("-g, --global", "Use global scope").option("--dry-run", "Preview without writing").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2091
2363
  await executeCleoUninstall(opts, "cleo.uninstall");
2092
2364
  });
2093
- cleo.command("show").description("Show installed CLEO channel profiles").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Use global scope").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2365
+ cleo.command("show").description("Show installed CLEO channel profiles").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Inspect all detected providers").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--channel <channel>", "Filter channel: stable|beta|dev").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2094
2366
  await executeCleoShow(opts, "cleo.show");
2095
2367
  });
2368
+ cleo.command("repair").description("Repair lock file by backfilling untracked CLEO entries").option("--provider <id>", "Target provider (repeatable)", collect, []).option("--all", "Scan all detected providers").option("-g, --global", "Global scope only").option("-p, --project", "Project scope only").option("--prune", "Remove orphaned lock entries not in any config").option("--dry-run", "Preview without writing").option("--json", "Output as JSON (default)").option("--human", "Output in human-readable format").action(async (opts) => {
2369
+ await executeCleoRepair(opts, "cleo.repair");
2370
+ });
2096
2371
  }
2097
2372
 
2098
2373
  // src/commands/mcp/detect.ts