@hasna/skills 0.1.5 → 0.1.6

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
@@ -1878,7 +1878,7 @@ var package_default;
1878
1878
  var init_package = __esm(() => {
1879
1879
  package_default = {
1880
1880
  name: "@hasna/skills",
1881
- version: "0.1.5",
1881
+ version: "0.1.6",
1882
1882
  description: "Skills library for AI coding agents",
1883
1883
  type: "module",
1884
1884
  bin: {
@@ -1957,6 +1957,45 @@ var init_package = __esm(() => {
1957
1957
  function getSkillsByCategory(category) {
1958
1958
  return SKILLS.filter((s) => s.category === category);
1959
1959
  }
1960
+ function editDistance(a, b) {
1961
+ if (a === b)
1962
+ return 0;
1963
+ if (a.length === 0)
1964
+ return b.length;
1965
+ if (b.length === 0)
1966
+ return a.length;
1967
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
1968
+ const curr = new Array(b.length + 1);
1969
+ for (let i = 1;i <= a.length; i++) {
1970
+ curr[0] = i;
1971
+ for (let j = 1;j <= b.length; j++) {
1972
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1973
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1974
+ }
1975
+ prev.splice(0, prev.length, ...curr);
1976
+ }
1977
+ return prev[b.length];
1978
+ }
1979
+ function fuzzyMatchScore(word, target) {
1980
+ if (target.includes(word))
1981
+ return 1;
1982
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
1983
+ for (const token of tokens) {
1984
+ if (token.startsWith(word))
1985
+ return 0.6;
1986
+ }
1987
+ if (word.length >= 3) {
1988
+ const maxDist = word.length <= 3 ? 1 : 2;
1989
+ for (const token of tokens) {
1990
+ if (Math.abs(token.length - word.length) <= maxDist) {
1991
+ const dist = editDistance(word, token);
1992
+ if (dist <= maxDist)
1993
+ return 0.4;
1994
+ }
1995
+ }
1996
+ }
1997
+ return 0;
1998
+ }
1960
1999
  function searchSkills(query) {
1961
2000
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
1962
2001
  if (words.length === 0)
@@ -1967,30 +2006,28 @@ function searchSkills(query) {
1967
2006
  const displayNameLower = skill.displayName.toLowerCase();
1968
2007
  const descriptionLower = skill.description.toLowerCase();
1969
2008
  const tagsLower = skill.tags.map((t) => t.toLowerCase());
2009
+ const tagsCombined = tagsLower.join(" ");
1970
2010
  let score = 0;
1971
2011
  let allWordsMatch = true;
1972
2012
  for (const word of words) {
1973
- let wordMatched = false;
1974
- if (nameLower.includes(word)) {
1975
- score += 10;
1976
- wordMatched = true;
1977
- }
1978
- if (displayNameLower.includes(word)) {
1979
- score += 7;
1980
- wordMatched = true;
1981
- }
1982
- if (tagsLower.some((t) => t.includes(word))) {
1983
- score += 5;
1984
- wordMatched = true;
1985
- }
1986
- if (descriptionLower.includes(word)) {
1987
- score += 2;
1988
- wordMatched = true;
1989
- }
1990
- if (!wordMatched) {
2013
+ let wordScore = 0;
2014
+ const nameMatch = fuzzyMatchScore(word, nameLower);
2015
+ if (nameMatch > 0)
2016
+ wordScore += 10 * nameMatch;
2017
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
2018
+ if (displayMatch > 0)
2019
+ wordScore += 7 * displayMatch;
2020
+ const tagMatch = Math.max(...tagsLower.map((t) => fuzzyMatchScore(word, t)), fuzzyMatchScore(word, tagsCombined));
2021
+ if (tagMatch > 0)
2022
+ wordScore += 5 * tagMatch;
2023
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
2024
+ if (descMatch > 0)
2025
+ wordScore += 2 * descMatch;
2026
+ if (wordScore === 0) {
1991
2027
  allWordsMatch = false;
1992
2028
  break;
1993
2029
  }
2030
+ score += wordScore;
1994
2031
  }
1995
2032
  if (allWordsMatch && score > 0) {
1996
2033
  scored.push({ skill, score });
@@ -5395,6 +5432,93 @@ async function runSkill(name, args, options = {}) {
5395
5432
  const exitCode = await proc.exited;
5396
5433
  return { exitCode };
5397
5434
  }
5435
+ function detectProjectSkills(cwd = process.cwd()) {
5436
+ const pkgPath = join2(cwd, "package.json");
5437
+ if (!existsSync2(pkgPath)) {
5438
+ const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5439
+ const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5440
+ return { detected: [], recommended: recommended2 };
5441
+ }
5442
+ let pkg;
5443
+ try {
5444
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5445
+ } catch {
5446
+ const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5447
+ const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5448
+ return { detected: [], recommended: recommended2 };
5449
+ }
5450
+ const allDeps = {
5451
+ ...pkg.dependencies,
5452
+ ...pkg.devDependencies
5453
+ };
5454
+ const depNames = Object.keys(allDeps);
5455
+ const detected = [];
5456
+ const recommendedNames = new Set;
5457
+ for (const name of ["implementation-plan", "write", "deepresearch"]) {
5458
+ recommendedNames.add(name);
5459
+ }
5460
+ const frontendDeps = ["next", "react", "vue", "svelte", "nuxt", "@nuxtjs/nuxt"];
5461
+ for (const dep of frontendDeps) {
5462
+ if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
5463
+ detected.push(dep);
5464
+ for (const name of ["image", "generate-favicon", "seo-brief-builder"]) {
5465
+ recommendedNames.add(name);
5466
+ }
5467
+ break;
5468
+ }
5469
+ }
5470
+ const backendDeps = ["express", "fastify", "hono", "koa", "@hono/hono"];
5471
+ for (const dep of backendDeps) {
5472
+ if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
5473
+ detected.push(dep);
5474
+ for (const name of ["api-test-suite", "apidocs"]) {
5475
+ recommendedNames.add(name);
5476
+ }
5477
+ break;
5478
+ }
5479
+ }
5480
+ const aiDeps = ["@anthropic-ai/sdk", "openai", "@openai/openai", "anthropic"];
5481
+ for (const dep of aiDeps) {
5482
+ if (depNames.includes(dep)) {
5483
+ detected.push(dep);
5484
+ for (const name of ["deepresearch", "webcrawling"]) {
5485
+ recommendedNames.add(name);
5486
+ }
5487
+ break;
5488
+ }
5489
+ }
5490
+ if (depNames.includes("stripe")) {
5491
+ detected.push("stripe");
5492
+ recommendedNames.add("invoice");
5493
+ }
5494
+ const emailDeps = ["nodemailer", "@sendgrid/mail", "@sendgrid/client"];
5495
+ for (const dep of emailDeps) {
5496
+ if (depNames.includes(dep)) {
5497
+ detected.push(dep);
5498
+ for (const name of ["gmail", "email-campaign"]) {
5499
+ recommendedNames.add(name);
5500
+ }
5501
+ break;
5502
+ }
5503
+ }
5504
+ const testDeps = ["vitest", "jest", "mocha", "@jest/core"];
5505
+ for (const dep of testDeps) {
5506
+ if (depNames.includes(dep)) {
5507
+ detected.push(dep);
5508
+ recommendedNames.add("api-test-suite");
5509
+ break;
5510
+ }
5511
+ }
5512
+ if (depNames.includes("typescript")) {
5513
+ detected.push("typescript");
5514
+ for (const name of ["scaffold-project", "deploy"]) {
5515
+ recommendedNames.add(name);
5516
+ }
5517
+ }
5518
+ const uniqueDetected = Array.from(new Set(detected));
5519
+ const recommended = Array.from(recommendedNames).map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5520
+ return { detected: uniqueDetected, recommended };
5521
+ }
5398
5522
  function generateSkillMd(name) {
5399
5523
  const meta = getSkill(name);
5400
5524
  if (!meta)
@@ -34796,6 +34920,19 @@ var init_mcp2 = __esm(() => {
34796
34920
  }));
34797
34921
  return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
34798
34922
  });
34923
+ server.registerTool("list_tags", {
34924
+ title: "List Tags",
34925
+ description: "List all unique tags across all skills with their occurrence counts"
34926
+ }, async () => {
34927
+ const tagCounts = new Map;
34928
+ for (const skill of SKILLS) {
34929
+ for (const tag of skill.tags) {
34930
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
34931
+ }
34932
+ }
34933
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
34934
+ return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
34935
+ });
34799
34936
  server.registerTool("get_requirements", {
34800
34937
  title: "Get Requirements",
34801
34938
  description: "Get environment variables, system dependencies, and npm dependencies for a skill",
@@ -34959,6 +35096,16 @@ function createFetchHandler(options) {
34959
35096
  }));
34960
35097
  return json2(counts);
34961
35098
  }
35099
+ if (path === "/api/tags" && method === "GET") {
35100
+ const tagCounts = new Map;
35101
+ for (const skill of SKILLS) {
35102
+ for (const tag of skill.tags) {
35103
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35104
+ }
35105
+ }
35106
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
35107
+ return json2(sorted);
35108
+ }
34962
35109
  if (path === "/api/skills/search" && method === "GET") {
34963
35110
  const query = url2.searchParams.get("q") || "";
34964
35111
  if (!query.trim())
@@ -36416,7 +36563,7 @@ Skills installed to .skills/`));
36416
36563
  process.exitCode = 1;
36417
36564
  }
36418
36565
  });
36419
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-i, --installed", "Show only installed skills", false).option("--json", "Output as JSON", false).description("List available or installed skills").action((options) => {
36566
+ program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-i, --installed", "Show only installed skills", false).option("-t, --tags <tags>", "Filter by comma-separated tags (OR logic, case-insensitive)").option("--json", "Output as JSON", false).description("List available or installed skills").action((options) => {
36420
36567
  if (options.installed) {
36421
36568
  const installed = getInstalledSkills();
36422
36569
  if (options.json) {
@@ -36435,6 +36582,7 @@ Installed skills (${installed.length}):
36435
36582
  }
36436
36583
  return;
36437
36584
  }
36585
+ const tagFilter = options.tags ? options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean) : null;
36438
36586
  if (options.category) {
36439
36587
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
36440
36588
  if (!category) {
@@ -36443,7 +36591,10 @@ Installed skills (${installed.length}):
36443
36591
  process.exitCode = 1;
36444
36592
  return;
36445
36593
  }
36446
- const skills = getSkillsByCategory(category);
36594
+ let skills = getSkillsByCategory(category);
36595
+ if (tagFilter) {
36596
+ skills = skills.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36597
+ }
36447
36598
  if (options.json) {
36448
36599
  console.log(JSON.stringify(skills, null, 2));
36449
36600
  return;
@@ -36456,6 +36607,20 @@ ${category} (${skills.length}):
36456
36607
  }
36457
36608
  return;
36458
36609
  }
36610
+ if (tagFilter) {
36611
+ const skills = SKILLS.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36612
+ if (options.json) {
36613
+ console.log(JSON.stringify(skills, null, 2));
36614
+ return;
36615
+ }
36616
+ console.log(chalk2.bold(`
36617
+ Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
36618
+ `));
36619
+ for (const s of skills) {
36620
+ console.log(` ${chalk2.cyan(s.name)} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
36621
+ }
36622
+ return;
36623
+ }
36459
36624
  if (options.json) {
36460
36625
  console.log(JSON.stringify(SKILLS, null, 2));
36461
36626
  return;
@@ -36472,7 +36637,7 @@ Available skills (${SKILLS.length}):
36472
36637
  console.log();
36473
36638
  }
36474
36639
  });
36475
- program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("-c, --category <category>", "Filter results by category").description("Search for skills").action((query, options) => {
36640
+ program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("-c, --category <category>", "Filter results by category").option("-t, --tags <tags>", "Filter results by comma-separated tags (OR logic, case-insensitive)").description("Search for skills").action((query, options) => {
36476
36641
  let results = searchSkills(query);
36477
36642
  if (options.category) {
36478
36643
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
@@ -36484,6 +36649,10 @@ program2.command("search").argument("<query>", "Search term").option("--json", "
36484
36649
  }
36485
36650
  results = results.filter((s) => s.category === category);
36486
36651
  }
36652
+ if (options.tags) {
36653
+ const tagFilter = options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
36654
+ results = results.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36655
+ }
36487
36656
  if (options.json) {
36488
36657
  console.log(JSON.stringify(results, null, 2));
36489
36658
  return;
@@ -36628,10 +36797,76 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
36628
36797
  }
36629
36798
  process.exitCode = result.exitCode;
36630
36799
  });
36631
- program2.command("init").option("--json", "Output as JSON", false).description("Initialize project for installed skills (.env.example, .gitignore)").action((options) => {
36800
+ program2.command("init").option("--json", "Output as JSON", false).option("--for <agent>", "Detect project type and install recommended skills for agent: claude, codex, gemini, or all").option("--scope <scope>", "Install scope: global or project", "global").description("Initialize project for installed skills (.env.example, .gitignore)").action((options) => {
36632
36801
  const cwd = process.cwd();
36802
+ if (options.for) {
36803
+ let agents;
36804
+ try {
36805
+ agents = resolveAgents(options.for);
36806
+ } catch (err) {
36807
+ console.error(chalk2.red(err.message));
36808
+ process.exitCode = 1;
36809
+ return;
36810
+ }
36811
+ const { detected, recommended } = detectProjectSkills(cwd);
36812
+ if (options.json) {
36813
+ const installResults2 = [];
36814
+ for (const skill of recommended) {
36815
+ for (const agent of agents) {
36816
+ const result = installSkillForAgent(skill.name, {
36817
+ agent,
36818
+ scope: options.scope
36819
+ }, generateSkillMd);
36820
+ installResults2.push({ ...result, agent, scope: options.scope });
36821
+ }
36822
+ }
36823
+ console.log(JSON.stringify({
36824
+ detected,
36825
+ recommended: recommended.map((s) => s.name),
36826
+ installed: installResults2
36827
+ }, null, 2));
36828
+ return;
36829
+ }
36830
+ if (detected.length > 0) {
36831
+ console.log(chalk2.bold(`
36832
+ Detected project technologies:`));
36833
+ for (const tech of detected) {
36834
+ console.log(` ${chalk2.cyan(tech)}`);
36835
+ }
36836
+ } else {
36837
+ console.log(chalk2.dim(`
36838
+ No specific project dependencies detected`));
36839
+ }
36840
+ console.log(chalk2.bold(`
36841
+ Recommended skills (${recommended.length}):`));
36842
+ for (const skill of recommended) {
36843
+ console.log(` ${chalk2.cyan(skill.name)} - ${skill.description}`);
36844
+ }
36845
+ console.log(chalk2.bold(`
36846
+ Installing recommended skills for ${options.for} (${options.scope})...
36847
+ `));
36848
+ const installResults = [];
36849
+ for (const skill of recommended) {
36850
+ for (const agent of agents) {
36851
+ const result = installSkillForAgent(skill.name, {
36852
+ agent,
36853
+ scope: options.scope
36854
+ }, generateSkillMd);
36855
+ installResults.push({ ...result, agent });
36856
+ const label = `${skill.name} \u2192 ${agent} (${options.scope})`;
36857
+ if (result.success) {
36858
+ console.log(chalk2.green(`\u2713 ${label}`));
36859
+ } else {
36860
+ console.log(chalk2.red(`\u2717 ${label}: ${result.error}`));
36861
+ }
36862
+ }
36863
+ }
36864
+ if (installResults.some((r) => !r.success)) {
36865
+ process.exitCode = 1;
36866
+ }
36867
+ }
36633
36868
  const installed = getInstalledSkills();
36634
- if (installed.length === 0) {
36869
+ if (installed.length === 0 && !options.for) {
36635
36870
  if (options.json) {
36636
36871
  console.log(JSON.stringify({ skills: [], envVars: 0, gitignoreUpdated: false }));
36637
36872
  } else {
@@ -36639,6 +36874,8 @@ program2.command("init").option("--json", "Output as JSON", false).description("
36639
36874
  }
36640
36875
  return;
36641
36876
  }
36877
+ if (installed.length === 0)
36878
+ return;
36642
36879
  const envMap = new Map;
36643
36880
  for (const name of installed) {
36644
36881
  const reqs = getSkillRequirements(name);
@@ -36712,25 +36949,27 @@ ${gitignoreEntry}
36712
36949
  console.log(chalk2.dim(" .skills/ already in .gitignore"));
36713
36950
  }
36714
36951
  }
36715
- if (options.json) {
36716
- console.log(JSON.stringify({
36717
- skills: installed,
36718
- envVars: envVarCount,
36719
- gitignoreUpdated
36720
- }, null, 2));
36721
- } else {
36722
- if (envMap.size > 0) {
36723
- console.log(chalk2.bold(`
36952
+ if (!options.for) {
36953
+ if (options.json) {
36954
+ console.log(JSON.stringify({
36955
+ skills: installed,
36956
+ envVars: envVarCount,
36957
+ gitignoreUpdated
36958
+ }, null, 2));
36959
+ } else {
36960
+ if (envMap.size > 0) {
36961
+ console.log(chalk2.bold(`
36724
36962
  Skill environment requirements:`));
36725
- for (const name of installed) {
36726
- const reqs = getSkillRequirements(name);
36727
- if (reqs?.envVars.length) {
36728
- console.log(` ${chalk2.cyan(name)}: ${reqs.envVars.join(", ")}`);
36963
+ for (const name of installed) {
36964
+ const reqs = getSkillRequirements(name);
36965
+ if (reqs?.envVars.length) {
36966
+ console.log(` ${chalk2.cyan(name)}: ${reqs.envVars.join(", ")}`);
36967
+ }
36729
36968
  }
36730
36969
  }
36731
- }
36732
- console.log(chalk2.bold(`
36970
+ console.log(chalk2.bold(`
36733
36971
  Initialized for ${installed.length} installed skill(s)`));
36972
+ }
36734
36973
  }
36735
36974
  });
36736
36975
  program2.command("remove").alias("rm").argument("<skill>", "Skill to remove").option("--json", "Output as JSON", false).option("--for <agent>", "Remove from agent: claude, codex, gemini, or all").option("--scope <scope>", "Remove scope: global or project", "global").option("--dry-run", "Print what would happen without actually removing", false).description("Remove an installed skill").action((skill, options) => {
@@ -36872,6 +37111,25 @@ Categories:
36872
37111
  console.log(` ${name} (${count})`);
36873
37112
  }
36874
37113
  });
37114
+ program2.command("tags").option("--json", "Output as JSON", false).description("List all unique tags with counts").action((options) => {
37115
+ const tagCounts = new Map;
37116
+ for (const skill of SKILLS) {
37117
+ for (const tag of skill.tags) {
37118
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
37119
+ }
37120
+ }
37121
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
37122
+ if (options.json) {
37123
+ console.log(JSON.stringify(sorted, null, 2));
37124
+ return;
37125
+ }
37126
+ console.log(chalk2.bold(`
37127
+ Tags:
37128
+ `));
37129
+ for (const { name, count } of sorted) {
37130
+ console.log(` ${chalk2.cyan(name)} (${count})`);
37131
+ }
37132
+ });
36875
37133
  program2.command("mcp").option("--register <agent>", "Register MCP server with agent: claude, codex, gemini, or all").description("Start MCP server (stdio) or register with an agent").action(async (options) => {
36876
37134
  if (options.register) {
36877
37135
  const agents = options.register === "all" ? ["claude", "codex", "gemini"] : [options.register];
package/bin/mcp.js CHANGED
@@ -28599,7 +28599,7 @@ class StdioServerTransport {
28599
28599
  // package.json
28600
28600
  var package_default = {
28601
28601
  name: "@hasna/skills",
28602
- version: "0.1.5",
28602
+ version: "0.1.6",
28603
28603
  description: "Skills library for AI coding agents",
28604
28604
  type: "module",
28605
28605
  bin: {
@@ -30113,6 +30113,45 @@ var SKILLS = [
30113
30113
  function getSkillsByCategory(category) {
30114
30114
  return SKILLS.filter((s) => s.category === category);
30115
30115
  }
30116
+ function editDistance(a, b) {
30117
+ if (a === b)
30118
+ return 0;
30119
+ if (a.length === 0)
30120
+ return b.length;
30121
+ if (b.length === 0)
30122
+ return a.length;
30123
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
30124
+ const curr = new Array(b.length + 1);
30125
+ for (let i = 1;i <= a.length; i++) {
30126
+ curr[0] = i;
30127
+ for (let j = 1;j <= b.length; j++) {
30128
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
30129
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
30130
+ }
30131
+ prev.splice(0, prev.length, ...curr);
30132
+ }
30133
+ return prev[b.length];
30134
+ }
30135
+ function fuzzyMatchScore(word, target) {
30136
+ if (target.includes(word))
30137
+ return 1;
30138
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
30139
+ for (const token of tokens) {
30140
+ if (token.startsWith(word))
30141
+ return 0.6;
30142
+ }
30143
+ if (word.length >= 3) {
30144
+ const maxDist = word.length <= 3 ? 1 : 2;
30145
+ for (const token of tokens) {
30146
+ if (Math.abs(token.length - word.length) <= maxDist) {
30147
+ const dist = editDistance(word, token);
30148
+ if (dist <= maxDist)
30149
+ return 0.4;
30150
+ }
30151
+ }
30152
+ }
30153
+ return 0;
30154
+ }
30116
30155
  function searchSkills(query) {
30117
30156
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
30118
30157
  if (words.length === 0)
@@ -30123,30 +30162,28 @@ function searchSkills(query) {
30123
30162
  const displayNameLower = skill.displayName.toLowerCase();
30124
30163
  const descriptionLower = skill.description.toLowerCase();
30125
30164
  const tagsLower = skill.tags.map((t) => t.toLowerCase());
30165
+ const tagsCombined = tagsLower.join(" ");
30126
30166
  let score = 0;
30127
30167
  let allWordsMatch = true;
30128
30168
  for (const word of words) {
30129
- let wordMatched = false;
30130
- if (nameLower.includes(word)) {
30131
- score += 10;
30132
- wordMatched = true;
30133
- }
30134
- if (displayNameLower.includes(word)) {
30135
- score += 7;
30136
- wordMatched = true;
30137
- }
30138
- if (tagsLower.some((t) => t.includes(word))) {
30139
- score += 5;
30140
- wordMatched = true;
30141
- }
30142
- if (descriptionLower.includes(word)) {
30143
- score += 2;
30144
- wordMatched = true;
30145
- }
30146
- if (!wordMatched) {
30169
+ let wordScore = 0;
30170
+ const nameMatch = fuzzyMatchScore(word, nameLower);
30171
+ if (nameMatch > 0)
30172
+ wordScore += 10 * nameMatch;
30173
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
30174
+ if (displayMatch > 0)
30175
+ wordScore += 7 * displayMatch;
30176
+ const tagMatch = Math.max(...tagsLower.map((t) => fuzzyMatchScore(word, t)), fuzzyMatchScore(word, tagsCombined));
30177
+ if (tagMatch > 0)
30178
+ wordScore += 5 * tagMatch;
30179
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
30180
+ if (descMatch > 0)
30181
+ wordScore += 2 * descMatch;
30182
+ if (wordScore === 0) {
30147
30183
  allWordsMatch = false;
30148
30184
  break;
30149
30185
  }
30186
+ score += wordScore;
30150
30187
  }
30151
30188
  if (allWordsMatch && score > 0) {
30152
30189
  scored.push({ skill, score });
@@ -30709,6 +30746,19 @@ server.registerTool("list_categories", {
30709
30746
  }));
30710
30747
  return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
30711
30748
  });
30749
+ server.registerTool("list_tags", {
30750
+ title: "List Tags",
30751
+ description: "List all unique tags across all skills with their occurrence counts"
30752
+ }, async () => {
30753
+ const tagCounts = new Map;
30754
+ for (const skill of SKILLS) {
30755
+ for (const tag of skill.tags) {
30756
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
30757
+ }
30758
+ }
30759
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
30760
+ return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
30761
+ });
30712
30762
  server.registerTool("get_requirements", {
30713
30763
  title: "Get Requirements",
30714
30764
  description: "Get environment variables, system dependencies, and npm dependencies for a skill",
package/dist/index.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * Or use the interactive CLI:
8
8
  * skills
9
9
  */
10
- export { SKILLS, CATEGORIES, getSkill, getSkillsByCategory, searchSkills, type SkillMeta, type Category, } from "./lib/registry.js";
10
+ export { SKILLS, CATEGORIES, getSkill, getSkillsByCategory, searchSkills, getSkillsByTag, getAllTags, type SkillMeta, type Category, } from "./lib/registry.js";
11
11
  export { installSkill, installSkills, installSkillForAgent, removeSkillForAgent, getInstalledSkills, removeSkill, skillExists, getSkillPath, getAgentSkillsDir, getAgentSkillPath, AGENT_TARGETS, type InstallResult, type InstallOptions, type AgentTarget, type AgentScope, type AgentInstallOptions, } from "./lib/installer.js";
12
12
  export { getSkillDocs, getSkillBestDoc, getSkillRequirements, runSkill, generateEnvExample, generateSkillMd, type SkillDocs, type SkillRequirements, } from "./lib/skillinfo.js";
package/dist/index.js CHANGED
@@ -1439,6 +1439,45 @@ var SKILLS = [
1439
1439
  function getSkillsByCategory(category) {
1440
1440
  return SKILLS.filter((s) => s.category === category);
1441
1441
  }
1442
+ function editDistance(a, b) {
1443
+ if (a === b)
1444
+ return 0;
1445
+ if (a.length === 0)
1446
+ return b.length;
1447
+ if (b.length === 0)
1448
+ return a.length;
1449
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
1450
+ const curr = new Array(b.length + 1);
1451
+ for (let i = 1;i <= a.length; i++) {
1452
+ curr[0] = i;
1453
+ for (let j = 1;j <= b.length; j++) {
1454
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1455
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1456
+ }
1457
+ prev.splice(0, prev.length, ...curr);
1458
+ }
1459
+ return prev[b.length];
1460
+ }
1461
+ function fuzzyMatchScore(word, target) {
1462
+ if (target.includes(word))
1463
+ return 1;
1464
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
1465
+ for (const token of tokens) {
1466
+ if (token.startsWith(word))
1467
+ return 0.6;
1468
+ }
1469
+ if (word.length >= 3) {
1470
+ const maxDist = word.length <= 3 ? 1 : 2;
1471
+ for (const token of tokens) {
1472
+ if (Math.abs(token.length - word.length) <= maxDist) {
1473
+ const dist = editDistance(word, token);
1474
+ if (dist <= maxDist)
1475
+ return 0.4;
1476
+ }
1477
+ }
1478
+ }
1479
+ return 0;
1480
+ }
1442
1481
  function searchSkills(query) {
1443
1482
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
1444
1483
  if (words.length === 0)
@@ -1449,30 +1488,28 @@ function searchSkills(query) {
1449
1488
  const displayNameLower = skill.displayName.toLowerCase();
1450
1489
  const descriptionLower = skill.description.toLowerCase();
1451
1490
  const tagsLower = skill.tags.map((t) => t.toLowerCase());
1491
+ const tagsCombined = tagsLower.join(" ");
1452
1492
  let score = 0;
1453
1493
  let allWordsMatch = true;
1454
1494
  for (const word of words) {
1455
- let wordMatched = false;
1456
- if (nameLower.includes(word)) {
1457
- score += 10;
1458
- wordMatched = true;
1459
- }
1460
- if (displayNameLower.includes(word)) {
1461
- score += 7;
1462
- wordMatched = true;
1463
- }
1464
- if (tagsLower.some((t) => t.includes(word))) {
1465
- score += 5;
1466
- wordMatched = true;
1467
- }
1468
- if (descriptionLower.includes(word)) {
1469
- score += 2;
1470
- wordMatched = true;
1471
- }
1472
- if (!wordMatched) {
1495
+ let wordScore = 0;
1496
+ const nameMatch = fuzzyMatchScore(word, nameLower);
1497
+ if (nameMatch > 0)
1498
+ wordScore += 10 * nameMatch;
1499
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
1500
+ if (displayMatch > 0)
1501
+ wordScore += 7 * displayMatch;
1502
+ const tagMatch = Math.max(...tagsLower.map((t) => fuzzyMatchScore(word, t)), fuzzyMatchScore(word, tagsCombined));
1503
+ if (tagMatch > 0)
1504
+ wordScore += 5 * tagMatch;
1505
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
1506
+ if (descMatch > 0)
1507
+ wordScore += 2 * descMatch;
1508
+ if (wordScore === 0) {
1473
1509
  allWordsMatch = false;
1474
1510
  break;
1475
1511
  }
1512
+ score += wordScore;
1476
1513
  }
1477
1514
  if (allWordsMatch && score > 0) {
1478
1515
  scored.push({ skill, score });
@@ -1484,6 +1521,19 @@ function searchSkills(query) {
1484
1521
  function getSkill(name) {
1485
1522
  return SKILLS.find((s) => s.name === name);
1486
1523
  }
1524
+ function getSkillsByTag(tag) {
1525
+ const needle = tag.toLowerCase();
1526
+ return SKILLS.filter((s) => s.tags.some((t) => t.toLowerCase().includes(needle)));
1527
+ }
1528
+ function getAllTags() {
1529
+ const tagSet = new Set;
1530
+ for (const skill of SKILLS) {
1531
+ for (const tag of skill.tags) {
1532
+ tagSet.add(tag.toLowerCase());
1533
+ }
1534
+ }
1535
+ return Array.from(tagSet).sort();
1536
+ }
1487
1537
  // src/lib/installer.ts
1488
1538
  import { existsSync, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync } from "fs";
1489
1539
  import { join, dirname } from "path";
@@ -1957,6 +2007,7 @@ export {
1957
2007
  installSkills,
1958
2008
  installSkillForAgent,
1959
2009
  installSkill,
2010
+ getSkillsByTag,
1960
2011
  getSkillsByCategory,
1961
2012
  getSkillRequirements,
1962
2013
  getSkillPath,
@@ -1964,6 +2015,7 @@ export {
1964
2015
  getSkillBestDoc,
1965
2016
  getSkill,
1966
2017
  getInstalledSkills,
2018
+ getAllTags,
1967
2019
  getAgentSkillsDir,
1968
2020
  getAgentSkillPath,
1969
2021
  generateSkillMd,
@@ -15,3 +15,11 @@ export declare const SKILLS: SkillMeta[];
15
15
  export declare function getSkillsByCategory(category: Category): SkillMeta[];
16
16
  export declare function searchSkills(query: string): SkillMeta[];
17
17
  export declare function getSkill(name: string): SkillMeta | undefined;
18
+ /**
19
+ * Return all skills whose tags include a partial case-insensitive match for `tag`.
20
+ */
21
+ export declare function getSkillsByTag(tag: string): SkillMeta[];
22
+ /**
23
+ * Return all unique tags across every skill, sorted alphabetically.
24
+ */
25
+ export declare function getAllTags(): string[];
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Skill info - reads docs, requirements, and metadata from skill source
3
3
  */
4
+ import { type SkillMeta } from "./registry.js";
4
5
  export interface SkillDocs {
5
6
  skillMd: string | null;
6
7
  readme: string | null;
@@ -33,6 +34,14 @@ export declare function runSkill(name: string, args: string[], options?: {
33
34
  exitCode: number;
34
35
  error?: string;
35
36
  }>;
37
+ export interface DetectedProjectSkills {
38
+ detected: string[];
39
+ recommended: SkillMeta[];
40
+ }
41
+ /**
42
+ * Detect project type from package.json and recommend relevant skills
43
+ */
44
+ export declare function detectProjectSkills(cwd?: string): DetectedProjectSkills;
36
45
  /**
37
46
  * Generate a .env.example from installed skills
38
47
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {