@hasna/skills 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ npx @hasna/skills
10
10
 
11
11
  - **202 ready-to-use skills** across development, business, content, data, media, design, and more
12
12
  - **Interactive TUI** -- browse by category, search, and install from the terminal
13
- - **MCP server** -- 9 tools and 2 resources for AI agent integration
13
+ - **MCP server** -- 10 tools and 2 resources for AI agent integration
14
14
  - **HTTP dashboard** -- React web UI to browse, search, install, and manage skills
15
15
  - **Agent-aware installs** -- copies SKILL.md to `~/.claude/skills/`, `~/.codex/skills/`, or `~/.gemini/skills/`
16
16
  - **Auto-generated index** -- `.skills/index.ts` is updated on every install for easy imports
@@ -52,6 +52,12 @@ skills run image --prompt "a sunset over mountains"
52
52
 
53
53
  # Start the web dashboard
54
54
  skills serve
55
+
56
+ # Smart init: detect project type and install skills for Claude
57
+ skills init --for claude
58
+
59
+ # Browse skills by tag
60
+ skills list --tags api,testing
55
61
  ```
56
62
 
57
63
  ## Categories
@@ -84,23 +90,24 @@ skills serve
84
90
  | `skills install <names...>` | `skills add` | Install one or more skills to `.skills/` |
85
91
  | `skills install <name> --for <agent>` | | Install SKILL.md for claude, codex, gemini, or all |
86
92
  | `skills remove <name>` | `skills rm` | Remove an installed skill |
87
- | `skills list` | `skills ls` | List all available skills |
93
+ | `skills list` | `skills ls` | List all available skills (supports `--tags` to filter by tags) |
88
94
  | `skills list --category <cat>` | | List skills in a category |
89
95
  | `skills list --installed` | | List installed skills |
90
- | `skills search <query>` | | Search skills by name, description, or tags |
96
+ | `skills search <query>` | | Search skills by name, description, or tags (supports `--tags` to filter by tags) |
91
97
  | `skills info <name>` | | Show skill metadata, requirements, and env vars |
92
98
  | `skills docs <name>` | | Show skill documentation (SKILL.md/README.md/CLAUDE.md) |
93
99
  | `skills requires <name>` | | Show env vars, system deps, and npm dependencies |
94
100
  | `skills run <name> [args...]` | | Run a skill directly |
95
101
  | `skills categories` | | List all categories with counts |
96
- | `skills init` | | Generate `.env.example` and update `.gitignore` |
102
+ | `skills tags` | | List all tags with skill counts |
103
+ | `skills init` | | Initialize project, detect deps, and optionally install for agents |
97
104
  | `skills update [names...]` | | Update installed skills (reinstall with overwrite) |
98
105
  | `skills serve` | | Start the HTTP dashboard (auto-assigns free port) |
99
106
  | `skills mcp` | | Start the MCP server on stdio |
100
107
  | `skills mcp --register <agent>` | | Register MCP server with claude, codex, gemini, or all |
101
108
  | `skills self-update` | | Update `@hasna/skills` to the latest version |
102
109
 
103
- All list/search/info commands support `--json` for machine-readable output.
110
+ All list/search/info commands support `--json` for machine-readable output. Search uses fuzzy matching -- typos and abbreviations are tolerated.
104
111
 
105
112
  ## MCP Server
106
113
 
@@ -133,7 +140,7 @@ Add to your MCP config:
133
140
  }
134
141
  ```
135
142
 
136
- ### Tools (9)
143
+ ### Tools (10)
137
144
 
138
145
  | Tool | Description |
139
146
  |------|-------------|
@@ -144,6 +151,7 @@ Add to your MCP config:
144
151
  | `install_skill` | Install a skill (full source to `.skills/` or SKILL.md to agent dir) |
145
152
  | `remove_skill` | Remove an installed skill |
146
153
  | `list_categories` | List all categories with skill counts |
154
+ | `list_tags` | List all skill tags with counts |
147
155
  | `get_requirements` | Get env vars, system deps, and npm dependencies |
148
156
  | `run_skill` | Run a skill by name with optional arguments |
149
157
 
@@ -190,6 +198,7 @@ The dashboard server also exposes a REST API:
190
198
  | `/api/skills/:name/docs` | GET | Raw documentation text |
191
199
  | `/api/skills/:name/install` | POST | Install a skill |
192
200
  | `/api/skills/:name/remove` | POST | Remove a skill |
201
+ | `/api/tags` | GET | All tags with skill counts |
193
202
  | `/api/version` | GET | Current package version |
194
203
  | `/api/self-update` | POST | Update to latest version |
195
204
 
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.7",
1882
1882
  description: "Skills library for AI coding agents",
1883
1883
  type: "module",
1884
1884
  bin: {
@@ -1919,6 +1919,12 @@ var init_package = __esm(() => {
1919
1919
  "typescript",
1920
1920
  "bun",
1921
1921
  "claude",
1922
+ "codex",
1923
+ "gemini",
1924
+ "mcp",
1925
+ "model-context-protocol",
1926
+ "open-source",
1927
+ "skill-library",
1922
1928
  "automation"
1923
1929
  ],
1924
1930
  author: "Hasna",
@@ -1957,6 +1963,45 @@ var init_package = __esm(() => {
1957
1963
  function getSkillsByCategory(category) {
1958
1964
  return SKILLS.filter((s) => s.category === category);
1959
1965
  }
1966
+ function editDistance(a, b) {
1967
+ if (a === b)
1968
+ return 0;
1969
+ if (a.length === 0)
1970
+ return b.length;
1971
+ if (b.length === 0)
1972
+ return a.length;
1973
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
1974
+ const curr = new Array(b.length + 1);
1975
+ for (let i = 1;i <= a.length; i++) {
1976
+ curr[0] = i;
1977
+ for (let j = 1;j <= b.length; j++) {
1978
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1979
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
1980
+ }
1981
+ prev.splice(0, prev.length, ...curr);
1982
+ }
1983
+ return prev[b.length];
1984
+ }
1985
+ function fuzzyMatchScore(word, target) {
1986
+ if (target.includes(word))
1987
+ return 1;
1988
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
1989
+ for (const token of tokens) {
1990
+ if (token.startsWith(word))
1991
+ return 0.6;
1992
+ }
1993
+ if (word.length >= 3) {
1994
+ const maxDist = word.length <= 3 ? 1 : 2;
1995
+ for (const token of tokens) {
1996
+ if (Math.abs(token.length - word.length) <= maxDist) {
1997
+ const dist = editDistance(word, token);
1998
+ if (dist <= maxDist)
1999
+ return 0.4;
2000
+ }
2001
+ }
2002
+ }
2003
+ return 0;
2004
+ }
1960
2005
  function searchSkills(query) {
1961
2006
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
1962
2007
  if (words.length === 0)
@@ -1967,30 +2012,28 @@ function searchSkills(query) {
1967
2012
  const displayNameLower = skill.displayName.toLowerCase();
1968
2013
  const descriptionLower = skill.description.toLowerCase();
1969
2014
  const tagsLower = skill.tags.map((t) => t.toLowerCase());
2015
+ const tagsCombined = tagsLower.join(" ");
1970
2016
  let score = 0;
1971
2017
  let allWordsMatch = true;
1972
2018
  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) {
2019
+ let wordScore = 0;
2020
+ const nameMatch = fuzzyMatchScore(word, nameLower);
2021
+ if (nameMatch > 0)
2022
+ wordScore += 10 * nameMatch;
2023
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
2024
+ if (displayMatch > 0)
2025
+ wordScore += 7 * displayMatch;
2026
+ const tagMatch = Math.max(...tagsLower.map((t) => fuzzyMatchScore(word, t)), fuzzyMatchScore(word, tagsCombined));
2027
+ if (tagMatch > 0)
2028
+ wordScore += 5 * tagMatch;
2029
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
2030
+ if (descMatch > 0)
2031
+ wordScore += 2 * descMatch;
2032
+ if (wordScore === 0) {
1991
2033
  allWordsMatch = false;
1992
2034
  break;
1993
2035
  }
2036
+ score += wordScore;
1994
2037
  }
1995
2038
  if (allWordsMatch && score > 0) {
1996
2039
  scored.push({ skill, score });
@@ -5395,6 +5438,93 @@ async function runSkill(name, args, options = {}) {
5395
5438
  const exitCode = await proc.exited;
5396
5439
  return { exitCode };
5397
5440
  }
5441
+ function detectProjectSkills(cwd = process.cwd()) {
5442
+ const pkgPath = join2(cwd, "package.json");
5443
+ if (!existsSync2(pkgPath)) {
5444
+ const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5445
+ const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5446
+ return { detected: [], recommended: recommended2 };
5447
+ }
5448
+ let pkg;
5449
+ try {
5450
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5451
+ } catch {
5452
+ const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5453
+ const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5454
+ return { detected: [], recommended: recommended2 };
5455
+ }
5456
+ const allDeps = {
5457
+ ...pkg.dependencies,
5458
+ ...pkg.devDependencies
5459
+ };
5460
+ const depNames = Object.keys(allDeps);
5461
+ const detected = [];
5462
+ const recommendedNames = new Set;
5463
+ for (const name of ["implementation-plan", "write", "deepresearch"]) {
5464
+ recommendedNames.add(name);
5465
+ }
5466
+ const frontendDeps = ["next", "react", "vue", "svelte", "nuxt", "@nuxtjs/nuxt"];
5467
+ for (const dep of frontendDeps) {
5468
+ if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
5469
+ detected.push(dep);
5470
+ for (const name of ["image", "generate-favicon", "seo-brief-builder"]) {
5471
+ recommendedNames.add(name);
5472
+ }
5473
+ break;
5474
+ }
5475
+ }
5476
+ const backendDeps = ["express", "fastify", "hono", "koa", "@hono/hono"];
5477
+ for (const dep of backendDeps) {
5478
+ if (depNames.some((d) => d === dep || d.startsWith(`${dep}/`))) {
5479
+ detected.push(dep);
5480
+ for (const name of ["api-test-suite", "apidocs"]) {
5481
+ recommendedNames.add(name);
5482
+ }
5483
+ break;
5484
+ }
5485
+ }
5486
+ const aiDeps = ["@anthropic-ai/sdk", "openai", "@openai/openai", "anthropic"];
5487
+ for (const dep of aiDeps) {
5488
+ if (depNames.includes(dep)) {
5489
+ detected.push(dep);
5490
+ for (const name of ["deepresearch", "webcrawling"]) {
5491
+ recommendedNames.add(name);
5492
+ }
5493
+ break;
5494
+ }
5495
+ }
5496
+ if (depNames.includes("stripe")) {
5497
+ detected.push("stripe");
5498
+ recommendedNames.add("invoice");
5499
+ }
5500
+ const emailDeps = ["nodemailer", "@sendgrid/mail", "@sendgrid/client"];
5501
+ for (const dep of emailDeps) {
5502
+ if (depNames.includes(dep)) {
5503
+ detected.push(dep);
5504
+ for (const name of ["gmail", "email-campaign"]) {
5505
+ recommendedNames.add(name);
5506
+ }
5507
+ break;
5508
+ }
5509
+ }
5510
+ const testDeps = ["vitest", "jest", "mocha", "@jest/core"];
5511
+ for (const dep of testDeps) {
5512
+ if (depNames.includes(dep)) {
5513
+ detected.push(dep);
5514
+ recommendedNames.add("api-test-suite");
5515
+ break;
5516
+ }
5517
+ }
5518
+ if (depNames.includes("typescript")) {
5519
+ detected.push("typescript");
5520
+ for (const name of ["scaffold-project", "deploy"]) {
5521
+ recommendedNames.add(name);
5522
+ }
5523
+ }
5524
+ const uniqueDetected = Array.from(new Set(detected));
5525
+ const recommended = Array.from(recommendedNames).map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5526
+ return { detected: uniqueDetected, recommended };
5527
+ }
5398
5528
  function generateSkillMd(name) {
5399
5529
  const meta = getSkill(name);
5400
5530
  if (!meta)
@@ -34796,6 +34926,19 @@ var init_mcp2 = __esm(() => {
34796
34926
  }));
34797
34927
  return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
34798
34928
  });
34929
+ server.registerTool("list_tags", {
34930
+ title: "List Tags",
34931
+ description: "List all unique tags across all skills with their occurrence counts"
34932
+ }, async () => {
34933
+ const tagCounts = new Map;
34934
+ for (const skill of SKILLS) {
34935
+ for (const tag of skill.tags) {
34936
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
34937
+ }
34938
+ }
34939
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
34940
+ return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
34941
+ });
34799
34942
  server.registerTool("get_requirements", {
34800
34943
  title: "Get Requirements",
34801
34944
  description: "Get environment variables, system dependencies, and npm dependencies for a skill",
@@ -34959,6 +35102,16 @@ function createFetchHandler(options) {
34959
35102
  }));
34960
35103
  return json2(counts);
34961
35104
  }
35105
+ if (path === "/api/tags" && method === "GET") {
35106
+ const tagCounts = new Map;
35107
+ for (const skill of SKILLS) {
35108
+ for (const tag of skill.tags) {
35109
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35110
+ }
35111
+ }
35112
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
35113
+ return json2(sorted);
35114
+ }
34962
35115
  if (path === "/api/skills/search" && method === "GET") {
34963
35116
  const query = url2.searchParams.get("q") || "";
34964
35117
  if (!query.trim())
@@ -36316,15 +36469,7 @@ var program2 = new Command;
36316
36469
  program2.name("skills").description("Install AI agent skills for your project").version(package_default.version).option("--verbose", "Enable verbose logging", false).enablePositionalOptions();
36317
36470
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive skill browser (TUI)").action(() => {
36318
36471
  if (!isTTY) {
36319
- console.log(`Non-interactive environment detected. Use a subcommand:
36320
- `);
36321
- console.log(" skills list List available skills");
36322
- console.log(" skills search <q> Search skills");
36323
- console.log(" skills install <n> Install a skill");
36324
- console.log(" skills info <n> Show skill details");
36325
- console.log(" skills serve Start web dashboard");
36326
- console.log(` skills --help Show all commands
36327
- `);
36472
+ console.log(JSON.stringify(SKILLS, null, 2));
36328
36473
  process.exit(0);
36329
36474
  }
36330
36475
  render(/* @__PURE__ */ jsxDEV7(App, {}, undefined, false, undefined, this));
@@ -36416,7 +36561,7 @@ Skills installed to .skills/`));
36416
36561
  process.exitCode = 1;
36417
36562
  }
36418
36563
  });
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) => {
36564
+ 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
36565
  if (options.installed) {
36421
36566
  const installed = getInstalledSkills();
36422
36567
  if (options.json) {
@@ -36435,6 +36580,7 @@ Installed skills (${installed.length}):
36435
36580
  }
36436
36581
  return;
36437
36582
  }
36583
+ const tagFilter = options.tags ? options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean) : null;
36438
36584
  if (options.category) {
36439
36585
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
36440
36586
  if (!category) {
@@ -36443,7 +36589,10 @@ Installed skills (${installed.length}):
36443
36589
  process.exitCode = 1;
36444
36590
  return;
36445
36591
  }
36446
- const skills = getSkillsByCategory(category);
36592
+ let skills = getSkillsByCategory(category);
36593
+ if (tagFilter) {
36594
+ skills = skills.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36595
+ }
36447
36596
  if (options.json) {
36448
36597
  console.log(JSON.stringify(skills, null, 2));
36449
36598
  return;
@@ -36456,6 +36605,20 @@ ${category} (${skills.length}):
36456
36605
  }
36457
36606
  return;
36458
36607
  }
36608
+ if (tagFilter) {
36609
+ const skills = SKILLS.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36610
+ if (options.json) {
36611
+ console.log(JSON.stringify(skills, null, 2));
36612
+ return;
36613
+ }
36614
+ console.log(chalk2.bold(`
36615
+ Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
36616
+ `));
36617
+ for (const s of skills) {
36618
+ console.log(` ${chalk2.cyan(s.name)} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
36619
+ }
36620
+ return;
36621
+ }
36459
36622
  if (options.json) {
36460
36623
  console.log(JSON.stringify(SKILLS, null, 2));
36461
36624
  return;
@@ -36472,7 +36635,7 @@ Available skills (${SKILLS.length}):
36472
36635
  console.log();
36473
36636
  }
36474
36637
  });
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) => {
36638
+ 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
36639
  let results = searchSkills(query);
36477
36640
  if (options.category) {
36478
36641
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
@@ -36484,6 +36647,10 @@ program2.command("search").argument("<query>", "Search term").option("--json", "
36484
36647
  }
36485
36648
  results = results.filter((s) => s.category === category);
36486
36649
  }
36650
+ if (options.tags) {
36651
+ const tagFilter = options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
36652
+ results = results.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36653
+ }
36487
36654
  if (options.json) {
36488
36655
  console.log(JSON.stringify(results, null, 2));
36489
36656
  return;
@@ -36628,10 +36795,76 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
36628
36795
  }
36629
36796
  process.exitCode = result.exitCode;
36630
36797
  });
36631
- program2.command("init").option("--json", "Output as JSON", false).description("Initialize project for installed skills (.env.example, .gitignore)").action((options) => {
36798
+ 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
36799
  const cwd = process.cwd();
36800
+ if (options.for) {
36801
+ let agents;
36802
+ try {
36803
+ agents = resolveAgents(options.for);
36804
+ } catch (err) {
36805
+ console.error(chalk2.red(err.message));
36806
+ process.exitCode = 1;
36807
+ return;
36808
+ }
36809
+ const { detected, recommended } = detectProjectSkills(cwd);
36810
+ if (options.json) {
36811
+ const installResults2 = [];
36812
+ for (const skill of recommended) {
36813
+ for (const agent of agents) {
36814
+ const result = installSkillForAgent(skill.name, {
36815
+ agent,
36816
+ scope: options.scope
36817
+ }, generateSkillMd);
36818
+ installResults2.push({ ...result, agent, scope: options.scope });
36819
+ }
36820
+ }
36821
+ console.log(JSON.stringify({
36822
+ detected,
36823
+ recommended: recommended.map((s) => s.name),
36824
+ installed: installResults2
36825
+ }, null, 2));
36826
+ return;
36827
+ }
36828
+ if (detected.length > 0) {
36829
+ console.log(chalk2.bold(`
36830
+ Detected project technologies:`));
36831
+ for (const tech of detected) {
36832
+ console.log(` ${chalk2.cyan(tech)}`);
36833
+ }
36834
+ } else {
36835
+ console.log(chalk2.dim(`
36836
+ No specific project dependencies detected`));
36837
+ }
36838
+ console.log(chalk2.bold(`
36839
+ Recommended skills (${recommended.length}):`));
36840
+ for (const skill of recommended) {
36841
+ console.log(` ${chalk2.cyan(skill.name)} - ${skill.description}`);
36842
+ }
36843
+ console.log(chalk2.bold(`
36844
+ Installing recommended skills for ${options.for} (${options.scope})...
36845
+ `));
36846
+ const installResults = [];
36847
+ for (const skill of recommended) {
36848
+ for (const agent of agents) {
36849
+ const result = installSkillForAgent(skill.name, {
36850
+ agent,
36851
+ scope: options.scope
36852
+ }, generateSkillMd);
36853
+ installResults.push({ ...result, agent });
36854
+ const label = `${skill.name} \u2192 ${agent} (${options.scope})`;
36855
+ if (result.success) {
36856
+ console.log(chalk2.green(`\u2713 ${label}`));
36857
+ } else {
36858
+ console.log(chalk2.red(`\u2717 ${label}: ${result.error}`));
36859
+ }
36860
+ }
36861
+ }
36862
+ if (installResults.some((r) => !r.success)) {
36863
+ process.exitCode = 1;
36864
+ }
36865
+ }
36633
36866
  const installed = getInstalledSkills();
36634
- if (installed.length === 0) {
36867
+ if (installed.length === 0 && !options.for) {
36635
36868
  if (options.json) {
36636
36869
  console.log(JSON.stringify({ skills: [], envVars: 0, gitignoreUpdated: false }));
36637
36870
  } else {
@@ -36639,6 +36872,8 @@ program2.command("init").option("--json", "Output as JSON", false).description("
36639
36872
  }
36640
36873
  return;
36641
36874
  }
36875
+ if (installed.length === 0)
36876
+ return;
36642
36877
  const envMap = new Map;
36643
36878
  for (const name of installed) {
36644
36879
  const reqs = getSkillRequirements(name);
@@ -36712,25 +36947,27 @@ ${gitignoreEntry}
36712
36947
  console.log(chalk2.dim(" .skills/ already in .gitignore"));
36713
36948
  }
36714
36949
  }
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(`
36950
+ if (!options.for) {
36951
+ if (options.json) {
36952
+ console.log(JSON.stringify({
36953
+ skills: installed,
36954
+ envVars: envVarCount,
36955
+ gitignoreUpdated
36956
+ }, null, 2));
36957
+ } else {
36958
+ if (envMap.size > 0) {
36959
+ console.log(chalk2.bold(`
36724
36960
  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(", ")}`);
36961
+ for (const name of installed) {
36962
+ const reqs = getSkillRequirements(name);
36963
+ if (reqs?.envVars.length) {
36964
+ console.log(` ${chalk2.cyan(name)}: ${reqs.envVars.join(", ")}`);
36965
+ }
36729
36966
  }
36730
36967
  }
36731
- }
36732
- console.log(chalk2.bold(`
36968
+ console.log(chalk2.bold(`
36733
36969
  Initialized for ${installed.length} installed skill(s)`));
36970
+ }
36734
36971
  }
36735
36972
  });
36736
36973
  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 +37109,25 @@ Categories:
36872
37109
  console.log(` ${name} (${count})`);
36873
37110
  }
36874
37111
  });
37112
+ program2.command("tags").option("--json", "Output as JSON", false).description("List all unique tags with counts").action((options) => {
37113
+ const tagCounts = new Map;
37114
+ for (const skill of SKILLS) {
37115
+ for (const tag of skill.tags) {
37116
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
37117
+ }
37118
+ }
37119
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
37120
+ if (options.json) {
37121
+ console.log(JSON.stringify(sorted, null, 2));
37122
+ return;
37123
+ }
37124
+ console.log(chalk2.bold(`
37125
+ Tags:
37126
+ `));
37127
+ for (const { name, count } of sorted) {
37128
+ console.log(` ${chalk2.cyan(name)} (${count})`);
37129
+ }
37130
+ });
36875
37131
  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
37132
  if (options.register) {
36877
37133
  const agents = options.register === "all" ? ["claude", "codex", "gemini"] : [options.register];
@@ -36953,12 +37209,14 @@ program2.command("completion").argument("<shell>", "Shell type: bash, zsh, or fi
36953
37209
  "remove",
36954
37210
  "update",
36955
37211
  "categories",
37212
+ "tags",
36956
37213
  "mcp",
36957
37214
  "serve",
36958
37215
  "init",
36959
37216
  "self-update",
36960
37217
  "completion",
36961
- "outdated"
37218
+ "outdated",
37219
+ "doctor"
36962
37220
  ];
36963
37221
  const skillNames = SKILLS.map((s) => s.name);
36964
37222
  const categoryNames = CATEGORIES.map((c) => c);
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.7",
28603
28603
  description: "Skills library for AI coding agents",
28604
28604
  type: "module",
28605
28605
  bin: {
@@ -28640,6 +28640,12 @@ var package_default = {
28640
28640
  "typescript",
28641
28641
  "bun",
28642
28642
  "claude",
28643
+ "codex",
28644
+ "gemini",
28645
+ "mcp",
28646
+ "model-context-protocol",
28647
+ "open-source",
28648
+ "skill-library",
28643
28649
  "automation"
28644
28650
  ],
28645
28651
  author: "Hasna",
@@ -30113,6 +30119,45 @@ var SKILLS = [
30113
30119
  function getSkillsByCategory(category) {
30114
30120
  return SKILLS.filter((s) => s.category === category);
30115
30121
  }
30122
+ function editDistance(a, b) {
30123
+ if (a === b)
30124
+ return 0;
30125
+ if (a.length === 0)
30126
+ return b.length;
30127
+ if (b.length === 0)
30128
+ return a.length;
30129
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
30130
+ const curr = new Array(b.length + 1);
30131
+ for (let i = 1;i <= a.length; i++) {
30132
+ curr[0] = i;
30133
+ for (let j = 1;j <= b.length; j++) {
30134
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
30135
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
30136
+ }
30137
+ prev.splice(0, prev.length, ...curr);
30138
+ }
30139
+ return prev[b.length];
30140
+ }
30141
+ function fuzzyMatchScore(word, target) {
30142
+ if (target.includes(word))
30143
+ return 1;
30144
+ const tokens = target.split(/[\s\-_]+/).filter(Boolean);
30145
+ for (const token of tokens) {
30146
+ if (token.startsWith(word))
30147
+ return 0.6;
30148
+ }
30149
+ if (word.length >= 3) {
30150
+ const maxDist = word.length <= 3 ? 1 : 2;
30151
+ for (const token of tokens) {
30152
+ if (Math.abs(token.length - word.length) <= maxDist) {
30153
+ const dist = editDistance(word, token);
30154
+ if (dist <= maxDist)
30155
+ return 0.4;
30156
+ }
30157
+ }
30158
+ }
30159
+ return 0;
30160
+ }
30116
30161
  function searchSkills(query) {
30117
30162
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
30118
30163
  if (words.length === 0)
@@ -30123,30 +30168,28 @@ function searchSkills(query) {
30123
30168
  const displayNameLower = skill.displayName.toLowerCase();
30124
30169
  const descriptionLower = skill.description.toLowerCase();
30125
30170
  const tagsLower = skill.tags.map((t) => t.toLowerCase());
30171
+ const tagsCombined = tagsLower.join(" ");
30126
30172
  let score = 0;
30127
30173
  let allWordsMatch = true;
30128
30174
  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) {
30175
+ let wordScore = 0;
30176
+ const nameMatch = fuzzyMatchScore(word, nameLower);
30177
+ if (nameMatch > 0)
30178
+ wordScore += 10 * nameMatch;
30179
+ const displayMatch = fuzzyMatchScore(word, displayNameLower);
30180
+ if (displayMatch > 0)
30181
+ wordScore += 7 * displayMatch;
30182
+ const tagMatch = Math.max(...tagsLower.map((t) => fuzzyMatchScore(word, t)), fuzzyMatchScore(word, tagsCombined));
30183
+ if (tagMatch > 0)
30184
+ wordScore += 5 * tagMatch;
30185
+ const descMatch = fuzzyMatchScore(word, descriptionLower);
30186
+ if (descMatch > 0)
30187
+ wordScore += 2 * descMatch;
30188
+ if (wordScore === 0) {
30147
30189
  allWordsMatch = false;
30148
30190
  break;
30149
30191
  }
30192
+ score += wordScore;
30150
30193
  }
30151
30194
  if (allWordsMatch && score > 0) {
30152
30195
  scored.push({ skill, score });
@@ -30709,6 +30752,19 @@ server.registerTool("list_categories", {
30709
30752
  }));
30710
30753
  return { content: [{ type: "text", text: JSON.stringify(cats, null, 2) }] };
30711
30754
  });
30755
+ server.registerTool("list_tags", {
30756
+ title: "List Tags",
30757
+ description: "List all unique tags across all skills with their occurrence counts"
30758
+ }, async () => {
30759
+ const tagCounts = new Map;
30760
+ for (const skill of SKILLS) {
30761
+ for (const tag of skill.tags) {
30762
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
30763
+ }
30764
+ }
30765
+ const sorted = Array.from(tagCounts.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([name, count]) => ({ name, count }));
30766
+ return { content: [{ type: "text", text: JSON.stringify(sorted, null, 2) }] };
30767
+ });
30712
30768
  server.registerTool("get_requirements", {
30713
30769
  title: "Get Requirements",
30714
30770
  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.7",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,12 @@
41
41
  "typescript",
42
42
  "bun",
43
43
  "claude",
44
+ "codex",
45
+ "gemini",
46
+ "mcp",
47
+ "model-context-protocol",
48
+ "open-source",
49
+ "skill-library",
44
50
  "automation"
45
51
  ],
46
52
  "author": "Hasna",