@hasna/skills 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1212 -220
- package/bin/mcp.js +516 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +391 -105
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/installer.d.ts +11 -2
- package/dist/lib/registry.d.ts +13 -0
- package/dist/lib/scheduler.d.ts +47 -0
- package/package.json +2 -1
- package/skills/_common/index.ts +4 -0
- package/skills/_common/vision.ts +374 -0
- package/skills/skill-colorextract/SKILL.md +35 -0
- package/skills/skill-colorextract/bun.lock +102 -0
- package/skills/skill-colorextract/package.json +13 -0
- package/skills/skill-colorextract/src/index.ts +405 -0
- package/skills/skill-siteanalyze/SKILL.md +25 -0
- package/skills/skill-siteanalyze/package.json +13 -0
- package/skills/skill-siteanalyze/src/index.ts +592 -0
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
|
|
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
|
|
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
|
|
1625
|
+
return loadRegistry().find((s) => s.name === name);
|
|
1523
1626
|
}
|
|
1524
1627
|
function getSkillsByTag(tag) {
|
|
1525
1628
|
const needle = tag.toLowerCase();
|
|
1526
|
-
return
|
|
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
|
|
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 =
|
|
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 =
|
|
1569
|
-
if (
|
|
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
|
|
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
|
|
1682
|
+
return join2(SKILLS_DIR, skillName);
|
|
1580
1683
|
}
|
|
1581
1684
|
function skillExists(name) {
|
|
1582
|
-
return
|
|
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 =
|
|
1589
|
-
const destPath =
|
|
1590
|
-
if (!
|
|
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 (
|
|
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 (!
|
|
1709
|
+
if (!existsSync2(destDir)) {
|
|
1607
1710
|
mkdirSync(destDir, { recursive: true });
|
|
1608
1711
|
}
|
|
1609
|
-
if (
|
|
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 =
|
|
1751
|
+
const indexPath = join2(skillsDir, "index.ts");
|
|
1649
1752
|
const meta = loadMeta(skillsDir);
|
|
1650
1753
|
const disabledSet = new Set(meta.disabled || []);
|
|
1651
|
-
const skills =
|
|
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
|
|
1770
|
+
return join2(skillsDir, ".meta.json");
|
|
1668
1771
|
}
|
|
1669
1772
|
function loadMeta(skillsDir) {
|
|
1670
1773
|
const metaPath = getMetaPath(skillsDir);
|
|
1671
|
-
if (
|
|
1774
|
+
if (existsSync2(metaPath)) {
|
|
1672
1775
|
try {
|
|
1673
|
-
return JSON.parse(
|
|
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 =
|
|
1687
|
-
if (
|
|
1688
|
-
const pkg = JSON.parse(
|
|
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(
|
|
1804
|
+
return loadMeta(join2(targetDir, ".skills"));
|
|
1702
1805
|
}
|
|
1703
1806
|
function disableSkill(name, targetDir = process.cwd()) {
|
|
1704
|
-
const skillsDir =
|
|
1807
|
+
const skillsDir = join2(targetDir, ".skills");
|
|
1705
1808
|
const skillName = normalizeSkillName(name);
|
|
1706
|
-
if (!
|
|
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 =
|
|
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(
|
|
1834
|
+
const meta = loadMeta(join2(targetDir, ".skills"));
|
|
1732
1835
|
return meta.disabled || [];
|
|
1733
1836
|
}
|
|
1734
1837
|
function getInstalledSkills(targetDir = process.cwd()) {
|
|
1735
|
-
const skillsDir =
|
|
1736
|
-
if (!
|
|
1838
|
+
const skillsDir = join2(targetDir, ".skills");
|
|
1839
|
+
if (!existsSync2(skillsDir)) {
|
|
1737
1840
|
return [];
|
|
1738
1841
|
}
|
|
1739
|
-
return
|
|
1740
|
-
const fullPath =
|
|
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 =
|
|
1747
|
-
const skillPath =
|
|
1748
|
-
if (!
|
|
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
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
1777
|
-
if (
|
|
1778
|
-
skillMdContent =
|
|
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 =
|
|
1788
|
-
if (!
|
|
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 ${
|
|
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(
|
|
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 (!
|
|
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
|
|
1833
|
-
import { join as
|
|
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 (!
|
|
1945
|
+
if (!existsSync3(skillPath))
|
|
1837
1946
|
return null;
|
|
1838
1947
|
return {
|
|
1839
|
-
skillMd: readIfExists(
|
|
1840
|
-
readme: readIfExists(
|
|
1841
|
-
claudeMd: readIfExists(
|
|
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 (!
|
|
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(
|
|
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 =
|
|
1884
|
-
if (
|
|
1992
|
+
const pkgPath = join3(skillPath, "package.json");
|
|
1993
|
+
if (existsSync3(pkgPath)) {
|
|
1885
1994
|
try {
|
|
1886
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
2015
|
+
skillPath = join3(process.cwd(), ".skills", skillName);
|
|
1907
2016
|
} else {
|
|
1908
|
-
const installedPath =
|
|
1909
|
-
if (
|
|
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 (!
|
|
2024
|
+
if (!existsSync3(skillPath)) {
|
|
1916
2025
|
return { exitCode: 1, error: `Skill '${name}' not found` };
|
|
1917
2026
|
}
|
|
1918
|
-
const pkgPath =
|
|
1919
|
-
if (!
|
|
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(
|
|
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 =
|
|
1939
|
-
if (!
|
|
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 =
|
|
1943
|
-
if (!
|
|
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 =
|
|
1962
|
-
if (!
|
|
2070
|
+
const skillsDir = join3(targetDir, ".skills");
|
|
2071
|
+
if (!existsSync3(skillsDir))
|
|
1963
2072
|
return "";
|
|
1964
|
-
const dirs =
|
|
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 =
|
|
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(
|
|
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 (!
|
|
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(
|
|
2026
|
-
const claudeMd = readIfExists(
|
|
2134
|
+
const readme = readIfExists(join3(skillPath, "README.md"));
|
|
2135
|
+
const claudeMd = readIfExists(join3(skillPath, "CLAUDE.md"));
|
|
2027
2136
|
let cliCommand = null;
|
|
2028
|
-
const pkgPath =
|
|
2029
|
-
if (
|
|
2137
|
+
const pkgPath = join3(skillPath, "package.json");
|
|
2138
|
+
if (existsSync3(pkgPath)) {
|
|
2030
2139
|
try {
|
|
2031
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
2107
|
-
return
|
|
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
|
|
2114
|
-
import { join as
|
|
2115
|
-
import { homedir as
|
|
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
|
|
2245
|
+
return join4(getDataDir(), "config.json");
|
|
2124
2246
|
}
|
|
2125
|
-
return
|
|
2247
|
+
return join4(process.cwd(), "skills.config.json");
|
|
2126
2248
|
}
|
|
2127
2249
|
function readConfigFile(path) {
|
|
2128
|
-
if (!
|
|
2250
|
+
if (!existsSync4(path))
|
|
2129
2251
|
return {};
|
|
2130
2252
|
try {
|
|
2131
|
-
const raw =
|
|
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 (
|
|
2284
|
+
if (existsSync4(filePath)) {
|
|
2163
2285
|
try {
|
|
2164
|
-
existing = JSON.parse(
|
|
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 (!
|
|
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
|