@hasna/skills 0.1.14 → 0.1.15

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.15",
1900
1918
  description: "Skills library for AI coding agents",
1901
1919
  type: "module",
1902
1920
  bin: {
@@ -1981,8 +1999,91 @@ var init_package = __esm(() => {
1981
1999
  });
1982
2000
 
1983
2001
  // src/lib/registry.ts
2002
+ import { existsSync, readFileSync, readdirSync } from "fs";
2003
+ import { join } from "path";
2004
+ import { homedir } from "os";
2005
+ function parseSkillMdFrontmatter(content) {
2006
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
2007
+ if (!match)
2008
+ return null;
2009
+ const result = {};
2010
+ for (const line of match[1].split(`
2011
+ `)) {
2012
+ const colon = line.indexOf(":");
2013
+ if (colon === -1)
2014
+ continue;
2015
+ const key = line.slice(0, colon).trim();
2016
+ const value = line.slice(colon + 1).trim();
2017
+ if (!key || !value)
2018
+ continue;
2019
+ if (key === "name")
2020
+ result.name = value;
2021
+ else if (key === "description")
2022
+ result.description = value;
2023
+ else if (key === "displayName" || key === "display_name")
2024
+ result.displayName = value;
2025
+ else if (key === "category")
2026
+ result.category = value;
2027
+ else if (key === "tags") {
2028
+ result.tags = value.replace(/[\[\]]/g, "").split(",").map((t) => t.trim()).filter(Boolean);
2029
+ }
2030
+ }
2031
+ return Object.keys(result).length > 0 ? result : null;
2032
+ }
2033
+ function discoverSkillsInDir(dir) {
2034
+ if (!existsSync(dir))
2035
+ return [];
2036
+ const result = [];
2037
+ try {
2038
+ const entries = readdirSync(dir, { withFileTypes: true });
2039
+ for (const entry of entries) {
2040
+ if (!entry.isDirectory())
2041
+ continue;
2042
+ const skillMdPath = join(dir, entry.name, "SKILL.md");
2043
+ if (!existsSync(skillMdPath))
2044
+ continue;
2045
+ let content;
2046
+ try {
2047
+ content = readFileSync(skillMdPath, "utf-8");
2048
+ } catch {
2049
+ continue;
2050
+ }
2051
+ const fm = parseSkillMdFrontmatter(content);
2052
+ if (!fm?.name)
2053
+ continue;
2054
+ const name = fm.name.replace(/^skill-/, "");
2055
+ result.push({
2056
+ name,
2057
+ displayName: fm.displayName || name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
2058
+ description: fm.description || "",
2059
+ category: fm.category || "Development Tools",
2060
+ tags: fm.tags || [],
2061
+ source: "custom"
2062
+ });
2063
+ }
2064
+ } catch {}
2065
+ return result;
2066
+ }
2067
+ function loadRegistry(cwd) {
2068
+ const now = Date.now();
2069
+ if (_registryCache && now - _registryCacheTime < REGISTRY_CACHE_TTL) {
2070
+ return _registryCache;
2071
+ }
2072
+ const official = SKILLS.map((s) => ({ ...s, source: "official" }));
2073
+ const globalCustom = discoverSkillsInDir(join(homedir(), ".skills"));
2074
+ const projectCustom = discoverSkillsInDir(join(cwd || process.cwd(), ".skills", "custom-skills"));
2075
+ const customNames = new Set([...globalCustom, ...projectCustom].map((s) => s.name));
2076
+ const filtered = official.filter((s) => !customNames.has(s.name));
2077
+ _registryCache = [...filtered, ...globalCustom, ...projectCustom];
2078
+ _registryCacheTime = now;
2079
+ return _registryCache;
2080
+ }
2081
+ function clearRegistryCache() {
2082
+ _registryCache = null;
2083
+ _registryCacheTime = 0;
2084
+ }
1984
2085
  function getSkillsByCategory(category) {
1985
- return SKILLS.filter((s) => s.category === category);
2086
+ return loadRegistry().filter((s) => s.category === category);
1986
2087
  }
1987
2088
  function editDistance(a, b) {
1988
2089
  if (a === b)
@@ -2028,7 +2129,7 @@ function searchSkills(query) {
2028
2129
  if (words.length === 0)
2029
2130
  return [];
2030
2131
  const scored = [];
2031
- for (const skill of SKILLS) {
2132
+ for (const skill of loadRegistry()) {
2032
2133
  const nameLower = skill.name.toLowerCase();
2033
2134
  const displayNameLower = skill.displayName.toLowerCase();
2034
2135
  const descriptionLower = skill.description.toLowerCase();
@@ -2064,7 +2165,7 @@ function searchSkills(query) {
2064
2165
  return scored.map((s) => s.skill);
2065
2166
  }
2066
2167
  function getSkill(name) {
2067
- return SKILLS.find((s) => s.name === name);
2168
+ return loadRegistry().find((s) => s.name === name);
2068
2169
  }
2069
2170
  function levenshtein(a, b) {
2070
2171
  const m = a.length, n = b.length;
@@ -2078,10 +2179,10 @@ function levenshtein(a, b) {
2078
2179
  }
2079
2180
  function findSimilarSkills(query, maxResults = 3) {
2080
2181
  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);
2182
+ 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
2183
  return scored.slice(0, maxResults).map((s) => s.name);
2083
2184
  }
2084
- var CATEGORIES, SKILLS;
2185
+ var CATEGORIES, SKILLS, _registryCache = null, _registryCacheTime = 0, REGISTRY_CACHE_TTL = 5000;
2085
2186
  var init_registry = __esm(() => {
2086
2187
  CATEGORIES = [
2087
2188
  "Development Tools",
@@ -3119,6 +3220,20 @@ var init_registry = __esm(() => {
3119
3220
  category: "Design & Branding",
3120
3221
  tags: ["testimonials", "graphics", "social-proof", "marketing"]
3121
3222
  },
3223
+ {
3224
+ name: "colorextract",
3225
+ displayName: "Color Extract",
3226
+ description: "Extract complete color palettes from screenshots and images using Claude Vision. Outputs open-styles compatible profiles.",
3227
+ category: "Design & Branding",
3228
+ tags: ["colors", "palette", "design", "vision", "screenshot", "extract", "open-styles"]
3229
+ },
3230
+ {
3231
+ name: "siteanalyze",
3232
+ displayName: "Site Analyze",
3233
+ description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
3234
+ category: "Design & Branding",
3235
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
3236
+ },
3122
3237
  {
3123
3238
  name: "browse",
3124
3239
  displayName: "Browse",
@@ -5162,39 +5277,39 @@ var require_cli_spinners = __commonJS((exports, module) => {
5162
5277
  });
5163
5278
 
5164
5279
  // 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";
5280
+ import { existsSync as existsSync2, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, accessSync, constants } from "fs";
5281
+ import { join as join2, dirname } from "path";
5282
+ import { homedir as homedir2 } from "os";
5168
5283
  import { fileURLToPath } from "url";
5169
5284
  function findSkillsDir() {
5170
5285
  let dir = __dirname2;
5171
5286
  for (let i = 0;i < 5; i++) {
5172
- const candidate = join(dir, "skills");
5173
- if (existsSync(candidate)) {
5287
+ const candidate = join2(dir, "skills");
5288
+ if (existsSync2(candidate) && !dir.includes(".skills")) {
5174
5289
  return candidate;
5175
5290
  }
5176
5291
  dir = dirname(dir);
5177
5292
  }
5178
- return join(__dirname2, "..", "skills");
5293
+ return join2(__dirname2, "..", "skills");
5179
5294
  }
5180
5295
  function getSkillPath(name) {
5181
5296
  const skillName = normalizeSkillName(name);
5182
- return join(SKILLS_DIR, skillName);
5297
+ return join2(SKILLS_DIR, skillName);
5183
5298
  }
5184
5299
  function installSkill(name, options = {}) {
5185
5300
  const { targetDir = process.cwd(), overwrite = false } = options;
5186
5301
  const skillName = normalizeSkillName(name);
5187
5302
  const sourcePath = getSkillPath(name);
5188
- const destDir = join(targetDir, ".skills");
5189
- const destPath = join(destDir, skillName);
5190
- if (!existsSync(sourcePath)) {
5303
+ const destDir = join2(targetDir, ".skills");
5304
+ const destPath = join2(destDir, skillName);
5305
+ if (!existsSync2(sourcePath)) {
5191
5306
  return {
5192
5307
  skill: name,
5193
5308
  success: false,
5194
5309
  error: `Skill '${name}' not found`
5195
5310
  };
5196
5311
  }
5197
- if (existsSync(destPath) && !overwrite) {
5312
+ if (existsSync2(destPath) && !overwrite) {
5198
5313
  return {
5199
5314
  skill: name,
5200
5315
  success: false,
@@ -5203,10 +5318,10 @@ function installSkill(name, options = {}) {
5203
5318
  };
5204
5319
  }
5205
5320
  try {
5206
- if (!existsSync(destDir)) {
5321
+ if (!existsSync2(destDir)) {
5207
5322
  mkdirSync(destDir, { recursive: true });
5208
5323
  }
5209
- if (existsSync(destPath) && overwrite) {
5324
+ if (existsSync2(destPath) && overwrite) {
5210
5325
  rmSync(destPath, { recursive: true, force: true });
5211
5326
  }
5212
5327
  cpSync(sourcePath, destPath, {
@@ -5242,10 +5357,10 @@ function installSkill(name, options = {}) {
5242
5357
  }
5243
5358
  }
5244
5359
  function updateSkillsIndex(skillsDir) {
5245
- const indexPath = join(skillsDir, "index.ts");
5360
+ const indexPath = join2(skillsDir, "index.ts");
5246
5361
  const meta = loadMeta(skillsDir);
5247
5362
  const disabledSet = new Set(meta.disabled || []);
5248
- const skills = readdirSync(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
5363
+ const skills = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
5249
5364
  const exports = skills.map((s) => {
5250
5365
  const name = s.replace("skill-", "").replace(/-/g, "_");
5251
5366
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -5261,13 +5376,13 @@ ${exports}
5261
5376
  writeFileSync(indexPath, content);
5262
5377
  }
5263
5378
  function getMetaPath(skillsDir) {
5264
- return join(skillsDir, ".meta.json");
5379
+ return join2(skillsDir, ".meta.json");
5265
5380
  }
5266
5381
  function loadMeta(skillsDir) {
5267
5382
  const metaPath = getMetaPath(skillsDir);
5268
- if (existsSync(metaPath)) {
5383
+ if (existsSync2(metaPath)) {
5269
5384
  try {
5270
- return JSON.parse(readFileSync(metaPath, "utf-8"));
5385
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
5271
5386
  } catch {}
5272
5387
  }
5273
5388
  return { skills: {} };
@@ -5280,9 +5395,9 @@ function recordInstall(skillsDir, name) {
5280
5395
  const skillName = normalizeSkillName(name);
5281
5396
  let version = "unknown";
5282
5397
  try {
5283
- const pkgPath = join(skillsDir, skillName, "package.json");
5284
- if (existsSync(pkgPath)) {
5285
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
5398
+ const pkgPath = join2(skillsDir, skillName, "package.json");
5399
+ if (existsSync2(pkgPath)) {
5400
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5286
5401
  version = pkg.version || "unknown";
5287
5402
  }
5288
5403
  } catch {}
@@ -5295,23 +5410,23 @@ function recordRemove(skillsDir, name) {
5295
5410
  saveMeta(skillsDir, meta);
5296
5411
  }
5297
5412
  function getInstallMeta(targetDir = process.cwd()) {
5298
- return loadMeta(join(targetDir, ".skills"));
5413
+ return loadMeta(join2(targetDir, ".skills"));
5299
5414
  }
5300
5415
  function getInstalledSkills(targetDir = process.cwd()) {
5301
- const skillsDir = join(targetDir, ".skills");
5302
- if (!existsSync(skillsDir)) {
5416
+ const skillsDir = join2(targetDir, ".skills");
5417
+ if (!existsSync2(skillsDir)) {
5303
5418
  return [];
5304
5419
  }
5305
- return readdirSync(skillsDir).filter((f) => {
5306
- const fullPath = join(skillsDir, f);
5420
+ return readdirSync2(skillsDir).filter((f) => {
5421
+ const fullPath = join2(skillsDir, f);
5307
5422
  return f.startsWith("skill-") && statSync(fullPath).isDirectory();
5308
5423
  }).map((f) => f.replace("skill-", ""));
5309
5424
  }
5310
5425
  function removeSkill(name, targetDir = process.cwd()) {
5311
5426
  const skillName = normalizeSkillName(name);
5312
- const skillsDir = join(targetDir, ".skills");
5313
- const skillPath = join(skillsDir, skillName);
5314
- if (!existsSync(skillPath)) {
5427
+ const skillsDir = join2(targetDir, ".skills");
5428
+ const skillPath = join2(skillsDir, skillName);
5429
+ if (!existsSync2(skillPath)) {
5315
5430
  return false;
5316
5431
  }
5317
5432
  rmSync(skillPath, { recursive: true, force: true });
@@ -5329,27 +5444,31 @@ function resolveAgents(agentArg) {
5329
5444
  return [agent];
5330
5445
  }
5331
5446
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
5332
- const agentDir = `.${agent}`;
5333
- if (scope === "project") {
5334
- return join(projectDir || process.cwd(), agentDir, "skills");
5447
+ const base = projectDir || process.cwd();
5448
+ switch (agent) {
5449
+ case "pi":
5450
+ return scope === "project" ? join2(base, ".pi", "skills") : join2(homedir2(), ".pi", "agent", "skills");
5451
+ case "opencode":
5452
+ return scope === "project" ? join2(base, ".opencode", "skills") : join2(homedir2(), ".opencode", "skills");
5453
+ default:
5454
+ return scope === "project" ? join2(base, `.${agent}`, "skills") : join2(homedir2(), `.${agent}`, "skills");
5335
5455
  }
5336
- return join(homedir(), agentDir, "skills");
5337
5456
  }
5338
5457
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
5339
5458
  const skillName = normalizeSkillName(name);
5340
- return join(getAgentSkillsDir(agent, scope, projectDir), skillName);
5459
+ return join2(getAgentSkillsDir(agent, scope, projectDir), skillName);
5341
5460
  }
5342
5461
  function installSkillForAgent(name, options, generateSkillMd) {
5343
5462
  const { agent, scope = "global", projectDir } = options;
5344
5463
  const skillName = normalizeSkillName(name);
5345
5464
  const sourcePath = getSkillPath(name);
5346
- if (!existsSync(sourcePath)) {
5465
+ if (!existsSync2(sourcePath)) {
5347
5466
  return { skill: name, success: false, error: `Skill '${name}' not found` };
5348
5467
  }
5349
5468
  let skillMdContent = null;
5350
- const skillMdPath = join(sourcePath, "SKILL.md");
5351
- if (existsSync(skillMdPath)) {
5352
- skillMdContent = readFileSync(skillMdPath, "utf-8");
5469
+ const skillMdPath = join2(sourcePath, "SKILL.md");
5470
+ if (existsSync2(skillMdPath)) {
5471
+ skillMdContent = readFileSync2(skillMdPath, "utf-8");
5353
5472
  } else if (generateSkillMd) {
5354
5473
  skillMdContent = generateSkillMd(name);
5355
5474
  }
@@ -5358,17 +5477,12 @@ function installSkillForAgent(name, options, generateSkillMd) {
5358
5477
  }
5359
5478
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
5360
5479
  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
- };
5480
+ const agentBaseDir = agent === "pi" ? join2(homedir2(), ".pi", "agent") : join2(homedir2(), `.${agent}`);
5481
+ if (!existsSync2(agentBaseDir)) {
5368
5482
  return {
5369
5483
  skill: name,
5370
5484
  success: false,
5371
- error: `Agent directory ${agentBaseDir} does not exist. Is ${agentLabels[agent]} installed?`
5485
+ error: `Agent directory ${agentBaseDir} does not exist. Is ${AGENT_LABELS[agent]} installed?`
5372
5486
  };
5373
5487
  }
5374
5488
  try {
@@ -5383,7 +5497,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
5383
5497
  }
5384
5498
  try {
5385
5499
  mkdirSync(destDir, { recursive: true });
5386
- writeFileSync(join(destDir, "SKILL.md"), skillMdContent);
5500
+ writeFileSync(join2(destDir, "SKILL.md"), skillMdContent);
5387
5501
  return { skill: name, success: true, path: destDir };
5388
5502
  } catch (error) {
5389
5503
  return {
@@ -5396,31 +5510,48 @@ function installSkillForAgent(name, options, generateSkillMd) {
5396
5510
  function removeSkillForAgent(name, options) {
5397
5511
  const { agent, scope = "global", projectDir } = options;
5398
5512
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
5399
- if (!existsSync(destDir)) {
5513
+ if (!existsSync2(destDir)) {
5400
5514
  return false;
5401
5515
  }
5402
5516
  rmSync(destDir, { recursive: true, force: true });
5403
5517
  return true;
5404
5518
  }
5405
- var __dirname2, SKILLS_DIR, AGENT_TARGETS;
5519
+ var __dirname2, SKILLS_DIR, AGENT_TARGETS, AGENT_LABELS;
5406
5520
  var init_installer = __esm(() => {
5407
5521
  init_registry();
5408
5522
  __dirname2 = dirname(fileURLToPath(import.meta.url));
5409
5523
  SKILLS_DIR = findSkillsDir();
5410
- AGENT_TARGETS = ["claude", "codex", "gemini"];
5524
+ AGENT_TARGETS = ["claude", "codex", "gemini", "pi", "opencode"];
5525
+ AGENT_LABELS = {
5526
+ claude: "Claude Code",
5527
+ codex: "Codex CLI",
5528
+ gemini: "Gemini CLI",
5529
+ pi: "pi.dev",
5530
+ opencode: "OpenCode"
5531
+ };
5411
5532
  });
5412
5533
 
5413
5534
  // src/lib/skillinfo.ts
5414
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
5415
- import { join as join2 } from "path";
5535
+ var exports_skillinfo = {};
5536
+ __export(exports_skillinfo, {
5537
+ runSkill: () => runSkill,
5538
+ getSkillRequirements: () => getSkillRequirements,
5539
+ getSkillDocs: () => getSkillDocs,
5540
+ getSkillBestDoc: () => getSkillBestDoc,
5541
+ generateSkillMd: () => generateSkillMd,
5542
+ generateEnvExample: () => generateEnvExample,
5543
+ detectProjectSkills: () => detectProjectSkills
5544
+ });
5545
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
5546
+ import { join as join3 } from "path";
5416
5547
  function getSkillDocs(name) {
5417
5548
  const skillPath = getSkillPath(name);
5418
- if (!existsSync2(skillPath))
5549
+ if (!existsSync3(skillPath))
5419
5550
  return null;
5420
5551
  return {
5421
- skillMd: readIfExists(join2(skillPath, "SKILL.md")),
5422
- readme: readIfExists(join2(skillPath, "README.md")),
5423
- claudeMd: readIfExists(join2(skillPath, "CLAUDE.md"))
5552
+ skillMd: readIfExists(join3(skillPath, "SKILL.md")),
5553
+ readme: readIfExists(join3(skillPath, "README.md")),
5554
+ claudeMd: readIfExists(join3(skillPath, "CLAUDE.md"))
5424
5555
  };
5425
5556
  }
5426
5557
  function getSkillBestDoc(name) {
@@ -5431,11 +5562,11 @@ function getSkillBestDoc(name) {
5431
5562
  }
5432
5563
  function getSkillRequirements(name) {
5433
5564
  const skillPath = getSkillPath(name);
5434
- if (!existsSync2(skillPath))
5565
+ if (!existsSync3(skillPath))
5435
5566
  return null;
5436
5567
  const texts = [];
5437
5568
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
5438
- const content = readIfExists(join2(skillPath, file));
5569
+ const content = readIfExists(join3(skillPath, file));
5439
5570
  if (content)
5440
5571
  texts.push(content);
5441
5572
  }
@@ -5462,10 +5593,10 @@ function getSkillRequirements(name) {
5462
5593
  }
5463
5594
  let cliCommand = null;
5464
5595
  let dependencies = {};
5465
- const pkgPath = join2(skillPath, "package.json");
5466
- if (existsSync2(pkgPath)) {
5596
+ const pkgPath = join3(skillPath, "package.json");
5597
+ if (existsSync3(pkgPath)) {
5467
5598
  try {
5468
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5599
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5469
5600
  if (pkg.bin) {
5470
5601
  const binKeys = Object.keys(pkg.bin);
5471
5602
  if (binKeys.length > 0)
@@ -5485,25 +5616,25 @@ async function runSkill(name, args, options = {}) {
5485
5616
  const skillName = normalizeSkillName(name);
5486
5617
  let skillPath;
5487
5618
  if (options.installed) {
5488
- skillPath = join2(process.cwd(), ".skills", skillName);
5619
+ skillPath = join3(process.cwd(), ".skills", skillName);
5489
5620
  } else {
5490
- const installedPath = join2(process.cwd(), ".skills", skillName);
5491
- if (existsSync2(installedPath)) {
5621
+ const installedPath = join3(process.cwd(), ".skills", skillName);
5622
+ if (existsSync3(installedPath)) {
5492
5623
  skillPath = installedPath;
5493
5624
  } else {
5494
5625
  skillPath = getSkillPath(name);
5495
5626
  }
5496
5627
  }
5497
- if (!existsSync2(skillPath)) {
5628
+ if (!existsSync3(skillPath)) {
5498
5629
  return { exitCode: 1, error: `Skill '${name}' not found` };
5499
5630
  }
5500
- const pkgPath = join2(skillPath, "package.json");
5501
- if (!existsSync2(pkgPath)) {
5631
+ const pkgPath = join3(skillPath, "package.json");
5632
+ if (!existsSync3(pkgPath)) {
5502
5633
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
5503
5634
  }
5504
5635
  let entryPoint;
5505
5636
  try {
5506
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5637
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5507
5638
  if (pkg.bin) {
5508
5639
  const binValues = Object.values(pkg.bin);
5509
5640
  entryPoint = binValues[0];
@@ -5517,12 +5648,12 @@ async function runSkill(name, args, options = {}) {
5517
5648
  } catch {
5518
5649
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
5519
5650
  }
5520
- const entryPath = join2(skillPath, entryPoint);
5521
- if (!existsSync2(entryPath)) {
5651
+ const entryPath = join3(skillPath, entryPoint);
5652
+ if (!existsSync3(entryPath)) {
5522
5653
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
5523
5654
  }
5524
- const nodeModules = join2(skillPath, "node_modules");
5525
- if (!existsSync2(nodeModules)) {
5655
+ const nodeModules = join3(skillPath, "node_modules");
5656
+ if (!existsSync3(nodeModules)) {
5526
5657
  const install = Bun.spawn(["bun", "install", "--no-save"], {
5527
5658
  cwd: skillPath,
5528
5659
  stdout: "pipe",
@@ -5540,18 +5671,18 @@ async function runSkill(name, args, options = {}) {
5540
5671
  return { exitCode };
5541
5672
  }
5542
5673
  function detectProjectSkills(cwd = process.cwd()) {
5543
- const pkgPath = join2(cwd, "package.json");
5544
- if (!existsSync2(pkgPath)) {
5674
+ const pkgPath = join3(cwd, "package.json");
5675
+ if (!existsSync3(pkgPath)) {
5545
5676
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5546
- const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5677
+ const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5547
5678
  return { detected: [], recommended: recommended2 };
5548
5679
  }
5549
5680
  let pkg;
5550
5681
  try {
5551
- pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5682
+ pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5552
5683
  } catch {
5553
5684
  const alwaysRecommend = ["implementation-plan", "write", "deepresearch"];
5554
- const recommended2 = alwaysRecommend.map((name) => SKILLS.find((s) => s.name === name)).filter((s) => s !== undefined);
5685
+ const recommended2 = alwaysRecommend.map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5555
5686
  return { detected: [], recommended: recommended2 };
5556
5687
  }
5557
5688
  const allDeps = {
@@ -5623,15 +5754,66 @@ function detectProjectSkills(cwd = process.cwd()) {
5623
5754
  }
5624
5755
  }
5625
5756
  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);
5757
+ const recommended = Array.from(recommendedNames).map((name) => loadRegistry().find((s) => s.name === name)).filter((s) => s !== undefined);
5627
5758
  return { detected: uniqueDetected, recommended };
5628
5759
  }
5760
+ function generateEnvExample(targetDir = process.cwd()) {
5761
+ const skillsDir = join3(targetDir, ".skills");
5762
+ if (!existsSync3(skillsDir))
5763
+ return "";
5764
+ const dirs = readdirSync3(skillsDir).filter((f) => f.startsWith("skill-") && existsSync3(join3(skillsDir, f, "package.json")));
5765
+ const envMap = new Map;
5766
+ for (const dir of dirs) {
5767
+ const skillName = dir.replace("skill-", "");
5768
+ const skillPath = join3(skillsDir, dir);
5769
+ const texts = [];
5770
+ for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example"]) {
5771
+ const content = readIfExists(join3(skillPath, file));
5772
+ if (content)
5773
+ texts.push(content);
5774
+ }
5775
+ const allText = texts.join(`
5776
+ `);
5777
+ const foundVars = extractEnvVars(allText);
5778
+ for (const envVar of foundVars) {
5779
+ if (!envMap.has(envVar)) {
5780
+ envMap.set(envVar, []);
5781
+ }
5782
+ if (!envMap.get(envVar).includes(skillName)) {
5783
+ envMap.get(envVar).push(skillName);
5784
+ }
5785
+ }
5786
+ }
5787
+ if (envMap.size === 0)
5788
+ return "";
5789
+ const lines = [
5790
+ "# Environment variables for installed skills",
5791
+ "# Auto-generated by: skills init",
5792
+ ""
5793
+ ];
5794
+ const sorted = Array.from(envMap.entries()).sort(([a], [b]) => a.localeCompare(b));
5795
+ let lastPrefix = "";
5796
+ for (const [envVar, skills] of sorted) {
5797
+ const prefix = envVar.split("_")[0];
5798
+ if (prefix !== lastPrefix) {
5799
+ if (lastPrefix)
5800
+ lines.push("");
5801
+ lines.push(`# ${prefix}`);
5802
+ lastPrefix = prefix;
5803
+ }
5804
+ lines.push(`# Used by: ${skills.join(", ")}`);
5805
+ lines.push(`${envVar}=`);
5806
+ }
5807
+ return lines.join(`
5808
+ `) + `
5809
+ `;
5810
+ }
5629
5811
  function generateSkillMd(name) {
5630
5812
  const meta = getSkill(name);
5631
5813
  if (!meta)
5632
5814
  return null;
5633
5815
  const skillPath = getSkillPath(name);
5634
- if (!existsSync2(skillPath))
5816
+ if (!existsSync3(skillPath))
5635
5817
  return null;
5636
5818
  const frontmatter = [
5637
5819
  "---",
@@ -5640,13 +5822,13 @@ function generateSkillMd(name) {
5640
5822
  "---"
5641
5823
  ].join(`
5642
5824
  `);
5643
- const readme = readIfExists(join2(skillPath, "README.md"));
5644
- const claudeMd = readIfExists(join2(skillPath, "CLAUDE.md"));
5825
+ const readme = readIfExists(join3(skillPath, "README.md"));
5826
+ const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
5645
5827
  let cliCommand = null;
5646
- const pkgPath = join2(skillPath, "package.json");
5647
- if (existsSync2(pkgPath)) {
5828
+ const pkgPath = join3(skillPath, "package.json");
5829
+ if (existsSync3(pkgPath)) {
5648
5830
  try {
5649
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5831
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
5650
5832
  if (pkg.bin) {
5651
5833
  const binKeys = Object.keys(pkg.bin);
5652
5834
  if (binKeys.length > 0)
@@ -5719,8 +5901,8 @@ function extractEnvVars(text) {
5719
5901
  }
5720
5902
  function readIfExists(path) {
5721
5903
  try {
5722
- if (existsSync2(path)) {
5723
- return readFileSync2(path, "utf-8");
5904
+ if (existsSync3(path)) {
5905
+ return readFileSync3(path, "utf-8");
5724
5906
  }
5725
5907
  } catch {}
5726
5908
  return null;
@@ -5733,6 +5915,173 @@ var init_skillinfo = __esm(() => {
5733
5915
  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
5916
  });
5735
5917
 
5918
+ // src/lib/scheduler.ts
5919
+ var exports_scheduler = {};
5920
+ __export(exports_scheduler, {
5921
+ validateCron: () => validateCron,
5922
+ setScheduleEnabled: () => setScheduleEnabled,
5923
+ removeSchedule: () => removeSchedule,
5924
+ recordScheduleRun: () => recordScheduleRun,
5925
+ listSchedules: () => listSchedules,
5926
+ getNextRun: () => getNextRun,
5927
+ getDueSchedules: () => getDueSchedules,
5928
+ addSchedule: () => addSchedule
5929
+ });
5930
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
5931
+ import { join as join5 } from "path";
5932
+ function getSchedulesPath(targetDir = process.cwd()) {
5933
+ return join5(targetDir, ".skills", "schedules.json");
5934
+ }
5935
+ function loadSchedules(targetDir = process.cwd()) {
5936
+ const path = getSchedulesPath(targetDir);
5937
+ if (existsSync5(path)) {
5938
+ try {
5939
+ return JSON.parse(readFileSync5(path, "utf-8"));
5940
+ } catch {}
5941
+ }
5942
+ return { version: 1, schedules: [] };
5943
+ }
5944
+ function saveSchedules(data, targetDir = process.cwd()) {
5945
+ const path = getSchedulesPath(targetDir);
5946
+ const dir = join5(targetDir, ".skills");
5947
+ if (!existsSync5(dir))
5948
+ mkdirSync3(dir, { recursive: true });
5949
+ writeFileSync3(path, JSON.stringify(data, null, 2));
5950
+ }
5951
+ function validateCron(expr) {
5952
+ const fields = expr.trim().split(/\s+/);
5953
+ if (fields.length !== 5) {
5954
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
5955
+ }
5956
+ return { valid: true };
5957
+ }
5958
+ function getNextRun(cron, from = new Date) {
5959
+ const { valid } = validateCron(cron);
5960
+ if (!valid)
5961
+ return null;
5962
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
5963
+ function parseField(f, min, max) {
5964
+ if (f === "*")
5965
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
5966
+ if (f.startsWith("*/")) {
5967
+ const step = parseInt(f.slice(2));
5968
+ if (isNaN(step))
5969
+ return [];
5970
+ const vals = [];
5971
+ for (let i = min;i <= max; i += step)
5972
+ vals.push(i);
5973
+ return vals;
5974
+ }
5975
+ return f.split(",").flatMap((part) => {
5976
+ if (part.includes("-")) {
5977
+ const [lo, hi] = part.split("-").map(Number);
5978
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
5979
+ }
5980
+ const n = parseInt(part);
5981
+ return isNaN(n) ? [] : [n];
5982
+ });
5983
+ }
5984
+ const minutes = parseField(minuteF, 0, 59);
5985
+ const hours = parseField(hourF, 0, 23);
5986
+ const doms = parseField(domF, 1, 31);
5987
+ const months = parseField(monthF, 1, 12);
5988
+ const dows = parseField(dowF, 0, 6);
5989
+ const candidate = new Date(from);
5990
+ candidate.setSeconds(0, 0);
5991
+ candidate.setMinutes(candidate.getMinutes() + 1);
5992
+ const limit = new Date(from);
5993
+ limit.setFullYear(limit.getFullYear() + 1);
5994
+ while (candidate < limit) {
5995
+ const month = candidate.getMonth() + 1;
5996
+ const dom = candidate.getDate();
5997
+ const dow = candidate.getDay();
5998
+ const hour = candidate.getHours();
5999
+ const minute = candidate.getMinutes();
6000
+ if (!months.includes(month)) {
6001
+ candidate.setMonth(candidate.getMonth() + 1, 1);
6002
+ candidate.setHours(0, 0, 0, 0);
6003
+ continue;
6004
+ }
6005
+ if (!doms.includes(dom) || !dows.includes(dow)) {
6006
+ candidate.setDate(candidate.getDate() + 1);
6007
+ candidate.setHours(0, 0, 0, 0);
6008
+ continue;
6009
+ }
6010
+ if (!hours.includes(hour)) {
6011
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
6012
+ continue;
6013
+ }
6014
+ if (!minutes.includes(minute)) {
6015
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
6016
+ continue;
6017
+ }
6018
+ return new Date(candidate);
6019
+ }
6020
+ return null;
6021
+ }
6022
+ function addSchedule(skill, cron, options = {}) {
6023
+ const { valid, error } = validateCron(cron);
6024
+ if (!valid)
6025
+ return { schedule: null, error };
6026
+ const data = loadSchedules(options.targetDir);
6027
+ const id = `${skill}-${Date.now()}`;
6028
+ const now = new Date;
6029
+ const nextRun = getNextRun(cron, now);
6030
+ const schedule = {
6031
+ id,
6032
+ name: options.name || `${skill} (${cron})`,
6033
+ skill,
6034
+ cron,
6035
+ args: options.args,
6036
+ enabled: true,
6037
+ createdAt: now.toISOString(),
6038
+ nextRun: nextRun?.toISOString()
6039
+ };
6040
+ data.schedules.push(schedule);
6041
+ saveSchedules(data, options.targetDir);
6042
+ return { schedule };
6043
+ }
6044
+ function listSchedules(targetDir) {
6045
+ return loadSchedules(targetDir).schedules;
6046
+ }
6047
+ function removeSchedule(idOrName, targetDir) {
6048
+ const data = loadSchedules(targetDir);
6049
+ const before = data.schedules.length;
6050
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
6051
+ if (data.schedules.length === before)
6052
+ return false;
6053
+ saveSchedules(data, targetDir);
6054
+ return true;
6055
+ }
6056
+ function setScheduleEnabled(idOrName, enabled, targetDir) {
6057
+ const data = loadSchedules(targetDir);
6058
+ const schedule = data.schedules.find((s) => s.id === idOrName || s.name === idOrName);
6059
+ if (!schedule)
6060
+ return false;
6061
+ schedule.enabled = enabled;
6062
+ if (enabled) {
6063
+ schedule.nextRun = getNextRun(schedule.cron)?.toISOString();
6064
+ }
6065
+ saveSchedules(data, targetDir);
6066
+ return true;
6067
+ }
6068
+ function getDueSchedules(targetDir) {
6069
+ const now = new Date;
6070
+ return listSchedules(targetDir).filter((s) => s.enabled && s.nextRun && new Date(s.nextRun) <= now);
6071
+ }
6072
+ function recordScheduleRun(id, status, targetDir) {
6073
+ const data = loadSchedules(targetDir);
6074
+ const schedule = data.schedules.find((s) => s.id === id);
6075
+ if (!schedule)
6076
+ return;
6077
+ const now = new Date;
6078
+ schedule.lastRun = now.toISOString();
6079
+ schedule.lastRunStatus = status;
6080
+ schedule.nextRun = getNextRun(schedule.cron, now)?.toISOString();
6081
+ saveSchedules(data, targetDir);
6082
+ }
6083
+ var init_scheduler = () => {};
6084
+
5736
6085
  // ../../../../hasnastudio/hasnastudio-alumia/platform/platformdev/platform-alumia/node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/util.js
5737
6086
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
5738
6087
  const t = typeof data;
@@ -34823,9 +35172,8 @@ var init_stdio2 = __esm(() => {
34823
35172
 
34824
35173
  // src/mcp/index.ts
34825
35174
  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";
35175
+ import { existsSync as existsSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
35176
+ import { join as join6 } from "path";
34829
35177
  function stripNulls(obj) {
34830
35178
  return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== null && v !== undefined && !(Array.isArray(v) && v.length === 0)));
34831
35179
  }
@@ -34865,6 +35213,7 @@ var init_mcp2 = __esm(() => {
34865
35213
  init_registry();
34866
35214
  init_installer();
34867
35215
  init_skillinfo();
35216
+ init_scheduler();
34868
35217
  server = new McpServer({
34869
35218
  name: "skills",
34870
35219
  version: package_default.version
@@ -34880,7 +35229,7 @@ var init_mcp2 = __esm(() => {
34880
35229
  offset: exports_external.number().optional()
34881
35230
  }
34882
35231
  }, async ({ category, detail, limit, offset }) => {
34883
- const skills = category ? getSkillsByCategory(category) : SKILLS;
35232
+ const skills = category ? getSkillsByCategory(category) : loadRegistry();
34884
35233
  const mapped = detail ? skills : skills.map((s) => ({ name: s.name, category: s.category }));
34885
35234
  if (limit !== undefined || offset !== undefined) {
34886
35235
  const start = offset || 0;
@@ -34965,7 +35314,7 @@ var init_mcp2 = __esm(() => {
34965
35314
  });
34966
35315
  server.registerTool("install_skill", {
34967
35316
  title: "Install Skill",
34968
- description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|all).",
35317
+ description: "Install a skill to .skills/ or to an agent dir (for: claude|codex|gemini|pi|opencode|all).",
34969
35318
  inputSchema: {
34970
35319
  name: exports_external.string(),
34971
35320
  for: exports_external.string().optional(),
@@ -34977,7 +35326,7 @@ var init_mcp2 = __esm(() => {
34977
35326
  try {
34978
35327
  agents = resolveAgents(agentArg);
34979
35328
  } catch (err) {
34980
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35329
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
34981
35330
  }
34982
35331
  const results = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
34983
35332
  return {
@@ -35015,7 +35364,7 @@ var init_mcp2 = __esm(() => {
35015
35364
  try {
35016
35365
  agents = resolveAgents(agentArg);
35017
35366
  } catch (err) {
35018
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35367
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35019
35368
  }
35020
35369
  const results2 = [];
35021
35370
  for (const name of names) {
@@ -35049,7 +35398,7 @@ var init_mcp2 = __esm(() => {
35049
35398
  try {
35050
35399
  agents = resolveAgents(agentArg);
35051
35400
  } catch (err) {
35052
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35401
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35053
35402
  }
35054
35403
  const results = agents.map((a) => ({
35055
35404
  skill: name,
@@ -35082,7 +35431,7 @@ var init_mcp2 = __esm(() => {
35082
35431
  description: "List all unique skill tags with occurrence counts."
35083
35432
  }, async () => {
35084
35433
  const tagCounts = new Map;
35085
- for (const skill of SKILLS) {
35434
+ for (const skill of loadRegistry()) {
35086
35435
  for (const tag of skill.tags) {
35087
35436
  tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35088
35437
  }
@@ -35156,7 +35505,7 @@ var init_mcp2 = __esm(() => {
35156
35505
  try {
35157
35506
  agents = resolveAgents(agentArg);
35158
35507
  } catch (err) {
35159
- return mcpError("INVALID_AGENT", err.message, ["claude", "codex", "gemini", "all"]);
35508
+ return mcpError("INVALID_AGENT", err.message, [...AGENT_TARGETS, "all"]);
35160
35509
  }
35161
35510
  for (const name of skillList) {
35162
35511
  const agentResults = agents.map((a) => installSkillForAgent(name, { agent: a, scope: scope || "global" }, generateSkillMd));
@@ -35184,21 +35533,20 @@ var init_mcp2 = __esm(() => {
35184
35533
  const version2 = package_default.version;
35185
35534
  const cwd = process.cwd();
35186
35535
  const installed = getInstalledSkills();
35187
- const agentNames = ["claude", "codex", "gemini"];
35188
35536
  const agents = [];
35189
- for (const agent of agentNames) {
35190
- const agentSkillsPath = join4(homedir3(), `.${agent}`, "skills");
35191
- const exists = existsSync4(agentSkillsPath);
35537
+ for (const agent of AGENT_TARGETS) {
35538
+ const agentSkillsPath = getAgentSkillsDir(agent, "global");
35539
+ const exists = existsSync6(agentSkillsPath);
35192
35540
  let skillCount = 0;
35193
35541
  if (exists) {
35194
35542
  try {
35195
- skillCount = readdirSync3(agentSkillsPath).filter((f) => {
35196
- const full = join4(agentSkillsPath, f);
35543
+ skillCount = readdirSync4(agentSkillsPath).filter((f) => {
35544
+ const full = join6(agentSkillsPath, f);
35197
35545
  return f.startsWith("skill-") && statSync2(full).isDirectory();
35198
35546
  }).length;
35199
35547
  } catch {}
35200
35548
  }
35201
- agents.push({ agent, path: agentSkillsPath, exists, skillCount });
35549
+ agents.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
35202
35550
  }
35203
35551
  const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
35204
35552
  const result = {
@@ -35211,12 +35559,107 @@ var init_mcp2 = __esm(() => {
35211
35559
  };
35212
35560
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
35213
35561
  });
35562
+ server.registerTool("schedule_skill", {
35563
+ title: "Schedule Skill",
35564
+ 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).",
35565
+ inputSchema: {
35566
+ skill: exports_external.string(),
35567
+ cron: exports_external.string(),
35568
+ name: exports_external.string().optional(),
35569
+ args: exports_external.array(exports_external.string()).optional()
35570
+ }
35571
+ }, async ({ skill, cron, name, args }) => {
35572
+ const { schedule, error: error48 } = addSchedule(skill, cron, { name, args });
35573
+ if (error48 || !schedule) {
35574
+ return { content: [{ type: "text", text: JSON.stringify({ error: error48 || "Failed to add schedule" }) }] };
35575
+ }
35576
+ return { content: [{ type: "text", text: JSON.stringify(schedule, null, 2) }] };
35577
+ });
35578
+ server.registerTool("list_schedules", {
35579
+ title: "List Schedules",
35580
+ description: "List all scheduled skill runs.",
35581
+ inputSchema: {}
35582
+ }, async () => {
35583
+ const schedules = listSchedules();
35584
+ return { content: [{ type: "text", text: JSON.stringify(schedules, null, 2) }] };
35585
+ });
35586
+ server.registerTool("remove_schedule", {
35587
+ title: "Remove Schedule",
35588
+ description: "Remove a schedule by its ID or name.",
35589
+ inputSchema: {
35590
+ id_or_name: exports_external.string()
35591
+ }
35592
+ }, async ({ id_or_name }) => {
35593
+ const removed = removeSchedule(id_or_name);
35594
+ return { content: [{ type: "text", text: JSON.stringify({ removed, id_or_name }) }] };
35595
+ });
35596
+ server.registerTool("detect_project_skills", {
35597
+ title: "Detect Project Skills",
35598
+ description: "Detect project type from package.json and return recommended skills based on dependencies.",
35599
+ inputSchema: {
35600
+ directory: exports_external.string().optional()
35601
+ }
35602
+ }, async ({ directory }) => {
35603
+ const cwd = directory || process.cwd();
35604
+ const { detected, recommended } = detectProjectSkills(cwd);
35605
+ return {
35606
+ content: [{
35607
+ type: "text",
35608
+ text: JSON.stringify({
35609
+ directory: cwd,
35610
+ detected,
35611
+ recommended: recommended.map((s) => ({ name: s.name, displayName: s.displayName, description: s.description, category: s.category }))
35612
+ }, null, 2)
35613
+ }]
35614
+ };
35615
+ });
35616
+ server.registerTool("validate_skill", {
35617
+ title: "Validate Skill",
35618
+ 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.",
35619
+ inputSchema: {
35620
+ name: exports_external.string()
35621
+ }
35622
+ }, async ({ name }) => {
35623
+ const skillPath = getSkillPath(name);
35624
+ const issues = [];
35625
+ if (!existsSync6(skillPath)) {
35626
+ return {
35627
+ content: [{ type: "text", text: JSON.stringify({ name, valid: false, issues: [`Skill directory not found: ${skillPath}`] }) }]
35628
+ };
35629
+ }
35630
+ if (!existsSync6(join6(skillPath, "SKILL.md")))
35631
+ issues.push("Missing SKILL.md");
35632
+ if (!existsSync6(join6(skillPath, "tsconfig.json")))
35633
+ issues.push("Missing tsconfig.json");
35634
+ const pkgPath = join6(skillPath, "package.json");
35635
+ if (!existsSync6(pkgPath)) {
35636
+ issues.push("Missing package.json");
35637
+ } else {
35638
+ try {
35639
+ const pkg = JSON.parse(__require("fs").readFileSync(pkgPath, "utf-8"));
35640
+ if (!pkg.bin || Object.keys(pkg.bin).length === 0)
35641
+ issues.push("package.json missing 'bin' entry");
35642
+ } catch {
35643
+ issues.push("package.json is invalid JSON");
35644
+ }
35645
+ }
35646
+ const srcDir = join6(skillPath, "src");
35647
+ if (!existsSync6(srcDir)) {
35648
+ issues.push("Missing src/ directory");
35649
+ } else if (!existsSync6(join6(srcDir, "index.ts"))) {
35650
+ issues.push("Missing src/index.ts");
35651
+ }
35652
+ const valid = issues.length === 0;
35653
+ return {
35654
+ content: [{ type: "text", text: JSON.stringify({ name, valid, path: skillPath, issues }) }]
35655
+ };
35656
+ });
35214
35657
  server.registerResource("Skills Registry", "skills://registry", {
35215
35658
  description: "Compact skill list [{name,category}]. Use skills://{name} for detail."
35216
35659
  }, async () => ({
35217
35660
  contents: [{
35218
35661
  uri: "skills://registry",
35219
- text: JSON.stringify(SKILLS.map((s) => ({ name: s.name, category: s.category }))),
35662
+ text: JSON.stringify(loadRegistry().map((s) => ({ name: s.name, category: s.category }))),
35220
35663
  mimeType: "application/json"
35221
35664
  }]
35222
35665
  }));
@@ -35325,16 +35768,16 @@ __export(exports_serve, {
35325
35768
  startServer: () => startServer,
35326
35769
  createFetchHandler: () => createFetchHandler
35327
35770
  });
35328
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
35329
- import { join as join5, dirname as dirname3, extname } from "path";
35771
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
35772
+ import { join as join7, dirname as dirname3, extname } from "path";
35330
35773
  import { fileURLToPath as fileURLToPath2 } from "url";
35331
35774
  function getPackageJson() {
35332
35775
  try {
35333
35776
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
35334
35777
  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"));
35778
+ const pkgPath = join7(scriptDir, rel, "package.json");
35779
+ if (existsSync7(pkgPath)) {
35780
+ const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
35338
35781
  return { version: pkg.version || "unknown", name: pkg.name || "skills" };
35339
35782
  }
35340
35783
  }
@@ -35345,20 +35788,20 @@ function resolveDashboardDir() {
35345
35788
  const candidates = [];
35346
35789
  try {
35347
35790
  const scriptDir = dirname3(fileURLToPath2(import.meta.url));
35348
- candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
35349
- candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
35791
+ candidates.push(join7(scriptDir, "..", "dashboard", "dist"));
35792
+ candidates.push(join7(scriptDir, "..", "..", "dashboard", "dist"));
35350
35793
  } catch {}
35351
35794
  if (process.argv[1]) {
35352
35795
  const mainDir = dirname3(process.argv[1]);
35353
- candidates.push(join5(mainDir, "..", "dashboard", "dist"));
35354
- candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
35796
+ candidates.push(join7(mainDir, "..", "dashboard", "dist"));
35797
+ candidates.push(join7(mainDir, "..", "..", "dashboard", "dist"));
35355
35798
  }
35356
- candidates.push(join5(process.cwd(), "dashboard", "dist"));
35799
+ candidates.push(join7(process.cwd(), "dashboard", "dist"));
35357
35800
  for (const candidate of candidates) {
35358
- if (existsSync5(candidate))
35801
+ if (existsSync7(candidate))
35359
35802
  return candidate;
35360
35803
  }
35361
- return join5(process.cwd(), "dashboard", "dist");
35804
+ return join7(process.cwd(), "dashboard", "dist");
35362
35805
  }
35363
35806
  function json2(data, status = 200) {
35364
35807
  return new Response(JSON.stringify(data), {
@@ -35387,7 +35830,7 @@ function parseFields(searchParams) {
35387
35830
  }
35388
35831
  function getAllSkillsWithStatus() {
35389
35832
  const installed = new Set(getInstalledSkills());
35390
- return SKILLS.map((meta3) => {
35833
+ return loadRegistry().map((meta3) => {
35391
35834
  const reqs = getSkillRequirements(meta3.name);
35392
35835
  const envVars = reqs?.envVars || [];
35393
35836
  return {
@@ -35400,12 +35843,13 @@ function getAllSkillsWithStatus() {
35400
35843
  envVars,
35401
35844
  envVarsSet: envVars.filter((v) => !!process.env[v]),
35402
35845
  systemDeps: reqs?.systemDeps || [],
35403
- cliCommand: reqs?.cliCommand || null
35846
+ cliCommand: reqs?.cliCommand || null,
35847
+ source: meta3.source ?? "official"
35404
35848
  };
35405
35849
  });
35406
35850
  }
35407
35851
  function serveStaticFile(filePath) {
35408
- if (!existsSync5(filePath))
35852
+ if (!existsSync7(filePath))
35409
35853
  return null;
35410
35854
  const ext = extname(filePath);
35411
35855
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -35415,7 +35859,7 @@ function serveStaticFile(filePath) {
35415
35859
  }
35416
35860
  function createFetchHandler(options) {
35417
35861
  const dashboardDir = options?.dashboardDir ?? resolveDashboardDir();
35418
- const dashboardExists = options?.dashboardExists ?? existsSync5(dashboardDir);
35862
+ const dashboardExists = options?.dashboardExists ?? existsSync7(dashboardDir);
35419
35863
  return async function fetchHandler(req) {
35420
35864
  const url2 = new URL(req.url);
35421
35865
  const path = url2.pathname.replace(/^\/api\/v1\//, "/api/");
@@ -35426,7 +35870,7 @@ function createFetchHandler(options) {
35426
35870
  status: "ok",
35427
35871
  version: pkg.version,
35428
35872
  uptime: Math.floor(process.uptime()),
35429
- skillCount: SKILLS.length
35873
+ skillCount: loadRegistry().length
35430
35874
  });
35431
35875
  }
35432
35876
  if (path === "/api/skills" && method === "GET") {
@@ -35467,7 +35911,7 @@ function createFetchHandler(options) {
35467
35911
  }
35468
35912
  if (path === "/api/tags" && method === "GET") {
35469
35913
  const tagCounts = new Map;
35470
- for (const skill of SKILLS) {
35914
+ for (const skill of loadRegistry()) {
35471
35915
  for (const tag of skill.tags) {
35472
35916
  tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
35473
35917
  }
@@ -35699,12 +36143,12 @@ function createFetchHandler(options) {
35699
36143
  }
35700
36144
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
35701
36145
  if (path !== "/") {
35702
- const filePath = join5(dashboardDir, path);
36146
+ const filePath = join7(dashboardDir, path);
35703
36147
  const res2 = serveStaticFile(filePath);
35704
36148
  if (res2)
35705
36149
  return res2;
35706
36150
  }
35707
- const indexPath = join5(dashboardDir, "index.html");
36151
+ const indexPath = join7(dashboardDir, "index.html");
35708
36152
  const res = serveStaticFile(indexPath);
35709
36153
  if (res)
35710
36154
  return res;
@@ -35715,7 +36159,7 @@ function createFetchHandler(options) {
35715
36159
  async function startServer(port = 0, options) {
35716
36160
  const shouldOpen = options?.open ?? true;
35717
36161
  const dashboardDir = resolveDashboardDir();
35718
- const dashboardExists = existsSync5(dashboardDir);
36162
+ const dashboardExists = existsSync7(dashboardDir);
35719
36163
  if (!dashboardExists) {
35720
36164
  console.error(`
35721
36165
  Dashboard not found at: ${dashboardDir}`);
@@ -35795,8 +36239,8 @@ var {
35795
36239
  // src/cli/index.tsx
35796
36240
  init_package();
35797
36241
  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";
36242
+ import { existsSync as existsSync8, writeFileSync as writeFileSync4, appendFileSync, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync3, mkdirSync as mkdirSync4 } from "fs";
36243
+ import { join as join8 } from "path";
35800
36244
 
35801
36245
  // src/cli/components/App.tsx
35802
36246
  import { useState as useState7 } from "react";
@@ -36922,9 +37366,9 @@ init_installer();
36922
37366
  init_skillinfo();
36923
37367
 
36924
37368
  // 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";
37369
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
37370
+ import { join as join4, dirname as dirname2 } from "path";
37371
+ import { homedir as homedir3 } from "os";
36928
37372
  var VALID_KEYS = {
36929
37373
  defaultAgent: ["claude", "codex", "gemini", "all"],
36930
37374
  defaultScope: ["global", "project"],
@@ -36932,15 +37376,15 @@ var VALID_KEYS = {
36932
37376
  };
36933
37377
  function getConfigPath(scope) {
36934
37378
  if (scope === "global") {
36935
- return join3(homedir2(), ".skillsrc");
37379
+ return join4(homedir3(), ".skillsrc");
36936
37380
  }
36937
- return join3(process.cwd(), "skills.config.json");
37381
+ return join4(process.cwd(), "skills.config.json");
36938
37382
  }
36939
37383
  function readConfigFile(path) {
36940
- if (!existsSync3(path))
37384
+ if (!existsSync4(path))
36941
37385
  return {};
36942
37386
  try {
36943
- const raw = readFileSync3(path, "utf-8");
37387
+ const raw = readFileSync4(path, "utf-8");
36944
37388
  const parsed = JSON.parse(raw);
36945
37389
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
36946
37390
  return {};
@@ -36971,9 +37415,9 @@ function saveConfig(key, value, scope = "project") {
36971
37415
  }
36972
37416
  const filePath = getConfigPath(scope);
36973
37417
  let existing = {};
36974
- if (existsSync3(filePath)) {
37418
+ if (existsSync4(filePath)) {
36975
37419
  try {
36976
- existing = JSON.parse(readFileSync3(filePath, "utf-8"));
37420
+ existing = JSON.parse(readFileSync4(filePath, "utf-8"));
36977
37421
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
36978
37422
  existing = {};
36979
37423
  }
@@ -36982,7 +37426,7 @@ function saveConfig(key, value, scope = "project") {
36982
37426
  }
36983
37427
  } else {
36984
37428
  const dir = dirname2(filePath);
36985
- if (!existsSync3(dir)) {
37429
+ if (!existsSync4(dir)) {
36986
37430
  mkdirSync2(dir, { recursive: true });
36987
37431
  }
36988
37432
  }
@@ -36992,6 +37436,7 @@ function saveConfig(key, value, scope = "project") {
36992
37436
  }
36993
37437
 
36994
37438
  // src/cli/index.tsx
37439
+ init_scheduler();
36995
37440
  import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
36996
37441
  var isTTY = (process.stdout.isTTY ?? false) && (process.stdin.isTTY ?? false);
36997
37442
  if (process.argv.includes("--no-color")) {
@@ -37024,7 +37469,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
37024
37469
  }
37025
37470
  render(/* @__PURE__ */ jsxDEV7(App, {}, undefined, false, undefined, this));
37026
37471
  });
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) => {
37472
+ 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
37473
  const config2 = loadConfig();
37029
37474
  if (!options.for && config2.defaultAgent) {
37030
37475
  options.for = config2.defaultAgent;
@@ -37165,7 +37610,14 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
37165
37610
  if (options.installed) {
37166
37611
  const installed = getInstalledSkills();
37167
37612
  if (options.json) {
37168
- console.log(JSON.stringify(installed));
37613
+ const meta4 = getInstallMeta();
37614
+ const registry3 = loadRegistry();
37615
+ const output = installed.map((name) => {
37616
+ const m = meta4.skills[name];
37617
+ const s = registry3.find((r) => r.name === name);
37618
+ return { name, version: m?.version ?? null, installedAt: m?.installedAt ?? null, source: s?.source ?? "official" };
37619
+ });
37620
+ console.log(JSON.stringify(output));
37169
37621
  return;
37170
37622
  }
37171
37623
  if (installed.length === 0) {
@@ -37173,16 +37625,22 @@ program2.command("list").alias("ls").option("-c, --category <category>", "Filter
37173
37625
  return;
37174
37626
  }
37175
37627
  if (brief) {
37176
- for (const name of installed) {
37628
+ for (const name of installed)
37177
37629
  console.log(name);
37178
- }
37179
37630
  return;
37180
37631
  }
37632
+ const meta3 = getInstallMeta();
37633
+ const registry2 = loadRegistry();
37181
37634
  console.log(chalk2.bold(`
37182
37635
  Installed skills (${installed.length}):
37183
37636
  `));
37184
37637
  for (const name of installed) {
37185
- console.log(` ${name}`);
37638
+ const m = meta3.skills[name];
37639
+ const s = registry2.find((r) => r.name === name);
37640
+ const version2 = m?.version ? chalk2.dim(`v${m.version}`) : "";
37641
+ const installedAt = m?.installedAt ? chalk2.dim(new Date(m.installedAt).toLocaleDateString()) : "";
37642
+ const source = s?.source === "custom" ? chalk2.yellow(" [custom]") : "";
37643
+ console.log(` ${chalk2.cyan(name)}${source} ${version2} ${installedAt}`);
37186
37644
  }
37187
37645
  return;
37188
37646
  }
@@ -37218,7 +37676,7 @@ ${category} (${skills.length}):
37218
37676
  return;
37219
37677
  }
37220
37678
  if (tagFilter) {
37221
- const skills = SKILLS.filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
37679
+ const skills = loadRegistry().filter((s) => s.tags.some((tag) => tagFilter.includes(tag.toLowerCase())));
37222
37680
  if (options.json) {
37223
37681
  console.log(JSON.stringify(skills, null, 2));
37224
37682
  return;
@@ -37233,29 +37691,31 @@ ${category} (${skills.length}):
37233
37691
  Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
37234
37692
  `));
37235
37693
  for (const s of skills) {
37236
- console.log(` ${chalk2.cyan(s.name)} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
37694
+ const customBadge = s.source === "custom" ? chalk2.yellow(" [custom]") : "";
37695
+ console.log(` ${chalk2.cyan(s.name)}${customBadge} ${chalk2.dim(`[${s.category}]`)} - ${s.description}`);
37237
37696
  }
37238
37697
  return;
37239
37698
  }
37699
+ const allSkills = loadRegistry();
37240
37700
  if (options.json) {
37241
- console.log(JSON.stringify(SKILLS, null, 2));
37701
+ console.log(JSON.stringify(allSkills, null, 2));
37242
37702
  return;
37243
37703
  }
37244
37704
  if (fmt === "compact") {
37245
- for (const s of SKILLS)
37705
+ for (const s of allSkills)
37246
37706
  console.log(s.name);
37247
37707
  return;
37248
37708
  }
37249
37709
  if (fmt === "csv") {
37250
- console.log("name,category,description");
37251
- for (const s of SKILLS) {
37710
+ console.log("name,category,description,source");
37711
+ for (const s of allSkills) {
37252
37712
  const desc = s.description.replace(/"/g, '""');
37253
- console.log(`${s.name},${s.category},"${desc}"`);
37713
+ console.log(`${s.name},${s.category},"${desc}",${s.source ?? "official"}`);
37254
37714
  }
37255
37715
  return;
37256
37716
  }
37257
37717
  if (brief) {
37258
- const sorted = [...SKILLS].sort((a, b) => {
37718
+ const sorted = [...allSkills].sort((a, b) => {
37259
37719
  const catCmp = a.category.localeCompare(b.category);
37260
37720
  return catCmp !== 0 ? catCmp : a.name.localeCompare(b.name);
37261
37721
  });
@@ -37265,16 +37725,26 @@ Skills matching tags [${tagFilter.join(", ")}] (${skills.length}):
37265
37725
  return;
37266
37726
  }
37267
37727
  console.log(chalk2.bold(`
37268
- Available skills (${SKILLS.length}):
37728
+ Available skills (${allSkills.length}):
37269
37729
  `));
37270
37730
  for (const category of CATEGORIES) {
37271
37731
  const skills = getSkillsByCategory(category);
37732
+ if (skills.length === 0)
37733
+ continue;
37272
37734
  console.log(chalk2.bold(`${category} (${skills.length}):`));
37273
37735
  for (const s of skills) {
37274
- console.log(` ${chalk2.cyan(s.name)} - ${s.description}`);
37736
+ const customBadge = s.source === "custom" ? chalk2.yellow(" [custom]") : "";
37737
+ console.log(` ${chalk2.cyan(s.name)}${customBadge} - ${s.description}`);
37275
37738
  }
37276
37739
  console.log();
37277
37740
  }
37741
+ const customUncategorized = allSkills.filter((s) => s.source === "custom" && !CATEGORIES.includes(s.category));
37742
+ if (customUncategorized.length > 0) {
37743
+ console.log(chalk2.bold(`Custom (${customUncategorized.length}):`));
37744
+ for (const s of customUncategorized) {
37745
+ console.log(` ${chalk2.yellow(s.name)} - ${s.description}`);
37746
+ }
37747
+ }
37278
37748
  });
37279
37749
  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
37750
  let results = searchSkills(query);
@@ -37350,8 +37820,17 @@ program2.command("info").argument("<skill>", "Skill name").option("--json", "Out
37350
37820
  console.log(`${skill.name} \u2014 ${skill.description} [${skill.category}] (tags: ${skill.tags.join(", ")})`);
37351
37821
  return;
37352
37822
  }
37823
+ const { execSync: execSyncInfo } = __require("child_process");
37824
+ function cmdAvailable(cmd) {
37825
+ try {
37826
+ execSyncInfo(`which ${cmd}`, { stdio: "ignore" });
37827
+ return true;
37828
+ } catch {
37829
+ return false;
37830
+ }
37831
+ }
37353
37832
  console.log(`
37354
- ${chalk2.bold(skill.displayName)}`);
37833
+ ${chalk2.bold(skill.displayName)}${skill.source === "custom" ? chalk2.yellow(" [custom]") : ""}`);
37355
37834
  console.log(`${skill.description}`);
37356
37835
  console.log(`${chalk2.dim("Category:")} ${skill.category}`);
37357
37836
  console.log(`${chalk2.dim("Tags:")} ${skill.tags.join(", ")}`);
@@ -37359,10 +37838,18 @@ ${chalk2.bold(skill.displayName)}`);
37359
37838
  console.log(`${chalk2.dim("CLI:")} ${reqs.cliCommand}`);
37360
37839
  }
37361
37840
  if (reqs?.envVars.length) {
37362
- console.log(`${chalk2.dim("Env vars:")} ${reqs.envVars.join(", ")}`);
37841
+ console.log(`${chalk2.dim("Env vars:")}`);
37842
+ for (const v of reqs.envVars) {
37843
+ const set3 = !!process.env[v];
37844
+ console.log(` ${set3 ? chalk2.green("\u2713") : chalk2.red("\u2717")} ${v}${set3 ? "" : chalk2.dim(" (not set)")}`);
37845
+ }
37363
37846
  }
37364
37847
  if (reqs?.systemDeps.length) {
37365
- console.log(`${chalk2.dim("System deps:")} ${reqs.systemDeps.join(", ")}`);
37848
+ console.log(`${chalk2.dim("System deps:")}`);
37849
+ for (const d of reqs.systemDeps) {
37850
+ const avail = cmdAvailable(d);
37851
+ console.log(` ${avail ? chalk2.green("\u2713") : chalk2.red("\u2717")} ${d}${avail ? "" : chalk2.dim(" (not found)")}`);
37852
+ }
37366
37853
  }
37367
37854
  console.log(`${chalk2.dim("Install:")} skills install ${skill.name}`);
37368
37855
  });
@@ -37466,7 +37953,7 @@ program2.command("run").argument("<skill>", "Skill name").argument("[args...]",
37466
37953
  }
37467
37954
  process.exitCode = result.exitCode;
37468
37955
  });
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) => {
37956
+ 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
37957
  const cwd = process.cwd();
37471
37958
  if (options.for) {
37472
37959
  let agents;
@@ -37580,8 +38067,8 @@ Installing recommended skills for ${options.for} (${options.scope})...
37580
38067
  const envContent = lines.join(`
37581
38068
  `) + `
37582
38069
  `;
37583
- const envPath = join6(cwd, ".env.example");
37584
- writeFileSync3(envPath, envContent);
38070
+ const envPath = join8(cwd, ".env.example");
38071
+ writeFileSync4(envPath, envContent);
37585
38072
  envVarCount = envMap.size;
37586
38073
  if (!options.json) {
37587
38074
  console.log(chalk2.green(`\u2713 Generated .env.example (${envVarCount} variables from ${installed.length} skills)`));
@@ -37591,11 +38078,11 @@ Installing recommended skills for ${options.for} (${options.scope})...
37591
38078
  console.log(chalk2.dim(" No environment variables detected across installed skills"));
37592
38079
  }
37593
38080
  }
37594
- const gitignorePath = join6(cwd, ".gitignore");
38081
+ const gitignorePath = join8(cwd, ".gitignore");
37595
38082
  const gitignoreEntry = ".skills/";
37596
38083
  let gitignoreContent = "";
37597
- if (existsSync6(gitignorePath)) {
37598
- gitignoreContent = readFileSync5(gitignorePath, "utf-8");
38084
+ if (existsSync8(gitignorePath)) {
38085
+ gitignoreContent = readFileSync7(gitignorePath, "utf-8");
37599
38086
  }
37600
38087
  let gitignoreUpdated = false;
37601
38088
  if (!gitignoreContent.includes(gitignoreEntry)) {
@@ -37641,7 +38128,7 @@ Initialized for ${installed.length} installed skill(s)`));
37641
38128
  }
37642
38129
  }
37643
38130
  });
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) => {
38131
+ 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
38132
  debug(`remove: skill=${skill} for=${options.for ?? "none"} scope=${options.scope} dryRun=${options.dryRun}`);
37646
38133
  if (!options.yes && !options.dryRun && isTTY) {
37647
38134
  const skillName = normalizeSkillName(skill);
@@ -37724,10 +38211,10 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37724
38211
  }
37725
38212
  function collectFiles(dir, base = "") {
37726
38213
  const files = new Set;
37727
- if (!existsSync6(dir))
38214
+ if (!existsSync8(dir))
37728
38215
  return files;
37729
- for (const entry of readdirSync4(dir)) {
37730
- const full = join6(dir, entry);
38216
+ for (const entry of readdirSync5(dir)) {
38217
+ const full = join8(dir, entry);
37731
38218
  const rel = base ? `${base}/${entry}` : entry;
37732
38219
  if (statSync3(full).isDirectory()) {
37733
38220
  for (const f of collectFiles(full, rel))
@@ -37741,7 +38228,7 @@ program2.command("update").argument("[skills...]", "Skills to update (default: a
37741
38228
  const updateResults = [];
37742
38229
  for (const name of toUpdate) {
37743
38230
  const skillName = normalizeSkillName(name);
37744
- const destPath = join6(process.cwd(), ".skills", skillName);
38231
+ const destPath = join8(process.cwd(), ".skills", skillName);
37745
38232
  const beforeFiles = collectFiles(destPath);
37746
38233
  const result = installSkill(name, { overwrite: true });
37747
38234
  const afterFiles = collectFiles(destPath);
@@ -37819,10 +38306,11 @@ Tags:
37819
38306
  console.log(` ${chalk2.cyan(name)} (${count})`);
37820
38307
  }
37821
38308
  });
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) => {
38309
+ 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
38310
  if (options.register) {
37824
- const agents = options.register === "all" ? ["claude", "codex", "gemini"] : [options.register];
37825
- const binPath = join6(import.meta.dir, "..", "mcp", "index.ts");
38311
+ const agents = options.register === "all" ? [...AGENT_TARGETS] : [options.register];
38312
+ const binPath = join8(import.meta.dir, "..", "mcp", "index.ts");
38313
+ const { homedir: hd } = await import("os");
37826
38314
  for (const agent of agents) {
37827
38315
  if (agent === "claude") {
37828
38316
  try {
@@ -37836,8 +38324,7 @@ program2.command("mcp").option("--register <agent>", "Register MCP server with a
37836
38324
  console.log(chalk2.yellow(`Manual registration: claude mcp add skills -- bun run ${binPath}`));
37837
38325
  }
37838
38326
  } else if (agent === "codex") {
37839
- const { homedir: homedir4 } = await import("os");
37840
- const configPath = join6(homedir4(), ".codex", "config.toml");
38327
+ const configPath = join8(hd(), ".codex", "config.toml");
37841
38328
  console.log(chalk2.bold(`
37842
38329
  Add to ${configPath}:`));
37843
38330
  console.log(chalk2.dim(`[mcp_servers.skills]
@@ -37845,16 +38332,25 @@ command = "bun"
37845
38332
  args = ["run", "${binPath}"]`));
37846
38333
  console.log(chalk2.green(`\u2713 Codex MCP config shown above`));
37847
38334
  } else if (agent === "gemini") {
37848
- const { homedir: homedir4 } = await import("os");
37849
- const configPath = join6(homedir4(), ".gemini", "settings.json");
38335
+ const configPath = join8(hd(), ".gemini", "settings.json");
37850
38336
  console.log(chalk2.bold(`
37851
38337
  Add to ${configPath} mcpServers:`));
37852
- console.log(chalk2.dim(JSON.stringify({
37853
- skills: { command: "bun", args: ["run", binPath] }
37854
- }, null, 2)));
38338
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
37855
38339
  console.log(chalk2.green(`\u2713 Gemini MCP config shown above`));
38340
+ } else if (agent === "pi") {
38341
+ const configPath = join8(hd(), ".pi", "agent", "mcp.json");
38342
+ console.log(chalk2.bold(`
38343
+ Add to ${configPath}:`));
38344
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
38345
+ console.log(chalk2.green(`\u2713 pi.dev MCP config shown above`));
38346
+ } else if (agent === "opencode") {
38347
+ const configPath = join8(hd(), ".opencode", "config.json");
38348
+ console.log(chalk2.bold(`
38349
+ Add to ${configPath} mcp section:`));
38350
+ console.log(chalk2.dim(JSON.stringify({ skills: { command: "bun", args: ["run", binPath] } }, null, 2)));
38351
+ console.log(chalk2.green(`\u2713 OpenCode MCP config shown above`));
37856
38352
  } else {
37857
- console.error(chalk2.red(`Unknown agent: ${agent}. Available: claude, codex, gemini, all`));
38353
+ console.error(chalk2.red(`Unknown agent: ${agent}. Available: ${AGENT_TARGETS.join(", ")}, all`));
37858
38354
  process.exitCode = 1;
37859
38355
  }
37860
38356
  }
@@ -38040,18 +38536,18 @@ program2.command("export").option("--json", "Output as JSON (default behavior)",
38040
38536
  };
38041
38537
  console.log(JSON.stringify(payload, null, 2));
38042
38538
  });
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) => {
38539
+ 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
38540
  let raw;
38045
38541
  try {
38046
38542
  if (file2 === "-") {
38047
38543
  raw = await new Response(process.stdin).text();
38048
38544
  } else {
38049
- if (!existsSync6(file2)) {
38545
+ if (!existsSync8(file2)) {
38050
38546
  console.error(chalk2.red(`File not found: ${file2}`));
38051
38547
  process.exitCode = 1;
38052
38548
  return;
38053
38549
  }
38054
- raw = readFileSync5(file2, "utf-8");
38550
+ raw = readFileSync7(file2, "utf-8");
38055
38551
  }
38056
38552
  } catch (err) {
38057
38553
  console.error(chalk2.red(`Failed to read file: ${err.message}`));
@@ -38142,7 +38638,8 @@ Imported ${succeeded}/${total} skill(s)${failed > 0 ? ` (${failed} failed)` : ""
38142
38638
  process.exitCode = 1;
38143
38639
  }
38144
38640
  });
38145
- program2.command("doctor").option("--json", "Output as JSON", false).description("Check environment variables for installed skills").action((options) => {
38641
+ program2.command("doctor").option("--json", "Output as JSON", false).description("Check env vars, system deps, and install health for installed skills").action((options) => {
38642
+ const { execSync } = __require("child_process");
38146
38643
  const installed = getInstalledSkills();
38147
38644
  if (installed.length === 0) {
38148
38645
  if (options.json) {
@@ -38152,37 +38649,53 @@ program2.command("doctor").option("--json", "Output as JSON", false).description
38152
38649
  }
38153
38650
  return;
38154
38651
  }
38652
+ function isCommandAvailable(cmd) {
38653
+ try {
38654
+ execSync(`which ${cmd}`, { stdio: "ignore" });
38655
+ return true;
38656
+ } catch {
38657
+ return false;
38658
+ }
38659
+ }
38155
38660
  const report = [];
38156
38661
  for (const name of installed) {
38157
38662
  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 });
38663
+ const envVars = (reqs?.envVars ?? []).map((v) => ({ name: v, set: !!process.env[v] }));
38664
+ const systemDeps = (reqs?.systemDeps ?? []).map((d) => ({ name: d, available: isCommandAvailable(d) }));
38665
+ const healthy = envVars.every((v) => v.set) && systemDeps.every((d) => d.available);
38666
+ report.push({ skill: name, envVars, systemDeps, healthy });
38163
38667
  }
38164
38668
  if (options.json) {
38165
38669
  console.log(JSON.stringify(report, null, 2));
38166
38670
  return;
38167
38671
  }
38672
+ const issues = report.filter((r) => !r.healthy);
38168
38673
  console.log(chalk2.bold(`
38169
- Skills Doctor (${installed.length} installed):
38674
+ Skills Doctor \u2014 ${installed.length} installed, ${issues.length} with issues:
38170
38675
  `));
38171
38676
  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
- }
38677
+ const icon = entry.healthy ? chalk2.green("\u2713") : chalk2.red("\u2717");
38678
+ console.log(` ${icon} ${chalk2.bold(entry.skill)}`);
38679
+ for (const v of entry.envVars) {
38680
+ const status = v.set ? chalk2.green("set") : chalk2.red("missing");
38681
+ console.log(` ${v.name} [${status}]`);
38682
+ }
38683
+ for (const d of entry.systemDeps) {
38684
+ const status = d.available ? chalk2.green("available") : chalk2.red("not found");
38685
+ console.log(` ${d.name} [${status}]`);
38180
38686
  }
38687
+ if (entry.envVars.length === 0 && entry.systemDeps.length === 0) {
38688
+ console.log(chalk2.dim(" No requirements"));
38689
+ }
38690
+ }
38691
+ if (issues.length === 0) {
38692
+ console.log(chalk2.green(`
38693
+ All skills healthy! \u2713`));
38181
38694
  }
38182
38695
  });
38183
38696
  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
38697
  const cwd = process.cwd();
38185
- const envFilePath = join6(cwd, ".env");
38698
+ const envFilePath = join8(cwd, ".env");
38186
38699
  if (options.set) {
38187
38700
  const eqIdx = options.set.indexOf("=");
38188
38701
  if (eqIdx === -1) {
@@ -38198,8 +38711,8 @@ program2.command("auth").argument("[skill]", "Skill name (omit to check all inst
38198
38711
  return;
38199
38712
  }
38200
38713
  let existing = "";
38201
- if (existsSync6(envFilePath)) {
38202
- existing = readFileSync5(envFilePath, "utf-8");
38714
+ if (existsSync8(envFilePath)) {
38715
+ existing = readFileSync7(envFilePath, "utf-8");
38203
38716
  }
38204
38717
  const keyPattern = new RegExp(`^${key}=.*$`, "m");
38205
38718
  let updated;
@@ -38212,7 +38725,7 @@ program2.command("auth").argument("[skill]", "Skill name (omit to check all inst
38212
38725
  ${key}=${value}
38213
38726
  `;
38214
38727
  }
38215
- writeFileSync3(envFilePath, updated, "utf-8");
38728
+ writeFileSync4(envFilePath, updated, "utf-8");
38216
38729
  console.log(chalk2.green(`Set ${key} in ${envFilePath}`));
38217
38730
  return;
38218
38731
  }
@@ -38282,21 +38795,20 @@ program2.command("whoami").option("--json", "Output as JSON", false).description
38282
38795
  const version2 = package_default.version;
38283
38796
  const cwd = process.cwd();
38284
38797
  const installed = getInstalledSkills();
38285
- const agentNames = ["claude", "codex", "gemini"];
38286
38798
  const agentConfigs = [];
38287
- for (const agent of agentNames) {
38288
- const agentSkillsPath = join6(homedir4(), `.${agent}`, "skills");
38289
- const exists = existsSync6(agentSkillsPath);
38799
+ for (const agent of AGENT_TARGETS) {
38800
+ const agentSkillsPath = getAgentSkillsDir(agent, "global");
38801
+ const exists = existsSync8(agentSkillsPath);
38290
38802
  let skillCount = 0;
38291
38803
  if (exists) {
38292
38804
  try {
38293
- skillCount = readdirSync4(agentSkillsPath).filter((f) => {
38294
- const full = join6(agentSkillsPath, f);
38805
+ skillCount = readdirSync5(agentSkillsPath).filter((f) => {
38806
+ const full = join8(agentSkillsPath, f);
38295
38807
  return f.startsWith("skill-") && statSync3(full).isDirectory();
38296
38808
  }).length;
38297
38809
  } catch {}
38298
38810
  }
38299
- agentConfigs.push({ agent, path: agentSkillsPath, exists, skillCount });
38811
+ agentConfigs.push({ agent, label: AGENT_LABELS[agent], path: agentSkillsPath, exists, skillCount });
38300
38812
  }
38301
38813
  const skillsDir = getSkillPath("image").replace(/[/\\][^/\\]*$/, "");
38302
38814
  if (options.json) {
@@ -38436,19 +38948,19 @@ program2.command("outdated").option("--json", "Output as JSON", false).descripti
38436
38948
  const upToDate = [];
38437
38949
  for (const name of installed) {
38438
38950
  const skillName = normalizeSkillName(name);
38439
- const installedPkgPath = join6(cwd, ".skills", skillName, "package.json");
38951
+ const installedPkgPath = join8(cwd, ".skills", skillName, "package.json");
38440
38952
  let installedVersion = "unknown";
38441
- if (existsSync6(installedPkgPath)) {
38953
+ if (existsSync8(installedPkgPath)) {
38442
38954
  try {
38443
- installedVersion = JSON.parse(readFileSync5(installedPkgPath, "utf-8")).version || "unknown";
38955
+ installedVersion = JSON.parse(readFileSync7(installedPkgPath, "utf-8")).version || "unknown";
38444
38956
  } catch {}
38445
38957
  }
38446
38958
  const registryPath = getSkillPath(name);
38447
- const registryPkgPath = join6(registryPath, "package.json");
38959
+ const registryPkgPath = join8(registryPath, "package.json");
38448
38960
  let registryVersion = "unknown";
38449
- if (existsSync6(registryPkgPath)) {
38961
+ if (existsSync8(registryPkgPath)) {
38450
38962
  try {
38451
- registryVersion = JSON.parse(readFileSync5(registryPkgPath, "utf-8")).version || "unknown";
38963
+ registryVersion = JSON.parse(readFileSync7(registryPkgPath, "utf-8")).version || "unknown";
38452
38964
  } catch {}
38453
38965
  }
38454
38966
  if (installedVersion !== registryVersion) {
@@ -38513,7 +39025,470 @@ configCmd.command("get <key>").description("Get a specific configuration value")
38513
39025
  configCmd.command("path").description("Show configuration file paths").action(() => {
38514
39026
  const globalPath = getConfigPath("global");
38515
39027
  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)")}`);
39028
+ console.log(`${chalk2.cyan("global")}: ${globalPath}${existsSync8(globalPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
39029
+ console.log(`${chalk2.cyan("project")}: ${projectPath}${existsSync8(projectPath) ? chalk2.green(" (exists)") : chalk2.dim(" (not found)")}`);
39030
+ });
39031
+ 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 ~/.skills/ instead of .skills/custom-skills/", false).option("--json", "Output result as JSON", false).description("Scaffold a new custom skill directory").action((name, options) => {
39032
+ const { homedir: homedir4 } = __require("os");
39033
+ const bare = name.replace(/^skill-/, "");
39034
+ const dirName = `skill-${bare}`;
39035
+ const baseDir = options.global ? join8(homedir4(), ".skills") : join8(process.cwd(), ".skills", "custom-skills");
39036
+ const skillDir = join8(baseDir, dirName);
39037
+ if (existsSync8(skillDir)) {
39038
+ if (options.json) {
39039
+ console.log(JSON.stringify({ error: `Skill '${bare}' already exists at ${skillDir}` }));
39040
+ } else {
39041
+ console.error(chalk2.red(`Skill '${bare}' already exists at ${skillDir}`));
39042
+ }
39043
+ process.exitCode = 1;
39044
+ return;
39045
+ }
39046
+ const description = options.description || `${bare} skill`;
39047
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [bare];
39048
+ const displayName = bare.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
39049
+ const category = options.category;
39050
+ mkdirSync4(join8(skillDir, "src"), { recursive: true });
39051
+ writeFileSync4(join8(skillDir, "SKILL.md"), [
39052
+ "---",
39053
+ `name: ${bare}`,
39054
+ `description: ${description}`,
39055
+ `displayName: ${displayName}`,
39056
+ `category: ${category}`,
39057
+ `tags: [${tags.join(", ")}]`,
39058
+ "---",
39059
+ "",
39060
+ `# ${displayName}`,
39061
+ "",
39062
+ description,
39063
+ "",
39064
+ "## Usage",
39065
+ "",
39066
+ "```bash",
39067
+ `${bare} --help`,
39068
+ "```",
39069
+ ""
39070
+ ].join(`
39071
+ `));
39072
+ writeFileSync4(join8(skillDir, "src", "index.ts"), [
39073
+ `#!/usr/bin/env bun`,
39074
+ `/**`,
39075
+ ` * ${displayName} \u2014 ${description}`,
39076
+ ` */`,
39077
+ "",
39078
+ `console.log("${displayName}");`,
39079
+ ""
39080
+ ].join(`
39081
+ `));
39082
+ writeFileSync4(join8(skillDir, "package.json"), JSON.stringify({
39083
+ name: `skill-${bare}`,
39084
+ version: "0.1.0",
39085
+ description,
39086
+ bin: { [bare]: "./src/index.ts" },
39087
+ scripts: { dev: `bun src/index.ts` },
39088
+ dependencies: {}
39089
+ }, null, 2) + `
39090
+ `);
39091
+ writeFileSync4(join8(skillDir, "tsconfig.json"), JSON.stringify({
39092
+ compilerOptions: {
39093
+ target: "ES2022",
39094
+ module: "ESNext",
39095
+ moduleResolution: "bundler",
39096
+ strict: true,
39097
+ outDir: "dist"
39098
+ },
39099
+ include: ["src/**/*.ts"]
39100
+ }, null, 2) + `
39101
+ `);
39102
+ clearRegistryCache();
39103
+ if (options.json) {
39104
+ console.log(JSON.stringify({ created: true, name: bare, path: skillDir, category, tags }));
39105
+ } else {
39106
+ console.log(chalk2.green(`\u2713 Created custom skill '${bare}' at ${skillDir}`));
39107
+ console.log(chalk2.dim(` Category: ${category}`));
39108
+ console.log(chalk2.dim(` Tags: ${tags.join(", ")}`));
39109
+ console.log("");
39110
+ console.log(` ${chalk2.cyan("Edit:")} ${join8(skillDir, "src", "index.ts")}`);
39111
+ console.log(` ${chalk2.cyan("Run:")} bun ${join8(skillDir, "src", "index.ts")}`);
39112
+ console.log(` ${chalk2.cyan("Docs:")} ${join8(skillDir, "SKILL.md")}`);
39113
+ }
39114
+ });
39115
+ 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 ~/.skills/ 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) => {
39116
+ const { homedir: homedir4 } = __require("os");
39117
+ if (!options.to && !options.from) {
39118
+ console.error(chalk2.red("Specify --to <agent> or --from <agent>"));
39119
+ process.exitCode = 1;
39120
+ return;
39121
+ }
39122
+ if (options.from) {
39123
+ const agentName = options.from;
39124
+ if (!AGENT_TARGETS.includes(agentName)) {
39125
+ console.error(chalk2.red(`Unknown agent: ${agentName}. Available: ${AGENT_TARGETS.join(", ")}`));
39126
+ process.exitCode = 1;
39127
+ return;
39128
+ }
39129
+ const agentDir = getAgentSkillsDir(agentName, options.scope);
39130
+ if (!existsSync8(agentDir)) {
39131
+ if (options.json) {
39132
+ console.log(JSON.stringify({ agentDir, skills: [], message: "Directory not found" }));
39133
+ } else {
39134
+ console.log(chalk2.dim(`No skills directory found at ${agentDir}`));
39135
+ }
39136
+ return;
39137
+ }
39138
+ const registry2 = loadRegistry();
39139
+ const registryNames = new Set(registry2.map((s) => s.name));
39140
+ const found = [];
39141
+ for (const entry of readdirSync5(agentDir, { withFileTypes: true })) {
39142
+ if (!entry.isDirectory())
39143
+ continue;
39144
+ const bare = entry.name.replace(/^skill-/, "");
39145
+ found.push({
39146
+ name: bare,
39147
+ path: join8(agentDir, entry.name),
39148
+ inRegistry: registryNames.has(bare)
39149
+ });
39150
+ }
39151
+ const unknown3 = found.filter((s) => !s.inRegistry);
39152
+ if (options.register && unknown3.length > 0) {
39153
+ const globalSkillsDir = join8(homedir4(), ".skills");
39154
+ const registered = [];
39155
+ for (const s of unknown3) {
39156
+ const srcSkillMd = join8(s.path, "SKILL.md");
39157
+ if (!existsSync8(srcSkillMd))
39158
+ continue;
39159
+ const destDir = join8(globalSkillsDir, `skill-${s.name}`);
39160
+ if (!existsSync8(destDir)) {
39161
+ mkdirSync4(destDir, { recursive: true });
39162
+ }
39163
+ writeFileSync4(join8(destDir, "SKILL.md"), readFileSync7(srcSkillMd, "utf-8"));
39164
+ registered.push(s.name);
39165
+ }
39166
+ clearRegistryCache();
39167
+ if (options.json) {
39168
+ console.log(JSON.stringify({ agentDir, skills: found, registered }));
39169
+ } else {
39170
+ for (const name of registered) {
39171
+ console.log(chalk2.green(`\u2713 Registered '${name}' into ~/.skills/ (global custom)`));
39172
+ }
39173
+ if (registered.length === 0)
39174
+ console.log(chalk2.dim("No new skills to register (all SKILL.md files missing)"));
39175
+ }
39176
+ return;
39177
+ }
39178
+ if (options.json) {
39179
+ console.log(JSON.stringify({ agentDir, skills: found }));
39180
+ } else {
39181
+ console.log(chalk2.bold(`
39182
+ Agent skills in ~/.${agentName}/skills/ (${found.length} found):
39183
+ `));
39184
+ for (const s of found) {
39185
+ const label = s.inRegistry ? chalk2.green("\u2713 in registry") : chalk2.yellow("\u2717 not in registry");
39186
+ console.log(` ${chalk2.cyan(s.name)} \u2014 ${label}`);
39187
+ }
39188
+ if (unknown3.length > 0) {
39189
+ console.log("");
39190
+ console.log(chalk2.dim(`Tip: ${unknown3.length} skill(s) not in registry. Run with --register to add them to ~/.skills/.`));
39191
+ }
39192
+ }
39193
+ return;
39194
+ }
39195
+ if (options.to) {
39196
+ let agents;
39197
+ try {
39198
+ agents = resolveAgents(options.to);
39199
+ } catch (err) {
39200
+ console.error(chalk2.red(err.message));
39201
+ process.exitCode = 1;
39202
+ return;
39203
+ }
39204
+ const registry2 = loadRegistry();
39205
+ const customSkills = registry2.filter((s) => s.source === "custom");
39206
+ if (customSkills.length === 0) {
39207
+ if (options.json) {
39208
+ console.log(JSON.stringify({ pushed: 0, message: "No custom skills found" }));
39209
+ } else {
39210
+ console.log(chalk2.dim("No custom skills found. Use 'skills create <name>' to scaffold one."));
39211
+ }
39212
+ return;
39213
+ }
39214
+ const results = [];
39215
+ for (const skill of customSkills) {
39216
+ for (const agent of agents) {
39217
+ const result = installSkillForAgent(skill.name, {
39218
+ agent,
39219
+ scope: options.scope
39220
+ }, generateSkillMd);
39221
+ results.push({ skill: skill.name, agent, success: result.success, error: result.error });
39222
+ }
39223
+ }
39224
+ if (options.json) {
39225
+ console.log(JSON.stringify({ pushed: results.filter((r) => r.success).length, results }));
39226
+ } else {
39227
+ for (const r of results) {
39228
+ if (r.success) {
39229
+ console.log(chalk2.green(`\u2713 ${r.skill} \u2192 ${r.agent}`));
39230
+ } else {
39231
+ console.log(chalk2.red(`\u2717 ${r.skill} \u2192 ${r.agent}: ${r.error}`));
39232
+ }
39233
+ }
39234
+ }
39235
+ }
39236
+ });
39237
+ 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) => {
39238
+ const skillPath = getSkillPath(name);
39239
+ const issues = [];
39240
+ if (!existsSync8(skillPath)) {
39241
+ if (options.json) {
39242
+ console.log(JSON.stringify({ name, valid: false, issues: [`Skill directory not found: ${skillPath}`] }));
39243
+ } else {
39244
+ console.error(chalk2.red(`Skill '${name}' not found at ${skillPath}`));
39245
+ }
39246
+ process.exitCode = 1;
39247
+ return;
39248
+ }
39249
+ if (!existsSync8(join8(skillPath, "SKILL.md")))
39250
+ issues.push("Missing SKILL.md");
39251
+ if (!existsSync8(join8(skillPath, "tsconfig.json")))
39252
+ issues.push("Missing tsconfig.json");
39253
+ const pkgPath = join8(skillPath, "package.json");
39254
+ if (!existsSync8(pkgPath)) {
39255
+ issues.push("Missing package.json");
39256
+ } else {
39257
+ try {
39258
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
39259
+ if (!pkg.bin || Object.keys(pkg.bin).length === 0)
39260
+ issues.push("package.json missing 'bin' entry");
39261
+ } catch {
39262
+ issues.push("package.json is invalid JSON");
39263
+ }
39264
+ }
39265
+ if (!existsSync8(join8(skillPath, "src"))) {
39266
+ issues.push("Missing src/ directory");
39267
+ } else if (!existsSync8(join8(skillPath, "src", "index.ts"))) {
39268
+ issues.push("Missing src/index.ts");
39269
+ }
39270
+ const valid = issues.length === 0;
39271
+ if (options.json) {
39272
+ console.log(JSON.stringify({ name, valid, path: skillPath, issues }));
39273
+ } else if (valid) {
39274
+ console.log(chalk2.green(`\u2713 ${name} \u2014 all checks passed`));
39275
+ } else {
39276
+ console.log(chalk2.red(`\u2717 ${name} \u2014 ${issues.length} issue(s):`));
39277
+ for (const issue2 of issues)
39278
+ console.log(chalk2.red(` \u2022 ${issue2}`));
39279
+ process.exitCode = 1;
39280
+ }
39281
+ });
39282
+ 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) => {
39283
+ const bare = name.replace(/^skill-/, "");
39284
+ const skillName = `skill-${bare}`;
39285
+ const destPath = join8(process.cwd(), ".skills", skillName);
39286
+ const sourcePath = getSkillPath(bare);
39287
+ if (!existsSync8(sourcePath)) {
39288
+ if (options.json) {
39289
+ console.log(JSON.stringify({ error: `Skill '${bare}' not found in registry` }));
39290
+ } else {
39291
+ skillNotFound(bare);
39292
+ }
39293
+ process.exitCode = 1;
39294
+ return;
39295
+ }
39296
+ if (!existsSync8(destPath)) {
39297
+ if (options.json) {
39298
+ console.log(JSON.stringify({ installed: false, message: `'${bare}' is not installed locally` }));
39299
+ } else {
39300
+ console.log(chalk2.dim(`'${bare}' is not installed. Run: skills install ${bare}`));
39301
+ }
39302
+ return;
39303
+ }
39304
+ function collectFiles(dir, base = "") {
39305
+ const files = new Map;
39306
+ if (!existsSync8(dir))
39307
+ return files;
39308
+ for (const entry of readdirSync5(dir)) {
39309
+ const full = join8(dir, entry);
39310
+ const rel = base ? `${base}/${entry}` : entry;
39311
+ if (statSync3(full).isDirectory()) {
39312
+ for (const [k, v] of collectFiles(full, rel))
39313
+ files.set(k, v);
39314
+ } else {
39315
+ try {
39316
+ files.set(rel, readFileSync7(full, "utf-8"));
39317
+ } catch {
39318
+ files.set(rel, "");
39319
+ }
39320
+ }
39321
+ }
39322
+ return files;
39323
+ }
39324
+ const installed = collectFiles(destPath);
39325
+ const source = collectFiles(sourcePath);
39326
+ const changed = [];
39327
+ const added = [];
39328
+ const removed = [];
39329
+ for (const [file2, content] of source) {
39330
+ if (!installed.has(file2))
39331
+ added.push(file2);
39332
+ else if (installed.get(file2) !== content)
39333
+ changed.push(file2);
39334
+ }
39335
+ for (const file2 of installed.keys()) {
39336
+ if (!source.has(file2))
39337
+ removed.push(file2);
39338
+ }
39339
+ if (options.json) {
39340
+ console.log(JSON.stringify({ name: bare, changed, added, removed, upToDate: changed.length === 0 && added.length === 0 && removed.length === 0 }));
39341
+ return;
39342
+ }
39343
+ if (changed.length === 0 && added.length === 0 && removed.length === 0) {
39344
+ console.log(chalk2.green(`\u2713 ${bare} \u2014 up to date`));
39345
+ return;
39346
+ }
39347
+ console.log(chalk2.bold(`
39348
+ Diff for '${bare}':
39349
+ `));
39350
+ for (const f of changed)
39351
+ console.log(chalk2.yellow(` ~ ${f}`));
39352
+ for (const f of added)
39353
+ console.log(chalk2.green(` + ${f}`));
39354
+ for (const f of removed)
39355
+ console.log(chalk2.red(` - ${f}`));
39356
+ console.log(`
39357
+ ${chalk2.dim(`Run 'skills update ${bare}' to apply changes`)}`);
39358
+ });
39359
+ var scheduleCmd = program2.command("schedule").description("Manage scheduled skill runs (cron-based)");
39360
+ 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) => {
39361
+ const args = options.args ? options.args.split(" ").filter(Boolean) : undefined;
39362
+ const { schedule, error: error48 } = addSchedule(skill, cron, { name: options.name, args });
39363
+ if (options.json) {
39364
+ console.log(JSON.stringify(schedule ? { schedule } : { error: error48 }));
39365
+ return;
39366
+ }
39367
+ if (error48 || !schedule) {
39368
+ console.error(chalk2.red(`\u2717 ${error48 || "Failed to add schedule"}`));
39369
+ process.exitCode = 1;
39370
+ return;
39371
+ }
39372
+ console.log(chalk2.green(`\u2713 Scheduled '${schedule.name}'`));
39373
+ console.log(chalk2.dim(` Cron: ${schedule.cron}`));
39374
+ if (schedule.nextRun) {
39375
+ console.log(chalk2.dim(` Next run: ${new Date(schedule.nextRun).toLocaleString()}`));
39376
+ }
39377
+ console.log(chalk2.dim(` ID: ${schedule.id}`));
39378
+ });
39379
+ scheduleCmd.command("list").option("--json", "Output as JSON", false).description("List all scheduled skills").action((options) => {
39380
+ const schedules = listSchedules();
39381
+ if (options.json) {
39382
+ console.log(JSON.stringify(schedules));
39383
+ return;
39384
+ }
39385
+ if (schedules.length === 0) {
39386
+ console.log(chalk2.dim("No schedules. Run: skills schedule add <skill> <cron>"));
39387
+ return;
39388
+ }
39389
+ console.log(chalk2.bold(`
39390
+ Scheduled skills (${schedules.length}):
39391
+ `));
39392
+ for (const s of schedules) {
39393
+ const status = s.enabled ? chalk2.green("enabled") : chalk2.dim("disabled");
39394
+ const last = s.lastRun ? `last: ${new Date(s.lastRun).toLocaleString()} [${s.lastRunStatus ?? "?"}]` : "never run";
39395
+ const next = s.nextRun ? `next: ${new Date(s.nextRun).toLocaleString()}` : "";
39396
+ console.log(` ${chalk2.cyan(s.name)} [${status}]`);
39397
+ console.log(chalk2.dim(` skill: ${s.skill} cron: ${s.cron} ${last} ${next}`));
39398
+ }
39399
+ });
39400
+ 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) => {
39401
+ const removed = removeSchedule(idOrName);
39402
+ if (options.json) {
39403
+ console.log(JSON.stringify({ removed, idOrName }));
39404
+ return;
39405
+ }
39406
+ if (removed) {
39407
+ console.log(chalk2.green(`\u2713 Removed schedule '${idOrName}'`));
39408
+ } else {
39409
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39410
+ process.exitCode = 1;
39411
+ }
39412
+ });
39413
+ scheduleCmd.command("enable").argument("<id-or-name>", "Schedule ID or name").description("Enable a disabled schedule").action((idOrName) => {
39414
+ const ok = setScheduleEnabled(idOrName, true);
39415
+ if (ok)
39416
+ console.log(chalk2.green(`\u2713 Enabled '${idOrName}'`));
39417
+ else {
39418
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39419
+ process.exitCode = 1;
39420
+ }
39421
+ });
39422
+ scheduleCmd.command("disable").argument("<id-or-name>", "Schedule ID or name").description("Disable a schedule without removing it").action((idOrName) => {
39423
+ const ok = setScheduleEnabled(idOrName, false);
39424
+ if (ok)
39425
+ console.log(chalk2.green(`\u2713 Disabled '${idOrName}'`));
39426
+ else {
39427
+ console.error(chalk2.red(`Schedule '${idOrName}' not found`));
39428
+ process.exitCode = 1;
39429
+ }
39430
+ });
39431
+ 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) => {
39432
+ const due = getDueSchedules();
39433
+ if (due.length === 0) {
39434
+ if (options.json)
39435
+ console.log(JSON.stringify({ ran: 0, schedules: [] }));
39436
+ else
39437
+ console.log(chalk2.dim("No schedules are due."));
39438
+ return;
39439
+ }
39440
+ if (options.dryRun) {
39441
+ if (options.json)
39442
+ console.log(JSON.stringify({ due: due.map((s) => s.name) }));
39443
+ else {
39444
+ console.log(chalk2.bold(`${due.length} schedule(s) due:
39445
+ `));
39446
+ for (const s of due)
39447
+ console.log(` ${chalk2.cyan(s.name)} \u2014 ${s.skill} (${s.cron})`);
39448
+ }
39449
+ return;
39450
+ }
39451
+ const results = [];
39452
+ for (const s of due) {
39453
+ try {
39454
+ const { runSkill: runSkill2 } = await Promise.resolve().then(() => (init_skillinfo(), exports_skillinfo));
39455
+ await runSkill2(s.skill, s.args ?? []);
39456
+ recordScheduleRun(s.id, "success");
39457
+ results.push({ name: s.name, skill: s.skill, status: "success" });
39458
+ } catch (err) {
39459
+ recordScheduleRun(s.id, "error");
39460
+ results.push({ name: s.name, skill: s.skill, status: "error", error: err.message });
39461
+ }
39462
+ }
39463
+ if (options.json) {
39464
+ console.log(JSON.stringify({ ran: results.length, results }));
39465
+ } else {
39466
+ for (const r of results) {
39467
+ const icon = r.status === "success" ? chalk2.green("\u2713") : chalk2.red("\u2717");
39468
+ console.log(`${icon} ${r.name} (${r.skill})`);
39469
+ if (r.error)
39470
+ console.log(chalk2.dim(` ${r.error}`));
39471
+ }
39472
+ }
39473
+ });
39474
+ scheduleCmd.command("validate").argument("<cron>", "Cron expression to validate").description("Validate a cron expression and show the next 5 run times").action((cron) => {
39475
+ const { getNextRun: getNextRun2 } = (init_scheduler(), __toCommonJS(exports_scheduler));
39476
+ const { valid, error: error48 } = validateCron(cron);
39477
+ if (!valid) {
39478
+ console.error(chalk2.red(`Invalid cron: ${error48}`));
39479
+ process.exitCode = 1;
39480
+ return;
39481
+ }
39482
+ console.log(chalk2.green(`\u2713 Valid cron: "${cron}"`));
39483
+ console.log(chalk2.dim(`
39484
+ Next 5 run times:`));
39485
+ let d = new Date;
39486
+ for (let i = 0;i < 5; i++) {
39487
+ const next = getNextRun2(cron, d);
39488
+ if (!next)
39489
+ break;
39490
+ console.log(` ${next.toLocaleString()}`);
39491
+ d = next;
39492
+ }
38518
39493
  });
38519
39494
  program2.parse();