@hasna/skills 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -4,6 +4,7 @@ var __create = Object.create;
4
4
  var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  function __accessProp(key) {
9
10
  return this[key];
@@ -30,6 +31,23 @@ var __toESM = (mod, isNodeMode, target) => {
30
31
  cache.set(mod, to);
31
32
  return to;
32
33
  };
34
+ var __toCommonJS = (from) => {
35
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
36
+ if (entry)
37
+ return entry;
38
+ entry = __defProp({}, "__esModule", { value: true });
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (var key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(entry, key))
42
+ __defProp(entry, key, {
43
+ get: __accessProp.bind(from, key),
44
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
+ });
46
+ }
47
+ __moduleCache.set(from, entry);
48
+ return entry;
49
+ };
50
+ var __moduleCache;
33
51
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
52
  var __returnValue = (v) => v;
35
53
  function __exportSetter(name, newValue) {
@@ -1896,7 +1914,7 @@ var package_default;
1896
1914
  var init_package = __esm(() => {
1897
1915
  package_default = {
1898
1916
  name: "@hasna/skills",
1899
- version: "0.1.14",
1917
+ version: "0.1.16",
1900
1918
  description: "Skills library for AI coding agents",
1901
1919
  type: "module",
1902
1920
  bin: {
@@ -1956,6 +1974,7 @@ var init_package = __esm(() => {
1956
1974
  typescript: "^5"
1957
1975
  },
1958
1976
  dependencies: {
1977
+ "@hasna/cloud": "^0.1.0",
1959
1978
  "@modelcontextprotocol/sdk": "^1.26.0",
1960
1979
  chalk: "^5.3.0",
1961
1980
  commander: "^12.1.0",
@@ -1981,8 +2000,94 @@ var init_package = __esm(() => {
1981
2000
  });
1982
2001
 
1983
2002
  // src/lib/registry.ts
2003
+ import { existsSync, readFileSync, readdirSync } from "fs";
2004
+ import { join } from "path";
2005
+ import { homedir } from "os";
2006
+ function parseSkillMdFrontmatter(content) {
2007
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
2008
+ if (!match)
2009
+ return null;
2010
+ const result = {};
2011
+ for (const line of match[1].split(`
2012
+ `)) {
2013
+ const colon = line.indexOf(":");
2014
+ if (colon === -1)
2015
+ continue;
2016
+ const key = line.slice(0, colon).trim();
2017
+ const value = line.slice(colon + 1).trim();
2018
+ if (!key || !value)
2019
+ continue;
2020
+ if (key === "name")
2021
+ result.name = value;
2022
+ else if (key === "description")
2023
+ result.description = value;
2024
+ else if (key === "displayName" || key === "display_name")
2025
+ result.displayName = value;
2026
+ else if (key === "category")
2027
+ result.category = value;
2028
+ else if (key === "tags") {
2029
+ result.tags = value.replace(/[\[\]]/g, "").split(",").map((t) => t.trim()).filter(Boolean);
2030
+ }
2031
+ }
2032
+ return Object.keys(result).length > 0 ? result : null;
2033
+ }
2034
+ function discoverSkillsInDir(dir) {
2035
+ if (!existsSync(dir))
2036
+ return [];
2037
+ const result = [];
2038
+ try {
2039
+ const entries = readdirSync(dir, { withFileTypes: true });
2040
+ for (const entry of entries) {
2041
+ if (!entry.isDirectory())
2042
+ continue;
2043
+ const skillMdPath = join(dir, entry.name, "SKILL.md");
2044
+ if (!existsSync(skillMdPath))
2045
+ continue;
2046
+ let content;
2047
+ try {
2048
+ content = readFileSync(skillMdPath, "utf-8");
2049
+ } catch {
2050
+ continue;
2051
+ }
2052
+ const fm = parseSkillMdFrontmatter(content);
2053
+ if (!fm?.name)
2054
+ continue;
2055
+ const name = fm.name.replace(/^skill-/, "");
2056
+ result.push({
2057
+ name,
2058
+ displayName: fm.displayName || name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
2059
+ description: fm.description || "",
2060
+ category: fm.category || "Development Tools",
2061
+ tags: fm.tags || [],
2062
+ source: "custom"
2063
+ });
2064
+ }
2065
+ } catch {}
2066
+ return result;
2067
+ }
2068
+ function loadRegistry(cwd) {
2069
+ const now = Date.now();
2070
+ if (_registryCache && now - _registryCacheTime < REGISTRY_CACHE_TTL) {
2071
+ return _registryCache;
2072
+ }
2073
+ const official = SKILLS.map((s) => ({ ...s, source: "official" }));
2074
+ const globalCustomNew = discoverSkillsInDir(join(homedir(), ".hasna", "skills", "custom"));
2075
+ const globalCustomOld = discoverSkillsInDir(join(homedir(), ".skills"));
2076
+ const oldNames = new Set(globalCustomNew.map((s) => s.name));
2077
+ const globalCustom = [...globalCustomNew, ...globalCustomOld.filter((s) => !oldNames.has(s.name))];
2078
+ const projectCustom = discoverSkillsInDir(join(cwd || process.cwd(), ".skills", "custom-skills"));
2079
+ const customNames = new Set([...globalCustom, ...projectCustom].map((s) => s.name));
2080
+ const filtered = official.filter((s) => !customNames.has(s.name));
2081
+ _registryCache = [...filtered, ...globalCustom, ...projectCustom];
2082
+ _registryCacheTime = now;
2083
+ return _registryCache;
2084
+ }
2085
+ function clearRegistryCache() {
2086
+ _registryCache = null;
2087
+ _registryCacheTime = 0;
2088
+ }
1984
2089
  function getSkillsByCategory(category) {
1985
- return SKILLS.filter((s) => s.category === category);
2090
+ return loadRegistry().filter((s) => s.category === category);
1986
2091
  }
1987
2092
  function editDistance(a, b) {
1988
2093
  if (a === b)
@@ -2028,7 +2133,7 @@ function searchSkills(query) {
2028
2133
  if (words.length === 0)
2029
2134
  return [];
2030
2135
  const scored = [];
2031
- for (const skill of SKILLS) {
2136
+ for (const skill of loadRegistry()) {
2032
2137
  const nameLower = skill.name.toLowerCase();
2033
2138
  const displayNameLower = skill.displayName.toLowerCase();
2034
2139
  const descriptionLower = skill.description.toLowerCase();
@@ -2064,7 +2169,7 @@ function searchSkills(query) {
2064
2169
  return scored.map((s) => s.skill);
2065
2170
  }
2066
2171
  function getSkill(name) {
2067
- return SKILLS.find((s) => s.name === name);
2172
+ return loadRegistry().find((s) => s.name === name);
2068
2173
  }
2069
2174
  function levenshtein(a, b) {
2070
2175
  const m = a.length, n = b.length;
@@ -2078,10 +2183,10 @@ function levenshtein(a, b) {
2078
2183
  }
2079
2184
  function findSimilarSkills(query, maxResults = 3) {
2080
2185
  const q = query.toLowerCase();
2081
- const scored = SKILLS.map((s) => ({ name: s.name, dist: levenshtein(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
2186
+ const scored = loadRegistry().map((s) => ({ name: s.name, dist: levenshtein(q, s.name.toLowerCase()) })).filter((s) => s.dist <= Math.max(3, Math.floor(q.length / 2))).sort((a, b) => a.dist - b.dist);
2082
2187
  return scored.slice(0, maxResults).map((s) => s.name);
2083
2188
  }
2084
- var CATEGORIES, SKILLS;
2189
+ var CATEGORIES, SKILLS, _registryCache = null, _registryCacheTime = 0, REGISTRY_CACHE_TTL = 5000;
2085
2190
  var init_registry = __esm(() => {
2086
2191
  CATEGORIES = [
2087
2192
  "Development Tools",
@@ -3119,6 +3224,20 @@ var init_registry = __esm(() => {
3119
3224
  category: "Design & Branding",
3120
3225
  tags: ["testimonials", "graphics", "social-proof", "marketing"]
3121
3226
  },
3227
+ {
3228
+ name: "colorextract",
3229
+ displayName: "Color Extract",
3230
+ description: "Extract complete color palettes from screenshots and images using Claude Vision. Outputs open-styles compatible profiles.",
3231
+ category: "Design & Branding",
3232
+ tags: ["colors", "palette", "design", "vision", "screenshot", "extract", "open-styles"]
3233
+ },
3234
+ {
3235
+ name: "siteanalyze",
3236
+ displayName: "Site Analyze",
3237
+ description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
3238
+ category: "Design & Branding",
3239
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
3240
+ },
3122
3241
  {
3123
3242
  name: "browse",
3124
3243
  displayName: "Browse",
@@ -5162,39 +5281,39 @@ var require_cli_spinners = __commonJS((exports, module) => {
5162
5281
  });
5163
5282
 
5164
5283
  // src/lib/installer.ts
5165
- import { existsSync, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync, accessSync, constants } from "fs";
5166
- import { join, dirname } from "path";
5167
- import { homedir } from "os";
5284
+ import { existsSync as existsSync2, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, accessSync, constants } from "fs";
5285
+ import { join as join2, dirname } from "path";
5286
+ import { homedir as homedir2 } from "os";
5168
5287
  import { fileURLToPath } from "url";
5169
5288
  function findSkillsDir() {
5170
5289
  let dir = __dirname2;
5171
5290
  for (let i = 0;i < 5; i++) {
5172
- const candidate = join(dir, "skills");
5173
- if (existsSync(candidate)) {
5291
+ const candidate = join2(dir, "skills");
5292
+ if (existsSync2(candidate) && !dir.includes(".skills")) {
5174
5293
  return candidate;
5175
5294
  }
5176
5295
  dir = dirname(dir);
5177
5296
  }
5178
- return join(__dirname2, "..", "skills");
5297
+ return join2(__dirname2, "..", "skills");
5179
5298
  }
5180
5299
  function getSkillPath(name) {
5181
5300
  const skillName = normalizeSkillName(name);
5182
- return join(SKILLS_DIR, skillName);
5301
+ return join2(SKILLS_DIR, skillName);
5183
5302
  }
5184
5303
  function installSkill(name, options = {}) {
5185
5304
  const { targetDir = process.cwd(), overwrite = false } = options;
5186
5305
  const skillName = normalizeSkillName(name);
5187
5306
  const sourcePath = getSkillPath(name);
5188
- const destDir = join(targetDir, ".skills");
5189
- const destPath = join(destDir, skillName);
5190
- if (!existsSync(sourcePath)) {
5307
+ const destDir = join2(targetDir, ".skills");
5308
+ const destPath = join2(destDir, skillName);
5309
+ if (!existsSync2(sourcePath)) {
5191
5310
  return {
5192
5311
  skill: name,
5193
5312
  success: false,
5194
5313
  error: `Skill '${name}' not found`
5195
5314
  };
5196
5315
  }
5197
- if (existsSync(destPath) && !overwrite) {
5316
+ if (existsSync2(destPath) && !overwrite) {
5198
5317
  return {
5199
5318
  skill: name,
5200
5319
  success: false,
@@ -5203,10 +5322,10 @@ function installSkill(name, options = {}) {
5203
5322
  };
5204
5323
  }
5205
5324
  try {
5206
- if (!existsSync(destDir)) {
5325
+ if (!existsSync2(destDir)) {
5207
5326
  mkdirSync(destDir, { recursive: true });
5208
5327
  }
5209
- if (existsSync(destPath) && overwrite) {
5328
+ if (existsSync2(destPath) && overwrite) {
5210
5329
  rmSync(destPath, { recursive: true, force: true });
5211
5330
  }
5212
5331
  cpSync(sourcePath, destPath, {
@@ -5242,10 +5361,10 @@ function installSkill(name, options = {}) {
5242
5361
  }
5243
5362
  }
5244
5363
  function updateSkillsIndex(skillsDir) {
5245
- const indexPath = join(skillsDir, "index.ts");
5364
+ const indexPath = join2(skillsDir, "index.ts");
5246
5365
  const meta = loadMeta(skillsDir);
5247
5366
  const disabledSet = new Set(meta.disabled || []);
5248
- const skills = readdirSync(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
5367
+ const skills = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
5249
5368
  const exports = skills.map((s) => {
5250
5369
  const name = s.replace("skill-", "").replace(/-/g, "_");
5251
5370
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -5261,13 +5380,13 @@ ${exports}
5261
5380
  writeFileSync(indexPath, content);
5262
5381
  }
5263
5382
  function getMetaPath(skillsDir) {
5264
- return join(skillsDir, ".meta.json");
5383
+ return join2(skillsDir, ".meta.json");
5265
5384
  }
5266
5385
  function loadMeta(skillsDir) {
5267
5386
  const metaPath = getMetaPath(skillsDir);
5268
- if (existsSync(metaPath)) {
5387
+ if (existsSync2(metaPath)) {
5269
5388
  try {
5270
- return JSON.parse(readFileSync(metaPath, "utf-8"));
5389
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
5271
5390
  } catch {}
5272
5391
  }
5273
5392
  return { skills: {} };
@@ -5280,9 +5399,9 @@ function recordInstall(skillsDir, name) {
5280
5399
  const skillName = normalizeSkillName(name);
5281
5400
  let version = "unknown";
5282
5401
  try {
5283
- const pkgPath = join(skillsDir, skillName, "package.json");
5284
- if (existsSync(pkgPath)) {
5285
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
5402
+ const pkgPath = join2(skillsDir, skillName, "package.json");
5403
+ if (existsSync2(pkgPath)) {
5404
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5286
5405
  version = pkg.version || "unknown";
5287
5406
  }
5288
5407
  } catch {}
@@ -5295,23 +5414,23 @@ function recordRemove(skillsDir, name) {
5295
5414
  saveMeta(skillsDir, meta);
5296
5415
  }
5297
5416
  function getInstallMeta(targetDir = process.cwd()) {
5298
- return loadMeta(join(targetDir, ".skills"));
5417
+ return loadMeta(join2(targetDir, ".skills"));
5299
5418
  }
5300
5419
  function getInstalledSkills(targetDir = process.cwd()) {
5301
- const skillsDir = join(targetDir, ".skills");
5302
- if (!existsSync(skillsDir)) {
5420
+ const skillsDir = join2(targetDir, ".skills");
5421
+ if (!existsSync2(skillsDir)) {
5303
5422
  return [];
5304
5423
  }
5305
- return readdirSync(skillsDir).filter((f) => {
5306
- const fullPath = join(skillsDir, f);
5424
+ return readdirSync2(skillsDir).filter((f) => {
5425
+ const fullPath = join2(skillsDir, f);
5307
5426
  return f.startsWith("skill-") && statSync(fullPath).isDirectory();
5308
5427
  }).map((f) => f.replace("skill-", ""));
5309
5428
  }
5310
5429
  function removeSkill(name, targetDir = process.cwd()) {
5311
5430
  const skillName = normalizeSkillName(name);
5312
- const skillsDir = join(targetDir, ".skills");
5313
- const skillPath = join(skillsDir, skillName);
5314
- if (!existsSync(skillPath)) {
5431
+ const skillsDir = join2(targetDir, ".skills");
5432
+ const skillPath = join2(skillsDir, skillName);
5433
+ if (!existsSync2(skillPath)) {
5315
5434
  return false;
5316
5435
  }
5317
5436
  rmSync(skillPath, { recursive: true, force: true });
@@ -5329,27 +5448,31 @@ function resolveAgents(agentArg) {
5329
5448
  return [agent];
5330
5449
  }
5331
5450
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
5332
- const agentDir = `.${agent}`;
5333
- if (scope === "project") {
5334
- return join(projectDir || process.cwd(), agentDir, "skills");
5451
+ const base = projectDir || process.cwd();
5452
+ switch (agent) {
5453
+ case "pi":
5454
+ return scope === "project" ? join2(base, ".pi", "skills") : join2(homedir2(), ".pi", "agent", "skills");
5455
+ case "opencode":
5456
+ return scope === "project" ? join2(base, ".opencode", "skills") : join2(homedir2(), ".opencode", "skills");
5457
+ default:
5458
+ return scope === "project" ? join2(base, `.${agent}`, "skills") : join2(homedir2(), `.${agent}`, "skills");
5335
5459
  }
5336
- return join(homedir(), agentDir, "skills");
5337
5460
  }
5338
5461
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
5339
5462
  const skillName = normalizeSkillName(name);
5340
- return join(getAgentSkillsDir(agent, scope, projectDir), skillName);
5463
+ return join2(getAgentSkillsDir(agent, scope, projectDir), skillName);
5341
5464
  }
5342
5465
  function installSkillForAgent(name, options, generateSkillMd) {
5343
5466
  const { agent, scope = "global", projectDir } = options;
5344
5467
  const skillName = normalizeSkillName(name);
5345
5468
  const sourcePath = getSkillPath(name);
5346
- if (!existsSync(sourcePath)) {
5469
+ if (!existsSync2(sourcePath)) {
5347
5470
  return { skill: name, success: false, error: `Skill '${name}' not found` };
5348
5471
  }
5349
5472
  let skillMdContent = null;
5350
- const skillMdPath = join(sourcePath, "SKILL.md");
5351
- if (existsSync(skillMdPath)) {
5352
- skillMdContent = readFileSync(skillMdPath, "utf-8");
5473
+ const skillMdPath = join2(sourcePath, "SKILL.md");
5474
+ if (existsSync2(skillMdPath)) {
5475
+ skillMdContent = readFileSync2(skillMdPath, "utf-8");
5353
5476
  } else if (generateSkillMd) {
5354
5477
  skillMdContent = generateSkillMd(name);
5355
5478
  }
@@ -5358,17 +5481,12 @@ function installSkillForAgent(name, options, generateSkillMd) {
5358
5481
  }
5359
5482
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
5360
5483
  if (scope === "global") {
5361
- const agentBaseDir = join(homedir(), `.${agent}`);
5362
- if (!existsSync(agentBaseDir)) {
5363
- const agentLabels = {
5364
- claude: "Claude Code",
5365
- codex: "Codex CLI",
5366
- gemini: "Gemini CLI"
5367
- };
5484
+ const agentBaseDir = agent === "pi" ? join2(homedir2(), ".pi", "agent") : join2(homedir2(), `.${agent}`);
5485
+ if (!existsSync2(agentBaseDir)) {
5368
5486
  return {
5369
5487
  skill: name,
5370
5488
  success: false,
5371
- error: `Agent directory ${agentBaseDir} does not exist. Is ${agentLabels[agent]} installed?`
5489
+ error: `Agent directory ${agentBaseDir} does not exist. Is ${AGENT_LABELS[agent]} installed?`
5372
5490
  };
5373
5491
  }
5374
5492
  try {
@@ -5383,7 +5501,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
5383
5501
  }
5384
5502
  try {
5385
5503
  mkdirSync(destDir, { recursive: true });
5386
- writeFileSync(join(destDir, "SKILL.md"), skillMdContent);
5504
+ writeFileSync(join2(destDir, "SKILL.md"), skillMdContent);
5387
5505
  return { skill: name, success: true, path: destDir };
5388
5506
  } catch (error) {
5389
5507
  return {
@@ -5396,31 +5514,48 @@ function installSkillForAgent(name, options, generateSkillMd) {
5396
5514
  function removeSkillForAgent(name, options) {
5397
5515
  const { agent, scope = "global", projectDir } = options;
5398
5516
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
5399
- if (!existsSync(destDir)) {
5517
+ if (!existsSync2(destDir)) {
5400
5518
  return false;
5401
5519
  }
5402
5520
  rmSync(destDir, { recursive: true, force: true });
5403
5521
  return true;
5404
5522
  }
5405
- var __dirname2, SKILLS_DIR, AGENT_TARGETS;
5523
+ var __dirname2, SKILLS_DIR, AGENT_TARGETS, AGENT_LABELS;
5406
5524
  var init_installer = __esm(() => {
5407
5525
  init_registry();
5408
5526
  __dirname2 = dirname(fileURLToPath(import.meta.url));
5409
5527
  SKILLS_DIR = findSkillsDir();
5410
- AGENT_TARGETS = ["claude", "codex", "gemini"];
5528
+ AGENT_TARGETS = ["claude", "codex", "gemini", "pi", "opencode"];
5529
+ AGENT_LABELS = {
5530
+ claude: "Claude Code",
5531
+ codex: "Codex CLI",
5532
+ gemini: "Gemini CLI",
5533
+ pi: "pi.dev",
5534
+ opencode: "OpenCode"
5535
+ };
5411
5536
  });
5412
5537
 
5413
5538
  // src/lib/skillinfo.ts
5414
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
5415
- import { join as join2 } from "path";
5539
+ var exports_skillinfo = {};
5540
+ __export(exports_skillinfo, {
5541
+ runSkill: () => runSkill,
5542
+ getSkillRequirements: () => getSkillRequirements,
5543
+ getSkillDocs: () => getSkillDocs,
5544
+ getSkillBestDoc: () => getSkillBestDoc,
5545
+ generateSkillMd: () => generateSkillMd,
5546
+ generateEnvExample: () => generateEnvExample,
5547
+ detectProjectSkills: () => detectProjectSkills
5548
+ });
5549
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
5550
+ import { join as join3 } from "path";
5416
5551
  function getSkillDocs(name) {
5417
5552
  const skillPath = getSkillPath(name);
5418
- if (!existsSync2(skillPath))
5553
+ if (!existsSync3(skillPath))
5419
5554
  return null;
5420
5555
  return {
5421
- skillMd: readIfExists(join2(skillPath, "SKILL.md")),
5422
- readme: readIfExists(join2(skillPath, "README.md")),
5423
- claudeMd: readIfExists(join2(skillPath, "CLAUDE.md"))
5556
+ skillMd: readIfExists(join3(skillPath, "SKILL.md")),
5557
+ readme: readIfExists(join3(skillPath, "README.md")),
5558
+ claudeMd: readIfExists(join3(skillPath, "CLAUDE.md"))
5424
5559
  };
5425
5560
  }
5426
5561
  function getSkillBestDoc(name) {
@@ -5431,11 +5566,11 @@ function getSkillBestDoc(name) {
5431
5566
  }
5432
5567
  function getSkillRequirements(name) {
5433
5568
  const skillPath = getSkillPath(name);
5434
- if (!existsSync2(skillPath))
5569
+ if (!existsSync3(skillPath))
5435
5570
  return null;
5436
5571
  const texts = [];
5437
5572
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
5438
- const content = readIfExists(join2(skillPath, file));
5573
+ const content = readIfExists(join3(skillPath, file));
5439
5574
  if (content)
5440
5575
  texts.push(content);
5441
5576
  }
@@ -5462,10 +5597,10 @@ function getSkillRequirements(name) {
5462
5597
  }
5463
5598
  let cliCommand = null;
5464
5599
  let dependencies = {};
5465
- const pkgPath = join2(skillPath, "package.json");
5466
- if (existsSync2(pkgPath)) {
5600
+ const pkgPath = join3(skillPath, "package.json");
5601
+ if (existsSync3(pkgPath)) {
5467
5602
  try {
5468
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5603
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5469
5604
  if (pkg.bin) {
5470
5605
  const binKeys = Object.keys(pkg.bin);
5471
5606
  if (binKeys.length > 0)
@@ -5485,25 +5620,25 @@ async function runSkill(name, args, options = {}) {
5485
5620
  const skillName = normalizeSkillName(name);
5486
5621
  let skillPath;
5487
5622
  if (options.installed) {
5488
- skillPath = join2(process.cwd(), ".skills", skillName);
5623
+ skillPath = join3(process.cwd(), ".skills", skillName);
5489
5624
  } else {
5490
- const installedPath = join2(process.cwd(), ".skills", skillName);
5491
- if (existsSync2(installedPath)) {
5625
+ const installedPath = join3(process.cwd(), ".skills", skillName);
5626
+ if (existsSync3(installedPath)) {
5492
5627
  skillPath = installedPath;
5493
5628
  } else {
5494
5629
  skillPath = getSkillPath(name);
5495
5630
  }
5496
5631
  }
5497
- if (!existsSync2(skillPath)) {
5632
+ if (!existsSync3(skillPath)) {
5498
5633
  return { exitCode: 1, error: `Skill '${name}' not found` };
5499
5634
  }
5500
- const pkgPath = join2(skillPath, "package.json");
5501
- if (!existsSync2(pkgPath)) {
5635
+ const pkgPath = join3(skillPath, "package.json");
5636
+ if (!existsSync3(pkgPath)) {
5502
5637
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
5503
5638
  }
5504
5639
  let entryPoint;
5505
5640
  try {
5506
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5641
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5507
5642
  if (pkg.bin) {
5508
5643
  const binValues = Object.values(pkg.bin);
5509
5644
  entryPoint = binValues[0];
@@ -5517,12 +5652,12 @@ async function runSkill(name, args, options = {}) {
5517
5652
  } catch {
5518
5653
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
5519
5654
  }
5520
- const entryPath = join2(skillPath, entryPoint);
5521
- if (!existsSync2(entryPath)) {
5655
+ const entryPath = join3(skillPath, entryPoint);
5656
+ if (!existsSync3(entryPath)) {
5522
5657
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
5523
5658
  }
5524
- const nodeModules = join2(skillPath, "node_modules");
5525
- if (!existsSync2(nodeModules)) {
5659
+ const nodeModules = join3(skillPath, "node_modules");
5660
+ if (!existsSync3(nodeModules)) {
5526
5661
  const install = Bun.spawn(["bun", "install", "--no-save"], {
5527
5662
  cwd: skillPath,
5528
5663
  stdout: "pipe",
@@ -5540,18 +5675,18 @@ async function runSkill(name, args, options = {}) {
5540
5675
  return { exitCode };
5541
5676
  }
5542
5677
  function detectProjectSkills(cwd = process.cwd()) {
5543
- const pkgPath = join2(cwd, "package.json");
5544
- if (!existsSync2(pkgPath)) {
5678
+ const pkgPath = join3(cwd, "package.json");
5679
+ if (!existsSync3(pkgPath)) {
5545
5680
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5546
- const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5681
+ const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5547
5682
  return { detected: [], recommended: recommended2 };
5548
5683
  }
5549
5684
  let pkg;
5550
5685
  try {
5551
- pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5686
+ pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5552
5687
  } catch {
5553
5688
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5554
- const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5689
+ const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5555
5690
  return { detected: [], recommended: recommended2 };
5556
5691
  }
5557
5692
  const allDeps = {
@@ -5623,15 +5758,66 @@ function detectProjectSkills(cwd = process.cwd()) {
5623
5758
  }
5624
5759
  }
5625
5760
  const uniqueDetected = Array.from(new Set(detected));
5626
- const recommended = Array.from(recommendedNames).map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5761
+ const recommended = Array.from(recommendedNames).map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5627
5762
  return { detected: uniqueDetected, recommended };
5628
5763
  }
5764
+ function generateEnvExample(targetDir = process.cwd()) {
5765
+ const skillsDir = join3(targetDir, ".skills");
5766
+ if (!existsSync3(skillsDir))
5767
+ return "";
5768
+ const dirs = readdirSync3(skillsDir).filter((f) => f.startsWith("skill-") && existsSync3(join3(skillsDir, f, "package.json")));
5769
+ const envMap = new Map;
5770
+ for (const dir of dirs) {
5771
+ const skillName = dir.replace("skill-", "");
5772
+ const skillPath = join3(skillsDir, dir);
5773
+ const texts = [];
5774
+ for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example"]) {
5775
+ const content = readIfExists(join3(skillPath, file));
5776
+ if (content)
5777
+ texts.push(content);
5778
+ }
5779
+ const allText = texts.join(`
5780
+ `);
5781
+ const foundVars = extractEnvVars(allText);
5782
+ for (const envVar of foundVars) {
5783
+ if (!envMap.has(envVar)) {
5784
+ envMap.set(envVar, []);
5785
+ }
5786
+ if (!envMap.get(envVar).includes(skillName)) {
5787
+ envMap.get(envVar).push(skillName);
5788
+ }
5789
+ }
5790
+ }
5791
+ if (envMap.size === 0)
5792
+ return "";
5793
+ const lines = [
5794
+ "# Environment variables for installed skills",
5795
+ "# Auto-generated by: skills init",
5796
+ ""
5797
+ ];
5798
+ const sorted = Array.from(envMap.entries()).sort(([a], [b]) => a.localeCompare(b));
5799
+ let lastPrefix = "";
5800
+ for (const [envVar, skills] of sorted) {
5801
+ const prefix = envVar.split("_")[0];
5802
+ if (prefix !== lastPrefix) {
5803
+ if (lastPrefix)
5804
+ lines.push("");
5805
+ lines.push(`# ${prefix}`);
5806
+ lastPrefix = prefix;
5807
+ }
5808
+ lines.push(`# Used by: ${skills.join(", ")}`);
5809
+ lines.push(`${envVar}=`);
5810
+ }
5811
+ return lines.join(`
5812
+ `) + `
5813
+ `;
5814
+ }
5629
5815
  function generateSkillMd(name) {
5630
5816
  const meta = getSkill(name);
5631
5817
  if (!meta)
5632
5818
  return null;
5633
5819
  const skillPath = getSkillPath(name);
5634
- if (!existsSync2(skillPath))
5820
+ if (!existsSync3(skillPath))
5635
5821
  return null;
5636
5822
  const frontmatter = [
5637
5823
  "---",
@@ -5640,13 +5826,13 @@ function generateSkillMd(name) {
5640
5826
  "---"
5641
5827
  ].join(`
5642
5828
  `);
5643
- const readme = readIfExists(join2(skillPath, "README.md"));
5644
- const claudeMd = readIfExists(join2(skillPath, "CLAUDE.md"));
5829
+ const readme = readIfExists(join3(skillPath, "README.md"));
5830
+ const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
5645
5831
  let cliCommand = null;
5646
- const pkgPath = join2(skillPath, "package.json");
5647
- if (existsSync2(pkgPath)) {
5832
+ const pkgPath = join3(skillPath, "package.json");
5833
+ if (existsSync3(pkgPath)) {
5648
5834
  try {
5649
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5835
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5650
5836
  if (pkg.bin) {
5651
5837
  const binKeys = Object.keys(pkg.bin);
5652
5838
  if (binKeys.length > 0)
@@ -5719,8 +5905,8 @@ function extractEnvVars(text) {
5719
5905
  }
5720
5906
  function readIfExists(path) {
5721
5907
  try {
5722
- if (existsSync2(path)) {
5723
- return readFileSync2(path, "utf-8");
5908
+ if (existsSync3(path)) {
5909
+ return readFileSync3(path, "utf-8");
5724
5910
  }
5725
5911
  } catch {}
5726
5912
  return null;
@@ -5733,6 +5919,173 @@ var init_skillinfo = __esm(() => {
5733
5919
  GENERIC_ENV_PATTERN = /\b((?:OPENAI|ANTHROPIC|GEMINI|XAI|ELEVENLABS|DEEPGRAM|REPLICATE|FAL|STABILITY|EXA|FIRECRAWL|TWILIO|SENDGRID|RESEND|SLACK|DISCORD|NOTION|LINEAR|GITHUB|AWS|GOOGLE|CLOUDFLARE|VERCEL|SUPABASE|STRIPE)_[A-Z_]+)\b/g;
5734
5920
  });
5735
5921
 
5922
+ // src/lib/scheduler.ts
5923
+ var exports_scheduler = {};
5924
+ __export(exports_scheduler, {
5925
+ validateCron: () => validateCron,
5926
+ setScheduleEnabled: () => setScheduleEnabled,
5927
+ removeSchedule: () => removeSchedule,
5928
+ recordScheduleRun: () => recordScheduleRun,
5929
+ listSchedules: () => listSchedules,
5930
+ getNextRun: () => getNextRun,
5931
+ getDueSchedules: () => getDueSchedules,
5932
+ addSchedule: () => addSchedule
5933
+ });
5934
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5935
+ import { join as join5 } from "path";
5936
+ function getSchedulesPath(targetDir = process.cwd()) {
5937
+ return join5(targetDir, ".skills", "schedules.json");
5938
+ }
5939
+ function loadSchedules(targetDir = process.cwd()) {
5940
+ const path = getSchedulesPath(targetDir);
5941
+ if (existsSync5(path)) {
5942
+ try {
5943
+ return JSON.parse(readFileSync5(path, "utf-8"));
5944
+ } catch {}
5945
+ }
5946
+ return { version: 1, schedules: [] };
5947
+ }
5948
+ function saveSchedules(data, targetDir = process.cwd()) {
5949
+ const path = getSchedulesPath(targetDir);
5950
+ const dir = join5(targetDir, ".skills");
5951
+ if (!existsSync5(dir))
5952
+ mkdirSync3(dir, { recursive: true });
5953
+ writeFileSync3(path, JSON.stringify(data, null, 2));
5954
+ }
5955
+ function validateCron(expr) {
5956
+ const fields = expr.trim().split(/\s+/);
5957
+ if (fields.length !== 5) {
5958
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
5959
+ }
5960
+ return { valid: true };
5961
+ }
5962
+ function getNextRun(cron, from = new Date) {
5963
+ const { valid } = validateCron(cron);
5964
+ if (!valid)
5965
+ return null;
5966
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
5967
+ function parseField(f, min, max) {
5968
+ if (f === "*")
5969
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
5970
+ if (f.startsWith("*/")) {
5971
+ const step = parseInt(f.slice(2));
5972
+ if (isNaN(step))
5973
+ return [];
5974
+ const vals = [];
5975
+ for (let i = min;i <= max; i += step)
5976
+ vals.push(i);
5977
+ return vals;
5978
+ }
5979
+ return f.split(",").flatMap((part) => {
5980
+ if (part.includes("-")) {
5981
+ const [lo, hi] = part.split("-").map(Number);
5982
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
5983
+ }
5984
+ const n = parseInt(part);
5985
+ return isNaN(n) ? [] : [n];
5986
+ });
5987
+ }
5988
+ const minutes = parseField(minuteF, 0, 59);
5989
+ const hours = parseField(hourF, 0, 23);
5990
+ const doms = parseField(domF, 1, 31);
5991
+ const months = parseField(monthF, 1, 12);
5992
+ const dows = parseField(dowF, 0, 6);
5993
+ const candidate = new Date(from);
5994
+ candidate.setSeconds(0, 0);
5995
+ candidate.setMinutes(candidate.getMinutes() + 1);
5996
+ const limit = new Date(from);
5997
+ limit.setFullYear(limit.getFullYear() + 1);
5998
+ while (candidate < limit) {
5999
+ const month = candidate.getMonth() + 1;
6000
+ const dom = candidate.getDate();
6001
+ const dow = candidate.getDay();
6002
+ const hour = candidate.getHours();
6003
+ const minute = candidate.getMinutes();
6004
+ if (!months.includes(month)) {
6005
+ candidate.setMonth(candidate.getMonth() + 1, 1);
6006
+ candidate.setHours(0, 0, 0, 0);
6007
+ continue;
6008
+ }
6009
+ if (!doms.includes(dom) || !dows.includes(dow)) {
6010
+ candidate.setDate(candidate.getDate() + 1);
6011
+ candidate.setHours(0, 0, 0, 0);
6012
+ continue;
6013
+ }
6014
+ if (!hours.includes(hour)) {
6015
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
6016
+ continue;
6017
+ }
6018
+ if (!minutes.includes(minute)) {
6019
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
6020
+ continue;
6021
+ }
6022
+ return new Date(candidate);
6023
+ }
6024
+ return null;
6025
+ }
6026
+ function addSchedule(skill, cron, options = {}) {
6027
+ const { valid, error } = validateCron(cron);
6028
+ if (!valid)
6029
+ return { schedule: null, error };
6030
+ const data = loadSchedules(options.targetDir);
6031
+ const id = `${skill}-${Date.now()}`;
6032
+ const now = new Date;
6033
+ const nextRun = getNextRun(cron, now);
6034
+ const schedule = {
6035
+ id,
6036
+ name: options.name || `${skill} (${cron})`,
6037
+ skill,
6038
+ cron,
6039
+ args: options.args,
6040
+ enabled: true,
6041
+ createdAt: now.toISOString(),
6042
+ nextRun: nextRun?.toISOString()
6043
+ };
6044
+ data.schedules.push(schedule);
6045
+ saveSchedules(data, options.targetDir);
6046
+ return { schedule };
6047
+ }
6048
+ function listSchedules(targetDir) {
6049
+ return loadSchedules(targetDir).schedules;
6050
+ }
6051
+ function removeSchedule(idOrName, targetDir) {
6052
+ const data = loadSchedules(targetDir);
6053
+ const before = data.schedules.length;
6054
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
6055
+ if (data.schedules.length === before)
6056
+ return false;
6057
+ saveSchedules(data, targetDir);
6058
+ return true;
6059
+ }
6060
+ function setScheduleEnabled(idOrName, enabled, targetDir) {
6061
+ const data = loadSchedules(targetDir);
6062
+ const schedule = data.schedules.find((s) => s.id === idOrName || s.name === idOrName);
6063
+ if (!schedule)
6064
+ return false;
6065
+ schedule.enabled = enabled;
6066
+ if (enabled) {
6067
+ schedule.nextRun = getNextRun(schedule.cron)?.toISOString();
6068
+ }
6069
+ saveSchedules(data, targetDir);
6070
+ return true;
6071
+ }
6072
+ function getDueSchedules(targetDir) {
6073
+ const now = new Date;
6074
+ return listSchedules(targetDir).filter((s) => s.enabled && s.nextRun && new Date(s.nextRun) <= now);
6075
+ }
6076
+ function recordScheduleRun(id, status, targetDir) {
6077
+ const data = loadSchedules(targetDir);
6078
+ const schedule = data.schedules.find((s) => s.id === id);
6079
+ if (!schedule)
6080
+ return;
6081
+ const now = new Date;
6082
+ schedule.lastRun = now.toISOString();
6083
+ schedule.lastRunStatus = status;
6084
+ schedule.nextRun = getNextRun(schedule.cron, now)?.toISOString();
6085
+ saveSchedules(data, targetDir);
6086
+ }
6087
+ var init_scheduler = () => {};
6088
+
5736
6089
  // ../../../../hasnastudio/hasnastudio-alumia/platform/platformdev/platform-alumia/node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/util.js
5737
6090
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
5738
6091
  const t = typeof data;
@@ -34823,9 +35176,8 @@ var init_stdio2 = __esm(() => {
34823
35176
 
34824
35177
  // src/mcp/index.ts
34825
35178
  var exports_mcp = {};
34826
- import { existsSync as existsSync4, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
34827
- import { join as join4 } from "path";
34828
- import { homedir as homedir3 } from "os";
35179
+ import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
35180
+ import { join as join6 } from "path";
34829
35181
  function stripNulls(obj) {
34830
35182
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== null && v !== undefined && !(Array.isArray(v) && v.length === 0)));
34831
35183
  }
@@ -34865,6 +35217,7 @@ var init_mcp2 = __esm(() => {
34865
35217
  init_registry();
34866
35218
  init_installer();
34867
35219
  init_skillinfo();
35220
+ init_scheduler();
34868
35221
  server = new McpServer({
34869
35222
  name: "skills",
34870
35223
  version: package_default.version
@@ -34880,7 +35233,7 @@ var init_mcp2 = __esm(() => {
34880
35233
  offset: exports_external.number().optional()
34881
35234
  }
34882
35235
  }, async ({ category, detail, limit, offset }) => {
34883
- const skills = category ? getSkillsByCategory(category) : SKILLS;
35236
+ const skills = category ? getSkillsByCategory(category) : loadRegistry();
34884
35237
  const mapped = detail ? skills : skills.map((s) => ({ name: s.name, category: s.category }));
34885
35238
  if (limit !== undefined || offset !== undefined) {
34886
35239
  const start = offset || 0;
@@ -34965,7 +35318,7 @@ var init_mcp2 = __esm(() => {
34965
35318
  });
34966
35319
  server.registerTool("install_skill", {
34967
35320
  title: "Install Skill",
34968
- description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|all).",
35321
+ description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|pi|opencode|all).",
34969
35322
  inputSchema: {
34970
35323
  name: exports_external.string(),
34971
35324
  for: exports_external.string().optional(),
@@ -34977,7 +35330,7 @@ var init_mcp2 = __esm(() => {
34977
35330
  try {
34978
35331
  agents = resolveAgents(agentArg);
34979
35332
  } catch (err) {
34980
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35333
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
34981
35334
  }
34982
35335
  const results = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
34983
35336
  return {
@@ -35015,7 +35368,7 @@ var init_mcp2 = __esm(() => {
35015
35368
  try {
35016
35369
  agents = resolveAgents(agentArg);
35017
35370
  } catch (err) {
35018
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35371
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35019
35372
  }
35020
35373
  const results2 = [];
35021
35374
  for (const name of names) {
@@ -35049,7 +35402,7 @@ var init_mcp2 = __esm(() => {
35049
35402
  try {
35050
35403
  agents = resolveAgents(agentArg);
35051
35404
  } catch (err) {
35052
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35405
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35053
35406
  }
35054
35407
  const results = agents.map((a) => ({
35055
35408
  skill: name,
@@ -35082,7 +35435,7 @@ var init_mcp2 = __esm(() => {
35082
35435
  description: "List all unique skill tags with occurrence counts."
35083
35436
  }, async () => {
35084
35437
  const tagCounts = new Map;
35085
- for (const skill of SKILLS) {
35438
+ for (const skill of loadRegistry()) {
35086
35439
  for (const tag of skill.tags) {
35087
35440
  tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35088
35441
  }
@@ -35156,7 +35509,7 @@ var init_mcp2 = __esm(() => {
35156
35509
  try {
35157
35510
  agents = resolveAgents(agentArg);
35158
35511
  } catch (err) {
35159
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35512
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35160
35513
  }
35161
35514
  for (const name of skillList) {
35162
35515
  const agentResults = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
@@ -35184,21 +35537,20 @@ var init_mcp2 = __esm(() => {
35184
35537
  const version2 = package_default.version;
35185
35538
  const cwd = process.cwd();
35186
35539
  const installed = getInstalledSkills();
35187
- const agentNames = ["claude", "codex", "gemini"];
35188
35540
  const agents = [];
35189
- for (const agent of agentNames) {
35190
- const agentSkillsPath = join4(homedir3(), `.${agent}`, "skills");
35191
- const exists = existsSync4(agentSkillsPath);
35541
+ for (const agent of AGENT_TARGETS) {
35542
+ const agentSkillsPath = getAgentSkillsDir(agent, "global");
35543
+ const exists = existsSync6(agentSkillsPath);
35192
35544
  let skillCount = 0;
35193
35545
  if (exists) {
35194
35546
  try {
35195
- skillCount = readdirSync3(agentSkillsPath).filter((f) => {
35196
- const full = join4(agentSkillsPath, f);
35547
+ skillCount = readdirSync4(agentSkillsPath).filter((f) => {
35548
+ const full = join6(agentSkillsPath, f);
35197
35549
  return f.startsWith("skill-") && statSync2(full).isDirectory();
35198
35550
  }).length;
35199
35551
  } catch {}
35200
35552
  }
35201
- agents.push({ agent, path: agentSkillsPath, exists, skillCount });
35553
+ agents.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
35202
35554
  }
35203
35555
  const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
35204
35556
  const result = {
@@ -35211,12 +35563,107 @@ var init_mcp2 = __esm(() => {
35211
35563
  };
35212
35564
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
35213
35565
  });
35566
+ server.registerTool("schedule_skill", {
35567
+ title: "Schedule Skill",
35568
+ description: "Add a cron schedule to run a skill at a recurring time. Cron format: 'minute hour dom month dow' (e.g. '0 9 * * *' = daily at 9am).",
35569
+ inputSchema: {
35570
+ skill: exports_external.string(),
35571
+ cron: exports_external.string(),
35572
+ name: exports_external.string().optional(),
35573
+ args: exports_external.array(exports_external.string()).optional()
35574
+ }
35575
+ }, async ({ skill, cron, name, args }) => {
35576
+ const { schedule, error: error48 } = addSchedule(skill, cron, { name, args });
35577
+ if (error48 || !schedule) {
35578
+ return { content: [{ type: "text", text: JSON.stringify({ error: error48 || "Failed to add schedule" }) }] };
35579
+ }
35580
+ return { content: [{ type: "text", text: JSON.stringify(schedule, null, 2) }] };
35581
+ });
35582
+ server.registerTool("list_schedules", {
35583
+ title: "List Schedules",
35584
+ description: "List all scheduled skill runs.",
35585
+ inputSchema: {}
35586
+ }, async () => {
35587
+ const schedules = listSchedules();
35588
+ return { content: [{ type: "text", text: JSON.stringify(schedules, null, 2) }] };
35589
+ });
35590
+ server.registerTool("remove_schedule", {
35591
+ title: "Remove Schedule",
35592
+ description: "Remove a schedule by its ID or name.",
35593
+ inputSchema: {
35594
+ id_or_name: exports_external.string()
35595
+ }
35596
+ }, async ({ id_or_name }) => {
35597
+ const removed = removeSchedule(id_or_name);
35598
+ return { content: [{ type: "text", text: JSON.stringify({ removed, id_or_name }) }] };
35599
+ });
35600
+ server.registerTool("detect_project_skills", {
35601
+ title: "Detect Project Skills",
35602
+ description: "Detect project type from package.json and return recommended skills based on dependencies.",
35603
+ inputSchema: {
35604
+ directory: exports_external.string().optional()
35605
+ }
35606
+ }, async ({ directory }) => {
35607
+ const cwd = directory || process.cwd();
35608
+ const { detected, recommended } = detectProjectSkills(cwd);
35609
+ return {
35610
+ content: [{
35611
+ type: "text",
35612
+ text: JSON.stringify({
35613
+ directory: cwd,
35614
+ detected,
35615
+ recommended: recommended.map((s) => ({ name: s.name, displayName: s.displayName, description: s.description, category: s.category }))
35616
+ }, null, 2)
35617
+ }]
35618
+ };
35619
+ });
35620
+ server.registerTool("validate_skill", {
35621
+ title: "Validate Skill",
35622
+ description: "Check a skill's structure: SKILL.md, package.json with bin entry, tsconfig.json, src/index.ts. Returns validation result with list of issues.",
35623
+ inputSchema: {
35624
+ name: exports_external.string()
35625
+ }
35626
+ }, async ({ name }) => {
35627
+ const skillPath = getSkillPath(name);
35628
+ const issues = [];
35629
+ if (!existsSync6(skillPath)) {
35630
+ return {
35631
+ content: [{ type: "text", text: JSON.stringify({ name, valid: false, issues: [`Skill directory not found: ${skillPath}`] }) }]
35632
+ };
35633
+ }
35634
+ if (!existsSync6(join6(skillPath, "SKILL.md")))
35635
+ issues.push("Missing SKILL.md");
35636
+ if (!existsSync6(join6(skillPath, "tsconfig.json")))
35637
+ issues.push("Missing tsconfig.json");
35638
+ const pkgPath = join6(skillPath, "package.json");
35639
+ if (!existsSync6(pkgPath)) {
35640
+ issues.push("Missing package.json");
35641
+ } else {
35642
+ try {
35643
+ const pkg = JSON.parse(__require("fs").readFileSync(pkgPath, "utf-8"));
35644
+ if (!pkg.bin || Object.keys(pkg.bin).length === 0)
35645
+ issues.push("package.json missing 'bin' entry");
35646
+ } catch {
35647
+ issues.push("package.json is invalid JSON");
35648
+ }
35649
+ }
35650
+ const srcDir = join6(skillPath, "src");
35651
+ if (!existsSync6(srcDir)) {
35652
+ issues.push("Missing src/ directory");
35653
+ } else if (!existsSync6(join6(srcDir, "index.ts"))) {
35654
+ issues.push("Missing src/index.ts");
35655
+ }
35656
+ const valid = issues.length === 0;
35657
+ return {
35658
+ content: [{ type: "text", text: JSON.stringify({ name, valid, path: skillPath, issues }) }]
35659
+ };
35660
+ });
35214
35661
  server.registerResource("Skills Registry", "skills://registry", {
35215
35662
  description: "Compact skill list [{name,category}]. Use skills://{name} for detail."
35216
35663
  }, async () => ({
35217
35664
  contents: [{
35218
35665
  uri: "skills://registry",
35219
- text: JSON.stringify(SKILLS.map((s) => ({ name: s.name, category: s.category }))),
35666
+ text: JSON.stringify(loadRegistry().map((s) => ({ name: s.name, category: s.category }))),
35220
35667
  mimeType: "application/json"
35221
35668
  }]
35222
35669
  }));
@@ -35325,16 +35772,16 @@ __export(exports_serve, {
35325
35772
  startServer: () => startServer,
35326
35773
  createFetchHandler: () => createFetchHandler
35327
35774
  });
35328
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
35329
- import { join as join5, dirname as dirname3, extname } from "path";
35775
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
35776
+ import { join as join7, dirname as dirname3, extname } from "path";
35330
35777
  import { fileURLToPath as fileURLToPath2 } from "url";
35331
35778
  function getPackageJson() {
35332
35779
  try {
35333
35780
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
35334
35781
  for (const rel of ["../..", ".."]) {
35335
- const pkgPath = join5(scriptDir, rel, "package.json");
35336
- if (existsSync5(pkgPath)) {
35337
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
35782
+ const pkgPath = join7(scriptDir, rel, "package.json");
35783
+ if (existsSync7(pkgPath)) {
35784
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
35338
35785
  return { version: pkg.version || "unknown", name: pkg.name || "skills" };
35339
35786
  }
35340
35787
  }
@@ -35345,20 +35792,20 @@ function resolveDashboardDir() {
35345
35792
  const candidates = [];
35346
35793
  try {
35347
35794
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
35348
- candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
35349
- candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
35795
+ candidates.push(join7(scriptDir, "..", "dashboard", "dist"));
35796
+ candidates.push(join7(scriptDir, "..", "..", "dashboard", "dist"));
35350
35797
  } catch {}
35351
35798
  if (process.argv[1]) {
35352
35799
  const mainDir = dirname3(process.argv[1]);
35353
- candidates.push(join5(mainDir, "..", "dashboard", "dist"));
35354
- candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
35800
+ candidates.push(join7(mainDir, "..", "dashboard", "dist"));
35801
+ candidates.push(join7(mainDir, "..", "..", "dashboard", "dist"));
35355
35802
  }
35356
- candidates.push(join5(process.cwd(), "dashboard", "dist"));
35803
+ candidates.push(join7(process.cwd(), "dashboard", "dist"));
35357
35804
  for (const candidate of candidates) {
35358
- if (existsSync5(candidate))
35805
+ if (existsSync7(candidate))
35359
35806
  return candidate;
35360
35807
  }
35361
- return join5(process.cwd(), "dashboard", "dist");
35808
+ return join7(process.cwd(), "dashboard", "dist");
35362
35809
  }
35363
35810
  function json2(data, status = 200) {
35364
35811
  return new Response(JSON.stringify(data), {
@@ -35387,7 +35834,7 @@ function parseFields(searchParams) {
35387
35834
  }
35388
35835
  function getAllSkillsWithStatus() {
35389
35836
  const installed = new Set(getInstalledSkills());
35390
- return SKILLS.map((meta3) => {
35837
+ return loadRegistry().map((meta3) => {
35391
35838
  const reqs = getSkillRequirements(meta3.name);
35392
35839
  const envVars = reqs?.envVars || [];
35393
35840
  return {
@@ -35400,12 +35847,13 @@ function getAllSkillsWithStatus() {
35400
35847
  envVars,
35401
35848
  envVarsSet: envVars.filter((v) => !!process.env[v]),
35402
35849
  systemDeps: reqs?.systemDeps || [],
35403
- cliCommand: reqs?.cliCommand || null
35850
+ cliCommand: reqs?.cliCommand || null,
35851
+ source: meta3.source ?? "official"
35404
35852
  };
35405
35853
  });
35406
35854
  }
35407
35855
  function serveStaticFile(filePath) {
35408
- if (!existsSync5(filePath))
35856
+ if (!existsSync7(filePath))
35409
35857
  return null;
35410
35858
  const ext = extname(filePath);
35411
35859
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -35415,7 +35863,7 @@ function serveStaticFile(filePath) {
35415
35863
  }
35416
35864
  function createFetchHandler(options) {
35417
35865
  const dashboardDir = options?.dashboardDir ?? resolveDashboardDir();
35418
- const dashboardExists = options?.dashboardExists ?? existsSync5(dashboardDir);
35866
+ const dashboardExists = options?.dashboardExists ?? existsSync7(dashboardDir);
35419
35867
  return async function fetchHandler(req) {
35420
35868
  const url2 = new URL(req.url);
35421
35869
  const path = url2.pathname.replace(/^\/api\/v1\//, "/api/");
@@ -35426,7 +35874,7 @@ function createFetchHandler(options) {
35426
35874
  status: "ok",
35427
35875
  version: pkg.version,
35428
35876
  uptime: Math.floor(process.uptime()),
35429
- skillCount: SKILLS.length
35877
+ skillCount: loadRegistry().length
35430
35878
  });
35431
35879
  }
35432
35880
  if (path === "/api/skills" && method === "GET") {
@@ -35467,7 +35915,7 @@ function createFetchHandler(options) {
35467
35915
  }
35468
35916
  if (path === "/api/tags" && method === "GET") {
35469
35917
  const tagCounts = new Map;
35470
- for (const skill of SKILLS) {
35918
+ for (const skill of loadRegistry()) {
35471
35919
  for (const tag of skill.tags) {
35472
35920
  tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35473
35921
  }
@@ -35699,12 +36147,12 @@ function createFetchHandler(options) {
35699
36147
  }
35700
36148
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
35701
36149
  if (path !== "/") {
35702
- const filePath = join5(dashboardDir, path);
36150
+ const filePath = join7(dashboardDir, path);
35703
36151
  const res2 = serveStaticFile(filePath);
35704
36152
  if (res2)
35705
36153
  return res2;
35706
36154
  }
35707
- const indexPath = join5(dashboardDir, "index.html");
36155
+ const indexPath = join7(dashboardDir, "index.html");
35708
36156
  const res = serveStaticFile(indexPath);
35709
36157
  if (res)
35710
36158
  return res;
@@ -35715,7 +36163,7 @@ function createFetchHandler(options) {
35715
36163
  async function startServer(port = 0, options) {
35716
36164
  const shouldOpen = options?.open ?? true;
35717
36165
  const dashboardDir = resolveDashboardDir();
35718
- const dashboardExists = existsSync5(dashboardDir);
36166
+ const dashboardExists = existsSync7(dashboardDir);
35719
36167
  if (!dashboardExists) {
35720
36168
  console.error(`
35721
36169
  Dashboard not found at: ${dashboardDir}`);
@@ -35795,8 +36243,8 @@ var {
35795
36243
  // src/cli/index.tsx
35796
36244
  init_package();
35797
36245
  import chalk2 from "chalk";
35798
- import { existsSync as existsSync6, writeFileSync as writeFileSync3, appendFileSync, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
35799
- import { join as join6 } from "path";
36246
+ import { existsSync as existsSync8, writeFileSync as writeFileSync4, appendFileSync, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync3, mkdirSync as mkdirSync4 } from "fs";
36247
+ import { join as join8 } from "path";
35800
36248
 
35801
36249
  // src/cli/components/App.tsx
35802
36250
  import { useState as useState7 } from "react";
@@ -36922,25 +37370,38 @@ init_installer();
36922
37370
  init_skillinfo();
36923
37371
 
36924
37372
  // src/lib/config.ts
36925
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
36926
- import { join as join3, dirname as dirname2 } from "path";
36927
- import { homedir as homedir2 } from "os";
37373
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
37374
+ import { join as join4, dirname as dirname2 } from "path";
37375
+ import { homedir as homedir3 } from "os";
36928
37376
  var VALID_KEYS = {
36929
37377
  defaultAgent: ["claude", "codex", "gemini", "all"],
36930
37378
  defaultScope: ["global", "project"],
36931
37379
  format: ["compact", "json", "csv"]
36932
37380
  };
37381
+ function getDataDir() {
37382
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
37383
+ const newDir = join4(home, ".hasna", "skills");
37384
+ const oldConfigFile = join4(home, ".skillsrc");
37385
+ if (existsSync4(oldConfigFile) && !existsSync4(join4(newDir, "config.json"))) {
37386
+ mkdirSync2(newDir, { recursive: true });
37387
+ try {
37388
+ copyFileSync(oldConfigFile, join4(newDir, "config.json"));
37389
+ } catch {}
37390
+ }
37391
+ mkdirSync2(newDir, { recursive: true });
37392
+ return newDir;
37393
+ }
36933
37394
  function getConfigPath(scope) {
36934
37395
  if (scope === "global") {
36935
- return join3(homedir2(), ".skillsrc");
37396
+ return join4(getDataDir(), "config.json");
36936
37397
  }
36937
- return join3(process.cwd(), "skills.config.json");
37398
+ return join4(process.cwd(), "skills.config.json");
36938
37399
  }
36939
37400
  function readConfigFile(path) {
36940
- if (!existsSync3(path))
37401
+ if (!existsSync4(path))
36941
37402
  return {};
36942
37403
  try {
36943
- const raw = readFileSync3(path, "utf-8");
37404
+ const raw = readFileSync4(path, "utf-8");
36944
37405
  const parsed = JSON.parse(raw);
36945
37406
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
36946
37407
  return {};
@@ -36971,9 +37432,9 @@ function saveConfig(key, value, scope = "project") {
36971
37432
  }
36972
37433
  const filePath = getConfigPath(scope);
36973
37434
  let existing = {};
36974
- if (existsSync3(filePath)) {
37435
+ if (existsSync4(filePath)) {
36975
37436
  try {
36976
- existing = JSON.parse(readFileSync3(filePath, "utf-8"));
37437
+ existing = JSON.parse(readFileSync4(filePath, "utf-8"));
36977
37438
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
36978
37439
  existing = {};
36979
37440
  }
@@ -36982,7 +37443,7 @@ function saveConfig(key, value, scope = "project") {
36982
37443
  }
36983
37444
  } else {
36984
37445
  const dir = dirname2(filePath);
36985
- if (!existsSync3(dir)) {
37446
+ if (!existsSync4(dir)) {
36986
37447
  mkdirSync2(dir, { recursive: true });
36987
37448
  }
36988
37449
  }
@@ -36992,6 +37453,7 @@ function saveConfig(key, value, scope = "project") {
36992
37453
  }
36993
37454
 
36994
37455
  // src/cli/index.tsx
37456
+ init_scheduler();
36995
37457
  import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
36996
37458
  var isTTY = (process.stdout.isTTY ?? false) && (process.stdin.isTTY ?? false);
36997
37459
  if (process.argv.includes("--no-color")) {
@@ -37024,7 +37486,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
37024
37486
  }
37025
37487
  render(/* @__PURE__ */ jsxDEV7(App, {}, undefined, false, undefined, this));
37026
37488
  });
37027
- program2.command("install").alias("add").argument("[skills...]", "Skills to install").option("--verbose", "Enable verbose debug logging", false).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) => {
37489
+ program2.command("install").alias("add").argument("[skills...]", "Skills to install").option("--verbose", "Enable verbose debug logging", false).option("-o, --overwrite", "Overwrite existing skills", false).option("--json", "Output results as JSON", false).option("--for <agent>", "Install for agent: claude, codex, gemini, pi, opencode, 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) => {
37028
37490
  const config2 = loadConfig();
37029
37491
  if (!options.for && config2.defaultAgent) {
37030
37492
  options.for = config2.defaultAgent;
@@ -37165,7 +37627,14 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
37165
37627
  if (options.installed) {
37166
37628
  const installed = getInstalledSkills();
37167
37629
  if (options.json) {
37168
- console.log(JSON.stringify(installed));
37630
+ const meta4 = getInstallMeta();
37631
+ const registry3 = loadRegistry();
37632
+ const output = installed.map((name) => {
37633
+ const m = meta4.skills[name];
37634
+ const s = registry3.find((r) => r.name === name);
37635
+ return { name, version: m?.version ?? null, installedAt: m?.installedAt ?? null, source: s?.source ?? "official" };
37636
+ });
37637
+ console.log(JSON.stringify(output));
37169
37638
  return;
37170
37639
  }
37171
37640
  if (installed.length === 0) {
@@ -37173,16 +37642,22 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
37173
37642
  return;
37174
37643
  }
37175
37644
  if (brief) {
37176
- for (const name of installed) {
37645
+ for (const name of installed)
37177
37646
  console.log(name);
37178
- }
37179
37647
  return;
37180
37648
  }
37649
+ const meta3 = getInstallMeta();
37650
+ const registry2 = loadRegistry();
37181
37651
  console.log(chalk2.bold(`
37182
37652
  Installed skills (${installed.length}):
37183
37653
  `));
37184
37654
  for (const name of installed) {
37185
- console.log(` ${name}`);
37655
+ const m = meta3.skills[name];
37656
+ const s = registry2.find((r) => r.name === name);
37657
+ const version2 = m?.version ? chalk2.dim(`v${m.version}`) : "";
37658
+ const installedAt = m?.installedAt ? chalk2.dim(new Date(m.installedAt).toLocaleDateString()) : "";
37659
+ const source = s?.source === "custom" ? chalk2.yellow(" [custom]") : "";
37660
+ console.log(` ${chalk2.cyan(name)}${source} ${version2} ${installedAt}`);
37186
37661
  }
37187
37662
  return;
37188
37663
  }
@@ -37218,7 +37693,7 @@ ${category} (${skills.length}):
37218
37693
  return;
37219
37694
  }
37220
37695
  if (tagFilter) {
37221
- const skills = SKILLS.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
37696
+ const skills = loadRegistry().filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
37222
37697
  if (options.json) {
37223
37698
  console.log(JSON.stringify(skills, null, 2));
37224
37699
  return;
@@ -37233,29 +37708,31 @@ ${category} (${skills.length}):
37233
37708
  Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
37234
37709
  `));
37235
37710
  for (const s of skills) {
37236
- console.log(` ${chalk2.cyan(s.name)} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
37711
+ const customBadge = s.source === "custom" ? chalk2.yellow(" [custom]") : "";
37712
+ console.log(` ${chalk2.cyan(s.name)}${customBadge} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
37237
37713
  }
37238
37714
  return;
37239
37715
  }
37716
+ const allSkills = loadRegistry();
37240
37717
  if (options.json) {
37241
- console.log(JSON.stringify(SKILLS, null, 2));
37718
+ console.log(JSON.stringify(allSkills, null, 2));
37242
37719
  return;
37243
37720
  }
37244
37721
  if (fmt === "compact") {
37245
- for (const s of SKILLS)
37722
+ for (const s of allSkills)
37246
37723
  console.log(s.name);
37247
37724
  return;
37248
37725
  }
37249
37726
  if (fmt === "csv") {
37250
- console.log("name,category,description");
37251
- for (const s of SKILLS) {
37727
+ console.log("name,category,description,source");
37728
+ for (const s of allSkills) {
37252
37729
  const desc = s.description.replace(/"/g, '""');
37253
- console.log(`${s.name},${s.category},"${desc}"`);
37730
+ console.log(`${s.name},${s.category},"${desc}",${s.source ?? "official"}`);
37254
37731
  }
37255
37732
  return;
37256
37733
  }
37257
37734
  if (brief) {
37258
- const sorted = [...SKILLS].sort((a, b) => {
37735
+ const sorted = [...allSkills].sort((a, b) => {
37259
37736
  const catCmp = a.category.localeCompare(b.category);
37260
37737
  return catCmp !== 0 ? catCmp : a.name.localeCompare(b.name);
37261
37738
  });
@@ -37265,16 +37742,26 @@ Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
37265
37742
  return;
37266
37743
  }
37267
37744
  console.log(chalk2.bold(`
37268
- Available skills (${SKILLS.length}):
37745
+ Available skills (${allSkills.length}):
37269
37746
  `));
37270
37747
  for (const category of CATEGORIES) {
37271
37748
  const skills = getSkillsByCategory(category);
37749
+ if (skills.length === 0)
37750
+ continue;
37272
37751
  console.log(chalk2.bold(`${category} (${skills.length}):`));
37273
37752
  for (const s of skills) {
37274
- console.log(` ${chalk2.cyan(s.name)} - ${s.description}`);
37753
+ const customBadge = s.source === "custom" ? chalk2.yellow(" [custom]") : "";
37754
+ console.log(` ${chalk2.cyan(s.name)}${customBadge} - ${s.description}`);
37275
37755
  }
37276
37756
  console.log();
37277
37757
  }
37758
+ const customUncategorized = allSkills.filter((s) => s.source === "custom" && !CATEGORIES.includes(s.category));
37759
+ if (customUncategorized.length > 0) {
37760
+ console.log(chalk2.bold(`Custom (${customUncategorized.length}):`));
37761
+ for (const s of customUncategorized) {
37762
+ console.log(` ${chalk2.yellow(s.name)} - ${s.description}`);
37763
+ }
37764
+ }
37278
37765
  });
37279
37766
  program2.command("search").alias("s").argument("<query>", "Search term").option("--verbose", "Enable verbose debug logging", false).option("--json", "Output as JSON", false).option("--brief", "One line per skill: name \u2014 description [category]", false).option("--format <format>", "Output format: compact (names only) or csv (name,category,description)").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) => {
37280
37767
  let results = searchSkills(query);
@@ -37350,8 +37837,17 @@ program2.command("info").argument("<skill>", "Skill name").option("--json", "Out
37350
37837
  console.log(`${skill.name} \u2014 ${skill.description} [${skill.category}] (tags: ${skill.tags.join(", ")})`);
37351
37838
  return;
37352
37839
  }
37840
+ const { execSync: execSyncInfo } = __require("child_process");
37841
+ function cmdAvailable(cmd) {
37842
+ try {
37843
+ execSyncInfo(`which ${cmd}`, { stdio: "ignore" });
37844
+ return true;
37845
+ } catch {
37846
+ return false;
37847
+ }
37848
+ }
37353
37849
  console.log(`
37354
- ${chalk2.bold(skill.displayName)}`);
37850
+ ${chalk2.bold(skill.displayName)}${skill.source === "custom" ? chalk2.yellow(" [custom]") : ""}`);
37355
37851
  console.log(`${skill.description}`);
37356
37852
  console.log(`${chalk2.dim("Category:")} ${skill.category}`);
37357
37853
  console.log(`${chalk2.dim("Tags:")} ${skill.tags.join(", ")}`);
@@ -37359,10 +37855,18 @@ ${chalk2.bold(skill.displayName)}`);
37359
37855
  console.log(`${chalk2.dim("CLI:")} ${reqs.cliCommand}`);
37360
37856
  }
37361
37857
  if (reqs?.envVars.length) {
37362
- console.log(`${chalk2.dim("Env vars:")} ${reqs.envVars.join(", ")}`);
37858
+ console.log(`${chalk2.dim("Env vars:")}`);
37859
+ for (const v of reqs.envVars) {
37860
+ const set3 = !!process.env[v];
37861
+ console.log(` ${set3 ? chalk2.green("\u2713") : chalk2.red("\u2717")} ${v}${set3 ? "" : chalk2.dim(" (not set)")}`);
37862
+ }
37363
37863
  }
37364
37864
  if (reqs?.systemDeps.length) {
37365
- console.log(`${chalk2.dim("System deps:")} ${reqs.systemDeps.join(", ")}`);
37865
+ console.log(`${chalk2.dim("System deps:")}`);
37866
+ for (const d of reqs.systemDeps) {
37867
+ const avail = cmdAvailable(d);
37868
+ console.log(` ${avail ? chalk2.green("\u2713") : chalk2.red("\u2717")} ${d}${avail ? "" : chalk2.dim(" (not found)")}`);
37869
+ }
37366
37870
  }
37367
37871
  console.log(`${chalk2.dim("Install:")} skills install ${skill.name}`);
37368
37872
  });
@@ -37466,7 +37970,7 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
37466
37970
  }
37467
37971
  process.exitCode = result.exitCode;
37468
37972
  });
37469
- 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) => {
37973
+ program2.command("init").option("--json", "Output as JSON", false).option("--for <agent>", "Detect project type and install recommended skills for agent: claude, codex, gemini, pi, opencode, or all").option("--scope <scope>", "Install scope: global or project", "global").description("Initialize project for installed skills (.env.example, .gitignore)").action((options) => {
37470
37974
  const cwd = process.cwd();
37471
37975
  if (options.for) {
37472
37976
  let agents;
@@ -37580,8 +38084,8 @@ Installing recommended skills for ${options.for} (${options.scope})...
37580
38084
  const envContent = lines.join(`
37581
38085
  `) + `
37582
38086
  `;
37583
- const envPath = join6(cwd, ".env.example");
37584
- writeFileSync3(envPath, envContent);
38087
+ const envPath = join8(cwd, ".env.example");
38088
+ writeFileSync4(envPath, envContent);
37585
38089
  envVarCount = envMap.size;
37586
38090
  if (!options.json) {
37587
38091
  console.log(chalk2.green(`\u2713 Generated .env.example (${envVarCount} variables from ${installed.length} skills)`));
@@ -37591,11 +38095,11 @@ Installing recommended skills for ${options.for} (${options.scope})...
37591
38095
  console.log(chalk2.dim(" No environment variables detected across installed skills"));
37592
38096
  }
37593
38097
  }
37594
- const gitignorePath = join6(cwd, ".gitignore");
38098
+ const gitignorePath = join8(cwd, ".gitignore");
37595
38099
  const gitignoreEntry = ".skills/";
37596
38100
  let gitignoreContent = "";
37597
- if (existsSync6(gitignorePath)) {
37598
- gitignoreContent = readFileSync5(gitignorePath, "utf-8");
38101
+ if (existsSync8(gitignorePath)) {
38102
+ gitignoreContent = readFileSync7(gitignorePath, "utf-8");
37599
38103
  }
37600
38104
  let gitignoreUpdated = false;
37601
38105
  if (!gitignoreContent.includes(gitignoreEntry)) {
@@ -37641,7 +38145,7 @@ Initialized for ${installed.length} installed skill(s)`));
37641
38145
  }
37642
38146
  }
37643
38147
  });
37644
- program2.command("remove").alias("rm").argument("<skill>", "Skill to remove").option("--verbose", "Enable verbose debug logging", false).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).option("-y, --yes", "Skip confirmation prompt", false).description("Remove an installed skill").action(async (skill, options) => {
38148
+ program2.command("remove").alias("rm").argument("<skill>", "Skill to remove").option("--verbose", "Enable verbose debug logging", false).option("--json", "Output as JSON", false).option("--for <agent>", "Remove from agent: claude, codex, gemini, pi, opencode, or all").option("--scope <scope>", "Remove scope: global or project", "global").option("--dry-run", "Print what would happen without actually removing", false).option("-y, --yes", "Skip confirmation prompt", false).description("Remove an installed skill").action(async (skill, options) => {
37645
38149
  debug(`remove: skill=${skill} for=${options.for ?? "none"} scope=${options.scope} dryRun=${options.dryRun}`);
37646
38150
  if (!options.yes && !options.dryRun && isTTY) {
37647
38151
  const skillName = normalizeSkillName(skill);
@@ -37724,10 +38228,10 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37724
38228
  }
37725
38229
  function collectFiles(dir, base = "") {
37726
38230
  const files = new Set;
37727
- if (!existsSync6(dir))
38231
+ if (!existsSync8(dir))
37728
38232
  return files;
37729
- for (const entry of readdirSync4(dir)) {
37730
- const full = join6(dir, entry);
38233
+ for (const entry of readdirSync5(dir)) {
38234
+ const full = join8(dir, entry);
37731
38235
  const rel = base ? `${base}/${entry}` : entry;
37732
38236
  if (statSync3(full).isDirectory()) {
37733
38237
  for (const f of collectFiles(full, rel))
@@ -37741,7 +38245,7 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37741
38245
  const updateResults = [];
37742
38246
  for (const name of toUpdate) {
37743
38247
  const skillName = normalizeSkillName(name);
37744
- const destPath = join6(process.cwd(), ".skills", skillName);
38248
+ const destPath = join8(process.cwd(), ".skills", skillName);
37745
38249
  const beforeFiles = collectFiles(destPath);
37746
38250
  const result = installSkill(name, { overwrite: true });
37747
38251
  const afterFiles = collectFiles(destPath);
@@ -37819,10 +38323,11 @@ Tags:
37819
38323
  console.log(` ${chalk2.cyan(name)} (${count})`);
37820
38324
  }
37821
38325
  });
37822
- 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) => {
38326
+ program2.command("mcp").option("--register <agent>", "Register MCP server with agent: claude, codex, gemini, pi, opencode, or all").description("Start MCP server (stdio) or register with an agent").action(async (options) => {
37823
38327
  if (options.register) {
37824
- const agents = options.register === "all" ? ["claude", "codex", "gemini"] : [options.register];
37825
- const binPath = join6(import.meta.dir, "..", "mcp", "index.ts");
38328
+ const agents = options.register === "all" ? [...AGENT_TARGETS] : [options.register];
38329
+ const binPath = join8(import.meta.dir, "..", "mcp", "index.ts");
38330
+ const { homedir: hd } = await import("os");
37826
38331
  for (const agent of agents) {
37827
38332
  if (agent === "claude") {
37828
38333
  try {
@@ -37836,8 +38341,7 @@ program2.command("mcp").option("--register <agent>", "Register MCP server with a
37836
38341
  console.log(chalk2.yellow(`Manual registration: claude mcp add skills -- bun run ${binPath}`));
37837
38342
  }
37838
38343
  } else if (agent === "codex") {
37839
- const { homedir: homedir4 } = await import("os");
37840
- const configPath = join6(homedir4(), ".codex", "config.toml");
38344
+ const configPath = join8(hd(), ".codex", "config.toml");
37841
38345
  console.log(chalk2.bold(`
37842
38346
  Add to ${configPath}:`));
37843
38347
  console.log(chalk2.dim(`[mcp_servers.skills]
@@ -37845,16 +38349,25 @@ command = "bun"
37845
38349
  args = ["run", "${binPath}"]`));
37846
38350
  console.log(chalk2.green(`\u2713 Codex MCP config shown above`));
37847
38351
  } else if (agent === "gemini") {
37848
- const { homedir: homedir4 } = await import("os");
37849
- const configPath = join6(homedir4(), ".gemini", "settings.json");
38352
+ const configPath = join8(hd(), ".gemini", "settings.json");
37850
38353
  console.log(chalk2.bold(`
37851
38354
  Add to ${configPath} mcpServers:`));
37852
- console.log(chalk2.dim(JSON.stringify({
37853
- skills: { command: "bun", args: ["run", binPath] }
37854
- }, null, 2)));
38355
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
37855
38356
  console.log(chalk2.green(`\u2713 Gemini MCP config shown above`));
38357
+ } else if (agent === "pi") {
38358
+ const configPath = join8(hd(), ".pi", "agent", "mcp.json");
38359
+ console.log(chalk2.bold(`
38360
+ Add to ${configPath}:`));
38361
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
38362
+ console.log(chalk2.green(`\u2713 pi.dev MCP config shown above`));
38363
+ } else if (agent === "opencode") {
38364
+ const configPath = join8(hd(), ".opencode", "config.json");
38365
+ console.log(chalk2.bold(`
38366
+ Add to ${configPath} mcp section:`));
38367
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
38368
+ console.log(chalk2.green(`\u2713 OpenCode MCP config shown above`));
37856
38369
  } else {
37857
- console.error(chalk2.red(`Unknown agent: ${agent}. Available: claude, codex, gemini, all`));
38370
+ console.error(chalk2.red(`Unknown agent: ${agent}. Available: ${AGENT_TARGETS.join(", ")}, all`));
37858
38371
  process.exitCode = 1;
37859
38372
  }
37860
38373
  }
@@ -38040,18 +38553,18 @@ program2.command("export").option("--json", "Output as JSON (default behavior)",
38040
38553
  };
38041
38554
  console.log(JSON.stringify(payload, null, 2));
38042
38555
  });
38043
- 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) => {
38556
+ 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, pi, opencode, 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) => {
38044
38557
  let raw;
38045
38558
  try {
38046
38559
  if (file2 === "-") {
38047
38560
  raw = await new Response(process.stdin).text();
38048
38561
  } else {
38049
- if (!existsSync6(file2)) {
38562
+ if (!existsSync8(file2)) {
38050
38563
  console.error(chalk2.red(`File not found: ${file2}`));
38051
38564
  process.exitCode = 1;
38052
38565
  return;
38053
38566
  }
38054
- raw = readFileSync5(file2, "utf-8");
38567
+ raw = readFileSync7(file2, "utf-8");
38055
38568
  }
38056
38569
  } catch (err) {
38057
38570
  console.error(chalk2.red(`Failed to read file: ${err.message}`));
@@ -38142,7 +38655,8 @@ Imported ${succeeded}/${total} skill(s)${failed > 0 ? ` (${failed} failed)` : ""
38142
38655
  process.exitCode = 1;
38143
38656
  }
38144
38657
  });
38145
- program2.command("doctor").option("--json", "Output as JSON", false).description("Check environment variables for installed skills").action((options) => {
38658
+ program2.command("doctor").option("--json", "Output as JSON", false).description("Check env vars, system deps, and install health for installed skills").action((options) => {
38659
+ const { execSync } = __require("child_process");
38146
38660
  const installed = getInstalledSkills();
38147
38661
  if (installed.length === 0) {
38148
38662
  if (options.json) {
@@ -38152,37 +38666,53 @@ program2.command("doctor").option("--json", "Output as JSON", false).description
38152
38666
  }
38153
38667
  return;
38154
38668
  }
38669
+ function isCommandAvailable(cmd) {
38670
+ try {
38671
+ execSync(`which ${cmd}`, { stdio: "ignore" });
38672
+ return true;
38673
+ } catch {
38674
+ return false;
38675
+ }
38676
+ }
38155
38677
  const report = [];
38156
38678
  for (const name of installed) {
38157
38679
  const reqs = getSkillRequirements(name);
38158
- const envVars = (reqs?.envVars ?? []).map((v) => ({
38159
- name: v,
38160
- set: !!process.env[v]
38161
- }));
38162
- report.push({ skill: name, envVars });
38680
+ const envVars = (reqs?.envVars ?? []).map((v) => ({ name: v, set: !!process.env[v] }));
38681
+ const systemDeps = (reqs?.systemDeps ?? []).map((d) => ({ name: d, available: isCommandAvailable(d) }));
38682
+ const healthy = envVars.every((v) => v.set) && systemDeps.every((d) => d.available);
38683
+ report.push({ skill: name, envVars, systemDeps, healthy });
38163
38684
  }
38164
38685
  if (options.json) {
38165
38686
  console.log(JSON.stringify(report, null, 2));
38166
38687
  return;
38167
38688
  }
38689
+ const issues = report.filter((r) => !r.healthy);
38168
38690
  console.log(chalk2.bold(`
38169
- Skills Doctor (${installed.length} installed):
38691
+ Skills Doctor \u2014 ${installed.length} installed, ${issues.length} with issues:
38170
38692
  `));
38171
38693
  for (const entry of report) {
38172
- console.log(chalk2.bold(` ${entry.skill}`));
38173
- if (entry.envVars.length === 0) {
38174
- console.log(chalk2.dim(" No environment variables required"));
38175
- } else {
38176
- for (const v of entry.envVars) {
38177
- const status = v.set ? chalk2.green("set") : chalk2.red("missing");
38178
- console.log(` ${v.name} [${status}]`);
38179
- }
38694
+ const icon = entry.healthy ? chalk2.green("\u2713") : chalk2.red("\u2717");
38695
+ console.log(` ${icon} ${chalk2.bold(entry.skill)}`);
38696
+ for (const v of entry.envVars) {
38697
+ const status = v.set ? chalk2.green("set") : chalk2.red("missing");
38698
+ console.log(` ${v.name} [${status}]`);
38180
38699
  }
38700
+ for (const d of entry.systemDeps) {
38701
+ const status = d.available ? chalk2.green("available") : chalk2.red("not found");
38702
+ console.log(` ${d.name} [${status}]`);
38703
+ }
38704
+ if (entry.envVars.length === 0 && entry.systemDeps.length === 0) {
38705
+ console.log(chalk2.dim(" No requirements"));
38706
+ }
38707
+ }
38708
+ if (issues.length === 0) {
38709
+ console.log(chalk2.green(`
38710
+ All skills healthy! \u2713`));
38181
38711
  }
38182
38712
  });
38183
38713
  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) => {
38184
38714
  const cwd = process.cwd();
38185
- const envFilePath = join6(cwd, ".env");
38715
+ const envFilePath = join8(cwd, ".env");
38186
38716
  if (options.set) {
38187
38717
  const eqIdx = options.set.indexOf("=");
38188
38718
  if (eqIdx === -1) {
@@ -38198,8 +38728,8 @@ program2.command("auth").argument("[skill]", "Skill name (omit to check all inst
38198
38728
  return;
38199
38729
  }
38200
38730
  let existing = "";
38201
- if (existsSync6(envFilePath)) {
38202
- existing = readFileSync5(envFilePath, "utf-8");
38731
+ if (existsSync8(envFilePath)) {
38732
+ existing = readFileSync7(envFilePath, "utf-8");
38203
38733
  }
38204
38734
  const keyPattern = new RegExp(`^${key}=.*$`, "m");
38205
38735
  let updated;
@@ -38212,7 +38742,7 @@ program2.command("auth").argument("[skill]", "Skill name (omit to check all inst
38212
38742
  ${key}=${value}
38213
38743
  `;
38214
38744
  }
38215
- writeFileSync3(envFilePath, updated, "utf-8");
38745
+ writeFileSync4(envFilePath, updated, "utf-8");
38216
38746
  console.log(chalk2.green(`Set ${key} in ${envFilePath}`));
38217
38747
  return;
38218
38748
  }
@@ -38282,21 +38812,20 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
38282
38812
  const version2 = package_default.version;
38283
38813
  const cwd = process.cwd();
38284
38814
  const installed = getInstalledSkills();
38285
- const agentNames = ["claude", "codex", "gemini"];
38286
38815
  const agentConfigs = [];
38287
- for (const agent of agentNames) {
38288
- const agentSkillsPath = join6(homedir4(), `.${agent}`, "skills");
38289
- const exists = existsSync6(agentSkillsPath);
38816
+ for (const agent of AGENT_TARGETS) {
38817
+ const agentSkillsPath = getAgentSkillsDir(agent, "global");
38818
+ const exists = existsSync8(agentSkillsPath);
38290
38819
  let skillCount = 0;
38291
38820
  if (exists) {
38292
38821
  try {
38293
- skillCount = readdirSync4(agentSkillsPath).filter((f) => {
38294
- const full = join6(agentSkillsPath, f);
38822
+ skillCount = readdirSync5(agentSkillsPath).filter((f) => {
38823
+ const full = join8(agentSkillsPath, f);
38295
38824
  return f.startsWith("skill-") && statSync3(full).isDirectory();
38296
38825
  }).length;
38297
38826
  } catch {}
38298
38827
  }
38299
- agentConfigs.push({ agent, path: agentSkillsPath, exists, skillCount });
38828
+ agentConfigs.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
38300
38829
  }
38301
38830
  const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
38302
38831
  if (options.json) {
@@ -38436,19 +38965,19 @@ program2.command("outdated").option("--json", "Output as JSON", false).descripti
38436
38965
  const upToDate = [];
38437
38966
  for (const name of installed) {
38438
38967
  const skillName = normalizeSkillName(name);
38439
- const installedPkgPath = join6(cwd, ".skills", skillName, "package.json");
38968
+ const installedPkgPath = join8(cwd, ".skills", skillName, "package.json");
38440
38969
  let installedVersion = "unknown";
38441
- if (existsSync6(installedPkgPath)) {
38970
+ if (existsSync8(installedPkgPath)) {
38442
38971
  try {
38443
- installedVersion = JSON.parse(readFileSync5(installedPkgPath, "utf-8")).version || "unknown";
38972
+ installedVersion = JSON.parse(readFileSync7(installedPkgPath, "utf-8")).version || "unknown";
38444
38973
  } catch {}
38445
38974
  }
38446
38975
  const registryPath = getSkillPath(name);
38447
- const registryPkgPath = join6(registryPath, "package.json");
38976
+ const registryPkgPath = join8(registryPath, "package.json");
38448
38977
  let registryVersion = "unknown";
38449
- if (existsSync6(registryPkgPath)) {
38978
+ if (existsSync8(registryPkgPath)) {
38450
38979
  try {
38451
- registryVersion = JSON.parse(readFileSync5(registryPkgPath, "utf-8")).version || "unknown";
38980
+ registryVersion = JSON.parse(readFileSync7(registryPkgPath, "utf-8")).version || "unknown";
38452
38981
  } catch {}
38453
38982
  }
38454
38983
  if (installedVersion !== registryVersion) {
@@ -38513,7 +39042,470 @@ configCmd.command("get <key>").description("Get a specific configuration value")
38513
39042
  configCmd.command("path").description("Show configuration file paths").action(() => {
38514
39043
  const globalPath = getConfigPath("global");
38515
39044
  const projectPath = getConfigPath("project");
38516
- console.log(`${chalk2.cyan("global")}: ${globalPath}${existsSync6(globalPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
38517
- console.log(`${chalk2.cyan("project")}: ${projectPath}${existsSync6(projectPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
39045
+ console.log(`${chalk2.cyan("global")}: ${globalPath}${existsSync8(globalPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
39046
+ console.log(`${chalk2.cyan("project")}: ${projectPath}${existsSync8(projectPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
39047
+ });
39048
+ program2.command("create").argument("<name>", "Skill name (e.g. my-tool)").option("--category <category>", "Skill category", "Development Tools").option("--description <description>", "Short description of what the skill does").option("--tags <tags>", "Comma-separated tags (e.g. api,testing,automation)").option("--global", "Create in ~/.hasna/skills/custom/ instead of .skills/custom-skills/", false).option("--json", "Output result as JSON", false).description("Scaffold a new custom skill directory").action((name, options) => {
39049
+ const { homedir: homedir4 } = __require("os");
39050
+ const bare = name.replace(/^skill-/, "");
39051
+ const dirName = `skill-${bare}`;
39052
+ const baseDir = options.global ? join8(homedir4(), ".hasna", "skills", "custom") : join8(process.cwd(), ".skills", "custom-skills");
39053
+ const skillDir = join8(baseDir, dirName);
39054
+ if (existsSync8(skillDir)) {
39055
+ if (options.json) {
39056
+ console.log(JSON.stringify({ error: `Skill '${bare}' already exists at ${skillDir}` }));
39057
+ } else {
39058
+ console.error(chalk2.red(`Skill '${bare}' already exists at ${skillDir}`));
39059
+ }
39060
+ process.exitCode = 1;
39061
+ return;
39062
+ }
39063
+ const description = options.description || `${bare} skill`;
39064
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [bare];
39065
+ const displayName = bare.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
39066
+ const category = options.category;
39067
+ mkdirSync4(join8(skillDir, "src"), { recursive: true });
39068
+ writeFileSync4(join8(skillDir, "SKILL.md"), [
39069
+ "---",
39070
+ `name: ${bare}`,
39071
+ `description: ${description}`,
39072
+ `displayName: ${displayName}`,
39073
+ `category: ${category}`,
39074
+ `tags: [${tags.join(", ")}]`,
39075
+ "---",
39076
+ "",
39077
+ `# ${displayName}`,
39078
+ "",
39079
+ description,
39080
+ "",
39081
+ "## Usage",
39082
+ "",
39083
+ "```bash",
39084
+ `${bare} --help`,
39085
+ "```",
39086
+ ""
39087
+ ].join(`
39088
+ `));
39089
+ writeFileSync4(join8(skillDir, "src", "index.ts"), [
39090
+ `#!/usr/bin/env bun`,
39091
+ `/**`,
39092
+ ` * ${displayName} \u2014 ${description}`,
39093
+ ` */`,
39094
+ "",
39095
+ `console.log("${displayName}");`,
39096
+ ""
39097
+ ].join(`
39098
+ `));
39099
+ writeFileSync4(join8(skillDir, "package.json"), JSON.stringify({
39100
+ name: `skill-${bare}`,
39101
+ version: "0.1.0",
39102
+ description,
39103
+ bin: { [bare]: "./src/index.ts" },
39104
+ scripts: { dev: `bun src/index.ts` },
39105
+ dependencies: {}
39106
+ }, null, 2) + `
39107
+ `);
39108
+ writeFileSync4(join8(skillDir, "tsconfig.json"), JSON.stringify({
39109
+ compilerOptions: {
39110
+ target: "ES2022",
39111
+ module: "ESNext",
39112
+ moduleResolution: "bundler",
39113
+ strict: true,
39114
+ outDir: "dist"
39115
+ },
39116
+ include: ["src/**/*.ts"]
39117
+ }, null, 2) + `
39118
+ `);
39119
+ clearRegistryCache();
39120
+ if (options.json) {
39121
+ console.log(JSON.stringify({ created: true, name: bare, path: skillDir, category, tags }));
39122
+ } else {
39123
+ console.log(chalk2.green(`\u2713 Created custom skill '${bare}' at ${skillDir}`));
39124
+ console.log(chalk2.dim(` Category: ${category}`));
39125
+ console.log(chalk2.dim(` Tags: ${tags.join(", ")}`));
39126
+ console.log("");
39127
+ console.log(` ${chalk2.cyan("Edit:")} ${join8(skillDir, "src", "index.ts")}`);
39128
+ console.log(` ${chalk2.cyan("Run:")} bun ${join8(skillDir, "src", "index.ts")}`);
39129
+ console.log(` ${chalk2.cyan("Docs:")} ${join8(skillDir, "SKILL.md")}`);
39130
+ }
39131
+ });
39132
+ program2.command("sync").option("--to <agent>", "Push custom skills to agent: claude, codex, gemini, pi, opencode, or all").option("--from <agent>", "List agent skills and show which are unknown to the registry").option("--register", "With --from: copy unknown agent skills into ~/.hasna/skills/custom/ to add them to the registry", false).option("--scope <scope>", "Agent install scope: global or project", "global").option("--json", "Output as JSON", false).description("Sync custom skills with agent directories (--to or --from)").action((options) => {
39133
+ const { homedir: homedir4 } = __require("os");
39134
+ if (!options.to && !options.from) {
39135
+ console.error(chalk2.red("Specify --to <agent> or --from <agent>"));
39136
+ process.exitCode = 1;
39137
+ return;
39138
+ }
39139
+ if (options.from) {
39140
+ const agentName = options.from;
39141
+ if (!AGENT_TARGETS.includes(agentName)) {
39142
+ console.error(chalk2.red(`Unknown agent: ${agentName}. Available: ${AGENT_TARGETS.join(", ")}`));
39143
+ process.exitCode = 1;
39144
+ return;
39145
+ }
39146
+ const agentDir = getAgentSkillsDir(agentName, options.scope);
39147
+ if (!existsSync8(agentDir)) {
39148
+ if (options.json) {
39149
+ console.log(JSON.stringify({ agentDir, skills: [], message: "Directory not found" }));
39150
+ } else {
39151
+ console.log(chalk2.dim(`No skills directory found at ${agentDir}`));
39152
+ }
39153
+ return;
39154
+ }
39155
+ const registry2 = loadRegistry();
39156
+ const registryNames = new Set(registry2.map((s) => s.name));
39157
+ const found = [];
39158
+ for (const entry of readdirSync5(agentDir, { withFileTypes: true })) {
39159
+ if (!entry.isDirectory())
39160
+ continue;
39161
+ const bare = entry.name.replace(/^skill-/, "");
39162
+ found.push({
39163
+ name: bare,
39164
+ path: join8(agentDir, entry.name),
39165
+ inRegistry: registryNames.has(bare)
39166
+ });
39167
+ }
39168
+ const unknown3 = found.filter((s) => !s.inRegistry);
39169
+ if (options.register && unknown3.length > 0) {
39170
+ const globalSkillsDir = join8(homedir4(), ".hasna", "skills", "custom");
39171
+ const registered = [];
39172
+ for (const s of unknown3) {
39173
+ const srcSkillMd = join8(s.path, "SKILL.md");
39174
+ if (!existsSync8(srcSkillMd))
39175
+ continue;
39176
+ const destDir = join8(globalSkillsDir, `skill-${s.name}`);
39177
+ if (!existsSync8(destDir)) {
39178
+ mkdirSync4(destDir, { recursive: true });
39179
+ }
39180
+ writeFileSync4(join8(destDir, "SKILL.md"), readFileSync7(srcSkillMd, "utf-8"));
39181
+ registered.push(s.name);
39182
+ }
39183
+ clearRegistryCache();
39184
+ if (options.json) {
39185
+ console.log(JSON.stringify({ agentDir, skills: found, registered }));
39186
+ } else {
39187
+ for (const name of registered) {
39188
+ console.log(chalk2.green(`\u2713 Registered '${name}' into ~/.hasna/skills/custom/ (global custom)`));
39189
+ }
39190
+ if (registered.length === 0)
39191
+ console.log(chalk2.dim("No new skills to register (all SKILL.md files missing)"));
39192
+ }
39193
+ return;
39194
+ }
39195
+ if (options.json) {
39196
+ console.log(JSON.stringify({ agentDir, skills: found }));
39197
+ } else {
39198
+ console.log(chalk2.bold(`
39199
+ Agent skills in ~/.${agentName}/skills/ (${found.length} found):
39200
+ `));
39201
+ for (const s of found) {
39202
+ const label = s.inRegistry ? chalk2.green("\u2713 in registry") : chalk2.yellow("\u2717 not in registry");
39203
+ console.log(` ${chalk2.cyan(s.name)} \u2014 ${label}`);
39204
+ }
39205
+ if (unknown3.length > 0) {
39206
+ console.log("");
39207
+ console.log(chalk2.dim(`Tip: ${unknown3.length} skill(s) not in registry. Run with --register to add them to ~/.hasna/skills/custom/.`));
39208
+ }
39209
+ }
39210
+ return;
39211
+ }
39212
+ if (options.to) {
39213
+ let agents;
39214
+ try {
39215
+ agents = resolveAgents(options.to);
39216
+ } catch (err) {
39217
+ console.error(chalk2.red(err.message));
39218
+ process.exitCode = 1;
39219
+ return;
39220
+ }
39221
+ const registry2 = loadRegistry();
39222
+ const customSkills = registry2.filter((s) => s.source === "custom");
39223
+ if (customSkills.length === 0) {
39224
+ if (options.json) {
39225
+ console.log(JSON.stringify({ pushed: 0, message: "No custom skills found" }));
39226
+ } else {
39227
+ console.log(chalk2.dim("No custom skills found. Use 'skills create <name>' to scaffold one."));
39228
+ }
39229
+ return;
39230
+ }
39231
+ const results = [];
39232
+ for (const skill of customSkills) {
39233
+ for (const agent of agents) {
39234
+ const result = installSkillForAgent(skill.name, {
39235
+ agent,
39236
+ scope: options.scope
39237
+ }, generateSkillMd);
39238
+ results.push({ skill: skill.name, agent, success: result.success, error: result.error });
39239
+ }
39240
+ }
39241
+ if (options.json) {
39242
+ console.log(JSON.stringify({ pushed: results.filter((r) => r.success).length, results }));
39243
+ } else {
39244
+ for (const r of results) {
39245
+ if (r.success) {
39246
+ console.log(chalk2.green(`\u2713 ${r.skill} \u2192 ${r.agent}`));
39247
+ } else {
39248
+ console.log(chalk2.red(`\u2717 ${r.skill} \u2192 ${r.agent}: ${r.error}`));
39249
+ }
39250
+ }
39251
+ }
39252
+ }
39253
+ });
39254
+ program2.command("validate").argument("<name>", "Skill name to validate").option("--json", "Output as JSON", false).description("Validate a skill's directory structure (SKILL.md, package.json, src/index.ts, tsconfig.json)").action((name, options) => {
39255
+ const skillPath = getSkillPath(name);
39256
+ const issues = [];
39257
+ if (!existsSync8(skillPath)) {
39258
+ if (options.json) {
39259
+ console.log(JSON.stringify({ name, valid: false, issues: [`Skill directory not found: ${skillPath}`] }));
39260
+ } else {
39261
+ console.error(chalk2.red(`Skill '${name}' not found at ${skillPath}`));
39262
+ }
39263
+ process.exitCode = 1;
39264
+ return;
39265
+ }
39266
+ if (!existsSync8(join8(skillPath, "SKILL.md")))
39267
+ issues.push("Missing SKILL.md");
39268
+ if (!existsSync8(join8(skillPath, "tsconfig.json")))
39269
+ issues.push("Missing tsconfig.json");
39270
+ const pkgPath = join8(skillPath, "package.json");
39271
+ if (!existsSync8(pkgPath)) {
39272
+ issues.push("Missing package.json");
39273
+ } else {
39274
+ try {
39275
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
39276
+ if (!pkg.bin || Object.keys(pkg.bin).length === 0)
39277
+ issues.push("package.json missing 'bin' entry");
39278
+ } catch {
39279
+ issues.push("package.json is invalid JSON");
39280
+ }
39281
+ }
39282
+ if (!existsSync8(join8(skillPath, "src"))) {
39283
+ issues.push("Missing src/ directory");
39284
+ } else if (!existsSync8(join8(skillPath, "src", "index.ts"))) {
39285
+ issues.push("Missing src/index.ts");
39286
+ }
39287
+ const valid = issues.length === 0;
39288
+ if (options.json) {
39289
+ console.log(JSON.stringify({ name, valid, path: skillPath, issues }));
39290
+ } else if (valid) {
39291
+ console.log(chalk2.green(`\u2713 ${name} \u2014 all checks passed`));
39292
+ } else {
39293
+ console.log(chalk2.red(`\u2717 ${name} \u2014 ${issues.length} issue(s):`));
39294
+ for (const issue2 of issues)
39295
+ console.log(chalk2.red(` \u2022 ${issue2}`));
39296
+ process.exitCode = 1;
39297
+ }
39298
+ });
39299
+ program2.command("diff").argument("<name>", "Skill name to diff").option("--json", "Output as JSON", false).description("Show files that differ between installed version and source (preview before update)").action((name, options) => {
39300
+ const bare = name.replace(/^skill-/, "");
39301
+ const skillName = `skill-${bare}`;
39302
+ const destPath = join8(process.cwd(), ".skills", skillName);
39303
+ const sourcePath = getSkillPath(bare);
39304
+ if (!existsSync8(sourcePath)) {
39305
+ if (options.json) {
39306
+ console.log(JSON.stringify({ error: `Skill '${bare}' not found in registry` }));
39307
+ } else {
39308
+ skillNotFound(bare);
39309
+ }
39310
+ process.exitCode = 1;
39311
+ return;
39312
+ }
39313
+ if (!existsSync8(destPath)) {
39314
+ if (options.json) {
39315
+ console.log(JSON.stringify({ installed: false, message: `'${bare}' is not installed locally` }));
39316
+ } else {
39317
+ console.log(chalk2.dim(`'${bare}' is not installed. Run: skills install ${bare}`));
39318
+ }
39319
+ return;
39320
+ }
39321
+ function collectFiles(dir, base = "") {
39322
+ const files = new Map;
39323
+ if (!existsSync8(dir))
39324
+ return files;
39325
+ for (const entry of readdirSync5(dir)) {
39326
+ const full = join8(dir, entry);
39327
+ const rel = base ? `${base}/${entry}` : entry;
39328
+ if (statSync3(full).isDirectory()) {
39329
+ for (const [k, v] of collectFiles(full, rel))
39330
+ files.set(k, v);
39331
+ } else {
39332
+ try {
39333
+ files.set(rel, readFileSync7(full, "utf-8"));
39334
+ } catch {
39335
+ files.set(rel, "");
39336
+ }
39337
+ }
39338
+ }
39339
+ return files;
39340
+ }
39341
+ const installed = collectFiles(destPath);
39342
+ const source = collectFiles(sourcePath);
39343
+ const changed = [];
39344
+ const added = [];
39345
+ const removed = [];
39346
+ for (const [file2, content] of source) {
39347
+ if (!installed.has(file2))
39348
+ added.push(file2);
39349
+ else if (installed.get(file2) !== content)
39350
+ changed.push(file2);
39351
+ }
39352
+ for (const file2 of installed.keys()) {
39353
+ if (!source.has(file2))
39354
+ removed.push(file2);
39355
+ }
39356
+ if (options.json) {
39357
+ console.log(JSON.stringify({ name: bare, changed, added, removed, upToDate: changed.length === 0 && added.length === 0 && removed.length === 0 }));
39358
+ return;
39359
+ }
39360
+ if (changed.length === 0 && added.length === 0 && removed.length === 0) {
39361
+ console.log(chalk2.green(`\u2713 ${bare} \u2014 up to date`));
39362
+ return;
39363
+ }
39364
+ console.log(chalk2.bold(`
39365
+ Diff for '${bare}':
39366
+ `));
39367
+ for (const f of changed)
39368
+ console.log(chalk2.yellow(` ~ ${f}`));
39369
+ for (const f of added)
39370
+ console.log(chalk2.green(` + ${f}`));
39371
+ for (const f of removed)
39372
+ console.log(chalk2.red(` - ${f}`));
39373
+ console.log(`
39374
+ ${chalk2.dim(`Run 'skills update ${bare}' to apply changes`)}`);
39375
+ });
39376
+ var scheduleCmd = program2.command("schedule").description("Manage scheduled skill runs (cron-based)");
39377
+ scheduleCmd.command("add").argument("<skill>", "Skill to schedule (bare name, e.g. image)").argument("<cron>", '5-field cron expression (e.g. "0 9 * * *" = daily at 9am)').option("--name <label>", "Human-readable label for this schedule").option("--args <args>", "Space-separated args to pass to the skill").option("--json", "Output as JSON", false).description("Add a cron schedule for a skill").action((skill, cron, options) => {
39378
+ const args = options.args ? options.args.split(" ").filter(Boolean) : undefined;
39379
+ const { schedule, error: error48 } = addSchedule(skill, cron, { name: options.name, args });
39380
+ if (options.json) {
39381
+ console.log(JSON.stringify(schedule ? { schedule } : { error: error48 }));
39382
+ return;
39383
+ }
39384
+ if (error48 || !schedule) {
39385
+ console.error(chalk2.red(`\u2717 ${error48 || "Failed to add schedule"}`));
39386
+ process.exitCode = 1;
39387
+ return;
39388
+ }
39389
+ console.log(chalk2.green(`\u2713 Scheduled '${schedule.name}'`));
39390
+ console.log(chalk2.dim(` Cron: ${schedule.cron}`));
39391
+ if (schedule.nextRun) {
39392
+ console.log(chalk2.dim(` Next run: ${new Date(schedule.nextRun).toLocaleString()}`));
39393
+ }
39394
+ console.log(chalk2.dim(` ID: ${schedule.id}`));
39395
+ });
39396
+ scheduleCmd.command("list").option("--json", "Output as JSON", false).description("List all scheduled skills").action((options) => {
39397
+ const schedules = listSchedules();
39398
+ if (options.json) {
39399
+ console.log(JSON.stringify(schedules));
39400
+ return;
39401
+ }
39402
+ if (schedules.length === 0) {
39403
+ console.log(chalk2.dim("No schedules. Run: skills schedule add <skill> <cron>"));
39404
+ return;
39405
+ }
39406
+ console.log(chalk2.bold(`
39407
+ Scheduled skills (${schedules.length}):
39408
+ `));
39409
+ for (const s of schedules) {
39410
+ const status = s.enabled ? chalk2.green("enabled") : chalk2.dim("disabled");
39411
+ const last = s.lastRun ? `last: ${new Date(s.lastRun).toLocaleString()} [${s.lastRunStatus ?? "?"}]` : "never run";
39412
+ const next = s.nextRun ? `next: ${new Date(s.nextRun).toLocaleString()}` : "";
39413
+ console.log(` ${chalk2.cyan(s.name)} [${status}]`);
39414
+ console.log(chalk2.dim(` skill: ${s.skill} cron: ${s.cron} ${last} ${next}`));
39415
+ }
39416
+ });
39417
+ scheduleCmd.command("remove").argument("<id-or-name>", "Schedule ID or name to remove").option("--json", "Output as JSON", false).description("Remove a schedule").action((idOrName, options) => {
39418
+ const removed = removeSchedule(idOrName);
39419
+ if (options.json) {
39420
+ console.log(JSON.stringify({ removed, idOrName }));
39421
+ return;
39422
+ }
39423
+ if (removed) {
39424
+ console.log(chalk2.green(`\u2713 Removed schedule '${idOrName}'`));
39425
+ } else {
39426
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39427
+ process.exitCode = 1;
39428
+ }
39429
+ });
39430
+ scheduleCmd.command("enable").argument("<id-or-name>", "Schedule ID or name").description("Enable a disabled schedule").action((idOrName) => {
39431
+ const ok = setScheduleEnabled(idOrName, true);
39432
+ if (ok)
39433
+ console.log(chalk2.green(`\u2713 Enabled '${idOrName}'`));
39434
+ else {
39435
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39436
+ process.exitCode = 1;
39437
+ }
39438
+ });
39439
+ scheduleCmd.command("disable").argument("<id-or-name>", "Schedule ID or name").description("Disable a schedule without removing it").action((idOrName) => {
39440
+ const ok = setScheduleEnabled(idOrName, false);
39441
+ if (ok)
39442
+ console.log(chalk2.green(`\u2713 Disabled '${idOrName}'`));
39443
+ else {
39444
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39445
+ process.exitCode = 1;
39446
+ }
39447
+ });
39448
+ scheduleCmd.command("run").option("--dry-run", "Show which schedules are due without running them", false).option("--json", "Output as JSON", false).description("Execute all due schedules now").action(async (options) => {
39449
+ const due = getDueSchedules();
39450
+ if (due.length === 0) {
39451
+ if (options.json)
39452
+ console.log(JSON.stringify({ ran: 0, schedules: [] }));
39453
+ else
39454
+ console.log(chalk2.dim("No schedules are due."));
39455
+ return;
39456
+ }
39457
+ if (options.dryRun) {
39458
+ if (options.json)
39459
+ console.log(JSON.stringify({ due: due.map((s) => s.name) }));
39460
+ else {
39461
+ console.log(chalk2.bold(`${due.length} schedule(s) due:
39462
+ `));
39463
+ for (const s of due)
39464
+ console.log(` ${chalk2.cyan(s.name)} \u2014 ${s.skill} (${s.cron})`);
39465
+ }
39466
+ return;
39467
+ }
39468
+ const results = [];
39469
+ for (const s of due) {
39470
+ try {
39471
+ const { runSkill: runSkill2 } = await Promise.resolve().then(() => (init_skillinfo(), exports_skillinfo));
39472
+ await runSkill2(s.skill, s.args ?? []);
39473
+ recordScheduleRun(s.id, "success");
39474
+ results.push({ name: s.name, skill: s.skill, status: "success" });
39475
+ } catch (err) {
39476
+ recordScheduleRun(s.id, "error");
39477
+ results.push({ name: s.name, skill: s.skill, status: "error", error: err.message });
39478
+ }
39479
+ }
39480
+ if (options.json) {
39481
+ console.log(JSON.stringify({ ran: results.length, results }));
39482
+ } else {
39483
+ for (const r of results) {
39484
+ const icon = r.status === "success" ? chalk2.green("\u2713") : chalk2.red("\u2717");
39485
+ console.log(`${icon} ${r.name} (${r.skill})`);
39486
+ if (r.error)
39487
+ console.log(chalk2.dim(` ${r.error}`));
39488
+ }
39489
+ }
39490
+ });
39491
+ scheduleCmd.command("validate").argument("<cron>", "Cron expression to validate").description("Validate a cron expression and show the next 5 run times").action((cron) => {
39492
+ const { getNextRun: getNextRun2 } = (init_scheduler(), __toCommonJS(exports_scheduler));
39493
+ const { valid, error: error48 } = validateCron(cron);
39494
+ if (!valid) {
39495
+ console.error(chalk2.red(`Invalid cron: ${error48}`));
39496
+ process.exitCode = 1;
39497
+ return;
39498
+ }
39499
+ console.log(chalk2.green(`\u2713 Valid cron: "${cron}"`));
39500
+ console.log(chalk2.dim(`
39501
+ Next 5 run times:`));
39502
+ let d = new Date;
39503
+ for (let i = 0;i < 5; i++) {
39504
+ const next = getNextRun2(cron, d);
39505
+ if (!next)
39506
+ break;
39507
+ console.log(` ${next.toLocaleString()}`);
39508
+ d = next;
39509
+ }
38518
39510
  });
38519
39511
  program2.parse();