@hasna/skills 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,94 @@ 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 globalCustomNew = discoverSkillsInDir(join(homedir(), ".hasna", "skills", "custom"));
1528
+ const globalCustomOld = discoverSkillsInDir(join(homedir(), ".skills"));
1529
+ const oldNames = new Set(globalCustomNew.map((s) => s.name));
1530
+ const globalCustom = [...globalCustomNew, ...globalCustomOld.filter((s) => !oldNames.has(s.name))];
1531
+ const projectCustom = discoverSkillsInDir(join(cwd || process.cwd(), ".skills", "custom-skills"));
1532
+ const customNames = new Set([...globalCustom, ...projectCustom].map((s) => s.name));
1533
+ const filtered = official.filter((s) => !customNames.has(s.name));
1534
+ _registryCache = [...filtered, ...globalCustom, ...projectCustom];
1535
+ _registryCacheTime = now;
1536
+ return _registryCache;
1537
+ }
1538
+ function clearRegistryCache() {
1539
+ _registryCache = null;
1540
+ _registryCacheTime = 0;
1541
+ }
1439
1542
  function getSkillsByCategory(category) {
1440
- return SKILLS.filter((s) => s.category === category);
1543
+ return loadRegistry().filter((s) => s.category === category);
1441
1544
  }
1442
1545
  function editDistance(a, b) {
1443
1546
  if (a === b)
@@ -1483,7 +1586,7 @@ function searchSkills(query) {
1483
1586
  if (words.length === 0)
1484
1587
  return [];
1485
1588
  const scored = [];
1486
- for (const skill of SKILLS) {
1589
+ for (const skill of loadRegistry()) {
1487
1590
  const nameLower = skill.name.toLowerCase();
1488
1591
  const displayNameLower = skill.displayName.toLowerCase();
1489
1592
  const descriptionLower = skill.description.toLowerCase();
@@ -1519,15 +1622,15 @@ function searchSkills(query) {
1519
1622
  return scored.map((s) => s.skill);
1520
1623
  }
1521
1624
  function getSkill(name) {
1522
- return SKILLS.find((s) => s.name === name);
1625
+ return loadRegistry().find((s) => s.name === name);
1523
1626
  }
1524
1627
  function getSkillsByTag(tag) {
1525
1628
  const needle = tag.toLowerCase();
1526
- return SKILLS.filter((s) => s.tags.some((t) => t.toLowerCase().includes(needle)));
1629
+ return loadRegistry().filter((s) => s.tags.some((t) => t.toLowerCase().includes(needle)));
1527
1630
  }
1528
1631
  function getAllTags() {
1529
1632
  const tagSet = new Set;
1530
- for (const skill of SKILLS) {
1633
+ for (const skill of loadRegistry()) {
1531
1634
  for (const tag of skill.tags) {
1532
1635
  tagSet.add(tag.toLowerCase());
1533
1636
  }
@@ -1546,13 +1649,13 @@ function levenshtein(a, b) {
1546
1649
  }
1547
1650
  function findSimilarSkills(query, maxResults = 3) {
1548
1651
  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);
1652
+ 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
1653
  return scored.slice(0, maxResults).map((s) => s.name);
1551
1654
  }
1552
1655
  // 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";
1656
+ import { existsSync as existsSync2, cpSync, mkdirSync, writeFileSync, rmSync, readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, accessSync, constants } from "fs";
1657
+ import { join as join2, dirname } from "path";
1658
+ import { homedir as homedir2 } from "os";
1556
1659
  import { fileURLToPath } from "url";
1557
1660
 
1558
1661
  // src/lib/utils.ts
@@ -1565,36 +1668,36 @@ var __dirname2 = dirname(fileURLToPath(import.meta.url));
1565
1668
  function findSkillsDir() {
1566
1669
  let dir = __dirname2;
1567
1670
  for (let i = 0;i < 5; i++) {
1568
- const candidate = join(dir, "skills");
1569
- if (existsSync(candidate)) {
1671
+ const candidate = join2(dir, "skills");
1672
+ if (existsSync2(candidate) && !dir.includes(".skills")) {
1570
1673
  return candidate;
1571
1674
  }
1572
1675
  dir = dirname(dir);
1573
1676
  }
1574
- return join(__dirname2, "..", "skills");
1677
+ return join2(__dirname2, "..", "skills");
1575
1678
  }
1576
1679
  var SKILLS_DIR = findSkillsDir();
1577
1680
  function getSkillPath(name) {
1578
1681
  const skillName = normalizeSkillName(name);
1579
- return join(SKILLS_DIR, skillName);
1682
+ return join2(SKILLS_DIR, skillName);
1580
1683
  }
1581
1684
  function skillExists(name) {
1582
- return existsSync(getSkillPath(name));
1685
+ return existsSync2(getSkillPath(name));
1583
1686
  }
1584
1687
  function installSkill(name, options = {}) {
1585
1688
  const { targetDir = process.cwd(), overwrite = false } = options;
1586
1689
  const skillName = normalizeSkillName(name);
1587
1690
  const sourcePath = getSkillPath(name);
1588
- const destDir = join(targetDir, ".skills");
1589
- const destPath = join(destDir, skillName);
1590
- if (!existsSync(sourcePath)) {
1691
+ const destDir = join2(targetDir, ".skills");
1692
+ const destPath = join2(destDir, skillName);
1693
+ if (!existsSync2(sourcePath)) {
1591
1694
  return {
1592
1695
  skill: name,
1593
1696
  success: false,
1594
1697
  error: `Skill '${name}' not found`
1595
1698
  };
1596
1699
  }
1597
- if (existsSync(destPath) && !overwrite) {
1700
+ if (existsSync2(destPath) && !overwrite) {
1598
1701
  return {
1599
1702
  skill: name,
1600
1703
  success: false,
@@ -1603,10 +1706,10 @@ function installSkill(name, options = {}) {
1603
1706
  };
1604
1707
  }
1605
1708
  try {
1606
- if (!existsSync(destDir)) {
1709
+ if (!existsSync2(destDir)) {
1607
1710
  mkdirSync(destDir, { recursive: true });
1608
1711
  }
1609
- if (existsSync(destPath) && overwrite) {
1712
+ if (existsSync2(destPath) && overwrite) {
1610
1713
  rmSync(destPath, { recursive: true, force: true });
1611
1714
  }
1612
1715
  cpSync(sourcePath, destPath, {
@@ -1645,10 +1748,10 @@ function installSkills(names, options = {}) {
1645
1748
  return names.map((name) => installSkill(name, options));
1646
1749
  }
1647
1750
  function updateSkillsIndex(skillsDir) {
1648
- const indexPath = join(skillsDir, "index.ts");
1751
+ const indexPath = join2(skillsDir, "index.ts");
1649
1752
  const meta = loadMeta(skillsDir);
1650
1753
  const disabledSet = new Set(meta.disabled || []);
1651
- const skills = readdirSync(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
1754
+ const skills = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
1652
1755
  const exports = skills.map((s) => {
1653
1756
  const name = s.replace("skill-", "").replace(/-/g, "_");
1654
1757
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -1664,13 +1767,13 @@ ${exports}
1664
1767
  writeFileSync(indexPath, content);
1665
1768
  }
1666
1769
  function getMetaPath(skillsDir) {
1667
- return join(skillsDir, ".meta.json");
1770
+ return join2(skillsDir, ".meta.json");
1668
1771
  }
1669
1772
  function loadMeta(skillsDir) {
1670
1773
  const metaPath = getMetaPath(skillsDir);
1671
- if (existsSync(metaPath)) {
1774
+ if (existsSync2(metaPath)) {
1672
1775
  try {
1673
- return JSON.parse(readFileSync(metaPath, "utf-8"));
1776
+ return JSON.parse(readFileSync2(metaPath, "utf-8"));
1674
1777
  } catch {}
1675
1778
  }
1676
1779
  return { skills: {} };
@@ -1683,9 +1786,9 @@ function recordInstall(skillsDir, name) {
1683
1786
  const skillName = normalizeSkillName(name);
1684
1787
  let version = "unknown";
1685
1788
  try {
1686
- const pkgPath = join(skillsDir, skillName, "package.json");
1687
- if (existsSync(pkgPath)) {
1688
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1789
+ const pkgPath = join2(skillsDir, skillName, "package.json");
1790
+ if (existsSync2(pkgPath)) {
1791
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1689
1792
  version = pkg.version || "unknown";
1690
1793
  }
1691
1794
  } catch {}
@@ -1698,12 +1801,12 @@ function recordRemove(skillsDir, name) {
1698
1801
  saveMeta(skillsDir, meta);
1699
1802
  }
1700
1803
  function getInstallMeta(targetDir = process.cwd()) {
1701
- return loadMeta(join(targetDir, ".skills"));
1804
+ return loadMeta(join2(targetDir, ".skills"));
1702
1805
  }
1703
1806
  function disableSkill(name, targetDir = process.cwd()) {
1704
- const skillsDir = join(targetDir, ".skills");
1807
+ const skillsDir = join2(targetDir, ".skills");
1705
1808
  const skillName = normalizeSkillName(name);
1706
- if (!existsSync(join(skillsDir, skillName)))
1809
+ if (!existsSync2(join2(skillsDir, skillName)))
1707
1810
  return false;
1708
1811
  const meta = loadMeta(skillsDir);
1709
1812
  const disabled = new Set(meta.disabled || []);
@@ -1716,7 +1819,7 @@ function disableSkill(name, targetDir = process.cwd()) {
1716
1819
  return true;
1717
1820
  }
1718
1821
  function enableSkill(name, targetDir = process.cwd()) {
1719
- const skillsDir = join(targetDir, ".skills");
1822
+ const skillsDir = join2(targetDir, ".skills");
1720
1823
  const meta = loadMeta(skillsDir);
1721
1824
  const disabled = new Set(meta.disabled || []);
1722
1825
  if (!disabled.has(name))
@@ -1728,24 +1831,24 @@ function enableSkill(name, targetDir = process.cwd()) {
1728
1831
  return true;
1729
1832
  }
1730
1833
  function getDisabledSkills(targetDir = process.cwd()) {
1731
- const meta = loadMeta(join(targetDir, ".skills"));
1834
+ const meta = loadMeta(join2(targetDir, ".skills"));
1732
1835
  return meta.disabled || [];
1733
1836
  }
1734
1837
  function getInstalledSkills(targetDir = process.cwd()) {
1735
- const skillsDir = join(targetDir, ".skills");
1736
- if (!existsSync(skillsDir)) {
1838
+ const skillsDir = join2(targetDir, ".skills");
1839
+ if (!existsSync2(skillsDir)) {
1737
1840
  return [];
1738
1841
  }
1739
- return readdirSync(skillsDir).filter((f) => {
1740
- const fullPath = join(skillsDir, f);
1842
+ return readdirSync2(skillsDir).filter((f) => {
1843
+ const fullPath = join2(skillsDir, f);
1741
1844
  return f.startsWith("skill-") && statSync(fullPath).isDirectory();
1742
1845
  }).map((f) => f.replace("skill-", ""));
1743
1846
  }
1744
1847
  function removeSkill(name, targetDir = process.cwd()) {
1745
1848
  const skillName = normalizeSkillName(name);
1746
- const skillsDir = join(targetDir, ".skills");
1747
- const skillPath = join(skillsDir, skillName);
1748
- if (!existsSync(skillPath)) {
1849
+ const skillsDir = join2(targetDir, ".skills");
1850
+ const skillPath = join2(skillsDir, skillName);
1851
+ if (!existsSync2(skillPath)) {
1749
1852
  return false;
1750
1853
  }
1751
1854
  rmSync(skillPath, { recursive: true, force: true });
@@ -1753,29 +1856,40 @@ function removeSkill(name, targetDir = process.cwd()) {
1753
1856
  recordRemove(skillsDir, name);
1754
1857
  return true;
1755
1858
  }
1756
- var AGENT_TARGETS = ["claude", "codex", "gemini"];
1859
+ var AGENT_TARGETS = ["claude", "codex", "gemini", "pi", "opencode"];
1860
+ var AGENT_LABELS = {
1861
+ claude: "Claude Code",
1862
+ codex: "Codex CLI",
1863
+ gemini: "Gemini CLI",
1864
+ pi: "pi.dev",
1865
+ opencode: "OpenCode"
1866
+ };
1757
1867
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
1758
- const agentDir = `.${agent}`;
1759
- if (scope === "project") {
1760
- return join(projectDir || process.cwd(), agentDir, "skills");
1868
+ const base = projectDir || process.cwd();
1869
+ switch (agent) {
1870
+ case "pi":
1871
+ return scope === "project" ? join2(base, ".pi", "skills") : join2(homedir2(), ".pi", "agent", "skills");
1872
+ case "opencode":
1873
+ return scope === "project" ? join2(base, ".opencode", "skills") : join2(homedir2(), ".opencode", "skills");
1874
+ default:
1875
+ return scope === "project" ? join2(base, `.${agent}`, "skills") : join2(homedir2(), `.${agent}`, "skills");
1761
1876
  }
1762
- return join(homedir(), agentDir, "skills");
1763
1877
  }
1764
1878
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
1765
1879
  const skillName = normalizeSkillName(name);
1766
- return join(getAgentSkillsDir(agent, scope, projectDir), skillName);
1880
+ return join2(getAgentSkillsDir(agent, scope, projectDir), skillName);
1767
1881
  }
1768
1882
  function installSkillForAgent(name, options, generateSkillMd) {
1769
1883
  const { agent, scope = "global", projectDir } = options;
1770
1884
  const skillName = normalizeSkillName(name);
1771
1885
  const sourcePath = getSkillPath(name);
1772
- if (!existsSync(sourcePath)) {
1886
+ if (!existsSync2(sourcePath)) {
1773
1887
  return { skill: name, success: false, error: `Skill '${name}' not found` };
1774
1888
  }
1775
1889
  let skillMdContent = null;
1776
- const skillMdPath = join(sourcePath, "SKILL.md");
1777
- if (existsSync(skillMdPath)) {
1778
- skillMdContent = readFileSync(skillMdPath, "utf-8");
1890
+ const skillMdPath = join2(sourcePath, "SKILL.md");
1891
+ if (existsSync2(skillMdPath)) {
1892
+ skillMdContent = readFileSync2(skillMdPath, "utf-8");
1779
1893
  } else if (generateSkillMd) {
1780
1894
  skillMdContent = generateSkillMd(name);
1781
1895
  }
@@ -1784,17 +1898,12 @@ function installSkillForAgent(name, options, generateSkillMd) {
1784
1898
  }
1785
1899
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
1786
1900
  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
- };
1901
+ const agentBaseDir = agent === "pi" ? join2(homedir2(), ".pi", "agent") : join2(homedir2(), `.${agent}`);
1902
+ if (!existsSync2(agentBaseDir)) {
1794
1903
  return {
1795
1904
  skill: name,
1796
1905
  success: false,
1797
- error: `Agent directory ${agentBaseDir} does not exist. Is ${agentLabels[agent]} installed?`
1906
+ error: `Agent directory ${agentBaseDir} does not exist. Is ${AGENT_LABELS[agent]} installed?`
1798
1907
  };
1799
1908
  }
1800
1909
  try {
@@ -1809,7 +1918,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
1809
1918
  }
1810
1919
  try {
1811
1920
  mkdirSync(destDir, { recursive: true });
1812
- writeFileSync(join(destDir, "SKILL.md"), skillMdContent);
1921
+ writeFileSync(join2(destDir, "SKILL.md"), skillMdContent);
1813
1922
  return { skill: name, success: true, path: destDir };
1814
1923
  } catch (error) {
1815
1924
  return {
@@ -1822,23 +1931,23 @@ function installSkillForAgent(name, options, generateSkillMd) {
1822
1931
  function removeSkillForAgent(name, options) {
1823
1932
  const { agent, scope = "global", projectDir } = options;
1824
1933
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
1825
- if (!existsSync(destDir)) {
1934
+ if (!existsSync2(destDir)) {
1826
1935
  return false;
1827
1936
  }
1828
1937
  rmSync(destDir, { recursive: true, force: true });
1829
1938
  return true;
1830
1939
  }
1831
1940
  // src/lib/skillinfo.ts
1832
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1833
- import { join as join2 } from "path";
1941
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "fs";
1942
+ import { join as join3 } from "path";
1834
1943
  function getSkillDocs(name) {
1835
1944
  const skillPath = getSkillPath(name);
1836
- if (!existsSync2(skillPath))
1945
+ if (!existsSync3(skillPath))
1837
1946
  return null;
1838
1947
  return {
1839
- skillMd: readIfExists(join2(skillPath, "SKILL.md")),
1840
- readme: readIfExists(join2(skillPath, "README.md")),
1841
- claudeMd: readIfExists(join2(skillPath, "CLAUDE.md"))
1948
+ skillMd: readIfExists(join3(skillPath, "SKILL.md")),
1949
+ readme: readIfExists(join3(skillPath, "README.md")),
1950
+ claudeMd: readIfExists(join3(skillPath, "CLAUDE.md"))
1842
1951
  };
1843
1952
  }
1844
1953
  function getSkillBestDoc(name) {
@@ -1849,11 +1958,11 @@ function getSkillBestDoc(name) {
1849
1958
  }
1850
1959
  function getSkillRequirements(name) {
1851
1960
  const skillPath = getSkillPath(name);
1852
- if (!existsSync2(skillPath))
1961
+ if (!existsSync3(skillPath))
1853
1962
  return null;
1854
1963
  const texts = [];
1855
1964
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example", ".env.local.example"]) {
1856
- const content = readIfExists(join2(skillPath, file));
1965
+ const content = readIfExists(join3(skillPath, file));
1857
1966
  if (content)
1858
1967
  texts.push(content);
1859
1968
  }
@@ -1880,10 +1989,10 @@ function getSkillRequirements(name) {
1880
1989
  }
1881
1990
  let cliCommand = null;
1882
1991
  let dependencies = {};
1883
- const pkgPath = join2(skillPath, "package.json");
1884
- if (existsSync2(pkgPath)) {
1992
+ const pkgPath = join3(skillPath, "package.json");
1993
+ if (existsSync3(pkgPath)) {
1885
1994
  try {
1886
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
1995
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1887
1996
  if (pkg.bin) {
1888
1997
  const binKeys = Object.keys(pkg.bin);
1889
1998
  if (binKeys.length > 0)
@@ -1903,25 +2012,25 @@ async function runSkill(name, args, options = {}) {
1903
2012
  const skillName = normalizeSkillName(name);
1904
2013
  let skillPath;
1905
2014
  if (options.installed) {
1906
- skillPath = join2(process.cwd(), ".skills", skillName);
2015
+ skillPath = join3(process.cwd(), ".skills", skillName);
1907
2016
  } else {
1908
- const installedPath = join2(process.cwd(), ".skills", skillName);
1909
- if (existsSync2(installedPath)) {
2017
+ const installedPath = join3(process.cwd(), ".skills", skillName);
2018
+ if (existsSync3(installedPath)) {
1910
2019
  skillPath = installedPath;
1911
2020
  } else {
1912
2021
  skillPath = getSkillPath(name);
1913
2022
  }
1914
2023
  }
1915
- if (!existsSync2(skillPath)) {
2024
+ if (!existsSync3(skillPath)) {
1916
2025
  return { exitCode: 1, error: `Skill '${name}' not found` };
1917
2026
  }
1918
- const pkgPath = join2(skillPath, "package.json");
1919
- if (!existsSync2(pkgPath)) {
2027
+ const pkgPath = join3(skillPath, "package.json");
2028
+ if (!existsSync3(pkgPath)) {
1920
2029
  return { exitCode: 1, error: `No package.json in skill '${name}'` };
1921
2030
  }
1922
2031
  let entryPoint;
1923
2032
  try {
1924
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
2033
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1925
2034
  if (pkg.bin) {
1926
2035
  const binValues = Object.values(pkg.bin);
1927
2036
  entryPoint = binValues[0];
@@ -1935,12 +2044,12 @@ async function runSkill(name, args, options = {}) {
1935
2044
  } catch {
1936
2045
  return { exitCode: 1, error: `Failed to parse package.json for skill '${name}'` };
1937
2046
  }
1938
- const entryPath = join2(skillPath, entryPoint);
1939
- if (!existsSync2(entryPath)) {
2047
+ const entryPath = join3(skillPath, entryPoint);
2048
+ if (!existsSync3(entryPath)) {
1940
2049
  return { exitCode: 1, error: `Entry point '${entryPoint}' not found in skill '${name}'` };
1941
2050
  }
1942
- const nodeModules = join2(skillPath, "node_modules");
1943
- if (!existsSync2(nodeModules)) {
2051
+ const nodeModules = join3(skillPath, "node_modules");
2052
+ if (!existsSync3(nodeModules)) {
1944
2053
  const install = Bun.spawn(["bun", "install", "--no-save"], {
1945
2054
  cwd: skillPath,
1946
2055
  stdout: "pipe",
@@ -1958,17 +2067,17 @@ async function runSkill(name, args, options = {}) {
1958
2067
  return { exitCode };
1959
2068
  }
1960
2069
  function generateEnvExample(targetDir = process.cwd()) {
1961
- const skillsDir = join2(targetDir, ".skills");
1962
- if (!existsSync2(skillsDir))
2070
+ const skillsDir = join3(targetDir, ".skills");
2071
+ if (!existsSync3(skillsDir))
1963
2072
  return "";
1964
- const dirs = readdirSync2(skillsDir).filter((f) => f.startsWith("skill-") && existsSync2(join2(skillsDir, f, "package.json")));
2073
+ const dirs = readdirSync3(skillsDir).filter((f) => f.startsWith("skill-") && existsSync3(join3(skillsDir, f, "package.json")));
1965
2074
  const envMap = new Map;
1966
2075
  for (const dir of dirs) {
1967
2076
  const skillName = dir.replace("skill-", "");
1968
- const skillPath = join2(skillsDir, dir);
2077
+ const skillPath = join3(skillsDir, dir);
1969
2078
  const texts = [];
1970
2079
  for (const file of ["SKILL.md", "README.md", "CLAUDE.md", ".env.example"]) {
1971
- const content = readIfExists(join2(skillPath, file));
2080
+ const content = readIfExists(join3(skillPath, file));
1972
2081
  if (content)
1973
2082
  texts.push(content);
1974
2083
  }
@@ -2013,7 +2122,7 @@ function generateSkillMd(name) {
2013
2122
  if (!meta)
2014
2123
  return null;
2015
2124
  const skillPath = getSkillPath(name);
2016
- if (!existsSync2(skillPath))
2125
+ if (!existsSync3(skillPath))
2017
2126
  return null;
2018
2127
  const frontmatter = [
2019
2128
  "---",
@@ -2022,13 +2131,13 @@ function generateSkillMd(name) {
2022
2131
  "---"
2023
2132
  ].join(`
2024
2133
  `);
2025
- const readme = readIfExists(join2(skillPath, "README.md"));
2026
- const claudeMd = readIfExists(join2(skillPath, "CLAUDE.md"));
2134
+ const readme = readIfExists(join3(skillPath, "README.md"));
2135
+ const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
2027
2136
  let cliCommand = null;
2028
- const pkgPath = join2(skillPath, "package.json");
2029
- if (existsSync2(pkgPath)) {
2137
+ const pkgPath = join3(skillPath, "package.json");
2138
+ if (existsSync3(pkgPath)) {
2030
2139
  try {
2031
- const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
2140
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
2032
2141
  if (pkg.bin) {
2033
2142
  const binKeys = Object.keys(pkg.bin);
2034
2143
  if (binKeys.length > 0)
@@ -2103,32 +2212,45 @@ function extractEnvVars(text) {
2103
2212
  }
2104
2213
  function readIfExists(path) {
2105
2214
  try {
2106
- if (existsSync2(path)) {
2107
- return readFileSync2(path, "utf-8");
2215
+ if (existsSync3(path)) {
2216
+ return readFileSync3(path, "utf-8");
2108
2217
  }
2109
2218
  } catch {}
2110
2219
  return null;
2111
2220
  }
2112
2221
  // 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";
2222
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
2223
+ import { join as join4, dirname as dirname2 } from "path";
2224
+ import { homedir as homedir3 } from "os";
2116
2225
  var VALID_KEYS = {
2117
2226
  defaultAgent: ["claude", "codex", "gemini", "all"],
2118
2227
  defaultScope: ["global", "project"],
2119
2228
  format: ["compact", "json", "csv"]
2120
2229
  };
2230
+ function getDataDir() {
2231
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
2232
+ const newDir = join4(home, ".hasna", "skills");
2233
+ const oldConfigFile = join4(home, ".skillsrc");
2234
+ if (existsSync4(oldConfigFile) && !existsSync4(join4(newDir, "config.json"))) {
2235
+ mkdirSync2(newDir, { recursive: true });
2236
+ try {
2237
+ copyFileSync(oldConfigFile, join4(newDir, "config.json"));
2238
+ } catch {}
2239
+ }
2240
+ mkdirSync2(newDir, { recursive: true });
2241
+ return newDir;
2242
+ }
2121
2243
  function getConfigPath(scope) {
2122
2244
  if (scope === "global") {
2123
- return join3(homedir2(), ".skillsrc");
2245
+ return join4(getDataDir(), "config.json");
2124
2246
  }
2125
- return join3(process.cwd(), "skills.config.json");
2247
+ return join4(process.cwd(), "skills.config.json");
2126
2248
  }
2127
2249
  function readConfigFile(path) {
2128
- if (!existsSync3(path))
2250
+ if (!existsSync4(path))
2129
2251
  return {};
2130
2252
  try {
2131
- const raw = readFileSync3(path, "utf-8");
2253
+ const raw = readFileSync4(path, "utf-8");
2132
2254
  const parsed = JSON.parse(raw);
2133
2255
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2134
2256
  return {};
@@ -2159,9 +2281,9 @@ function saveConfig(key, value, scope = "project") {
2159
2281
  }
2160
2282
  const filePath = getConfigPath(scope);
2161
2283
  let existing = {};
2162
- if (existsSync3(filePath)) {
2284
+ if (existsSync4(filePath)) {
2163
2285
  try {
2164
- existing = JSON.parse(readFileSync3(filePath, "utf-8"));
2286
+ existing = JSON.parse(readFileSync4(filePath, "utf-8"));
2165
2287
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
2166
2288
  existing = {};
2167
2289
  }
@@ -2170,7 +2292,7 @@ function saveConfig(key, value, scope = "project") {
2170
2292
  }
2171
2293
  } else {
2172
2294
  const dir = dirname2(filePath);
2173
- if (!existsSync3(dir)) {
2295
+ if (!existsSync4(dir)) {
2174
2296
  mkdirSync2(dir, { recursive: true });
2175
2297
  }
2176
2298
  }
@@ -2178,14 +2300,174 @@ function saveConfig(key, value, scope = "project") {
2178
2300
  writeFileSync2(filePath, JSON.stringify(existing, null, 2) + `
2179
2301
  `);
2180
2302
  }
2303
+ // src/lib/scheduler.ts
2304
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
2305
+ import { join as join5 } from "path";
2306
+ function getSchedulesPath(targetDir = process.cwd()) {
2307
+ return join5(targetDir, ".skills", "schedules.json");
2308
+ }
2309
+ function loadSchedules(targetDir = process.cwd()) {
2310
+ const path = getSchedulesPath(targetDir);
2311
+ if (existsSync5(path)) {
2312
+ try {
2313
+ return JSON.parse(readFileSync5(path, "utf-8"));
2314
+ } catch {}
2315
+ }
2316
+ return { version: 1, schedules: [] };
2317
+ }
2318
+ function saveSchedules(data, targetDir = process.cwd()) {
2319
+ const path = getSchedulesPath(targetDir);
2320
+ const dir = join5(targetDir, ".skills");
2321
+ if (!existsSync5(dir))
2322
+ mkdirSync3(dir, { recursive: true });
2323
+ writeFileSync3(path, JSON.stringify(data, null, 2));
2324
+ }
2325
+ function validateCron(expr) {
2326
+ const fields = expr.trim().split(/\s+/);
2327
+ if (fields.length !== 5) {
2328
+ return { valid: false, error: `Expected 5 fields, got ${fields.length}. Format: "minute hour day-of-month month day-of-week"` };
2329
+ }
2330
+ return { valid: true };
2331
+ }
2332
+ function getNextRun(cron, from = new Date) {
2333
+ const { valid } = validateCron(cron);
2334
+ if (!valid)
2335
+ return null;
2336
+ const [minuteF, hourF, domF, monthF, dowF] = cron.trim().split(/\s+/);
2337
+ function parseField(f, min, max) {
2338
+ if (f === "*")
2339
+ return Array.from({ length: max - min + 1 }, (_, i) => i + min);
2340
+ if (f.startsWith("*/")) {
2341
+ const step = parseInt(f.slice(2));
2342
+ if (isNaN(step))
2343
+ return [];
2344
+ const vals = [];
2345
+ for (let i = min;i <= max; i += step)
2346
+ vals.push(i);
2347
+ return vals;
2348
+ }
2349
+ return f.split(",").flatMap((part) => {
2350
+ if (part.includes("-")) {
2351
+ const [lo, hi] = part.split("-").map(Number);
2352
+ return Array.from({ length: hi - lo + 1 }, (_, i) => i + lo);
2353
+ }
2354
+ const n = parseInt(part);
2355
+ return isNaN(n) ? [] : [n];
2356
+ });
2357
+ }
2358
+ const minutes = parseField(minuteF, 0, 59);
2359
+ const hours = parseField(hourF, 0, 23);
2360
+ const doms = parseField(domF, 1, 31);
2361
+ const months = parseField(monthF, 1, 12);
2362
+ const dows = parseField(dowF, 0, 6);
2363
+ const candidate = new Date(from);
2364
+ candidate.setSeconds(0, 0);
2365
+ candidate.setMinutes(candidate.getMinutes() + 1);
2366
+ const limit = new Date(from);
2367
+ limit.setFullYear(limit.getFullYear() + 1);
2368
+ while (candidate < limit) {
2369
+ const month = candidate.getMonth() + 1;
2370
+ const dom = candidate.getDate();
2371
+ const dow = candidate.getDay();
2372
+ const hour = candidate.getHours();
2373
+ const minute = candidate.getMinutes();
2374
+ if (!months.includes(month)) {
2375
+ candidate.setMonth(candidate.getMonth() + 1, 1);
2376
+ candidate.setHours(0, 0, 0, 0);
2377
+ continue;
2378
+ }
2379
+ if (!doms.includes(dom) || !dows.includes(dow)) {
2380
+ candidate.setDate(candidate.getDate() + 1);
2381
+ candidate.setHours(0, 0, 0, 0);
2382
+ continue;
2383
+ }
2384
+ if (!hours.includes(hour)) {
2385
+ candidate.setHours(candidate.getHours() + 1, 0, 0, 0);
2386
+ continue;
2387
+ }
2388
+ if (!minutes.includes(minute)) {
2389
+ candidate.setMinutes(candidate.getMinutes() + 1, 0, 0);
2390
+ continue;
2391
+ }
2392
+ return new Date(candidate);
2393
+ }
2394
+ return null;
2395
+ }
2396
+ function addSchedule(skill, cron, options = {}) {
2397
+ const { valid, error } = validateCron(cron);
2398
+ if (!valid)
2399
+ return { schedule: null, error };
2400
+ const data = loadSchedules(options.targetDir);
2401
+ const id = `${skill}-${Date.now()}`;
2402
+ const now = new Date;
2403
+ const nextRun = getNextRun(cron, now);
2404
+ const schedule = {
2405
+ id,
2406
+ name: options.name || `${skill} (${cron})`,
2407
+ skill,
2408
+ cron,
2409
+ args: options.args,
2410
+ enabled: true,
2411
+ createdAt: now.toISOString(),
2412
+ nextRun: nextRun?.toISOString()
2413
+ };
2414
+ data.schedules.push(schedule);
2415
+ saveSchedules(data, options.targetDir);
2416
+ return { schedule };
2417
+ }
2418
+ function listSchedules(targetDir) {
2419
+ return loadSchedules(targetDir).schedules;
2420
+ }
2421
+ function removeSchedule(idOrName, targetDir) {
2422
+ const data = loadSchedules(targetDir);
2423
+ const before = data.schedules.length;
2424
+ data.schedules = data.schedules.filter((s) => s.id !== idOrName && s.name !== idOrName);
2425
+ if (data.schedules.length === before)
2426
+ return false;
2427
+ saveSchedules(data, targetDir);
2428
+ return true;
2429
+ }
2430
+ function setScheduleEnabled(idOrName, enabled, targetDir) {
2431
+ const data = loadSchedules(targetDir);
2432
+ const schedule = data.schedules.find((s) => s.id === idOrName || s.name === idOrName);
2433
+ if (!schedule)
2434
+ return false;
2435
+ schedule.enabled = enabled;
2436
+ if (enabled) {
2437
+ schedule.nextRun = getNextRun(schedule.cron)?.toISOString();
2438
+ }
2439
+ saveSchedules(data, targetDir);
2440
+ return true;
2441
+ }
2442
+ function getDueSchedules(targetDir) {
2443
+ const now = new Date;
2444
+ return listSchedules(targetDir).filter((s) => s.enabled && s.nextRun && new Date(s.nextRun) <= now);
2445
+ }
2446
+ function recordScheduleRun(id, status, targetDir) {
2447
+ const data = loadSchedules(targetDir);
2448
+ const schedule = data.schedules.find((s) => s.id === id);
2449
+ if (!schedule)
2450
+ return;
2451
+ const now = new Date;
2452
+ schedule.lastRun = now.toISOString();
2453
+ schedule.lastRunStatus = status;
2454
+ schedule.nextRun = getNextRun(schedule.cron, now)?.toISOString();
2455
+ saveSchedules(data, targetDir);
2456
+ }
2181
2457
  export {
2458
+ validateCron,
2182
2459
  skillExists,
2460
+ setScheduleEnabled,
2183
2461
  searchSkills,
2184
2462
  saveConfig,
2185
2463
  runSkill,
2186
2464
  removeSkillForAgent,
2187
2465
  removeSkill,
2466
+ removeSchedule,
2467
+ recordScheduleRun,
2468
+ loadRegistry,
2188
2469
  loadConfig,
2470
+ listSchedules,
2189
2471
  installSkills,
2190
2472
  installSkillForAgent,
2191
2473
  installSkill,
@@ -2196,8 +2478,10 @@ export {
2196
2478
  getSkillDocs,
2197
2479
  getSkillBestDoc,
2198
2480
  getSkill,
2481
+ getNextRun,
2199
2482
  getInstalledSkills,
2200
2483
  getInstallMeta,
2484
+ getDueSchedules,
2201
2485
  getDisabledSkills,
2202
2486
  getConfigPath,
2203
2487
  getAllTags,
@@ -2208,6 +2492,8 @@ export {
2208
2492
  findSimilarSkills,
2209
2493
  enableSkill,
2210
2494
  disableSkill,
2495
+ clearRegistryCache,
2496
+ addSchedule,
2211
2497
  SKILLS,
2212
2498
  CATEGORIES,
2213
2499
  AGENT_TARGETS