@hasna/skills 0.10.74 → 0.10.75

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/bin/index.js CHANGED
@@ -36888,7 +36888,7 @@ var package_default;
36888
36888
  var init_package = __esm(() => {
36889
36889
  package_default = {
36890
36890
  name: "@hasna/skills",
36891
- version: "0.10.74",
36891
+ version: "0.10.75",
36892
36892
  description: "Skills library for AI coding agents",
36893
36893
  type: "module",
36894
36894
  bin: {
@@ -60988,6 +60988,118 @@ var init_skillinfo = __esm(() => {
60988
60988
  GENERIC_ENV_PATTERN = /\b((?:OPENAI|ANTHROPIC|GEMINI|XAI|ELEVENLABS|DEEPGRAM|REPLICATE|FAL|STABILITY|EXA|FIRECRAWL|TWILIO|SENDGRID|RESEND|SLACK|DISCORD|NOTION|LINEAR|GITHUB|AWS|GOOGLE|CLOUDFLARE|VERCEL|SUPABASE|STRIPE)_[A-Z_]+)\b/g;
60989
60989
  });
60990
60990
 
60991
+ // src/lib/search.ts
60992
+ function editDistance(a, b) {
60993
+ if (a === b)
60994
+ return 0;
60995
+ if (a.length === 0)
60996
+ return b.length;
60997
+ if (b.length === 0)
60998
+ return a.length;
60999
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
61000
+ const curr = new Array(b.length + 1);
61001
+ for (let i = 1;i <= a.length; i++) {
61002
+ curr[0] = i;
61003
+ for (let j = 1;j <= b.length; j++) {
61004
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
61005
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
61006
+ }
61007
+ prev.splice(0, prev.length, ...curr);
61008
+ }
61009
+ return prev[b.length];
61010
+ }
61011
+ function fuzzyMatchScore(word, target) {
61012
+ if (target.includes(word))
61013
+ return 1;
61014
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
61015
+ for (const token of tokens) {
61016
+ if (token.startsWith(word))
61017
+ return 0.6;
61018
+ }
61019
+ if (word.length >= 3) {
61020
+ const maxDist = word.length <= 3 ? 1 : 2;
61021
+ for (const token of tokens) {
61022
+ if (Math.abs(token.length - word.length) <= maxDist) {
61023
+ const dist = editDistance(word, token);
61024
+ if (dist <= maxDist)
61025
+ return 0.4;
61026
+ }
61027
+ }
61028
+ }
61029
+ return 0;
61030
+ }
61031
+ function searchSkills(query, registry = loadRegistry()) {
61032
+ const words2 = query.toLowerCase().split(/\s+/).filter(Boolean);
61033
+ if (words2.length === 0)
61034
+ return [];
61035
+ const scored = [];
61036
+ for (const skill of registry) {
61037
+ const nameLower = skill.name.toLowerCase();
61038
+ const displayNameLower = skill.displayName.toLowerCase();
61039
+ const descriptionLower = skill.description.toLowerCase();
61040
+ const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
61041
+ const tagsCombined = tagsLower.join(" ");
61042
+ let score = 0;
61043
+ let allWordsMatch = true;
61044
+ for (const word of words2) {
61045
+ let wordScore = 0;
61046
+ const nameMatch = fuzzyMatchScore(word, nameLower);
61047
+ if (nameMatch > 0)
61048
+ wordScore += 10 * nameMatch;
61049
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
61050
+ if (displayMatch > 0)
61051
+ wordScore += 7 * displayMatch;
61052
+ const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
61053
+ if (tagMatch > 0)
61054
+ wordScore += 5 * tagMatch;
61055
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
61056
+ if (descMatch > 0)
61057
+ wordScore += 2 * descMatch;
61058
+ if (wordScore === 0) {
61059
+ allWordsMatch = false;
61060
+ break;
61061
+ }
61062
+ score += wordScore;
61063
+ }
61064
+ if (allWordsMatch && score > 0) {
61065
+ scored.push({ skill, score });
61066
+ }
61067
+ }
61068
+ scored.sort((a, b) => b.score - a.score);
61069
+ return scored.map((s) => s.skill);
61070
+ }
61071
+ function findCloseSkillNames(query, names, maxResults = 3) {
61072
+ if (maxResults <= 0)
61073
+ return [];
61074
+ const q = query.trim().toLowerCase();
61075
+ if (q.length === 0)
61076
+ return [];
61077
+ const tokens = q.split("-").filter(Boolean);
61078
+ const ranked = [];
61079
+ if (q.length >= 3) {
61080
+ for (const name of names) {
61081
+ const candidate = name.toLowerCase();
61082
+ const rank = candidate === q ? 0 : candidate.endsWith(`-${q}`) || candidate.startsWith(`${q}-`) ? 1 : tokens.length > 1 && tokens.every((token) => candidate.split("-").includes(token)) ? 2 : undefined;
61083
+ if (rank !== undefined)
61084
+ ranked.push([name, rank]);
61085
+ }
61086
+ }
61087
+ ranked.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || a[0].localeCompare(b[0]));
61088
+ const found = ranked.slice(0, maxResults).map(([name]) => name);
61089
+ if (found.length === maxResults)
61090
+ return found;
61091
+ const taken = new Set(found);
61092
+ const threshold = Math.max(3, Math.floor(q.length / 2));
61093
+ const byDistance = names.filter((name) => !taken.has(name)).map((name) => ({ name, distance: editDistance(q, name.toLowerCase()) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
61094
+ return [...found, ...byDistance.slice(0, maxResults - found.length).map((entry) => entry.name)];
61095
+ }
61096
+ function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
61097
+ return findCloseSkillNames(query, registry.map((skill) => skill.name), maxResults);
61098
+ }
61099
+ var init_search = __esm(() => {
61100
+ init_registry();
61101
+ });
61102
+
60991
61103
  // src/lib/selected-document.ts
60992
61104
  function readSelectedDocument(entries, file) {
60993
61105
  const entry = file !== undefined ? entries.find((candidate) => candidate.path === file) : DOCUMENT_PATHS.map((path) => entries.find((candidate) => candidate.path === path && candidate.bytes.length > 0)).find(Boolean);
@@ -61108,11 +61220,30 @@ function exactProfileSelection(spec, profile) {
61108
61220
  const at2 = spec.lastIndexOf("@");
61109
61221
  const slug = at2 > 0 ? spec.slice(0, at2) : spec;
61110
61222
  const version = at2 > 0 ? spec.slice(at2 + 1) : undefined;
61111
- const selection = profile.selections.find((entry) => (entry.slug === slug || entry.aliases?.includes(slug)) && (version === undefined || version === entry.version));
61112
- if (!selection || version === "")
61113
- throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
61223
+ const named = profile.selections.filter((entry) => entry.slug === slug || entry.aliases?.includes(slug));
61224
+ if (version === "")
61225
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", emptyVersionRefusal(spec));
61226
+ if (named.length === 0)
61227
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", unknownNameRefusal(slug, profile));
61228
+ const selection = version === undefined ? named[0] : named.find((entry) => entry.version === version);
61229
+ if (!selection)
61230
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", wrongVersionRefusal(slug, version, named));
61114
61231
  return selection;
61115
61232
  }
61233
+ function emptyVersionRefusal(spec) {
61234
+ return `The requested name '${spec}' has an empty version. ${ACCEPTED_FORM}`;
61235
+ }
61236
+ function wrongVersionRefusal(slug, version, named) {
61237
+ const versions = [...new Set(named.map((entry) => entry.version))].sort();
61238
+ const canonical9 = named[0].slug;
61239
+ return `Skill '${slug}' is selected at version '${versions.join("', '")}', but '${version}' was requested. ` + `Accepted form: '${canonical9}@${versions[0]}'.`;
61240
+ }
61241
+ function unknownNameRefusal(slug, profile) {
61242
+ const names = profile.selections.flatMap((entry) => [entry.slug, ...entry.aliases ?? []]);
61243
+ const close = findCloseSkillNames(slug, names);
61244
+ const hint = close.length > 0 ? ` Did you mean: ${close.join(", ")}?` : "";
61245
+ return `Skill '${slug}' is not selected by this Skills profile or project lock.${hint} ` + `Read the accepted names with 'skills search <term>' or 'skills list --all --json'; accept one with 'skills pin <name>'. ${ACCEPTED_FORM}`;
61246
+ }
61116
61247
  async function loadSelectedSkill(spec, profileId, options = {}) {
61117
61248
  const context = await resolveSelectionContext(profileId, options);
61118
61249
  const selection = exactProfileSelection(spec, context.receipt.profile);
@@ -61157,9 +61288,11 @@ async function selectedSkillRequirements(spec, profileId, options = {}) {
61157
61288
  }
61158
61289
  return { ...getSkillRequirementsFromContent(selection.slug, texts, dependencies), selection };
61159
61290
  }
61291
+ var ACCEPTED_FORM = "Accepted form: '<slug>' or '<slug>@<version>'.";
61160
61292
  var init_selection_resolver = __esm(() => {
61161
61293
  init_profile_client();
61162
61294
  init_skillinfo();
61295
+ init_search();
61163
61296
  init_selected_document();
61164
61297
  init_selection_cache();
61165
61298
  });
@@ -63981,95 +64114,6 @@ var init_registry_types = __esm(() => {
63981
64114
  BASIC_SKILL_NAMES = [];
63982
64115
  });
63983
64116
 
63984
- // src/lib/search.ts
63985
- function editDistance(a, b) {
63986
- if (a === b)
63987
- return 0;
63988
- if (a.length === 0)
63989
- return b.length;
63990
- if (b.length === 0)
63991
- return a.length;
63992
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
63993
- const curr = new Array(b.length + 1);
63994
- for (let i = 1;i <= a.length; i++) {
63995
- curr[0] = i;
63996
- for (let j = 1;j <= b.length; j++) {
63997
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
63998
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
63999
- }
64000
- prev.splice(0, prev.length, ...curr);
64001
- }
64002
- return prev[b.length];
64003
- }
64004
- function fuzzyMatchScore(word, target) {
64005
- if (target.includes(word))
64006
- return 1;
64007
- const tokens = target.split(/[\s\-_]+/).filter(Boolean);
64008
- for (const token of tokens) {
64009
- if (token.startsWith(word))
64010
- return 0.6;
64011
- }
64012
- if (word.length >= 3) {
64013
- const maxDist = word.length <= 3 ? 1 : 2;
64014
- for (const token of tokens) {
64015
- if (Math.abs(token.length - word.length) <= maxDist) {
64016
- const dist = editDistance(word, token);
64017
- if (dist <= maxDist)
64018
- return 0.4;
64019
- }
64020
- }
64021
- }
64022
- return 0;
64023
- }
64024
- function searchSkills(query, registry = loadRegistry()) {
64025
- const words2 = query.toLowerCase().split(/\s+/).filter(Boolean);
64026
- if (words2.length === 0)
64027
- return [];
64028
- const scored = [];
64029
- for (const skill of registry) {
64030
- const nameLower = skill.name.toLowerCase();
64031
- const displayNameLower = skill.displayName.toLowerCase();
64032
- const descriptionLower = skill.description.toLowerCase();
64033
- const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
64034
- const tagsCombined = tagsLower.join(" ");
64035
- let score = 0;
64036
- let allWordsMatch = true;
64037
- for (const word of words2) {
64038
- let wordScore = 0;
64039
- const nameMatch = fuzzyMatchScore(word, nameLower);
64040
- if (nameMatch > 0)
64041
- wordScore += 10 * nameMatch;
64042
- const displayMatch = fuzzyMatchScore(word, displayNameLower);
64043
- if (displayMatch > 0)
64044
- wordScore += 7 * displayMatch;
64045
- const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
64046
- if (tagMatch > 0)
64047
- wordScore += 5 * tagMatch;
64048
- const descMatch = fuzzyMatchScore(word, descriptionLower);
64049
- if (descMatch > 0)
64050
- wordScore += 2 * descMatch;
64051
- if (wordScore === 0) {
64052
- allWordsMatch = false;
64053
- break;
64054
- }
64055
- score += wordScore;
64056
- }
64057
- if (allWordsMatch && score > 0) {
64058
- scored.push({ skill, score });
64059
- }
64060
- }
64061
- scored.sort((a, b) => b.score - a.score);
64062
- return scored.map((s) => s.skill);
64063
- }
64064
- function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
64065
- const q = query.toLowerCase();
64066
- const scored = registry.map((s) => ({ name: s.name, dist: editDistance(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
64067
- return scored.slice(0, maxResults).map((s) => s.name);
64068
- }
64069
- var init_search = __esm(() => {
64070
- init_registry();
64071
- });
64072
-
64073
64117
  // src/lib/registry.ts
64074
64118
  var exports_registry = {};
64075
64119
  __export(exports_registry, {
@@ -88771,6 +88815,24 @@ function optionPrefix(program2, args) {
88771
88815
  return { indices, separator: -1 };
88772
88816
  }
88773
88817
 
88818
+ // src/cli/stray-arguments.ts
88819
+ function permitStrayArguments(command) {
88820
+ selfRefusing.add(command);
88821
+ return command.allowExcessArguments(true);
88822
+ }
88823
+ function rejectStrayArguments(command) {
88824
+ for (const child2 of command.commands) {
88825
+ if (child2.registeredArguments.length === 0 && !selfRefusing.has(child2)) {
88826
+ child2.allowExcessArguments(false);
88827
+ }
88828
+ rejectStrayArguments(child2);
88829
+ }
88830
+ }
88831
+ var selfRefusing;
88832
+ var init_stray_arguments = __esm(() => {
88833
+ selfRefusing = new WeakSet;
88834
+ });
88835
+
88774
88836
  // src/lib/revision.ts
88775
88837
  import { createHash as createHash33 } from "crypto";
88776
88838
  function revisionIdOf(content) {
@@ -90820,7 +90882,13 @@ function handleReadRefusal(error61) {
90820
90882
  async function handleInfoError(name, options, error61) {
90821
90883
  if (isSkillsFleetCredentialError(error61))
90822
90884
  return handleReadRefusal(error61);
90823
- const notFound = await resolveRemoteNotFound(name, options.remote, error61.message);
90885
+ let remote = options.remote === true;
90886
+ if (!remote) {
90887
+ try {
90888
+ remote = (await requireSkillsReadAccess()).mode === "hosted";
90889
+ } catch {}
90890
+ }
90891
+ const notFound = await resolveRemoteNotFound(name, remote, error61.message);
90824
90892
  if (options.json)
90825
90893
  console.log(JSON.stringify(notFound));
90826
90894
  else
@@ -117271,14 +117339,14 @@ function registerRuntime(parent) {
117271
117339
  const exportsCommand = parent.command("exports").description("Inspect or open skill run exports");
117272
117340
  exportsCommand.command("open").argument("<run-id>", "Run id").option("--json", "Output as JSON", false).description("Open the export directory for a run").action((runId2, options) => handleExportsOpen(runId2, options));
117273
117341
  exportsCommand.command("download").argument("<run-id>", "Remote run id").option("--json", "Output as JSON", false).description("Download remote run artifacts into .skills/exports").action((runId2, options) => handleExportsDownload(runId2, options));
117274
- parent.command("mcp").option("--register <agent>", "Register MCP server with agent").option("--json", "Output registration result as JSON", false).allowExcessArguments(true).description("Start MCP server (stdio) or register with an agent").action(async (options, command) => {
117342
+ permitStrayArguments(parent.command("mcp").option("--register <agent>", "Register MCP server with agent").option("--json", "Output registration result as JSON", false).description("Start MCP server (stdio) or register with an agent").action(async (options, command) => {
117275
117343
  const stray = command.args[0];
117276
117344
  if (stray !== undefined) {
117277
117345
  console.error(source_default.red(`error: unknown argument '${stray}'. 'skills mcp' takes no positional arguments. ` + `Valid forms: 'skills mcp' (start the MCP stdio server), ` + `'skills mcp --register <agent>', 'skills mcp --register all'`));
117278
117346
  process.exit(1);
117279
117347
  }
117280
117348
  await handleMcp(options);
117281
- });
117349
+ }));
117282
117350
  const setup = parent.command("setup").description("Point this CLI at a Skills API server, or register agent integrations").option("--api-url <url>", "Skills API origin to send remote work to").option("--global", "Accepted for compatibility; the API origin is always per-user", false).option("--json", "Output setup result as JSON", false).action(async (options) => handleSetup(options));
117283
117351
  setup.command("agents").option("--json", "Output registration result as JSON", false).description("Register the Skills MCP server with all supported agents").action(async (options) => handleMcp({ register: "all", json: options.json }));
117284
117352
  parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--reviewed-lock <path>", "Install from this private absolute package-lock.json path without resolving dependency ranges").option("--reviewed-lock-sha256 <sha256>", "Exact reviewed SHA-256 of the supplied package-lock.json").option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
@@ -118373,6 +118441,7 @@ var init_runtime = __esm(() => {
118373
118441
  init_execution_secrets();
118374
118442
  init_context();
118375
118443
  init_registry();
118444
+ init_stray_arguments();
118376
118445
  init_skillinfo();
118377
118446
  init_skill_contract();
118378
118447
  init_blog_article();
@@ -134114,6 +134183,7 @@ init_registry();
134114
134183
  init_discovery();
134115
134184
  init_fleet_credentials();
134116
134185
  init_read_access();
134186
+ init_stray_arguments();
134117
134187
  var jsx_dev_runtime8 = __toESM(require_jsx_dev_runtime(), 1);
134118
134188
  var isTTY2 = (process.stdout.isTTY ?? false) && (process.stdin.isTTY ?? false);
134119
134189
  var program2 = new Command;
@@ -134123,7 +134193,7 @@ program2.hook("preAction", () => {
134123
134193
  recordEnvironmentProfile(process.env.HASNA_PROFILE);
134124
134194
  selectCliSkillsCredentialProfile(profile);
134125
134195
  });
134126
- program2.command("interactive", { isDefault: true }).alias("i").allowExcessArguments(true).description("Interactive skill browser (TUI)").action(async (_options, command) => {
134196
+ permitStrayArguments(program2.command("interactive", { isDefault: true }).alias("i").description("Interactive skill browser (TUI)").action(async (_options, command) => {
134127
134197
  const stray = command.args[0];
134128
134198
  if (stray !== undefined) {
134129
134199
  const verbs = program2.commands.map((c) => c.name()).filter((n2) => n2 !== "interactive").sort();
@@ -134150,7 +134220,7 @@ program2.command("interactive", { isDefault: true }).alias("i").allowExcessArgum
134150
134220
  render_default(/* @__PURE__ */ jsx_dev_runtime8.jsxDEV(App2, {
134151
134221
  initialAccess: access
134152
134222
  }, undefined, false, undefined, this));
134153
- });
134223
+ }));
134154
134224
  var { registerInstall: registerInstall2 } = await Promise.resolve().then(() => (init_install(), exports_install));
134155
134225
  registerInstall2(program2);
134156
134226
  var { registerBrowse: registerBrowse2 } = await Promise.resolve().then(() => (init_list(), exports_list));
@@ -134212,6 +134282,7 @@ registerStorage2(program2);
134212
134282
  var { registerRegistryReconcile: registerRegistryReconcile2 } = await Promise.resolve().then(() => (init_registry_reconcile2(), exports_registry_reconcile));
134213
134283
  registerRegistryReconcile2(program2);
134214
134284
  registerEventsCommands(program2, { source: "skills" });
134285
+ rejectStrayArguments(program2);
134215
134286
  var cliArgs = process.argv.slice(2);
134216
134287
  var colorFlag = optionPrefix(program2, cliArgs).indices.find((index) => cliArgs[index] === "--no-color");
134217
134288
  if (colorFlag !== undefined) {
package/bin/mcp.js CHANGED
@@ -23,7 +23,7 @@ var package_default;
23
23
  var init_package = __esm(() => {
24
24
  package_default = {
25
25
  name: "@hasna/skills",
26
- version: "0.10.74",
26
+ version: "0.10.75",
27
27
  description: "Skills library for AI coding agents",
28
28
  type: "module",
29
29
  bin: {
@@ -36452,6 +36452,118 @@ var init_skillinfo = __esm(() => {
36452
36452
  GENERIC_ENV_PATTERN = /\b((?:OPENAI|ANTHROPIC|GEMINI|XAI|ELEVENLABS|DEEPGRAM|REPLICATE|FAL|STABILITY|EXA|FIRECRAWL|TWILIO|SENDGRID|RESEND|SLACK|DISCORD|NOTION|LINEAR|GITHUB|AWS|GOOGLE|CLOUDFLARE|VERCEL|SUPABASE|STRIPE)_[A-Z_]+)\b/g;
36453
36453
  });
36454
36454
 
36455
+ // src/lib/search.ts
36456
+ function editDistance(a, b) {
36457
+ if (a === b)
36458
+ return 0;
36459
+ if (a.length === 0)
36460
+ return b.length;
36461
+ if (b.length === 0)
36462
+ return a.length;
36463
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
36464
+ const curr = new Array(b.length + 1);
36465
+ for (let i = 1;i <= a.length; i++) {
36466
+ curr[0] = i;
36467
+ for (let j = 1;j <= b.length; j++) {
36468
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
36469
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
36470
+ }
36471
+ prev.splice(0, prev.length, ...curr);
36472
+ }
36473
+ return prev[b.length];
36474
+ }
36475
+ function fuzzyMatchScore(word, target) {
36476
+ if (target.includes(word))
36477
+ return 1;
36478
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
36479
+ for (const token of tokens) {
36480
+ if (token.startsWith(word))
36481
+ return 0.6;
36482
+ }
36483
+ if (word.length >= 3) {
36484
+ const maxDist = word.length <= 3 ? 1 : 2;
36485
+ for (const token of tokens) {
36486
+ if (Math.abs(token.length - word.length) <= maxDist) {
36487
+ const dist = editDistance(word, token);
36488
+ if (dist <= maxDist)
36489
+ return 0.4;
36490
+ }
36491
+ }
36492
+ }
36493
+ return 0;
36494
+ }
36495
+ function searchSkills(query, registry2 = loadRegistry()) {
36496
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
36497
+ if (words.length === 0)
36498
+ return [];
36499
+ const scored = [];
36500
+ for (const skill of registry2) {
36501
+ const nameLower = skill.name.toLowerCase();
36502
+ const displayNameLower = skill.displayName.toLowerCase();
36503
+ const descriptionLower = skill.description.toLowerCase();
36504
+ const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
36505
+ const tagsCombined = tagsLower.join(" ");
36506
+ let score = 0;
36507
+ let allWordsMatch = true;
36508
+ for (const word of words) {
36509
+ let wordScore = 0;
36510
+ const nameMatch = fuzzyMatchScore(word, nameLower);
36511
+ if (nameMatch > 0)
36512
+ wordScore += 10 * nameMatch;
36513
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
36514
+ if (displayMatch > 0)
36515
+ wordScore += 7 * displayMatch;
36516
+ const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
36517
+ if (tagMatch > 0)
36518
+ wordScore += 5 * tagMatch;
36519
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
36520
+ if (descMatch > 0)
36521
+ wordScore += 2 * descMatch;
36522
+ if (wordScore === 0) {
36523
+ allWordsMatch = false;
36524
+ break;
36525
+ }
36526
+ score += wordScore;
36527
+ }
36528
+ if (allWordsMatch && score > 0) {
36529
+ scored.push({ skill, score });
36530
+ }
36531
+ }
36532
+ scored.sort((a, b) => b.score - a.score);
36533
+ return scored.map((s) => s.skill);
36534
+ }
36535
+ function findCloseSkillNames(query, names, maxResults = 3) {
36536
+ if (maxResults <= 0)
36537
+ return [];
36538
+ const q = query.trim().toLowerCase();
36539
+ if (q.length === 0)
36540
+ return [];
36541
+ const tokens = q.split("-").filter(Boolean);
36542
+ const ranked = [];
36543
+ if (q.length >= 3) {
36544
+ for (const name of names) {
36545
+ const candidate = name.toLowerCase();
36546
+ const rank = candidate === q ? 0 : candidate.endsWith(`-${q}`) || candidate.startsWith(`${q}-`) ? 1 : tokens.length > 1 && tokens.every((token) => candidate.split("-").includes(token)) ? 2 : undefined;
36547
+ if (rank !== undefined)
36548
+ ranked.push([name, rank]);
36549
+ }
36550
+ }
36551
+ ranked.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || a[0].localeCompare(b[0]));
36552
+ const found = ranked.slice(0, maxResults).map(([name]) => name);
36553
+ if (found.length === maxResults)
36554
+ return found;
36555
+ const taken = new Set(found);
36556
+ const threshold = Math.max(3, Math.floor(q.length / 2));
36557
+ const byDistance = names.filter((name) => !taken.has(name)).map((name) => ({ name, distance: editDistance(q, name.toLowerCase()) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
36558
+ return [...found, ...byDistance.slice(0, maxResults - found.length).map((entry) => entry.name)];
36559
+ }
36560
+ function findSimilarSkills(query, maxResults = 3, registry2 = loadRegistry()) {
36561
+ return findCloseSkillNames(query, registry2.map((skill) => skill.name), maxResults);
36562
+ }
36563
+ var init_search = __esm(() => {
36564
+ init_registry();
36565
+ });
36566
+
36455
36567
  // src/lib/selected-document.ts
36456
36568
  function readSelectedDocument(entries, file2) {
36457
36569
  const entry = file2 !== undefined ? entries.find((candidate) => candidate.path === file2) : DOCUMENT_PATHS.map((path) => entries.find((candidate) => candidate.path === path && candidate.bytes.length > 0)).find(Boolean);
@@ -36532,11 +36644,30 @@ function exactProfileSelection(spec, profile) {
36532
36644
  const at = spec.lastIndexOf("@");
36533
36645
  const slug = at > 0 ? spec.slice(0, at) : spec;
36534
36646
  const version2 = at > 0 ? spec.slice(at + 1) : undefined;
36535
- const selection = profile.selections.find((entry) => (entry.slug === slug || entry.aliases?.includes(slug)) && (version2 === undefined || version2 === entry.version));
36536
- if (!selection || version2 === "")
36537
- throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
36647
+ const named = profile.selections.filter((entry) => entry.slug === slug || entry.aliases?.includes(slug));
36648
+ if (version2 === "")
36649
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", emptyVersionRefusal(spec));
36650
+ if (named.length === 0)
36651
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", unknownNameRefusal(slug, profile));
36652
+ const selection = version2 === undefined ? named[0] : named.find((entry) => entry.version === version2);
36653
+ if (!selection)
36654
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", wrongVersionRefusal(slug, version2, named));
36538
36655
  return selection;
36539
36656
  }
36657
+ function emptyVersionRefusal(spec) {
36658
+ return `The requested name '${spec}' has an empty version. ${ACCEPTED_FORM}`;
36659
+ }
36660
+ function wrongVersionRefusal(slug, version2, named) {
36661
+ const versions2 = [...new Set(named.map((entry) => entry.version))].sort();
36662
+ const canonical9 = named[0].slug;
36663
+ return `Skill '${slug}' is selected at version '${versions2.join("', '")}', but '${version2}' was requested. ` + `Accepted form: '${canonical9}@${versions2[0]}'.`;
36664
+ }
36665
+ function unknownNameRefusal(slug, profile) {
36666
+ const names = profile.selections.flatMap((entry) => [entry.slug, ...entry.aliases ?? []]);
36667
+ const close = findCloseSkillNames(slug, names);
36668
+ const hint = close.length > 0 ? ` Did you mean: ${close.join(", ")}?` : "";
36669
+ return `Skill '${slug}' is not selected by this Skills profile or project lock.${hint} ` + `Read the accepted names with 'skills search <term>' or 'skills list --all --json'; accept one with 'skills pin <name>'. ${ACCEPTED_FORM}`;
36670
+ }
36540
36671
  async function loadSelectedSkill(spec, profileId, options = {}) {
36541
36672
  const context = await resolveSelectionContext(profileId, options);
36542
36673
  const selection = exactProfileSelection(spec, context.receipt.profile);
@@ -36581,9 +36712,11 @@ async function selectedSkillRequirements(spec, profileId, options = {}) {
36581
36712
  }
36582
36713
  return { ...getSkillRequirementsFromContent(selection.slug, texts, dependencies), selection };
36583
36714
  }
36715
+ var ACCEPTED_FORM = "Accepted form: '<slug>' or '<slug>@<version>'.";
36584
36716
  var init_selection_resolver = __esm(() => {
36585
36717
  init_profile_client();
36586
36718
  init_skillinfo();
36719
+ init_search();
36587
36720
  init_selected_document();
36588
36721
  init_selection_cache();
36589
36722
  });
@@ -38615,95 +38748,6 @@ var init_registry_types = __esm(() => {
38615
38748
  ];
38616
38749
  });
38617
38750
 
38618
- // src/lib/search.ts
38619
- function editDistance(a, b) {
38620
- if (a === b)
38621
- return 0;
38622
- if (a.length === 0)
38623
- return b.length;
38624
- if (b.length === 0)
38625
- return a.length;
38626
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
38627
- const curr = new Array(b.length + 1);
38628
- for (let i = 1;i <= a.length; i++) {
38629
- curr[0] = i;
38630
- for (let j = 1;j <= b.length; j++) {
38631
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
38632
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
38633
- }
38634
- prev.splice(0, prev.length, ...curr);
38635
- }
38636
- return prev[b.length];
38637
- }
38638
- function fuzzyMatchScore(word, target) {
38639
- if (target.includes(word))
38640
- return 1;
38641
- const tokens = target.split(/[\s\-_]+/).filter(Boolean);
38642
- for (const token of tokens) {
38643
- if (token.startsWith(word))
38644
- return 0.6;
38645
- }
38646
- if (word.length >= 3) {
38647
- const maxDist = word.length <= 3 ? 1 : 2;
38648
- for (const token of tokens) {
38649
- if (Math.abs(token.length - word.length) <= maxDist) {
38650
- const dist = editDistance(word, token);
38651
- if (dist <= maxDist)
38652
- return 0.4;
38653
- }
38654
- }
38655
- }
38656
- return 0;
38657
- }
38658
- function searchSkills(query, registry2 = loadRegistry()) {
38659
- const words = query.toLowerCase().split(/\s+/).filter(Boolean);
38660
- if (words.length === 0)
38661
- return [];
38662
- const scored = [];
38663
- for (const skill of registry2) {
38664
- const nameLower = skill.name.toLowerCase();
38665
- const displayNameLower = skill.displayName.toLowerCase();
38666
- const descriptionLower = skill.description.toLowerCase();
38667
- const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
38668
- const tagsCombined = tagsLower.join(" ");
38669
- let score = 0;
38670
- let allWordsMatch = true;
38671
- for (const word of words) {
38672
- let wordScore = 0;
38673
- const nameMatch = fuzzyMatchScore(word, nameLower);
38674
- if (nameMatch > 0)
38675
- wordScore += 10 * nameMatch;
38676
- const displayMatch = fuzzyMatchScore(word, displayNameLower);
38677
- if (displayMatch > 0)
38678
- wordScore += 7 * displayMatch;
38679
- const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
38680
- if (tagMatch > 0)
38681
- wordScore += 5 * tagMatch;
38682
- const descMatch = fuzzyMatchScore(word, descriptionLower);
38683
- if (descMatch > 0)
38684
- wordScore += 2 * descMatch;
38685
- if (wordScore === 0) {
38686
- allWordsMatch = false;
38687
- break;
38688
- }
38689
- score += wordScore;
38690
- }
38691
- if (allWordsMatch && score > 0) {
38692
- scored.push({ skill, score });
38693
- }
38694
- }
38695
- scored.sort((a, b) => b.score - a.score);
38696
- return scored.map((s) => s.skill);
38697
- }
38698
- function findSimilarSkills(query, maxResults = 3, registry2 = loadRegistry()) {
38699
- const q = query.toLowerCase();
38700
- const scored = registry2.map((s) => ({ name: s.name, dist: editDistance(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
38701
- return scored.slice(0, maxResults).map((s) => s.name);
38702
- }
38703
- var init_search = __esm(() => {
38704
- init_registry();
38705
- });
38706
-
38707
38751
  // src/lib/registry.ts
38708
38752
  import { existsSync as existsSync13, readFileSync as readFileSync18, readdirSync as readdirSync10 } from "fs";
38709
38753
  import { join as join33 } from "path";
package/bin/migrate.js CHANGED
@@ -25,7 +25,7 @@ import { join as join5 } from "path";
25
25
  // package.json
26
26
  var package_default = {
27
27
  name: "@hasna/skills",
28
- version: "0.10.74",
28
+ version: "0.10.75",
29
29
  description: "Skills library for AI coding agents",
30
30
  type: "module",
31
31
  bin: {
package/bin/server.js CHANGED
@@ -24140,7 +24140,7 @@ restoreLaunchCwd();
24140
24140
  // package.json
24141
24141
  var package_default = {
24142
24142
  name: "@hasna/skills",
24143
- version: "0.10.74",
24143
+ version: "0.10.75",
24144
24144
  description: "Skills library for AI coding agents",
24145
24145
  type: "module",
24146
24146
  bin: {
package/bin/worker.js CHANGED
@@ -24143,7 +24143,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24143
24143
  // package.json
24144
24144
  var package_default = {
24145
24145
  name: "@hasna/skills",
24146
- version: "0.10.74",
24146
+ version: "0.10.75",
24147
24147
  description: "Skills library for AI coding agents",
24148
24148
  type: "module",
24149
24149
  bin: {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Stray positionals on a verb that declares none are a phantom verb, not a
3
+ * no-op. Commander's `allowExcessArguments` defaults to true (12.1.0,
4
+ * `lib/command.js:28`), so `skills categories typo` exited 0 with the ordinary
5
+ * output and `skills list typo` printed the whole catalogue: a caller reading
6
+ * the exit code is told the word was understood (BUG e3997558).
7
+ *
8
+ * The default command refuses by hand so it can name the verbs that exist, and
9
+ * `skills mcp` refuses by hand so it can name its three forms. Those two keep
10
+ * their own, better sentences. Every other command that declares no argument of
11
+ * its own is closed here in one pass, so the next command added gets the
12
+ * refusal without its author having to remember the hole exists.
13
+ */
14
+ import type { Command } from "commander";
15
+ /**
16
+ * Opt a command out of the pass because it owns a more precise refusal than
17
+ * commander's. Chainable, and it keeps `allowExcessArguments(true)` so the
18
+ * command still receives the stray word it wants to report.
19
+ */
20
+ export declare function permitStrayArguments(command: Command): Command;
21
+ /**
22
+ * Refuse positionals on every command that declares no argument of its own.
23
+ *
24
+ * A command that declares arguments keeps commander's own arity check, and a
25
+ * command with subcommands never reaches the excess check for a word that names
26
+ * a child (commander dispatches the subcommand first). Commands without an
27
+ * action handler reach it only with zero operands, so the flag is inert there.
28
+ */
29
+ export declare function rejectStrayArguments(command: Command): void;
package/dist/index.js CHANGED
@@ -15144,7 +15144,7 @@ import { basename as basename13, dirname as dirname24, isAbsolute as isAbsolute2
15144
15144
  import { homedir as homedir5 } from "os";
15145
15145
  import { isAbsolute as isAbsolute25, join as join36, resolve as resolve24 } from "path";
15146
15146
  // package.json
15147
- var version = "0.10.74";
15147
+ var version = "0.10.75";
15148
15148
  var package_default = {
15149
15149
  name: "@hasna/skills",
15150
15150
  version,
@@ -22727,6 +22727,115 @@ function readIfExists(path) {
22727
22727
  return null;
22728
22728
  }
22729
22729
 
22730
+ // src/lib/search.ts
22731
+ function editDistance(a, b) {
22732
+ if (a === b)
22733
+ return 0;
22734
+ if (a.length === 0)
22735
+ return b.length;
22736
+ if (b.length === 0)
22737
+ return a.length;
22738
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
22739
+ const curr = new Array(b.length + 1);
22740
+ for (let i = 1;i <= a.length; i++) {
22741
+ curr[0] = i;
22742
+ for (let j = 1;j <= b.length; j++) {
22743
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
22744
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
22745
+ }
22746
+ prev.splice(0, prev.length, ...curr);
22747
+ }
22748
+ return prev[b.length];
22749
+ }
22750
+ function fuzzyMatchScore(word, target) {
22751
+ if (target.includes(word))
22752
+ return 1;
22753
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
22754
+ for (const token of tokens) {
22755
+ if (token.startsWith(word))
22756
+ return 0.6;
22757
+ }
22758
+ if (word.length >= 3) {
22759
+ const maxDist = word.length <= 3 ? 1 : 2;
22760
+ for (const token of tokens) {
22761
+ if (Math.abs(token.length - word.length) <= maxDist) {
22762
+ const dist = editDistance(word, token);
22763
+ if (dist <= maxDist)
22764
+ return 0.4;
22765
+ }
22766
+ }
22767
+ }
22768
+ return 0;
22769
+ }
22770
+ function searchSkills(query, registry = loadRegistry()) {
22771
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
22772
+ if (words.length === 0)
22773
+ return [];
22774
+ const scored = [];
22775
+ for (const skill of registry) {
22776
+ const nameLower = skill.name.toLowerCase();
22777
+ const displayNameLower = skill.displayName.toLowerCase();
22778
+ const descriptionLower = skill.description.toLowerCase();
22779
+ const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
22780
+ const tagsCombined = tagsLower.join(" ");
22781
+ let score = 0;
22782
+ let allWordsMatch = true;
22783
+ for (const word of words) {
22784
+ let wordScore = 0;
22785
+ const nameMatch = fuzzyMatchScore(word, nameLower);
22786
+ if (nameMatch > 0)
22787
+ wordScore += 10 * nameMatch;
22788
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
22789
+ if (displayMatch > 0)
22790
+ wordScore += 7 * displayMatch;
22791
+ const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
22792
+ if (tagMatch > 0)
22793
+ wordScore += 5 * tagMatch;
22794
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
22795
+ if (descMatch > 0)
22796
+ wordScore += 2 * descMatch;
22797
+ if (wordScore === 0) {
22798
+ allWordsMatch = false;
22799
+ break;
22800
+ }
22801
+ score += wordScore;
22802
+ }
22803
+ if (allWordsMatch && score > 0) {
22804
+ scored.push({ skill, score });
22805
+ }
22806
+ }
22807
+ scored.sort((a, b) => b.score - a.score);
22808
+ return scored.map((s) => s.skill);
22809
+ }
22810
+ function findCloseSkillNames(query, names, maxResults = 3) {
22811
+ if (maxResults <= 0)
22812
+ return [];
22813
+ const q = query.trim().toLowerCase();
22814
+ if (q.length === 0)
22815
+ return [];
22816
+ const tokens = q.split("-").filter(Boolean);
22817
+ const ranked = [];
22818
+ if (q.length >= 3) {
22819
+ for (const name of names) {
22820
+ const candidate = name.toLowerCase();
22821
+ const rank = candidate === q ? 0 : candidate.endsWith(`-${q}`) || candidate.startsWith(`${q}-`) ? 1 : tokens.length > 1 && tokens.every((token) => candidate.split("-").includes(token)) ? 2 : undefined;
22822
+ if (rank !== undefined)
22823
+ ranked.push([name, rank]);
22824
+ }
22825
+ }
22826
+ ranked.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || a[0].localeCompare(b[0]));
22827
+ const found = ranked.slice(0, maxResults).map(([name]) => name);
22828
+ if (found.length === maxResults)
22829
+ return found;
22830
+ const taken = new Set(found);
22831
+ const threshold = Math.max(3, Math.floor(q.length / 2));
22832
+ const byDistance = names.filter((name) => !taken.has(name)).map((name) => ({ name, distance: editDistance(q, name.toLowerCase()) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
22833
+ return [...found, ...byDistance.slice(0, maxResults - found.length).map((entry) => entry.name)];
22834
+ }
22835
+ function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
22836
+ return findCloseSkillNames(query, registry.map((skill) => skill.name), maxResults);
22837
+ }
22838
+
22730
22839
  // src/lib/selected-document.ts
22731
22840
  var DOCUMENT_PATHS = ["SKILL.md", "README.md", "CLAUDE.md"];
22732
22841
  function readSelectedDocument(entries, file) {
@@ -22843,11 +22952,31 @@ function exactProfileSelection(spec, profile) {
22843
22952
  const at = spec.lastIndexOf("@");
22844
22953
  const slug = at > 0 ? spec.slice(0, at) : spec;
22845
22954
  const version2 = at > 0 ? spec.slice(at + 1) : undefined;
22846
- const selection = profile.selections.find((entry) => (entry.slug === slug || entry.aliases?.includes(slug)) && (version2 === undefined || version2 === entry.version));
22847
- if (!selection || version2 === "")
22848
- throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
22955
+ const named = profile.selections.filter((entry) => entry.slug === slug || entry.aliases?.includes(slug));
22956
+ if (version2 === "")
22957
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", emptyVersionRefusal(spec));
22958
+ if (named.length === 0)
22959
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", unknownNameRefusal(slug, profile));
22960
+ const selection = version2 === undefined ? named[0] : named.find((entry) => entry.version === version2);
22961
+ if (!selection)
22962
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", wrongVersionRefusal(slug, version2, named));
22849
22963
  return selection;
22850
22964
  }
22965
+ var ACCEPTED_FORM = "Accepted form: '<slug>' or '<slug>@<version>'.";
22966
+ function emptyVersionRefusal(spec) {
22967
+ return `The requested name '${spec}' has an empty version. ${ACCEPTED_FORM}`;
22968
+ }
22969
+ function wrongVersionRefusal(slug, version2, named) {
22970
+ const versions = [...new Set(named.map((entry) => entry.version))].sort();
22971
+ const canonical9 = named[0].slug;
22972
+ return `Skill '${slug}' is selected at version '${versions.join("', '")}', but '${version2}' was requested. ` + `Accepted form: '${canonical9}@${versions[0]}'.`;
22973
+ }
22974
+ function unknownNameRefusal(slug, profile) {
22975
+ const names = profile.selections.flatMap((entry) => [entry.slug, ...entry.aliases ?? []]);
22976
+ const close = findCloseSkillNames(slug, names);
22977
+ const hint = close.length > 0 ? ` Did you mean: ${close.join(", ")}?` : "";
22978
+ return `Skill '${slug}' is not selected by this Skills profile or project lock.${hint} ` + `Read the accepted names with 'skills search <term>' or 'skills list --all --json'; accept one with 'skills pin <name>'. ${ACCEPTED_FORM}`;
22979
+ }
22851
22980
 
22852
22981
  // src/lib/selected-manifest.ts
22853
22982
  function jsonEntry(entries, path) {
@@ -25354,92 +25483,6 @@ var CATEGORIES = [
25354
25483
  "Event Management"
25355
25484
  ];
25356
25485
  var BASIC_SKILL_NAMES = [];
25357
-
25358
- // src/lib/search.ts
25359
- function editDistance(a, b) {
25360
- if (a === b)
25361
- return 0;
25362
- if (a.length === 0)
25363
- return b.length;
25364
- if (b.length === 0)
25365
- return a.length;
25366
- const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
25367
- const curr = new Array(b.length + 1);
25368
- for (let i = 1;i <= a.length; i++) {
25369
- curr[0] = i;
25370
- for (let j = 1;j <= b.length; j++) {
25371
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
25372
- curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
25373
- }
25374
- prev.splice(0, prev.length, ...curr);
25375
- }
25376
- return prev[b.length];
25377
- }
25378
- function fuzzyMatchScore(word, target) {
25379
- if (target.includes(word))
25380
- return 1;
25381
- const tokens = target.split(/[\s\-_]+/).filter(Boolean);
25382
- for (const token of tokens) {
25383
- if (token.startsWith(word))
25384
- return 0.6;
25385
- }
25386
- if (word.length >= 3) {
25387
- const maxDist = word.length <= 3 ? 1 : 2;
25388
- for (const token of tokens) {
25389
- if (Math.abs(token.length - word.length) <= maxDist) {
25390
- const dist = editDistance(word, token);
25391
- if (dist <= maxDist)
25392
- return 0.4;
25393
- }
25394
- }
25395
- }
25396
- return 0;
25397
- }
25398
- function searchSkills(query, registry = loadRegistry()) {
25399
- const words = query.toLowerCase().split(/\s+/).filter(Boolean);
25400
- if (words.length === 0)
25401
- return [];
25402
- const scored = [];
25403
- for (const skill of registry) {
25404
- const nameLower = skill.name.toLowerCase();
25405
- const displayNameLower = skill.displayName.toLowerCase();
25406
- const descriptionLower = skill.description.toLowerCase();
25407
- const tagsLower = skill.tags.map((t2) => t2.toLowerCase());
25408
- const tagsCombined = tagsLower.join(" ");
25409
- let score = 0;
25410
- let allWordsMatch = true;
25411
- for (const word of words) {
25412
- let wordScore = 0;
25413
- const nameMatch = fuzzyMatchScore(word, nameLower);
25414
- if (nameMatch > 0)
25415
- wordScore += 10 * nameMatch;
25416
- const displayMatch = fuzzyMatchScore(word, displayNameLower);
25417
- if (displayMatch > 0)
25418
- wordScore += 7 * displayMatch;
25419
- const tagMatch = Math.max(...tagsLower.map((t2) => fuzzyMatchScore(word, t2)), fuzzyMatchScore(word, tagsCombined));
25420
- if (tagMatch > 0)
25421
- wordScore += 5 * tagMatch;
25422
- const descMatch = fuzzyMatchScore(word, descriptionLower);
25423
- if (descMatch > 0)
25424
- wordScore += 2 * descMatch;
25425
- if (wordScore === 0) {
25426
- allWordsMatch = false;
25427
- break;
25428
- }
25429
- score += wordScore;
25430
- }
25431
- if (allWordsMatch && score > 0) {
25432
- scored.push({ skill, score });
25433
- }
25434
- }
25435
- scored.sort((a, b) => b.score - a.score);
25436
- return scored.map((s) => s.skill);
25437
- }
25438
- function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
25439
- const q = query.toLowerCase();
25440
- const scored = registry.map((s) => ({ name: s.name, dist: editDistance(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
25441
- return scored.slice(0, maxResults).map((s) => s.name);
25442
- }
25443
25486
  // src/lib/registry.ts
25444
25487
  function isBasicSkillName(name) {
25445
25488
  return BASIC_SKILL_NAMES.includes(name);
@@ -11,6 +11,24 @@ export declare function editDistance(a: string, b: string): number;
11
11
  * Search skills by name, description, and tags using fuzzy matching.
12
12
  */
13
13
  export declare function searchSkills(query: string, registry?: SkillMeta[]): SkillMeta[];
14
+ /**
15
+ * Bounded close canonical names for a refused name, strongest evidence first:
16
+ *
17
+ * 0. the exact name;
18
+ * 1. a hyphen-delimited containment — the candidate owns the query as a whole
19
+ * leading or trailing segment (`fleet-refresh` → `instructions-fleet-refresh`);
20
+ * 2. a multi-word token subset of the candidate;
21
+ * 3. the edit-distance rule that was the only signal before.
22
+ *
23
+ * Containment is what edit distance cannot see, and it is the case that cost
24
+ * real time: the canonical name that owns a shorter name as its trailing
25
+ * segment is 13 edits from the short name, so a caller naming the procedure by
26
+ * its last words was told the procedure did not exist (Agents Audit A27). The
27
+ * distance pass still runs and fills any slots containment leaves, so a
28
+ * distance-only miss — including every query under three characters — keeps the
29
+ * answer it has always had.
30
+ */
31
+ export declare function findCloseSkillNames(query: string, names: readonly string[], maxResults?: number): string[];
14
32
  /**
15
33
  * Find skills with names similar to the given query (for "did you mean?" suggestions).
16
34
  */
@@ -49,7 +49,7 @@ var __require = import.meta.require;
49
49
  // src/runtime-prerequisites.ts
50
50
  import { isAbsolute as isAbsolute7 } from "path";
51
51
  // package.json
52
- var version = "0.10.74";
52
+ var version = "0.10.75";
53
53
  var package_default = {
54
54
  name: "@hasna/skills",
55
55
  version,
package/dist/sdk/index.js CHANGED
@@ -32371,7 +32371,7 @@ var init_event_sink = __esm(() => {
32371
32371
  init_dist();
32372
32372
  });
32373
32373
  // package.json
32374
- var version = "0.10.74";
32374
+ var version = "0.10.75";
32375
32375
  var package_default = {
32376
32376
  name: "@hasna/skills",
32377
32377
  version,
@@ -51913,10 +51913,33 @@ function searchSkills(query, registry = loadRegistry()) {
51913
51913
  scored.sort((a, b) => b.score - a.score);
51914
51914
  return scored.map((s) => s.skill);
51915
51915
  }
51916
+ function findCloseSkillNames(query, names, maxResults = 3) {
51917
+ if (maxResults <= 0)
51918
+ return [];
51919
+ const q = query.trim().toLowerCase();
51920
+ if (q.length === 0)
51921
+ return [];
51922
+ const tokens = q.split("-").filter(Boolean);
51923
+ const ranked = [];
51924
+ if (q.length >= 3) {
51925
+ for (const name of names) {
51926
+ const candidate2 = name.toLowerCase();
51927
+ const rank = candidate2 === q ? 0 : candidate2.endsWith(`-${q}`) || candidate2.startsWith(`${q}-`) ? 1 : tokens.length > 1 && tokens.every((token) => candidate2.split("-").includes(token)) ? 2 : undefined;
51928
+ if (rank !== undefined)
51929
+ ranked.push([name, rank]);
51930
+ }
51931
+ }
51932
+ ranked.sort((a, b) => a[1] - b[1] || a[0].length - b[0].length || a[0].localeCompare(b[0]));
51933
+ const found = ranked.slice(0, maxResults).map(([name]) => name);
51934
+ if (found.length === maxResults)
51935
+ return found;
51936
+ const taken = new Set(found);
51937
+ const threshold = Math.max(3, Math.floor(q.length / 2));
51938
+ const byDistance = names.filter((name) => !taken.has(name)).map((name) => ({ name, distance: editDistance(q, name.toLowerCase()) })).filter((entry) => entry.distance <= threshold).sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name));
51939
+ return [...found, ...byDistance.slice(0, maxResults - found.length).map((entry) => entry.name)];
51940
+ }
51916
51941
  function findSimilarSkills(query, maxResults = 3, registry = loadRegistry()) {
51917
- const q = query.toLowerCase();
51918
- const scored = registry.map((s) => ({ name: s.name, dist: editDistance(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
51919
- return scored.slice(0, maxResults).map((s) => s.name);
51942
+ return findCloseSkillNames(query, registry.map((skill) => skill.name), maxResults);
51920
51943
  }
51921
51944
  // src/lib/registry.ts
51922
51945
  function isBasicSkillName(name) {
@@ -52937,11 +52960,31 @@ function exactProfileSelection(spec, profile) {
52937
52960
  const at = spec.lastIndexOf("@");
52938
52961
  const slug = at > 0 ? spec.slice(0, at) : spec;
52939
52962
  const version2 = at > 0 ? spec.slice(at + 1) : undefined;
52940
- const selection2 = profile.selections.find((entry) => (entry.slug === slug || entry.aliases?.includes(slug)) && (version2 === undefined || version2 === entry.version));
52941
- if (!selection2 || version2 === "")
52942
- throw new SkillSelectionError("SKILL_NOT_SELECTED", "The requested exact skill version is not selected by this profile or project lock.");
52963
+ const named = profile.selections.filter((entry) => entry.slug === slug || entry.aliases?.includes(slug));
52964
+ if (version2 === "")
52965
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", emptyVersionRefusal(spec));
52966
+ if (named.length === 0)
52967
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", unknownNameRefusal(slug, profile));
52968
+ const selection2 = version2 === undefined ? named[0] : named.find((entry) => entry.version === version2);
52969
+ if (!selection2)
52970
+ throw new SkillSelectionError("SKILL_NOT_SELECTED", wrongVersionRefusal(slug, version2, named));
52943
52971
  return selection2;
52944
52972
  }
52973
+ var ACCEPTED_FORM = "Accepted form: '<slug>' or '<slug>@<version>'.";
52974
+ function emptyVersionRefusal(spec) {
52975
+ return `The requested name '${spec}' has an empty version. ${ACCEPTED_FORM}`;
52976
+ }
52977
+ function wrongVersionRefusal(slug, version2, named) {
52978
+ const versions = [...new Set(named.map((entry) => entry.version))].sort();
52979
+ const canonical9 = named[0].slug;
52980
+ return `Skill '${slug}' is selected at version '${versions.join("', '")}', but '${version2}' was requested. ` + `Accepted form: '${canonical9}@${versions[0]}'.`;
52981
+ }
52982
+ function unknownNameRefusal(slug, profile) {
52983
+ const names = profile.selections.flatMap((entry) => [entry.slug, ...entry.aliases ?? []]);
52984
+ const close = findCloseSkillNames(slug, names);
52985
+ const hint = close.length > 0 ? ` Did you mean: ${close.join(", ")}?` : "";
52986
+ return `Skill '${slug}' is not selected by this Skills profile or project lock.${hint} ` + `Read the accepted names with 'skills search <term>' or 'skills list --all --json'; accept one with 'skills pin <name>'. ${ACCEPTED_FORM}`;
52987
+ }
52945
52988
 
52946
52989
  // src/lib/selected-run.ts
52947
52990
  async function resolveSelectedRun(spec, profileId, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.74",
3
+ "version": "0.10.75",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {