@hasna/skills 0.1.6 → 0.1.8

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.6",
1881
+ version: "0.1.8",
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",
@@ -34776,6 +34782,9 @@ var init_stdio2 = __esm(() => {
34776
34782
 
34777
34783
  // src/mcp/index.ts
34778
34784
  var exports_mcp = {};
34785
+ import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
34786
+ import { join as join3 } from "path";
34787
+ import { homedir as homedir2 } from "os";
34779
34788
  async function main() {
34780
34789
  const transport = new StdioServerTransport;
34781
34790
  await server.connect(transport);
@@ -34877,6 +34886,52 @@ var init_mcp2 = __esm(() => {
34877
34886
  isError: !result.success
34878
34887
  };
34879
34888
  });
34889
+ server.registerTool("install_category", {
34890
+ title: "Install Category",
34891
+ description: "Install all skills in a category. Optionally install for a specific agent (claude, codex, gemini, or all) with a given scope.",
34892
+ inputSchema: {
34893
+ category: exports_external.string().describe("Category name (case-insensitive, e.g. 'Event Management')"),
34894
+ for: exports_external.string().optional().describe("Agent target: claude, codex, gemini, or all"),
34895
+ scope: exports_external.string().optional().describe("Install scope: global or project (default: global)")
34896
+ }
34897
+ }, async ({ category, for: agentArg, scope }) => {
34898
+ const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
34899
+ if (!matchedCategory) {
34900
+ return {
34901
+ content: [{ type: "text", text: `Unknown category: ${category}. Available: ${CATEGORIES.join(", ")}` }],
34902
+ isError: true
34903
+ };
34904
+ }
34905
+ const categorySkills = getSkillsByCategory(matchedCategory);
34906
+ const names = categorySkills.map((s) => s.name);
34907
+ if (agentArg) {
34908
+ let agents;
34909
+ try {
34910
+ agents = resolveAgents(agentArg);
34911
+ } catch (err) {
34912
+ return {
34913
+ content: [{ type: "text", text: err.message }],
34914
+ isError: true
34915
+ };
34916
+ }
34917
+ const results2 = [];
34918
+ for (const name of names) {
34919
+ for (const a of agents) {
34920
+ const r = installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd);
34921
+ results2.push({ ...r, agent: a, scope: scope || "global" });
34922
+ }
34923
+ }
34924
+ return {
34925
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results: results2 }, null, 2) }],
34926
+ isError: results2.some((r) => !r.success)
34927
+ };
34928
+ }
34929
+ const results = names.map((name) => installSkill(name));
34930
+ return {
34931
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results }, null, 2) }],
34932
+ isError: results.some((r) => !r.success)
34933
+ };
34934
+ });
34880
34935
  server.registerTool("remove_skill", {
34881
34936
  title: "Remove Skill",
34882
34937
  description: "Remove an installed skill. Without --for, removes from .skills/. With --for, removes from agent skill directory.",
@@ -34969,6 +35024,94 @@ var init_mcp2 = __esm(() => {
34969
35024
  content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, skill: name }, null, 2) }]
34970
35025
  };
34971
35026
  });
35027
+ server.registerTool("export_skills", {
35028
+ title: "Export Skills",
35029
+ description: "Export the list of currently installed skills as a JSON payload that can be imported elsewhere"
35030
+ }, async () => {
35031
+ const skills = getInstalledSkills();
35032
+ const payload = {
35033
+ version: 1,
35034
+ skills,
35035
+ timestamp: new Date().toISOString()
35036
+ };
35037
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
35038
+ });
35039
+ server.registerTool("import_skills", {
35040
+ title: "Import Skills",
35041
+ description: "Install a list of skills from an export payload. Supports agent-specific installs via the 'for' parameter.",
35042
+ inputSchema: {
35043
+ skills: exports_external.array(exports_external.string()).describe("List of skill names to install"),
35044
+ for: exports_external.string().optional().describe("Agent target: claude, codex, gemini, or all"),
35045
+ scope: exports_external.string().optional().describe("Install scope: global or project (default: global)")
35046
+ }
35047
+ }, async ({ skills: skillList, for: agentArg, scope }) => {
35048
+ if (!skillList || skillList.length === 0) {
35049
+ return { content: [{ type: "text", text: JSON.stringify({ imported: 0, results: [] }, null, 2) }] };
35050
+ }
35051
+ const results = [];
35052
+ if (agentArg) {
35053
+ let agents;
35054
+ try {
35055
+ agents = resolveAgents(agentArg);
35056
+ } catch (err) {
35057
+ return {
35058
+ content: [{ type: "text", text: err.message }],
35059
+ isError: true
35060
+ };
35061
+ }
35062
+ for (const name of skillList) {
35063
+ const agentResults = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
35064
+ const success2 = agentResults.every((r) => r.success);
35065
+ const errors4 = agentResults.filter((r) => !r.success).map((r) => r.error).filter(Boolean);
35066
+ results.push({ skill: name, success: success2, ...errors4.length > 0 ? { error: errors4.join("; ") } : {} });
35067
+ }
35068
+ } else {
35069
+ for (const name of skillList) {
35070
+ const result = installSkill(name);
35071
+ results.push({ skill: result.skill, success: result.success, ...result.error ? { error: result.error } : {} });
35072
+ }
35073
+ }
35074
+ const imported = results.filter((r) => r.success).length;
35075
+ const hasErrors = results.some((r) => !r.success);
35076
+ return {
35077
+ content: [{ type: "text", text: JSON.stringify({ imported, total: skillList.length, results }, null, 2) }],
35078
+ isError: hasErrors
35079
+ };
35080
+ });
35081
+ server.registerTool("whoami", {
35082
+ title: "Skills Whoami",
35083
+ description: "Show setup summary: package version, installed skills, agent configurations, skills directory location, and working directory"
35084
+ }, async () => {
35085
+ const version2 = package_default.version;
35086
+ const cwd = process.cwd();
35087
+ const installed = getInstalledSkills();
35088
+ const agentNames = ["claude", "codex", "gemini"];
35089
+ const agents = [];
35090
+ for (const agent of agentNames) {
35091
+ const agentSkillsPath = join3(homedir2(), `.${agent}`, "skills");
35092
+ const exists = existsSync3(agentSkillsPath);
35093
+ let skillCount = 0;
35094
+ if (exists) {
35095
+ try {
35096
+ skillCount = readdirSync3(agentSkillsPath).filter((f) => {
35097
+ const full = join3(agentSkillsPath, f);
35098
+ return f.startsWith("skill-") && statSync2(full).isDirectory();
35099
+ }).length;
35100
+ } catch {}
35101
+ }
35102
+ agents.push({ agent, path: agentSkillsPath, exists, skillCount });
35103
+ }
35104
+ const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
35105
+ const result = {
35106
+ version: version2,
35107
+ installedCount: installed.length,
35108
+ installed,
35109
+ agents,
35110
+ skillsDir,
35111
+ cwd
35112
+ };
35113
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
35114
+ });
34972
35115
  server.registerResource("Skills Registry", "skills://registry", {
34973
35116
  description: "Full list of all available skills as JSON"
34974
35117
  }, async () => ({
@@ -35004,15 +35147,15 @@ __export(exports_serve, {
35004
35147
  startServer: () => startServer,
35005
35148
  createFetchHandler: () => createFetchHandler
35006
35149
  });
35007
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
35008
- import { join as join3, dirname as dirname2, extname } from "path";
35150
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
35151
+ import { join as join4, dirname as dirname2, extname } from "path";
35009
35152
  import { fileURLToPath as fileURLToPath2 } from "url";
35010
35153
  function getPackageJson() {
35011
35154
  try {
35012
35155
  const scriptDir = dirname2(fileURLToPath2(import.meta.url));
35013
35156
  for (const rel of ["../..", ".."]) {
35014
- const pkgPath = join3(scriptDir, rel, "package.json");
35015
- if (existsSync3(pkgPath)) {
35157
+ const pkgPath = join4(scriptDir, rel, "package.json");
35158
+ if (existsSync4(pkgPath)) {
35016
35159
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
35017
35160
  return { version: pkg.version || "unknown", name: pkg.name || "skills" };
35018
35161
  }
@@ -35024,20 +35167,20 @@ function resolveDashboardDir() {
35024
35167
  const candidates = [];
35025
35168
  try {
35026
35169
  const scriptDir = dirname2(fileURLToPath2(import.meta.url));
35027
- candidates.push(join3(scriptDir, "..", "dashboard", "dist"));
35028
- candidates.push(join3(scriptDir, "..", "..", "dashboard", "dist"));
35170
+ candidates.push(join4(scriptDir, "..", "dashboard", "dist"));
35171
+ candidates.push(join4(scriptDir, "..", "..", "dashboard", "dist"));
35029
35172
  } catch {}
35030
35173
  if (process.argv[1]) {
35031
35174
  const mainDir = dirname2(process.argv[1]);
35032
- candidates.push(join3(mainDir, "..", "dashboard", "dist"));
35033
- candidates.push(join3(mainDir, "..", "..", "dashboard", "dist"));
35175
+ candidates.push(join4(mainDir, "..", "dashboard", "dist"));
35176
+ candidates.push(join4(mainDir, "..", "..", "dashboard", "dist"));
35034
35177
  }
35035
- candidates.push(join3(process.cwd(), "dashboard", "dist"));
35178
+ candidates.push(join4(process.cwd(), "dashboard", "dist"));
35036
35179
  for (const candidate of candidates) {
35037
- if (existsSync3(candidate))
35180
+ if (existsSync4(candidate))
35038
35181
  return candidate;
35039
35182
  }
35040
- return join3(process.cwd(), "dashboard", "dist");
35183
+ return join4(process.cwd(), "dashboard", "dist");
35041
35184
  }
35042
35185
  function json2(data, status = 200) {
35043
35186
  return new Response(JSON.stringify(data), {
@@ -35071,7 +35214,7 @@ function getAllSkillsWithStatus() {
35071
35214
  });
35072
35215
  }
35073
35216
  function serveStaticFile(filePath) {
35074
- if (!existsSync3(filePath))
35217
+ if (!existsSync4(filePath))
35075
35218
  return null;
35076
35219
  const ext = extname(filePath);
35077
35220
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -35081,7 +35224,7 @@ function serveStaticFile(filePath) {
35081
35224
  }
35082
35225
  function createFetchHandler(options) {
35083
35226
  const dashboardDir = options?.dashboardDir ?? resolveDashboardDir();
35084
- const dashboardExists = options?.dashboardExists ?? existsSync3(dashboardDir);
35227
+ const dashboardExists = options?.dashboardExists ?? existsSync4(dashboardDir);
35085
35228
  return async function fetchHandler(req) {
35086
35229
  const url2 = new URL(req.url);
35087
35230
  const path = url2.pathname;
@@ -35195,6 +35338,43 @@ function createFetchHandler(options) {
35195
35338
  return json2(result, result.success ? 200 : 400);
35196
35339
  }
35197
35340
  }
35341
+ if (path === "/api/skills/install-category" && method === "POST") {
35342
+ let body = {};
35343
+ try {
35344
+ const text = await req.text();
35345
+ if (text)
35346
+ body = JSON.parse(text);
35347
+ } catch {}
35348
+ if (!body.category) {
35349
+ return json2({ error: "Missing required field: category" }, 400);
35350
+ }
35351
+ const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === body.category.toLowerCase());
35352
+ if (!matchedCategory) {
35353
+ return json2({ error: `Unknown category: ${body.category}. Available: ${CATEGORIES.join(", ")}` }, 400);
35354
+ }
35355
+ const categorySkills = getSkillsByCategory(matchedCategory);
35356
+ const names = categorySkills.map((s) => s.name);
35357
+ if (body.for) {
35358
+ try {
35359
+ const agents = resolveAgents(body.for);
35360
+ const scope = body.scope === "project" ? "project" : "global";
35361
+ const results = [];
35362
+ for (const name of names) {
35363
+ for (const agent of agents) {
35364
+ results.push(installSkillForAgent(name, { agent, scope }, generateSkillMd));
35365
+ }
35366
+ }
35367
+ const allSuccess = results.every((r) => r.success);
35368
+ return json2({ category: matchedCategory, count: names.length, success: allSuccess, results }, allSuccess ? 200 : 207);
35369
+ } catch (e) {
35370
+ return json2({ success: false, error: e instanceof Error ? e.message : "Unknown error" }, 400);
35371
+ }
35372
+ } else {
35373
+ const results = names.map((name) => installSkill(name));
35374
+ const allSuccess = results.every((r) => r.success);
35375
+ return json2({ category: matchedCategory, count: names.length, success: allSuccess, results }, allSuccess ? 200 : 207);
35376
+ }
35377
+ }
35198
35378
  const removeMatch = path.match(/^\/api\/skills\/([^/]+)\/remove$/);
35199
35379
  if (removeMatch && method === "POST") {
35200
35380
  const name = removeMatch[1];
@@ -35207,6 +35387,50 @@ function createFetchHandler(options) {
35207
35387
  const pkg = getPackageJson();
35208
35388
  return json2({ version: pkg.version, name: pkg.name });
35209
35389
  }
35390
+ if (path === "/api/export" && method === "GET") {
35391
+ const skills = getInstalledSkills();
35392
+ return json2({
35393
+ version: 1,
35394
+ skills,
35395
+ timestamp: new Date().toISOString()
35396
+ });
35397
+ }
35398
+ if (path === "/api/import" && method === "POST") {
35399
+ let body = {};
35400
+ try {
35401
+ const text = await req.text();
35402
+ if (text)
35403
+ body = JSON.parse(text);
35404
+ } catch {
35405
+ return json2({ error: "Invalid JSON body" }, 400);
35406
+ }
35407
+ if (!Array.isArray(body.skills)) {
35408
+ return json2({ error: 'Body must include "skills" array' }, 400);
35409
+ }
35410
+ const skillList = body.skills;
35411
+ const results = [];
35412
+ if (body.for) {
35413
+ try {
35414
+ const agents = resolveAgents(body.for);
35415
+ const scope = body.scope === "project" ? "project" : "global";
35416
+ for (const name of skillList) {
35417
+ const agentResults = agents.map((agent) => installSkillForAgent(name, { agent, scope }, generateSkillMd));
35418
+ const success2 = agentResults.every((r) => r.success);
35419
+ const errors4 = agentResults.filter((r) => !r.success).map((r) => r.error).filter(Boolean);
35420
+ results.push({ skill: name, success: success2, ...errors4.length > 0 ? { error: errors4.join("; ") } : {} });
35421
+ }
35422
+ } catch (e) {
35423
+ return json2({ error: e instanceof Error ? e.message : "Unknown error" }, 400);
35424
+ }
35425
+ } else {
35426
+ for (const name of skillList) {
35427
+ const result = installSkill(name);
35428
+ results.push({ skill: result.skill, success: result.success, ...result.error ? { error: result.error } : {} });
35429
+ }
35430
+ }
35431
+ const imported = results.filter((r) => r.success).length;
35432
+ return json2({ imported, total: skillList.length, results }, imported === skillList.length ? 200 : 207);
35433
+ }
35210
35434
  if (path === "/api/self-update" && method === "POST") {
35211
35435
  try {
35212
35436
  const pkg = getPackageJson();
@@ -35236,12 +35460,12 @@ function createFetchHandler(options) {
35236
35460
  }
35237
35461
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
35238
35462
  if (path !== "/") {
35239
- const filePath = join3(dashboardDir, path);
35463
+ const filePath = join4(dashboardDir, path);
35240
35464
  const res2 = serveStaticFile(filePath);
35241
35465
  if (res2)
35242
35466
  return res2;
35243
35467
  }
35244
- const indexPath = join3(dashboardDir, "index.html");
35468
+ const indexPath = join4(dashboardDir, "index.html");
35245
35469
  const res = serveStaticFile(indexPath);
35246
35470
  if (res)
35247
35471
  return res;
@@ -35252,7 +35476,7 @@ function createFetchHandler(options) {
35252
35476
  async function startServer(port = 0, options) {
35253
35477
  const shouldOpen = options?.open ?? true;
35254
35478
  const dashboardDir = resolveDashboardDir();
35255
- const dashboardExists = existsSync3(dashboardDir);
35479
+ const dashboardExists = existsSync4(dashboardDir);
35256
35480
  if (!dashboardExists) {
35257
35481
  console.error(`
35258
35482
  Dashboard not found at: ${dashboardDir}`);
@@ -35332,8 +35556,8 @@ var {
35332
35556
  // src/cli/index.tsx
35333
35557
  init_package();
35334
35558
  import chalk2 from "chalk";
35335
- import { existsSync as existsSync4, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
35336
- import { join as join4 } from "path";
35559
+ import { existsSync as existsSync5, writeFileSync as writeFileSync2, appendFileSync, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
35560
+ import { join as join5 } from "path";
35337
35561
 
35338
35562
  // src/cli/components/App.tsx
35339
35563
  import { useState as useState7 } from "react";
@@ -36463,20 +36687,33 @@ var program2 = new Command;
36463
36687
  program2.name("skills").description("Install AI agent skills for your project").version(package_default.version).option("--verbose", "Enable verbose logging", false).enablePositionalOptions();
36464
36688
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive skill browser (TUI)").action(() => {
36465
36689
  if (!isTTY) {
36466
- console.log(`Non-interactive environment detected. Use a subcommand:
36467
- `);
36468
- console.log(" skills list List available skills");
36469
- console.log(" skills search <q> Search skills");
36470
- console.log(" skills install <n> Install a skill");
36471
- console.log(" skills info <n> Show skill details");
36472
- console.log(" skills serve Start web dashboard");
36473
- console.log(` skills --help Show all commands
36474
- `);
36690
+ console.log(JSON.stringify(SKILLS, null, 2));
36475
36691
  process.exit(0);
36476
36692
  }
36477
36693
  render(/* @__PURE__ */ jsxDEV7(App, {}, undefined, false, undefined, this));
36478
36694
  });
36479
- program2.command("install").alias("add").argument("<skills...>", "Skills to install").option("-o, --overwrite", "Overwrite existing skills", false).option("--json", "Output results as JSON", false).option("--for <agent>", "Install for agent: claude, codex, gemini, or all").option("--scope <scope>", "Install scope: global or project", "global").option("--dry-run", "Print what would happen without actually installing", false).description("Install one or more skills").action((skills, options) => {
36695
+ program2.command("install").alias("add").argument("[skills...]", "Skills to install").option("-o, --overwrite", "Overwrite existing skills", false).option("--json", "Output results as JSON", false).option("--for <agent>", "Install for agent: claude, codex, gemini, or all").option("--scope <scope>", "Install scope: global or project", "global").option("--dry-run", "Print what would happen without actually installing", false).option("--category <category>", "Install all skills in a category (case-insensitive)").description("Install one or more skills").action((skills, options) => {
36696
+ if (skills.length === 0 && !options.category) {
36697
+ console.error("error: missing required argument 'skills' or --category option");
36698
+ process.exitCode = 1;
36699
+ return;
36700
+ }
36701
+ if (options.category) {
36702
+ const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
36703
+ if (!matchedCategory) {
36704
+ console.error(`Unknown category: ${options.category}`);
36705
+ console.error(`Available: ${CATEGORIES.join(", ")}`);
36706
+ process.exitCode = 1;
36707
+ return;
36708
+ }
36709
+ const categorySkills = getSkillsByCategory(matchedCategory);
36710
+ skills = categorySkills.map((s) => s.name);
36711
+ if (!options.json) {
36712
+ console.log(chalk2.bold(`
36713
+ Installing ${skills.length} skills from "${matchedCategory}"...
36714
+ `));
36715
+ }
36716
+ }
36480
36717
  const results = [];
36481
36718
  if (options.for) {
36482
36719
  let agents;
@@ -36563,7 +36800,8 @@ Skills installed to .skills/`));
36563
36800
  process.exitCode = 1;
36564
36801
  }
36565
36802
  });
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) => {
36803
+ 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).option("--brief", "One line per skill: name \u2014 description [category]", false).description("List available or installed skills").action((options) => {
36804
+ const brief = options.brief && !options.json;
36567
36805
  if (options.installed) {
36568
36806
  const installed = getInstalledSkills();
36569
36807
  if (options.json) {
@@ -36574,6 +36812,12 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
36574
36812
  console.log(chalk2.dim("No skills installed"));
36575
36813
  return;
36576
36814
  }
36815
+ if (brief) {
36816
+ for (const name of installed) {
36817
+ console.log(name);
36818
+ }
36819
+ return;
36820
+ }
36577
36821
  console.log(chalk2.bold(`
36578
36822
  Installed skills (${installed.length}):
36579
36823
  `));
@@ -36599,6 +36843,12 @@ Installed skills (${installed.length}):
36599
36843
  console.log(JSON.stringify(skills, null, 2));
36600
36844
  return;
36601
36845
  }
36846
+ if (brief) {
36847
+ for (const s of skills) {
36848
+ console.log(`${s.name} \u2014 ${s.description} [${s.category}]`);
36849
+ }
36850
+ return;
36851
+ }
36602
36852
  console.log(chalk2.bold(`
36603
36853
  ${category} (${skills.length}):
36604
36854
  `));
@@ -36613,6 +36863,12 @@ ${category} (${skills.length}):
36613
36863
  console.log(JSON.stringify(skills, null, 2));
36614
36864
  return;
36615
36865
  }
36866
+ if (brief) {
36867
+ for (const s of skills) {
36868
+ console.log(`${s.name} \u2014 ${s.description} [${s.category}]`);
36869
+ }
36870
+ return;
36871
+ }
36616
36872
  console.log(chalk2.bold(`
36617
36873
  Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
36618
36874
  `));
@@ -36625,6 +36881,16 @@ Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
36625
36881
  console.log(JSON.stringify(SKILLS, null, 2));
36626
36882
  return;
36627
36883
  }
36884
+ if (brief) {
36885
+ const sorted = [...SKILLS].sort((a, b) => {
36886
+ const catCmp = a.category.localeCompare(b.category);
36887
+ return catCmp !== 0 ? catCmp : a.name.localeCompare(b.name);
36888
+ });
36889
+ for (const s of sorted) {
36890
+ console.log(`${s.name} \u2014 ${s.description} [${s.category}]`);
36891
+ }
36892
+ return;
36893
+ }
36628
36894
  console.log(chalk2.bold(`
36629
36895
  Available skills (${SKILLS.length}):
36630
36896
  `));
@@ -36637,7 +36903,7 @@ Available skills (${SKILLS.length}):
36637
36903
  console.log();
36638
36904
  }
36639
36905
  });
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) => {
36906
+ program2.command("search").argument("<query>", "Search term").option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", 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) => {
36641
36907
  let results = searchSkills(query);
36642
36908
  if (options.category) {
36643
36909
  const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
@@ -36653,6 +36919,7 @@ program2.command("search").argument("<query>", "Search term").option("--json", "
36653
36919
  const tagFilter = options.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
36654
36920
  results = results.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
36655
36921
  }
36922
+ const brief = options.brief && !options.json;
36656
36923
  if (options.json) {
36657
36924
  console.log(JSON.stringify(results, null, 2));
36658
36925
  return;
@@ -36661,6 +36928,12 @@ program2.command("search").argument("<query>", "Search term").option("--json", "
36661
36928
  console.log(chalk2.dim(`No skills found for "${query}"`));
36662
36929
  return;
36663
36930
  }
36931
+ if (brief) {
36932
+ for (const s of results) {
36933
+ console.log(`${s.name} \u2014 ${s.description} [${s.category}]`);
36934
+ }
36935
+ return;
36936
+ }
36664
36937
  console.log(chalk2.bold(`
36665
36938
  Found ${results.length} skill(s):
36666
36939
  `));
@@ -36669,7 +36942,7 @@ Found ${results.length} skill(s):
36669
36942
  console.log(` ${s.description}`);
36670
36943
  }
36671
36944
  });
36672
- program2.command("info").argument("<skill>", "Skill name").option("--json", "Output as JSON", false).description("Show details about a specific skill").action((name, options) => {
36945
+ program2.command("info").argument("<skill>", "Skill name").option("--json", "Output as JSON", false).option("--brief", "Single line: name \u2014 description [category] (tags: ...)", false).description("Show details about a specific skill").action((name, options) => {
36673
36946
  const skill = getSkill(name);
36674
36947
  if (!skill) {
36675
36948
  console.error(`Skill '${name}' not found`);
@@ -36681,6 +36954,10 @@ program2.command("info").argument("<skill>", "Skill name").option("--json", "Out
36681
36954
  console.log(JSON.stringify({ ...skill, ...reqs }, null, 2));
36682
36955
  return;
36683
36956
  }
36957
+ if (options.brief) {
36958
+ console.log(`${skill.name} \u2014 ${skill.description} [${skill.category}] (tags: ${skill.tags.join(", ")})`);
36959
+ return;
36960
+ }
36684
36961
  console.log(`
36685
36962
  ${chalk2.bold(skill.displayName)}`);
36686
36963
  console.log(`${skill.description}`);
@@ -36911,7 +37188,7 @@ Installing recommended skills for ${options.for} (${options.scope})...
36911
37188
  const envContent = lines.join(`
36912
37189
  `) + `
36913
37190
  `;
36914
- const envPath = join4(cwd, ".env.example");
37191
+ const envPath = join5(cwd, ".env.example");
36915
37192
  writeFileSync2(envPath, envContent);
36916
37193
  envVarCount = envMap.size;
36917
37194
  if (!options.json) {
@@ -36922,10 +37199,10 @@ Installing recommended skills for ${options.for} (${options.scope})...
36922
37199
  console.log(chalk2.dim(" No environment variables detected across installed skills"));
36923
37200
  }
36924
37201
  }
36925
- const gitignorePath = join4(cwd, ".gitignore");
37202
+ const gitignorePath = join5(cwd, ".gitignore");
36926
37203
  const gitignoreEntry = ".skills/";
36927
37204
  let gitignoreContent = "";
36928
- if (existsSync4(gitignorePath)) {
37205
+ if (existsSync5(gitignorePath)) {
36929
37206
  gitignoreContent = readFileSync4(gitignorePath, "utf-8");
36930
37207
  }
36931
37208
  let gitignoreUpdated = false;
@@ -37035,12 +37312,12 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37035
37312
  }
37036
37313
  function collectFiles(dir, base = "") {
37037
37314
  const files = new Set;
37038
- if (!existsSync4(dir))
37315
+ if (!existsSync5(dir))
37039
37316
  return files;
37040
- for (const entry of readdirSync3(dir)) {
37041
- const full = join4(dir, entry);
37317
+ for (const entry of readdirSync4(dir)) {
37318
+ const full = join5(dir, entry);
37042
37319
  const rel = base ? `${base}/${entry}` : entry;
37043
- if (statSync2(full).isDirectory()) {
37320
+ if (statSync3(full).isDirectory()) {
37044
37321
  for (const f of collectFiles(full, rel))
37045
37322
  files.add(f);
37046
37323
  } else {
@@ -37052,7 +37329,7 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37052
37329
  const updateResults = [];
37053
37330
  for (const name of toUpdate) {
37054
37331
  const skillName = normalizeSkillName(name);
37055
- const destPath = join4(process.cwd(), ".skills", skillName);
37332
+ const destPath = join5(process.cwd(), ".skills", skillName);
37056
37333
  const beforeFiles = collectFiles(destPath);
37057
37334
  const result = installSkill(name, { overwrite: true });
37058
37335
  const afterFiles = collectFiles(destPath);
@@ -37133,7 +37410,7 @@ Tags:
37133
37410
  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) => {
37134
37411
  if (options.register) {
37135
37412
  const agents = options.register === "all" ? ["claude", "codex", "gemini"] : [options.register];
37136
- const binPath = join4(import.meta.dir, "..", "mcp", "index.ts");
37413
+ const binPath = join5(import.meta.dir, "..", "mcp", "index.ts");
37137
37414
  for (const agent of agents) {
37138
37415
  if (agent === "claude") {
37139
37416
  try {
@@ -37147,8 +37424,8 @@ program2.command("mcp").option("--register <agent>", "Register MCP server with a
37147
37424
  console.log(chalk2.yellow(`Manual registration: claude mcp add skills -- bun run ${binPath}`));
37148
37425
  }
37149
37426
  } else if (agent === "codex") {
37150
- const { homedir: homedir2 } = await import("os");
37151
- const configPath = join4(homedir2(), ".codex", "config.toml");
37427
+ const { homedir: homedir3 } = await import("os");
37428
+ const configPath = join5(homedir3(), ".codex", "config.toml");
37152
37429
  console.log(chalk2.bold(`
37153
37430
  Add to ${configPath}:`));
37154
37431
  console.log(chalk2.dim(`[mcp_servers.skills]
@@ -37156,8 +37433,8 @@ command = "bun"
37156
37433
  args = ["run", "${binPath}"]`));
37157
37434
  console.log(chalk2.green(`\u2713 Codex MCP config shown above`));
37158
37435
  } else if (agent === "gemini") {
37159
- const { homedir: homedir2 } = await import("os");
37160
- const configPath = join4(homedir2(), ".gemini", "settings.json");
37436
+ const { homedir: homedir3 } = await import("os");
37437
+ const configPath = join5(homedir3(), ".gemini", "settings.json");
37161
37438
  console.log(chalk2.bold(`
37162
37439
  Add to ${configPath} mcpServers:`));
37163
37440
  console.log(chalk2.dim(JSON.stringify({
@@ -37211,12 +37488,15 @@ program2.command("completion").argument("<shell>", "Shell type: bash, zsh, or fi
37211
37488
  "remove",
37212
37489
  "update",
37213
37490
  "categories",
37491
+ "tags",
37214
37492
  "mcp",
37215
37493
  "serve",
37216
37494
  "init",
37217
37495
  "self-update",
37218
37496
  "completion",
37219
- "outdated"
37497
+ "outdated",
37498
+ "doctor",
37499
+ "auth"
37220
37500
  ];
37221
37501
  const skillNames = SKILLS.map((s) => s.name);
37222
37502
  const categoryNames = CATEGORIES.map((c) => c);
@@ -37339,6 +37619,117 @@ _skills "$@"
37339
37619
  process.exitCode = 1;
37340
37620
  }
37341
37621
  });
37622
+ program2.command("export").option("--json", "Output as JSON (default behavior)", false).description("Export installed skills to JSON for sharing or backup").action((_options) => {
37623
+ const skills = getInstalledSkills();
37624
+ const payload = {
37625
+ version: 1,
37626
+ skills,
37627
+ timestamp: new Date().toISOString()
37628
+ };
37629
+ console.log(JSON.stringify(payload, null, 2));
37630
+ });
37631
+ program2.command("import").argument("<file>", "JSON file to import (use - for stdin)").option("--json", "Output results as JSON", false).option("--for <agent>", "Install for agent: claude, codex, gemini, or all").option("--scope <scope>", "Install scope: global or project", "global").option("--dry-run", "Show what would be installed without actually installing", false).description("Import and install skills from a JSON export file").action(async (file2, options) => {
37632
+ let raw;
37633
+ try {
37634
+ if (file2 === "-") {
37635
+ raw = await new Response(process.stdin).text();
37636
+ } else {
37637
+ if (!existsSync5(file2)) {
37638
+ console.error(chalk2.red(`File not found: ${file2}`));
37639
+ process.exitCode = 1;
37640
+ return;
37641
+ }
37642
+ raw = readFileSync4(file2, "utf-8");
37643
+ }
37644
+ } catch (err) {
37645
+ console.error(chalk2.red(`Failed to read file: ${err.message}`));
37646
+ process.exitCode = 1;
37647
+ return;
37648
+ }
37649
+ let payload;
37650
+ try {
37651
+ payload = JSON.parse(raw);
37652
+ } catch {
37653
+ console.error(chalk2.red("Invalid JSON in import file"));
37654
+ process.exitCode = 1;
37655
+ return;
37656
+ }
37657
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.skills)) {
37658
+ console.error(chalk2.red('Invalid format: expected { "version": 1, "skills": [...] }'));
37659
+ process.exitCode = 1;
37660
+ return;
37661
+ }
37662
+ const skillList = payload.skills;
37663
+ const total = skillList.length;
37664
+ if (total === 0) {
37665
+ if (options.json) {
37666
+ console.log(JSON.stringify({ imported: 0, results: [] }));
37667
+ } else {
37668
+ console.log(chalk2.dim("No skills to import"));
37669
+ }
37670
+ return;
37671
+ }
37672
+ if (options.dryRun) {
37673
+ if (options.json) {
37674
+ console.log(JSON.stringify({ dryRun: true, skills: skillList }));
37675
+ } else {
37676
+ console.log(chalk2.bold(`
37677
+ [dry-run] Would install ${total} skill(s):
37678
+ `));
37679
+ for (let i = 0;i < total; i++) {
37680
+ const target = options.for ? ` for ${options.for} (${options.scope})` : " to .skills/";
37681
+ console.log(chalk2.dim(` [${i + 1}/${total}] ${skillList[i]}${target}`));
37682
+ }
37683
+ }
37684
+ return;
37685
+ }
37686
+ const results = [];
37687
+ if (options.for) {
37688
+ let agents;
37689
+ try {
37690
+ agents = resolveAgents(options.for);
37691
+ } catch (err) {
37692
+ console.error(chalk2.red(err.message));
37693
+ process.exitCode = 1;
37694
+ return;
37695
+ }
37696
+ for (let i = 0;i < total; i++) {
37697
+ const name = skillList[i];
37698
+ if (!options.json) {
37699
+ process.stdout.write(`[${i + 1}/${total}] Installing ${name}...`);
37700
+ }
37701
+ const agentResults = agents.map((agent) => installSkillForAgent(name, { agent, scope: options.scope }, generateSkillMd));
37702
+ const success2 = agentResults.every((r) => r.success);
37703
+ results.push({ skill: name, success: success2, agentResults });
37704
+ if (!options.json) {
37705
+ console.log(success2 ? " done" : " failed");
37706
+ }
37707
+ }
37708
+ } else {
37709
+ for (let i = 0;i < total; i++) {
37710
+ const name = skillList[i];
37711
+ if (!options.json) {
37712
+ process.stdout.write(`[${i + 1}/${total}] Installing ${name}...`);
37713
+ }
37714
+ const result = installSkill(name);
37715
+ results.push(result);
37716
+ if (!options.json) {
37717
+ console.log(result.success ? " done" : " failed");
37718
+ }
37719
+ }
37720
+ }
37721
+ if (options.json) {
37722
+ console.log(JSON.stringify({ imported: results.filter((r) => r.success).length, total, results }));
37723
+ } else {
37724
+ const succeeded = results.filter((r) => r.success).length;
37725
+ const failed = total - succeeded;
37726
+ console.log(chalk2.bold(`
37727
+ Imported ${succeeded}/${total} skill(s)${failed > 0 ? ` (${failed} failed)` : ""}`));
37728
+ }
37729
+ if (results.some((r) => !r.success)) {
37730
+ process.exitCode = 1;
37731
+ }
37732
+ });
37342
37733
  program2.command("doctor").option("--json", "Output as JSON", false).description("Check environment variables for installed skills").action((options) => {
37343
37734
  const installed = getInstalledSkills();
37344
37735
  if (installed.length === 0) {
@@ -37377,6 +37768,247 @@ Skills Doctor (${installed.length} installed):
37377
37768
  }
37378
37769
  }
37379
37770
  });
37771
+ program2.command("auth").argument("[skill]", "Skill name (omit to check all installed skills)").option("--set <assignment>", "Set an env var in .env file (format: KEY=VALUE)").option("--json", "Output as JSON", false).description("Show auth/env var status for a skill or all installed skills").action((name, options) => {
37772
+ const cwd = process.cwd();
37773
+ const envFilePath = join5(cwd, ".env");
37774
+ if (options.set) {
37775
+ const eqIdx = options.set.indexOf("=");
37776
+ if (eqIdx === -1) {
37777
+ console.error(chalk2.red(`Invalid format for --set. Expected KEY=VALUE, got: ${options.set}`));
37778
+ process.exitCode = 1;
37779
+ return;
37780
+ }
37781
+ const key = options.set.slice(0, eqIdx).trim();
37782
+ const value = options.set.slice(eqIdx + 1);
37783
+ if (!key) {
37784
+ console.error(chalk2.red("Key cannot be empty"));
37785
+ process.exitCode = 1;
37786
+ return;
37787
+ }
37788
+ let existing = "";
37789
+ if (existsSync5(envFilePath)) {
37790
+ existing = readFileSync4(envFilePath, "utf-8");
37791
+ }
37792
+ const keyPattern = new RegExp(`^${key}=.*$`, "m");
37793
+ let updated;
37794
+ if (keyPattern.test(existing)) {
37795
+ updated = existing.replace(keyPattern, `${key}=${value}`);
37796
+ } else {
37797
+ updated = existing.endsWith(`
37798
+ `) || existing === "" ? existing + `${key}=${value}
37799
+ ` : existing + `
37800
+ ${key}=${value}
37801
+ `;
37802
+ }
37803
+ writeFileSync2(envFilePath, updated, "utf-8");
37804
+ console.log(chalk2.green(`Set ${key} in ${envFilePath}`));
37805
+ return;
37806
+ }
37807
+ if (name) {
37808
+ const reqs = getSkillRequirements(name);
37809
+ if (!reqs) {
37810
+ console.error(`Skill '${name}' not found`);
37811
+ process.exitCode = 1;
37812
+ return;
37813
+ }
37814
+ const envVars = reqs.envVars.map((v) => ({ name: v, set: !!process.env[v] }));
37815
+ if (options.json) {
37816
+ console.log(JSON.stringify({ skill: name, envVars }, null, 2));
37817
+ return;
37818
+ }
37819
+ console.log(chalk2.bold(`
37820
+ Auth status for ${name}:
37821
+ `));
37822
+ if (envVars.length === 0) {
37823
+ console.log(chalk2.dim(" No environment variables required"));
37824
+ } else {
37825
+ for (const v of envVars) {
37826
+ const icon = v.set ? chalk2.green("\u2713") : chalk2.red("\u2717");
37827
+ const status = v.set ? chalk2.green("set") : chalk2.red("missing");
37828
+ console.log(` ${icon} ${v.name} (${status})`);
37829
+ }
37830
+ }
37831
+ return;
37832
+ }
37833
+ const installed = getInstalledSkills();
37834
+ if (installed.length === 0) {
37835
+ if (options.json) {
37836
+ console.log(JSON.stringify([]));
37837
+ } else {
37838
+ console.log("No skills installed");
37839
+ }
37840
+ return;
37841
+ }
37842
+ const report = [];
37843
+ for (const skillName of installed) {
37844
+ const reqs = getSkillRequirements(skillName);
37845
+ const envVars = (reqs?.envVars ?? []).map((v) => ({ name: v, set: !!process.env[v] }));
37846
+ report.push({ skill: skillName, envVars });
37847
+ }
37848
+ if (options.json) {
37849
+ console.log(JSON.stringify(report, null, 2));
37850
+ return;
37851
+ }
37852
+ console.log(chalk2.bold(`
37853
+ Auth status (${installed.length} installed skills):
37854
+ `));
37855
+ for (const entry of report) {
37856
+ console.log(chalk2.bold(` ${entry.skill}`));
37857
+ if (entry.envVars.length === 0) {
37858
+ console.log(chalk2.dim(" No environment variables required"));
37859
+ } else {
37860
+ for (const v of entry.envVars) {
37861
+ const icon = v.set ? chalk2.green("\u2713") : chalk2.red("\u2717");
37862
+ const status = v.set ? chalk2.green("set") : chalk2.red("missing");
37863
+ console.log(` ${icon} ${v.name} (${status})`);
37864
+ }
37865
+ }
37866
+ }
37867
+ });
37868
+ program2.command("whoami").option("--json", "Output as JSON", false).description("Show setup summary: version, installed skills, agent configs, and paths").action((options) => {
37869
+ const { homedir: homedir3 } = __require("os");
37870
+ const version2 = package_default.version;
37871
+ const cwd = process.cwd();
37872
+ const installed = getInstalledSkills();
37873
+ const agentNames = ["claude", "codex", "gemini"];
37874
+ const agentConfigs = [];
37875
+ for (const agent of agentNames) {
37876
+ const agentSkillsPath = join5(homedir3(), `.${agent}`, "skills");
37877
+ const exists = existsSync5(agentSkillsPath);
37878
+ let skillCount = 0;
37879
+ if (exists) {
37880
+ try {
37881
+ skillCount = readdirSync4(agentSkillsPath).filter((f) => {
37882
+ const full = join5(agentSkillsPath, f);
37883
+ return f.startsWith("skill-") && statSync3(full).isDirectory();
37884
+ }).length;
37885
+ } catch {}
37886
+ }
37887
+ agentConfigs.push({ agent, path: agentSkillsPath, exists, skillCount });
37888
+ }
37889
+ const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
37890
+ if (options.json) {
37891
+ console.log(JSON.stringify({
37892
+ version: version2,
37893
+ installedCount: installed.length,
37894
+ installed,
37895
+ agents: agentConfigs,
37896
+ skillsDir,
37897
+ cwd
37898
+ }, null, 2));
37899
+ return;
37900
+ }
37901
+ console.log(chalk2.bold(`
37902
+ skills v${version2}
37903
+ `));
37904
+ console.log(`${chalk2.dim("Working directory:")} ${cwd}`);
37905
+ console.log(`${chalk2.dim("Skills directory:")} ${skillsDir}`);
37906
+ console.log();
37907
+ if (installed.length === 0) {
37908
+ console.log(chalk2.dim("No skills installed in current project"));
37909
+ } else {
37910
+ console.log(chalk2.bold(`Installed skills (${installed.length}):`));
37911
+ for (const name of installed) {
37912
+ console.log(` ${chalk2.cyan(name)}`);
37913
+ }
37914
+ }
37915
+ console.log();
37916
+ console.log(chalk2.bold("Agent configurations:"));
37917
+ for (const cfg of agentConfigs) {
37918
+ if (cfg.exists) {
37919
+ console.log(` ${chalk2.green("\u2713")} ${cfg.agent} \u2014 ${cfg.skillCount} skill(s) at ${cfg.path}`);
37920
+ } else {
37921
+ console.log(` ${chalk2.dim("\u2717")} ${cfg.agent} \u2014 not configured`);
37922
+ }
37923
+ }
37924
+ });
37925
+ program2.command("test").argument("[skill]", "Skill name to test (omit to test all installed skills)").option("--json", "Output results as JSON", false).description("Test skill readiness: env vars, system deps, and npm deps").action(async (skillArg, options) => {
37926
+ let skillNames;
37927
+ if (skillArg) {
37928
+ const registryName = skillArg.startsWith("skill-") ? skillArg.replace("skill-", "") : skillArg;
37929
+ const skill = getSkill(registryName);
37930
+ if (!skill) {
37931
+ if (options.json) {
37932
+ console.log(JSON.stringify({ error: `Skill '${skillArg}' not found` }));
37933
+ } else {
37934
+ console.error(chalk2.red(`Skill '${skillArg}' not found`));
37935
+ }
37936
+ process.exitCode = 1;
37937
+ return;
37938
+ }
37939
+ skillNames = [registryName];
37940
+ } else {
37941
+ skillNames = getInstalledSkills();
37942
+ if (skillNames.length === 0) {
37943
+ if (options.json) {
37944
+ console.log(JSON.stringify([]));
37945
+ } else {
37946
+ console.log(chalk2.dim("No skills installed. Run: skills install <name>"));
37947
+ }
37948
+ return;
37949
+ }
37950
+ }
37951
+ const results = [];
37952
+ for (const name of skillNames) {
37953
+ const reqs = getSkillRequirements(name);
37954
+ const envVars = (reqs?.envVars ?? []).map((v) => ({
37955
+ name: v,
37956
+ set: !!process.env[v]
37957
+ }));
37958
+ const systemDeps = (reqs?.systemDeps ?? []).map((dep) => {
37959
+ const proc = Bun.spawnSync(["which", dep]);
37960
+ return { name: dep, available: proc.exitCode === 0 };
37961
+ });
37962
+ const npmDeps = Object.entries(reqs?.dependencies ?? {}).map(([pkgName, version2]) => ({
37963
+ name: pkgName,
37964
+ version: version2
37965
+ }));
37966
+ const ready = envVars.every((v) => v.set) && systemDeps.every((d) => d.available);
37967
+ results.push({ skill: name, envVars, systemDeps, npmDeps, ready });
37968
+ }
37969
+ if (options.json) {
37970
+ console.log(JSON.stringify(results, null, 2));
37971
+ return;
37972
+ }
37973
+ const allReady = results.every((r) => r.ready);
37974
+ console.log(chalk2.bold(`
37975
+ Skills Test (${results.length} skill${results.length === 1 ? "" : "s"}):
37976
+ `));
37977
+ for (const result of results) {
37978
+ const readyLabel = result.ready ? chalk2.green("ready") : chalk2.red("not ready");
37979
+ console.log(chalk2.bold(` ${result.skill}`) + chalk2.dim(` [${readyLabel}]`));
37980
+ if (result.envVars.length === 0 && result.systemDeps.length === 0) {
37981
+ console.log(chalk2.dim(" No requirements"));
37982
+ }
37983
+ for (const v of result.envVars) {
37984
+ if (v.set) {
37985
+ console.log(` ${chalk2.green("\u2713")} ${v.name}`);
37986
+ } else {
37987
+ console.log(` ${chalk2.red("\u2717")} ${v.name} ${chalk2.dim("(missing)")}`);
37988
+ }
37989
+ }
37990
+ for (const dep of result.systemDeps) {
37991
+ if (dep.available) {
37992
+ console.log(` ${chalk2.green("\u2713")} ${dep.name} ${chalk2.dim("(system)")}`);
37993
+ } else {
37994
+ console.log(` ${chalk2.red("\u2717")} ${dep.name} ${chalk2.dim("(not installed)")}`);
37995
+ }
37996
+ }
37997
+ if (result.npmDeps.length > 0) {
37998
+ console.log(chalk2.dim(` npm: ${result.npmDeps.map((d) => d.name).join(", ")}`));
37999
+ }
38000
+ }
38001
+ console.log();
38002
+ if (allReady) {
38003
+ console.log(chalk2.green(`All ${results.length} skill(s) ready`));
38004
+ } else {
38005
+ const notReady = results.filter((r) => !r.ready).length;
38006
+ console.log(chalk2.yellow(`${notReady} skill(s) not ready`));
38007
+ }
38008
+ if (!allReady) {
38009
+ process.exitCode = 1;
38010
+ }
38011
+ });
37380
38012
  program2.command("outdated").option("--json", "Output as JSON", false).description("Check for outdated installed skills").action((options) => {
37381
38013
  const installed = getInstalledSkills();
37382
38014
  if (installed.length === 0) {
@@ -37392,17 +38024,17 @@ program2.command("outdated").option("--json", "Output as JSON", false).descripti
37392
38024
  const upToDate = [];
37393
38025
  for (const name of installed) {
37394
38026
  const skillName = normalizeSkillName(name);
37395
- const installedPkgPath = join4(cwd, ".skills", skillName, "package.json");
38027
+ const installedPkgPath = join5(cwd, ".skills", skillName, "package.json");
37396
38028
  let installedVersion = "unknown";
37397
- if (existsSync4(installedPkgPath)) {
38029
+ if (existsSync5(installedPkgPath)) {
37398
38030
  try {
37399
38031
  installedVersion = JSON.parse(readFileSync4(installedPkgPath, "utf-8")).version || "unknown";
37400
38032
  } catch {}
37401
38033
  }
37402
38034
  const registryPath = getSkillPath(name);
37403
- const registryPkgPath = join4(registryPath, "package.json");
38035
+ const registryPkgPath = join5(registryPath, "package.json");
37404
38036
  let registryVersion = "unknown";
37405
- if (existsSync4(registryPkgPath)) {
38037
+ if (existsSync5(registryPkgPath)) {
37406
38038
  try {
37407
38039
  registryVersion = JSON.parse(readFileSync4(registryPkgPath, "utf-8")).version || "unknown";
37408
38040
  } catch {}
package/bin/mcp.js CHANGED
@@ -28596,10 +28596,15 @@ class StdioServerTransport {
28596
28596
  });
28597
28597
  }
28598
28598
  }
28599
+
28600
+ // src/mcp/index.ts
28601
+ import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
28602
+ import { join as join3 } from "path";
28603
+ import { homedir as homedir2 } from "os";
28599
28604
  // package.json
28600
28605
  var package_default = {
28601
28606
  name: "@hasna/skills",
28602
- version: "0.1.6",
28607
+ version: "0.1.8",
28603
28608
  description: "Skills library for AI coding agents",
28604
28609
  type: "module",
28605
28610
  bin: {
@@ -28640,6 +28645,12 @@ var package_default = {
28640
28645
  "typescript",
28641
28646
  "bun",
28642
28647
  "claude",
28648
+ "codex",
28649
+ "gemini",
28650
+ "mcp",
28651
+ "model-context-protocol",
28652
+ "open-source",
28653
+ "skill-library",
28643
28654
  "automation"
28644
28655
  ],
28645
28656
  author: "Hasna",
@@ -30703,6 +30714,52 @@ server.registerTool("install_skill", {
30703
30714
  isError: !result.success
30704
30715
  };
30705
30716
  });
30717
+ server.registerTool("install_category", {
30718
+ title: "Install Category",
30719
+ description: "Install all skills in a category. Optionally install for a specific agent (claude, codex, gemini, or all) with a given scope.",
30720
+ inputSchema: {
30721
+ category: exports_external.string().describe("Category name (case-insensitive, e.g. 'Event Management')"),
30722
+ for: exports_external.string().optional().describe("Agent target: claude, codex, gemini, or all"),
30723
+ scope: exports_external.string().optional().describe("Install scope: global or project (default: global)")
30724
+ }
30725
+ }, async ({ category, for: agentArg, scope }) => {
30726
+ const matchedCategory = CATEGORIES.find((c) => c.toLowerCase() === category.toLowerCase());
30727
+ if (!matchedCategory) {
30728
+ return {
30729
+ content: [{ type: "text", text: `Unknown category: ${category}. Available: ${CATEGORIES.join(", ")}` }],
30730
+ isError: true
30731
+ };
30732
+ }
30733
+ const categorySkills = getSkillsByCategory(matchedCategory);
30734
+ const names = categorySkills.map((s) => s.name);
30735
+ if (agentArg) {
30736
+ let agents;
30737
+ try {
30738
+ agents = resolveAgents(agentArg);
30739
+ } catch (err) {
30740
+ return {
30741
+ content: [{ type: "text", text: err.message }],
30742
+ isError: true
30743
+ };
30744
+ }
30745
+ const results2 = [];
30746
+ for (const name of names) {
30747
+ for (const a of agents) {
30748
+ const r = installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd);
30749
+ results2.push({ ...r, agent: a, scope: scope || "global" });
30750
+ }
30751
+ }
30752
+ return {
30753
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results: results2 }, null, 2) }],
30754
+ isError: results2.some((r) => !r.success)
30755
+ };
30756
+ }
30757
+ const results = names.map((name) => installSkill(name));
30758
+ return {
30759
+ content: [{ type: "text", text: JSON.stringify({ category: matchedCategory, count: names.length, results }, null, 2) }],
30760
+ isError: results.some((r) => !r.success)
30761
+ };
30762
+ });
30706
30763
  server.registerTool("remove_skill", {
30707
30764
  title: "Remove Skill",
30708
30765
  description: "Remove an installed skill. Without --for, removes from .skills/. With --for, removes from agent skill directory.",
@@ -30795,6 +30852,94 @@ server.registerTool("run_skill", {
30795
30852
  content: [{ type: "text", text: JSON.stringify({ exitCode: result.exitCode, skill: name }, null, 2) }]
30796
30853
  };
30797
30854
  });
30855
+ server.registerTool("export_skills", {
30856
+ title: "Export Skills",
30857
+ description: "Export the list of currently installed skills as a JSON payload that can be imported elsewhere"
30858
+ }, async () => {
30859
+ const skills = getInstalledSkills();
30860
+ const payload = {
30861
+ version: 1,
30862
+ skills,
30863
+ timestamp: new Date().toISOString()
30864
+ };
30865
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
30866
+ });
30867
+ server.registerTool("import_skills", {
30868
+ title: "Import Skills",
30869
+ description: "Install a list of skills from an export payload. Supports agent-specific installs via the 'for' parameter.",
30870
+ inputSchema: {
30871
+ skills: exports_external.array(exports_external.string()).describe("List of skill names to install"),
30872
+ for: exports_external.string().optional().describe("Agent target: claude, codex, gemini, or all"),
30873
+ scope: exports_external.string().optional().describe("Install scope: global or project (default: global)")
30874
+ }
30875
+ }, async ({ skills: skillList, for: agentArg, scope }) => {
30876
+ if (!skillList || skillList.length === 0) {
30877
+ return { content: [{ type: "text", text: JSON.stringify({ imported: 0, results: [] }, null, 2) }] };
30878
+ }
30879
+ const results = [];
30880
+ if (agentArg) {
30881
+ let agents;
30882
+ try {
30883
+ agents = resolveAgents(agentArg);
30884
+ } catch (err) {
30885
+ return {
30886
+ content: [{ type: "text", text: err.message }],
30887
+ isError: true
30888
+ };
30889
+ }
30890
+ for (const name of skillList) {
30891
+ const agentResults = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
30892
+ const success2 = agentResults.every((r) => r.success);
30893
+ const errors3 = agentResults.filter((r) => !r.success).map((r) => r.error).filter(Boolean);
30894
+ results.push({ skill: name, success: success2, ...errors3.length > 0 ? { error: errors3.join("; ") } : {} });
30895
+ }
30896
+ } else {
30897
+ for (const name of skillList) {
30898
+ const result = installSkill(name);
30899
+ results.push({ skill: result.skill, success: result.success, ...result.error ? { error: result.error } : {} });
30900
+ }
30901
+ }
30902
+ const imported = results.filter((r) => r.success).length;
30903
+ const hasErrors = results.some((r) => !r.success);
30904
+ return {
30905
+ content: [{ type: "text", text: JSON.stringify({ imported, total: skillList.length, results }, null, 2) }],
30906
+ isError: hasErrors
30907
+ };
30908
+ });
30909
+ server.registerTool("whoami", {
30910
+ title: "Skills Whoami",
30911
+ description: "Show setup summary: package version, installed skills, agent configurations, skills directory location, and working directory"
30912
+ }, async () => {
30913
+ const version2 = package_default.version;
30914
+ const cwd = process.cwd();
30915
+ const installed = getInstalledSkills();
30916
+ const agentNames = ["claude", "codex", "gemini"];
30917
+ const agents = [];
30918
+ for (const agent of agentNames) {
30919
+ const agentSkillsPath = join3(homedir2(), `.${agent}`, "skills");
30920
+ const exists = existsSync3(agentSkillsPath);
30921
+ let skillCount = 0;
30922
+ if (exists) {
30923
+ try {
30924
+ skillCount = readdirSync3(agentSkillsPath).filter((f) => {
30925
+ const full = join3(agentSkillsPath, f);
30926
+ return f.startsWith("skill-") && statSync2(full).isDirectory();
30927
+ }).length;
30928
+ } catch {}
30929
+ }
30930
+ agents.push({ agent, path: agentSkillsPath, exists, skillCount });
30931
+ }
30932
+ const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
30933
+ const result = {
30934
+ version: version2,
30935
+ installedCount: installed.length,
30936
+ installed,
30937
+ agents,
30938
+ skillsDir,
30939
+ cwd
30940
+ };
30941
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
30942
+ });
30798
30943
  server.registerResource("Skills Registry", "skills://registry", {
30799
30944
  description: "Full list of all available skills as JSON"
30800
30945
  }, async () => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",