@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/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // @bun
2
2
  // src/lib/registry.ts
3
+ import { existsSync, readFileSync, readdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
3
6
  var CATEGORIES = [
4
7
  "Development Tools",
5
8
  "Business & Marketing",
@@ -1036,6 +1039,20 @@ var SKILLS = [
1036
1039
  category: "Design & Branding",
1037
1040
  tags: ["testimonials", "graphics", "social-proof", "marketing"]
1038
1041
  },
1042
+ {
1043
+ name: "colorextract",
1044
+ displayName: "Color Extract",
1045
+ description: "Extract complete color palettes from screenshots and images using Claude Vision. Outputs open-styles compatible profiles.",
1046
+ category: "Design & Branding",
1047
+ tags: ["colors", "palette", "design", "vision", "screenshot", "extract", "open-styles"]
1048
+ },
1049
+ {
1050
+ name: "siteanalyze",
1051
+ displayName: "Site Analyze",
1052
+ description: "Analyze any website's design system \u2014 detects shadcn/ui, Tailwind, extracts colors, typography, and components via Playwright + Claude Vision.",
1053
+ category: "Design & Branding",
1054
+ tags: ["design", "shadcn", "tailwind", "colors", "typography", "playwright", "analysis", "open-styles"]
1055
+ },
1039
1056
  {
1040
1057
  name: "browse",
1041
1058
  displayName: "Browse",
@@ -1436,8 +1453,91 @@ var SKILLS = [
1436
1453
  tags: ["seating", "chart", "events", "venues"]
1437
1454
  }
1438
1455
  ];
1456
+ function parseSkillMdFrontmatter(content) {
1457
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
1458
+ if (!match)
1459
+ return null;
1460
+ const result = {};
1461
+ for (const line of match[1].split(`
1462
+ `)) {
1463
+ const colon = line.indexOf(":");
1464
+ if (colon === -1)
1465
+ continue;
1466
+ const key = line.slice(0, colon).trim();
1467
+ const value = line.slice(colon + 1).trim();
1468
+ if (!key || !value)
1469
+ continue;
1470
+ if (key === "name")
1471
+ result.name = value;
1472
+ else if (key === "description")
1473
+ result.description = value;
1474
+ else if (key === "displayName" || key === "display_name")
1475
+ result.displayName = value;
1476
+ else if (key === "category")
1477
+ result.category = value;
1478
+ else if (key === "tags") {
1479
+ result.tags = value.replace(/[\[\]]/g, "").split(",").map((t) => t.trim()).filter(Boolean);
1480
+ }
1481
+ }
1482
+ return Object.keys(result).length > 0 ? result : null;
1483
+ }
1484
+ function discoverSkillsInDir(dir) {
1485
+ if (!existsSync(dir))
1486
+ return [];
1487
+ const result = [];
1488
+ try {
1489
+ const entries = readdirSync(dir, { withFileTypes: true });
1490
+ for (const entry of entries) {
1491
+ if (!entry.isDirectory())
1492
+ continue;
1493
+ const skillMdPath = join(dir, entry.name, "SKILL.md");
1494
+ if (!existsSync(skillMdPath))
1495
+ continue;
1496
+ let content;
1497
+ try {
1498
+ content = readFileSync(skillMdPath, "utf-8");
1499
+ } catch {
1500
+ continue;
1501
+ }
1502
+ const fm = parseSkillMdFrontmatter(content);
1503
+ if (!fm?.name)
1504
+ continue;
1505
+ const name = fm.name.replace(/^skill-/, "");
1506
+ result.push({
1507
+ name,
1508
+ displayName: fm.displayName || name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
1509
+ description: fm.description || "",
1510
+ category: fm.category || "Development Tools",
1511
+ tags: fm.tags || [],
1512
+ source: "custom"
1513
+ });
1514
+ }
1515
+ } catch {}
1516
+ return result;
1517
+ }
1518
+ var _registryCache = null;
1519
+ var _registryCacheTime = 0;
1520
+ var REGISTRY_CACHE_TTL = 5000;
1521
+ function loadRegistry(cwd) {
1522
+ const now = Date.now();
1523
+ if (_registryCache && now - _registryCacheTime < REGISTRY_CACHE_TTL) {
1524
+ return _registryCache;
1525
+ }
1526
+ const official = SKILLS.map((s) => ({ ...s, source: "official" }));
1527
+ const globalCustom = discoverSkillsInDir(join(homedir(), ".skills"));
1528
+ const projectCustom = discoverSkillsInDir(join(cwd || process.cwd(), ".skills", "custom-skills"));
1529
+ const customNames = new Set([...globalCustom, ...projectCustom].map((s) => s.name));
1530
+ const filtered = official.filter((s) => !customNames.has(s.name));
1531
+ _registryCache = [...filtered, ...globalCustom, ...projectCustom];
1532
+ _registryCacheTime = now;
1533
+ return _registryCache;
1534
+ }
1535
+ function clearRegistryCache() {
1536
+ _registryCache = null;
1537
+ _registryCacheTime = 0;
1538
+ }
1439
1539
  function getSkillsByCategory(category) {
1440
- return SKILLS.filter((s) => s.category === category);
1540
+ return loadRegistry().filter((s) => s.category === category);
1441
1541
  }
1442
1542
  function editDistance(a, b) {
1443
1543
  if (a === b)
@@ -1483,7 +1583,7 @@ function searchSkills(query) {
1483
1583
  if (words.length === 0)
1484
1584
  return [];
1485
1585
  const scored = [];
1486
- for (const skill of SKILLS) {
1586
+ for (const skill of loadRegistry()) {
1487
1587
  const nameLower = skill.name.toLowerCase();
1488
1588
  const displayNameLower = skill.displayName.toLowerCase();
1489
1589
  const descriptionLower = skill.description.toLowerCase();
@@ -1519,15 +1619,15 @@ function searchSkills(query) {
1519
1619
  return scored.map((s) => s.skill);
1520
1620
  }
1521
1621
  function getSkill(name) {
1522
- return SKILLS.find((s) => s.name === name);
1622
+ return loadRegistry().find((s) => s.name === name);
1523
1623
  }
1524
1624
  function getSkillsByTag(tag) {
1525
1625
  const needle = tag.toLowerCase();
1526
- return SKILLS.filter((s) => s.tags.some((t) => t.toLowerCase().includes(needle)));
1626
+ return loadRegistry().filter((s) => s.tags.some((t) => t.toLowerCase().includes(needle)));
1527
1627
  }
1528
1628
  function getAllTags() {
1529
1629
  const tagSet = new Set;
1530
- for (const skill of SKILLS) {
1630
+ for (const skill of loadRegistry()) {
1531
1631
  for (const tag of skill.tags) {
1532
1632
  tagSet.add(tag.toLowerCase());
1533
1633
  }
@@ -1546,13 +1646,13 @@ function levenshtein(a, b) {
1546
1646
  }
1547
1647
  function findSimilarSkills(query, maxResults = 3) {
1548
1648
  const q = query.toLowerCase();
1549
- 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);
1649
+ 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);
1550
1650
  return scored.slice(0, maxResults).map((s) => s.name);
1551
1651
  }
1552
1652
  // src/lib/installer.ts
1553
- import { existsSync, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync, statSync, readFileSync, accessSync, constants } from "fs";
1554
- import { join, dirname } from "path";
1555
- import { homedir } from "os";
1653
+ import { existsSync as existsSync2, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, accessSync, constants } from "fs";
1654
+ import { join as join2, dirname } from "path";
1655
+ import { homedir as homedir2 } from "os";
1556
1656
  import { fileURLToPath } from "url";
1557
1657
 
1558
1658
  // src/lib/utils.ts
@@ -1565,36 +1665,36 @@ var __dirname2 = dirname(fileURLToPath(import.meta.url));
1565
1665
  function findSkillsDir() {
1566
1666
  let dir = __dirname2;
1567
1667
  for (let i = 0;i < 5; i++) {
1568
- const candidate = join(dir, "skills");
1569
- if (existsSync(candidate)) {
1668
+ const candidate = join2(dir, "skills");
1669
+ if (existsSync2(candidate) && !dir.includes(".skills")) {
1570
1670
  return candidate;
1571
1671
  }
1572
1672
  dir = dirname(dir);
1573
1673
  }
1574
- return join(__dirname2, "..", "skills");
1674
+ return join2(__dirname2, "..", "skills");
1575
1675
  }
1576
1676
  var SKILLS_DIR = findSkillsDir();
1577
1677
  function getSkillPath(name) {
1578
1678
  const skillName = normalizeSkillName(name);
1579
- return join(SKILLS_DIR, skillName);
1679
+ return join2(SKILLS_DIR, skillName);
1580
1680
  }
1581
1681
  function skillExists(name) {
1582
- return existsSync(getSkillPath(name));
1682
+ return existsSync2(getSkillPath(name));
1583
1683
  }
1584
1684
  function installSkill(name, options = {}) {
1585
1685
  const { targetDir = process.cwd(), overwrite = false } = options;
1586
1686
  const skillName = normalizeSkillName(name);
1587
1687
  const sourcePath = getSkillPath(name);
1588
- const destDir = join(targetDir, ".skills");
1589
- const destPath = join(destDir, skillName);
1590
- if (!existsSync(sourcePath)) {
1688
+ const destDir = join2(targetDir, ".skills");
1689
+ const destPath = join2(destDir, skillName);
1690
+ if (!existsSync2(sourcePath)) {
1591
1691
  return {
1592
1692
  skill: name,
1593
1693
  success: false,
1594
1694
  error: `Skill '${name}' not found`
1595
1695
  };
1596
1696
  }
1597
- if (existsSync(destPath) && !overwrite) {
1697
+ if (existsSync2(destPath) && !overwrite) {
1598
1698
  return {
1599
1699
  skill: name,
1600
1700
  success: false,
@@ -1603,10 +1703,10 @@ function installSkill(name, options = {}) {
1603
1703
  };
1604
1704
  }
1605
1705
  try {
1606
- if (!existsSync(destDir)) {
1706
+ if (!existsSync2(destDir)) {
1607
1707
  mkdirSync(destDir, { recursive: true });
1608
1708
  }
1609
- if (existsSync(destPath) && overwrite) {
1709
+ if (existsSync2(destPath) && overwrite) {
1610
1710
  rmSync(destPath, { recursive: true, force: true });
1611
1711
  }
1612
1712
  cpSync(sourcePath, destPath, {
@@ -1645,10 +1745,10 @@ function installSkills(names, options = {}) {
1645
1745
  return names.map((name) => installSkill(name, options));
1646
1746
  }
1647
1747
  function updateSkillsIndex(skillsDir) {
1648
- const indexPath = join(skillsDir, "index.ts");
1748
+ const indexPath = join2(skillsDir, "index.ts");
1649
1749
  const meta = loadMeta(skillsDir);
1650
1750
  const disabledSet = new Set(meta.disabled || []);
1651
- const skills = readdirSync(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
1751
+ const skills = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
1652
1752
  const exports = skills.map((s) => {
1653
1753
  const name = s.replace("skill-", "").replace(/-/g, "_");
1654
1754
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -1664,13 +1764,13 @@ ${exports}
1664
1764
  writeFileSync(indexPath, content);
1665
1765
  }
1666
1766
  function getMetaPath(skillsDir) {
1667
- return join(skillsDir, ".meta.json");
1767
+ return join2(skillsDir, ".meta.json");
1668
1768
  }
1669
1769
  function loadMeta(skillsDir) {
1670
1770
  const metaPath = getMetaPath(skillsDir);
1671
- if (existsSync(metaPath)) {
1771
+ if (existsSync2(metaPath)) {
1672
1772
  try {
1673
- return JSON.parse(readFileSync(metaPath, "utf-8"));
1773
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
1674
1774
  } catch {}
1675
1775
  }
1676
1776
  return { skills: {} };
@@ -1683,9 +1783,9 @@ function recordInstall(skillsDir, name) {
1683
1783
  const skillName = normalizeSkillName(name);
1684
1784
  let version = "unknown";
1685
1785
  try {
1686
- const pkgPath = join(skillsDir, skillName, "package.json");
1687
- if (existsSync(pkgPath)) {
1688
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1786
+ const pkgPath = join2(skillsDir, skillName, "package.json");
1787
+ if (existsSync2(pkgPath)) {
1788
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1689
1789
  version = pkg.version || "unknown";
1690
1790
  }
1691
1791
  } catch {}
@@ -1698,12 +1798,12 @@ function recordRemove(skillsDir, name) {
1698
1798
  saveMeta(skillsDir, meta);
1699
1799
  }
1700
1800
  function getInstallMeta(targetDir = process.cwd()) {
1701
- return loadMeta(join(targetDir, ".skills"));
1801
+ return loadMeta(join2(targetDir, ".skills"));
1702
1802
  }
1703
1803
  function disableSkill(name, targetDir = process.cwd()) {
1704
- const skillsDir = join(targetDir, ".skills");
1804
+ const skillsDir = join2(targetDir, ".skills");
1705
1805
  const skillName = normalizeSkillName(name);
1706
- if (!existsSync(join(skillsDir, skillName)))
1806
+ if (!existsSync2(join2(skillsDir, skillName)))
1707
1807
  return false;
1708
1808
  const meta = loadMeta(skillsDir);
1709
1809
  const disabled = new Set(meta.disabled || []);
@@ -1716,7 +1816,7 @@ function disableSkill(name, targetDir = process.cwd()) {
1716
1816
  return true;
1717
1817
  }
1718
1818
  function enableSkill(name, targetDir = process.cwd()) {
1719
- const skillsDir = join(targetDir, ".skills");
1819
+ const skillsDir = join2(targetDir, ".skills");
1720
1820
  const meta = loadMeta(skillsDir);
1721
1821
  const disabled = new Set(meta.disabled || []);
1722
1822
  if (!disabled.has(name))
@@ -1728,24 +1828,24 @@ function enableSkill(name, targetDir = process.cwd()) {
1728
1828
  return true;
1729
1829
  }
1730
1830
  function getDisabledSkills(targetDir = process.cwd()) {
1731
- const meta = loadMeta(join(targetDir, ".skills"));
1831
+ const meta = loadMeta(join2(targetDir, ".skills"));
1732
1832
  return meta.disabled || [];
1733
1833
  }
1734
1834
  function getInstalledSkills(targetDir = process.cwd()) {
1735
- const skillsDir = join(targetDir, ".skills");
1736
- if (!existsSync(skillsDir)) {
1835
+ const skillsDir = join2(targetDir, ".skills");
1836
+ if (!existsSync2(skillsDir)) {
1737
1837
  return [];
1738
1838
  }
1739
- return readdirSync(skillsDir).filter((f) => {
1740
- const fullPath = join(skillsDir, f);
1839
+ return readdirSync2(skillsDir).filter((f) => {
1840
+ const fullPath = join2(skillsDir, f);
1741
1841
  return f.startsWith("skill-") && statSync(fullPath).isDirectory();
1742
1842
  }).map((f) => f.replace("skill-", ""));
1743
1843
  }
1744
1844
  function removeSkill(name, targetDir = process.cwd()) {
1745
1845
  const skillName = normalizeSkillName(name);
1746
- const skillsDir = join(targetDir, ".skills");
1747
- const skillPath = join(skillsDir, skillName);
1748
- if (!existsSync(skillPath)) {
1846
+ const skillsDir = join2(targetDir, ".skills");
1847
+ const skillPath = join2(skillsDir, skillName);
1848
+ if (!existsSync2(skillPath)) {
1749
1849
  return false;
1750
1850
  }
1751
1851
  rmSync(skillPath, { recursive: true, force: true });
@@ -1753,29 +1853,40 @@ function removeSkill(name, targetDir = process.cwd()) {
1753
1853
  recordRemove(skillsDir, name);
1754
1854
  return true;
1755
1855
  }
1756
- var AGENT_TARGETS = ["claude", "codex", "gemini"];
1856
+ var AGENT_TARGETS = ["claude", "codex", "gemini", "pi", "opencode"];
1857
+ var AGENT_LABELS = {
1858
+ claude: "Claude Code",
1859
+ codex: "Codex CLI",
1860
+ gemini: "Gemini CLI",
1861
+ pi: "pi.dev",
1862
+ opencode: "OpenCode"
1863
+ };
1757
1864
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
1758
- const agentDir = `.${agent}`;
1759
- if (scope === "project") {
1760
- return join(projectDir || process.cwd(), agentDir, "skills");
1865
+ const base = projectDir || process.cwd();
1866
+ switch (agent) {
1867
+ case "pi":
1868
+ return scope === "project" ? join2(base, ".pi", "skills") : join2(homedir2(), ".pi", "agent", "skills");
1869
+ case "opencode":
1870
+ return scope === "project" ? join2(base, ".opencode", "skills") : join2(homedir2(), ".opencode", "skills");
1871
+ default:
1872
+ return scope === "project" ? join2(base, `.${agent}`, "skills") : join2(homedir2(), `.${agent}`, "skills");
1761
1873
  }
1762
- return join(homedir(), agentDir, "skills");
1763
1874
  }
1764
1875
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
1765
1876
  const skillName = normalizeSkillName(name);
1766
- return join(getAgentSkillsDir(agent, scope, projectDir), skillName);
1877
+ return join2(getAgentSkillsDir(agent, scope, projectDir), skillName);
1767
1878
  }
1768
1879
  function installSkillForAgent(name, options, generateSkillMd) {
1769
1880
  const { agent, scope = "global", projectDir } = options;
1770
1881
  const skillName = normalizeSkillName(name);
1771
1882
  const sourcePath = getSkillPath(name);
1772
- if (!existsSync(sourcePath)) {
1883
+ if (!existsSync2(sourcePath)) {
1773
1884
  return { skill: name, success: false, error: `Skill '${name}' not found` };
1774
1885
  }
1775
1886
  let skillMdContent = null;
1776
- const skillMdPath = join(sourcePath, "SKILL.md");
1777
- if (existsSync(skillMdPath)) {
1778
- skillMdContent = readFileSync(skillMdPath, "utf-8");
1887
+ const skillMdPath = join2(sourcePath, "SKILL.md");
1888
+ if (existsSync2(skillMdPath)) {
1889
+ skillMdContent = readFileSync2(skillMdPath, "utf-8");
1779
1890
  } else if (generateSkillMd) {
1780
1891
  skillMdContent = generateSkillMd(name);
1781
1892
  }
@@ -1784,17 +1895,12 @@ function installSkillForAgent(name, options, generateSkillMd) {
1784
1895
  }
1785
1896
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
1786
1897
  if (scope === "global") {
1787
- const agentBaseDir = join(homedir(), `.${agent}`);
1788
- if (!existsSync(agentBaseDir)) {
1789
- const agentLabels = {
1790
- claude: "Claude Code",
1791
- codex: "Codex CLI",
1792
- gemini: "Gemini CLI"
1793
- };
1898
+ const agentBaseDir = agent === "pi" ? join2(homedir2(), ".pi", "agent") : join2(homedir2(), `.${agent}`);
1899
+ if (!existsSync2(agentBaseDir)) {
1794
1900
  return {
1795
1901
  skill: name,
1796
1902
  success: false,
1797
- error: `Agent directory ${agentBaseDir} does not exist. Is ${agentLabels[agent]} installed?`
1903
+ error: `Agent directory ${agentBaseDir} does not exist. Is ${AGENT_LABELS[agent]} installed?`
1798
1904
  };
1799
1905
  }
1800
1906
  try {
@@ -1809,7 +1915,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
1809
1915
  }
1810
1916
  try {
1811
1917
  mkdirSync(destDir, { recursive: true });
1812
- writeFileSync(join(destDir, "SKILL.md"), skillMdContent);
1918
+ writeFileSync(join2(destDir, "SKILL.md"), skillMdContent);
1813
1919
  return { skill: name, success: true, path: destDir };
1814
1920
  } catch (error) {
1815
1921
  return {
@@ -1822,23 +1928,23 @@ function installSkillForAgent(name, options, generateSkillMd) {
1822
1928
  function removeSkillForAgent(name, options) {
1823
1929
  const { agent, scope = "global", projectDir } = options;
1824
1930
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
1825
- if (!existsSync(destDir)) {
1931
+ if (!existsSync2(destDir)) {
1826
1932
  return false;
1827
1933
  }
1828
1934
  rmSync(destDir, { recursive: true, force: true });
1829
1935
  return true;
1830
1936
  }
1831
1937
  // src/lib/skillinfo.ts
1832
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1833
- import { join as join2 } from "path";
1938
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
1939
+ import { join as join3 } from "path";
1834
1940
  function getSkillDocs(name) {
1835
1941
  const skillPath = getSkillPath(name);
1836
- if (!existsSync2(skillPath))
1942
+ if (!existsSync3(skillPath))
1837
1943
  return null;
1838
1944
  return {
1839
- skillMd: readIfExists(join2(skillPath, "SKILL.md")),
1840
- readme: readIfExists(join2(skillPath, "README.md")),
1841
- claudeMd: readIfExists(join2(skillPath, "CLAUDE.md"))
1945
+ skillMd: readIfExists(join3(skillPath, "SKILL.md")),
1946
+ readme: readIfExists(join3(skillPath, "README.md")),
1947
+ claudeMd: readIfExists(join3(skillPath, "CLAUDE.md"))
1842
1948
  };
1843
1949
  }
1844
1950
  function getSkillBestDoc(name) {
@@ -1849,11 +1955,11 @@ function getSkillBestDoc(name) {
1849
1955
  }
1850
1956
  function getSkillRequirements(name) {
1851
1957
  const skillPath = getSkillPath(name);
1852
- if (!existsSync2(skillPath))
1958
+ if (!existsSync3(skillPath))
1853
1959
  return null;
1854
1960
  const texts = [];
1855
1961
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
1856
- const content = readIfExists(join2(skillPath, file));
1962
+ const content = readIfExists(join3(skillPath, file));
1857
1963
  if (content)
1858
1964
  texts.push(content);
1859
1965
  }
@@ -1880,10 +1986,10 @@ function getSkillRequirements(name) {
1880
1986
  }
1881
1987
  let cliCommand = null;
1882
1988
  let dependencies = {};
1883
- const pkgPath = join2(skillPath, "package.json");
1884
- if (existsSync2(pkgPath)) {
1989
+ const pkgPath = join3(skillPath, "package.json");
1990
+ if (existsSync3(pkgPath)) {
1885
1991
  try {
1886
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1992
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1887
1993
  if (pkg.bin) {
1888
1994
  const binKeys = Object.keys(pkg.bin);
1889
1995
  if (binKeys.length > 0)
@@ -1903,25 +2009,25 @@ async function runSkill(name, args, options = {}) {
1903
2009
  const skillName = normalizeSkillName(name);
1904
2010
  let skillPath;
1905
2011
  if (options.installed) {
1906
- skillPath = join2(process.cwd(), ".skills", skillName);
2012
+ skillPath = join3(process.cwd(), ".skills", skillName);
1907
2013
  } else {
1908
- const installedPath = join2(process.cwd(), ".skills", skillName);
1909
- if (existsSync2(installedPath)) {
2014
+ const installedPath = join3(process.cwd(), ".skills", skillName);
2015
+ if (existsSync3(installedPath)) {
1910
2016
  skillPath = installedPath;
1911
2017
  } else {
1912
2018
  skillPath = getSkillPath(name);
1913
2019
  }
1914
2020
  }
1915
- if (!existsSync2(skillPath)) {
2021
+ if (!existsSync3(skillPath)) {
1916
2022
  return { exitCode: 1, error: `Skill '${name}' not found` };
1917
2023
  }
1918
- const pkgPath = join2(skillPath, "package.json");
1919
- if (!existsSync2(pkgPath)) {
2024
+ const pkgPath = join3(skillPath, "package.json");
2025
+ if (!existsSync3(pkgPath)) {
1920
2026
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
1921
2027
  }
1922
2028
  let entryPoint;
1923
2029
  try {
1924
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
2030
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1925
2031
  if (pkg.bin) {
1926
2032
  const binValues = Object.values(pkg.bin);
1927
2033
  entryPoint = binValues[0];
@@ -1935,12 +2041,12 @@ async function runSkill(name, args, options = {}) {
1935
2041
  } catch {
1936
2042
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
1937
2043
  }
1938
- const entryPath = join2(skillPath, entryPoint);
1939
- if (!existsSync2(entryPath)) {
2044
+ const entryPath = join3(skillPath, entryPoint);
2045
+ if (!existsSync3(entryPath)) {
1940
2046
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
1941
2047
  }
1942
- const nodeModules = join2(skillPath, "node_modules");
1943
- if (!existsSync2(nodeModules)) {
2048
+ const nodeModules = join3(skillPath, "node_modules");
2049
+ if (!existsSync3(nodeModules)) {
1944
2050
  const install = Bun.spawn(["bun", "install", "--no-save"], {
1945
2051
  cwd: skillPath,
1946
2052
  stdout: "pipe",
@@ -1958,17 +2064,17 @@ async function runSkill(name, args, options = {}) {
1958
2064
  return { exitCode };
1959
2065
  }
1960
2066
  function generateEnvExample(targetDir = process.cwd()) {
1961
- const skillsDir = join2(targetDir, ".skills");
1962
- if (!existsSync2(skillsDir))
2067
+ const skillsDir = join3(targetDir, ".skills");
2068
+ if (!existsSync3(skillsDir))
1963
2069
  return "";
1964
- const dirs = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && existsSync2(join2(skillsDir, f, "package.json")));
2070
+ const dirs = readdirSync3(skillsDir).filter((f) => f.startsWith("skill-") && existsSync3(join3(skillsDir, f, "package.json")));
1965
2071
  const envMap = new Map;
1966
2072
  for (const dir of dirs) {
1967
2073
  const skillName = dir.replace("skill-", "");
1968
- const skillPath = join2(skillsDir, dir);
2074
+ const skillPath = join3(skillsDir, dir);
1969
2075
  const texts = [];
1970
2076
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example"]) {
1971
- const content = readIfExists(join2(skillPath, file));
2077
+ const content = readIfExists(join3(skillPath, file));
1972
2078
  if (content)
1973
2079
  texts.push(content);
1974
2080
  }
@@ -2013,7 +2119,7 @@ function generateSkillMd(name) {
2013
2119
  if (!meta)
2014
2120
  return null;
2015
2121
  const skillPath = getSkillPath(name);
2016
- if (!existsSync2(skillPath))
2122
+ if (!existsSync3(skillPath))
2017
2123
  return null;
2018
2124
  const frontmatter = [
2019
2125
  "---",
@@ -2022,13 +2128,13 @@ function generateSkillMd(name) {
2022
2128
  "---"
2023
2129
  ].join(`
2024
2130
  `);
2025
- const readme = readIfExists(join2(skillPath, "README.md"));
2026
- const claudeMd = readIfExists(join2(skillPath, "CLAUDE.md"));
2131
+ const readme = readIfExists(join3(skillPath, "README.md"));
2132
+ const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
2027
2133
  let cliCommand = null;
2028
- const pkgPath = join2(skillPath, "package.json");
2029
- if (existsSync2(pkgPath)) {
2134
+ const pkgPath = join3(skillPath, "package.json");
2135
+ if (existsSync3(pkgPath)) {
2030
2136
  try {
2031
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
2137
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
2032
2138
  if (pkg.bin) {
2033
2139
  const binKeys = Object.keys(pkg.bin);
2034
2140
  if (binKeys.length > 0)
@@ -2103,16 +2209,16 @@ function extractEnvVars(text) {
2103
2209
  }
2104
2210
  function readIfExists(path) {
2105
2211
  try {
2106
- if (existsSync2(path)) {
2107
- return readFileSync2(path, "utf-8");
2212
+ if (existsSync3(path)) {
2213
+ return readFileSync3(path, "utf-8");
2108
2214
  }
2109
2215
  } catch {}
2110
2216
  return null;
2111
2217
  }
2112
2218
  // src/lib/config.ts
2113
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
2114
- import { join as join3, dirname as dirname2 } from "path";
2115
- import { homedir as homedir2 } from "os";
2219
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
2220
+ import { join as join4, dirname as dirname2 } from "path";
2221
+ import { homedir as homedir3 } from "os";
2116
2222
  var VALID_KEYS = {
2117
2223
  defaultAgent: ["claude", "codex", "gemini", "all"],
2118
2224
  defaultScope: ["global", "project"],
@@ -2120,15 +2226,15 @@ var VALID_KEYS = {
2120
2226
  };
2121
2227
  function getConfigPath(scope) {
2122
2228
  if (scope === "global") {
2123
- return join3(homedir2(), ".skillsrc");
2229
+ return join4(homedir3(), ".skillsrc");
2124
2230
  }
2125
- return join3(process.cwd(), "skills.config.json");
2231
+ return join4(process.cwd(), "skills.config.json");
2126
2232
  }
2127
2233
  function readConfigFile(path) {
2128
- if (!existsSync3(path))
2234
+ if (!existsSync4(path))
2129
2235
  return {};
2130
2236
  try {
2131
- const raw = readFileSync3(path, "utf-8");
2237
+ const raw = readFileSync4(path, "utf-8");
2132
2238
  const parsed = JSON.parse(raw);
2133
2239
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2134
2240
  return {};
@@ -2159,9 +2265,9 @@ function saveConfig(key, value, scope = "project") {
2159
2265
  }
2160
2266
  const filePath = getConfigPath(scope);
2161
2267
  let existing = {};
2162
- if (existsSync3(filePath)) {
2268
+ if (existsSync4(filePath)) {
2163
2269
  try {
2164
- existing = JSON.parse(readFileSync3(filePath, "utf-8"));
2270
+ existing = JSON.parse(readFileSync4(filePath, "utf-8"));
2165
2271
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
2166
2272
  existing = {};
2167
2273
  }
@@ -2170,7 +2276,7 @@ function saveConfig(key, value, scope = "project") {
2170
2276
  }
2171
2277
  } else {
2172
2278
  const dir = dirname2(filePath);
2173
- if (!existsSync3(dir)) {
2279
+ if (!existsSync4(dir)) {
2174
2280
  mkdirSync2(dir, { recursive: true });
2175
2281
  }
2176
2282
  }
@@ -2178,14 +2284,174 @@ function saveConfig(key, value, scope = "project") {
2178
2284
  writeFileSync2(filePath, JSON.stringify(existing, null, 2) + `
2179
2285
  `);
2180
2286
  }
2287
+ // src/lib/scheduler.ts
2288
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
2289
+ import { join as join5 } from "path";
2290
+ function getSchedulesPath(targetDir = process.cwd()) {
2291
+ return join5(targetDir, ".skills", "schedules.json");
2292
+ }
2293
+ function loadSchedules(targetDir = process.cwd()) {
2294
+ const path = getSchedulesPath(targetDir);
2295
+ if (existsSync5(path)) {
2296
+ try {
2297
+ return JSON.parse(readFileSync5(path, "utf-8"));
2298
+ } catch {}
2299
+ }
2300
+ return { version: 1, schedules: [] };
2301
+ }
2302
+ function saveSchedules(data, targetDir = process.cwd()) {
2303
+ const path = getSchedulesPath(targetDir);
2304
+ const dir = join5(targetDir, ".skills");
2305
+ if (!existsSync5(dir))
2306
+ mkdirSync3(dir, { recursive: true });
2307
+ writeFileSync3(path, JSON.stringify(data, null, 2));
2308
+ }
2309
+ function validateCron(expr) {
2310
+ const fields = expr.trim().split(/\s+/);
2311
+ if (fields.length !== 5) {
2312
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
2313
+ }
2314
+ return { valid: true };
2315
+ }
2316
+ function getNextRun(cron, from = new Date) {
2317
+ const { valid } = validateCron(cron);
2318
+ if (!valid)
2319
+ return null;
2320
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
2321
+ function parseField(f, min, max) {
2322
+ if (f === "*")
2323
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
2324
+ if (f.startsWith("*/")) {
2325
+ const step = parseInt(f.slice(2));
2326
+ if (isNaN(step))
2327
+ return [];
2328
+ const vals = [];
2329
+ for (let i = min;i <= max; i += step)
2330
+ vals.push(i);
2331
+ return vals;
2332
+ }
2333
+ return f.split(",").flatMap((part) => {
2334
+ if (part.includes("-")) {
2335
+ const [lo, hi] = part.split("-").map(Number);
2336
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
2337
+ }
2338
+ const n = parseInt(part);
2339
+ return isNaN(n) ? [] : [n];
2340
+ });
2341
+ }
2342
+ const minutes = parseField(minuteF, 0, 59);
2343
+ const hours = parseField(hourF, 0, 23);
2344
+ const doms = parseField(domF, 1, 31);
2345
+ const months = parseField(monthF, 1, 12);
2346
+ const dows = parseField(dowF, 0, 6);
2347
+ const candidate = new Date(from);
2348
+ candidate.setSeconds(0, 0);
2349
+ candidate.setMinutes(candidate.getMinutes() + 1);
2350
+ const limit = new Date(from);
2351
+ limit.setFullYear(limit.getFullYear() + 1);
2352
+ while (candidate < limit) {
2353
+ const month = candidate.getMonth() + 1;
2354
+ const dom = candidate.getDate();
2355
+ const dow = candidate.getDay();
2356
+ const hour = candidate.getHours();
2357
+ const minute = candidate.getMinutes();
2358
+ if (!months.includes(month)) {
2359
+ candidate.setMonth(candidate.getMonth() + 1, 1);
2360
+ candidate.setHours(0, 0, 0, 0);
2361
+ continue;
2362
+ }
2363
+ if (!doms.includes(dom) || !dows.includes(dow)) {
2364
+ candidate.setDate(candidate.getDate() + 1);
2365
+ candidate.setHours(0, 0, 0, 0);
2366
+ continue;
2367
+ }
2368
+ if (!hours.includes(hour)) {
2369
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
2370
+ continue;
2371
+ }
2372
+ if (!minutes.includes(minute)) {
2373
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
2374
+ continue;
2375
+ }
2376
+ return new Date(candidate);
2377
+ }
2378
+ return null;
2379
+ }
2380
+ function addSchedule(skill, cron, options = {}) {
2381
+ const { valid, error } = validateCron(cron);
2382
+ if (!valid)
2383
+ return { schedule: null, error };
2384
+ const data = loadSchedules(options.targetDir);
2385
+ const id = `${skill}-${Date.now()}`;
2386
+ const now = new Date;
2387
+ const nextRun = getNextRun(cron, now);
2388
+ const schedule = {
2389
+ id,
2390
+ name: options.name || `${skill} (${cron})`,
2391
+ skill,
2392
+ cron,
2393
+ args: options.args,
2394
+ enabled: true,
2395
+ createdAt: now.toISOString(),
2396
+ nextRun: nextRun?.toISOString()
2397
+ };
2398
+ data.schedules.push(schedule);
2399
+ saveSchedules(data, options.targetDir);
2400
+ return { schedule };
2401
+ }
2402
+ function listSchedules(targetDir) {
2403
+ return loadSchedules(targetDir).schedules;
2404
+ }
2405
+ function removeSchedule(idOrName, targetDir) {
2406
+ const data = loadSchedules(targetDir);
2407
+ const before = data.schedules.length;
2408
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
2409
+ if (data.schedules.length === before)
2410
+ return false;
2411
+ saveSchedules(data, targetDir);
2412
+ return true;
2413
+ }
2414
+ function setScheduleEnabled(idOrName, enabled, targetDir) {
2415
+ const data = loadSchedules(targetDir);
2416
+ const schedule = data.schedules.find((s) => s.id === idOrName || s.name === idOrName);
2417
+ if (!schedule)
2418
+ return false;
2419
+ schedule.enabled = enabled;
2420
+ if (enabled) {
2421
+ schedule.nextRun = getNextRun(schedule.cron)?.toISOString();
2422
+ }
2423
+ saveSchedules(data, targetDir);
2424
+ return true;
2425
+ }
2426
+ function getDueSchedules(targetDir) {
2427
+ const now = new Date;
2428
+ return listSchedules(targetDir).filter((s) => s.enabled && s.nextRun && new Date(s.nextRun) <= now);
2429
+ }
2430
+ function recordScheduleRun(id, status, targetDir) {
2431
+ const data = loadSchedules(targetDir);
2432
+ const schedule = data.schedules.find((s) => s.id === id);
2433
+ if (!schedule)
2434
+ return;
2435
+ const now = new Date;
2436
+ schedule.lastRun = now.toISOString();
2437
+ schedule.lastRunStatus = status;
2438
+ schedule.nextRun = getNextRun(schedule.cron, now)?.toISOString();
2439
+ saveSchedules(data, targetDir);
2440
+ }
2181
2441
  export {
2442
+ validateCron,
2182
2443
  skillExists,
2444
+ setScheduleEnabled,
2183
2445
  searchSkills,
2184
2446
  saveConfig,
2185
2447
  runSkill,
2186
2448
  removeSkillForAgent,
2187
2449
  removeSkill,
2450
+ removeSchedule,
2451
+ recordScheduleRun,
2452
+ loadRegistry,
2188
2453
  loadConfig,
2454
+ listSchedules,
2189
2455
  installSkills,
2190
2456
  installSkillForAgent,
2191
2457
  installSkill,
@@ -2196,8 +2462,10 @@ export {
2196
2462
  getSkillDocs,
2197
2463
  getSkillBestDoc,
2198
2464
  getSkill,
2465
+ getNextRun,
2199
2466
  getInstalledSkills,
2200
2467
  getInstallMeta,
2468
+ getDueSchedules,
2201
2469
  getDisabledSkills,
2202
2470
  getConfigPath,
2203
2471
  getAllTags,
@@ -2208,6 +2476,8 @@ export {
2208
2476
  findSimilarSkills,
2209
2477
  enableSkill,
2210
2478
  disableSkill,
2479
+ clearRegistryCache,
2480
+ addSchedule,
2211
2481
  SKILLS,
2212
2482
  CATEGORIES,
2213
2483
  AGENT_TARGETS