@ian2018cs/agenthub 0.1.25 → 0.1.27
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/assets/{index-1dh35QcB.js → index-CdP2lKlj.js} +14 -14
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/claude-sdk.js +37 -25
- package/server/database/db.js +47 -0
- package/server/index.js +165 -76
- package/server/routes/skills.js +133 -81
package/server/routes/skills.js
CHANGED
|
@@ -29,31 +29,24 @@ const upload = multer({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Parse skill metadata from
|
|
32
|
+
* Parse skill metadata from SKILL.md YAML frontmatter
|
|
33
33
|
*/
|
|
34
34
|
async function parseSkillMetadata(skillPath) {
|
|
35
35
|
try {
|
|
36
|
-
const skillsFile = path.join(skillPath, '
|
|
36
|
+
const skillsFile = path.join(skillPath, 'SKILL.md');
|
|
37
37
|
const content = await fs.readFile(skillsFile, 'utf-8');
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
41
|
-
const title = titleMatch ? titleMatch[1].trim() : path.basename(skillPath);
|
|
42
|
-
|
|
43
|
-
// Extract description from content after title (first paragraph)
|
|
44
|
-
const lines = content.split('\n');
|
|
39
|
+
let title = path.basename(skillPath);
|
|
45
40
|
let description = '';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
41
|
+
|
|
42
|
+
// Parse YAML frontmatter (between --- delimiters)
|
|
43
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
44
|
+
if (fmMatch) {
|
|
45
|
+
const fm = fmMatch[1];
|
|
46
|
+
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
|
47
|
+
const descMatch = fm.match(/^description:\s*(.+)$/m);
|
|
48
|
+
if (nameMatch) title = nameMatch[1].trim();
|
|
49
|
+
if (descMatch) description = descMatch[1].trim();
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
return { title, description };
|
|
@@ -63,27 +56,64 @@ async function parseSkillMetadata(skillPath) {
|
|
|
63
56
|
}
|
|
64
57
|
|
|
65
58
|
/**
|
|
66
|
-
* Check if a path is a valid skill directory
|
|
59
|
+
* Check if a path is a valid skill directory (must contain SKILL.md)
|
|
67
60
|
*/
|
|
68
61
|
async function isValidSkill(skillPath) {
|
|
69
62
|
try {
|
|
70
63
|
const stat = await fs.stat(skillPath);
|
|
71
64
|
if (!stat.isDirectory()) return false;
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
await fs.access(path.join(skillPath, 'SKILLS.md'));
|
|
76
|
-
return true;
|
|
77
|
-
} catch {
|
|
78
|
-
// Fallback: check for any .md files
|
|
79
|
-
const files = await fs.readdir(skillPath);
|
|
80
|
-
return files.some(f => f.endsWith('.md'));
|
|
81
|
-
}
|
|
66
|
+
await fs.access(path.join(skillPath, 'SKILL.md'));
|
|
67
|
+
return true;
|
|
82
68
|
} catch {
|
|
83
69
|
return false;
|
|
84
70
|
}
|
|
85
71
|
}
|
|
86
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Recursively scan a directory for valid skill subdirectories.
|
|
75
|
+
* Supports nested structures like skills/XXX (maxDepth controls recursion depth).
|
|
76
|
+
* If a directory is itself a valid skill it is collected and not recursed into.
|
|
77
|
+
*/
|
|
78
|
+
async function scanDirForSkills(dirPath, repository, installedSkills, maxDepth = 2) {
|
|
79
|
+
const found = [];
|
|
80
|
+
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
84
|
+
} catch {
|
|
85
|
+
return found;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (entry.name.startsWith('.') ||
|
|
90
|
+
entry.name.toLowerCase().startsWith('readme') ||
|
|
91
|
+
!entry.isDirectory()) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const skillPath = path.join(dirPath, entry.name);
|
|
96
|
+
|
|
97
|
+
if (await isValidSkill(skillPath)) {
|
|
98
|
+
const metadata = await parseSkillMetadata(skillPath);
|
|
99
|
+
found.push({
|
|
100
|
+
name: entry.name,
|
|
101
|
+
title: metadata.title,
|
|
102
|
+
description: metadata.description,
|
|
103
|
+
repository,
|
|
104
|
+
installed: installedSkills ? installedSkills.has(entry.name) : false,
|
|
105
|
+
path: skillPath
|
|
106
|
+
});
|
|
107
|
+
} else if (maxDepth > 0) {
|
|
108
|
+
// Not a skill itself — recurse into it looking for nested skills
|
|
109
|
+
const nested = await scanDirForSkills(skillPath, repository, installedSkills, maxDepth - 1);
|
|
110
|
+
found.push(...nested);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
/**
|
|
88
118
|
* Check if URL is SSH format (git@host:path)
|
|
89
119
|
*/
|
|
@@ -497,18 +527,18 @@ router.post('/import', upload.single('skillZip'), async (req, res) => {
|
|
|
497
527
|
}
|
|
498
528
|
|
|
499
529
|
// Determine skill name from zip structure
|
|
500
|
-
// Look for the root directory or first directory containing
|
|
530
|
+
// Look for the root directory or first directory containing SKILL.md
|
|
501
531
|
let skillName = null;
|
|
502
532
|
let rootDir = '';
|
|
503
533
|
|
|
504
534
|
for (const entry of zipEntries) {
|
|
505
|
-
if (entry.entryName.endsWith('
|
|
535
|
+
if (entry.entryName.endsWith('SKILL.md')) {
|
|
506
536
|
const parts = entry.entryName.split('/');
|
|
507
537
|
if (parts.length >= 2) {
|
|
508
538
|
skillName = parts[0];
|
|
509
539
|
rootDir = parts[0] + '/';
|
|
510
540
|
} else {
|
|
511
|
-
//
|
|
541
|
+
// SKILL.md is at root, use original zip filename
|
|
512
542
|
skillName = path.basename(req.file.originalname, '.zip');
|
|
513
543
|
}
|
|
514
544
|
break;
|
|
@@ -638,34 +668,9 @@ router.get('/available', async (req, res) => {
|
|
|
638
668
|
continue;
|
|
639
669
|
}
|
|
640
670
|
|
|
641
|
-
// Scan for skills in the repo
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
for (const entry of entries) {
|
|
645
|
-
// Skip hidden dirs, READMEs, and files
|
|
646
|
-
if (entry.name.startsWith('.') ||
|
|
647
|
-
entry.name.toLowerCase().startsWith('readme') ||
|
|
648
|
-
!entry.isDirectory()) {
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const skillPath = path.join(realRepoPath, entry.name);
|
|
653
|
-
|
|
654
|
-
if (!await isValidSkill(skillPath)) {
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const metadata = await parseSkillMetadata(skillPath);
|
|
659
|
-
|
|
660
|
-
skills.push({
|
|
661
|
-
name: entry.name,
|
|
662
|
-
title: metadata.title,
|
|
663
|
-
description: metadata.description,
|
|
664
|
-
repository: `${owner}/${repo}`,
|
|
665
|
-
installed: installedSkills.has(entry.name),
|
|
666
|
-
path: skillPath
|
|
667
|
-
});
|
|
668
|
-
}
|
|
671
|
+
// Scan for skills in the repo (supports nested dirs like skills/XXX)
|
|
672
|
+
const repoSkills = await scanDirForSkills(realRepoPath, `${owner}/${repo}`, installedSkills);
|
|
673
|
+
skills.push(...repoSkills);
|
|
669
674
|
}
|
|
670
675
|
}
|
|
671
676
|
} catch (err) {
|
|
@@ -778,19 +783,9 @@ router.get('/repos', async (req, res) => {
|
|
|
778
783
|
continue;
|
|
779
784
|
}
|
|
780
785
|
|
|
781
|
-
// Count skills in repo
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const entries = await fs.readdir(realPath, { withFileTypes: true });
|
|
785
|
-
for (const entry of entries) {
|
|
786
|
-
if (entry.name.startsWith('.') || !entry.isDirectory()) continue;
|
|
787
|
-
if (await isValidSkill(path.join(realPath, entry.name))) {
|
|
788
|
-
skillCount++;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
} catch {
|
|
792
|
-
// Ignore
|
|
793
|
-
}
|
|
786
|
+
// Count skills in repo (supports nested dirs like skills/XXX)
|
|
787
|
+
const repoSkills = await scanDirForSkills(realPath, `${owner}/${repo}`, null);
|
|
788
|
+
const skillCount = repoSkills.length;
|
|
794
789
|
|
|
795
790
|
repos.push({
|
|
796
791
|
owner,
|
|
@@ -882,16 +877,11 @@ router.post('/repos', async (req, res) => {
|
|
|
882
877
|
|
|
883
878
|
await fs.symlink(publicRepoPath, userRepoPath);
|
|
884
879
|
|
|
885
|
-
// Count skills
|
|
880
|
+
// Count skills (supports nested dirs like skills/XXX)
|
|
886
881
|
let skillCount = 0;
|
|
887
882
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
if (entry.name.startsWith('.') || !entry.isDirectory()) continue;
|
|
891
|
-
if (await isValidSkill(path.join(publicRepoPath, entry.name))) {
|
|
892
|
-
skillCount++;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
883
|
+
const repoSkills = await scanDirForSkills(publicRepoPath, `${owner}/${repo}`, null);
|
|
884
|
+
skillCount = repoSkills.length;
|
|
895
885
|
} catch {
|
|
896
886
|
// Ignore
|
|
897
887
|
}
|
|
@@ -912,6 +902,68 @@ router.post('/repos', async (req, res) => {
|
|
|
912
902
|
}
|
|
913
903
|
});
|
|
914
904
|
|
|
905
|
+
/**
|
|
906
|
+
* POST /api/skills/repos/refresh
|
|
907
|
+
* Pull latest updates for all user's skill repositories
|
|
908
|
+
*/
|
|
909
|
+
router.post('/repos/refresh', async (req, res) => {
|
|
910
|
+
try {
|
|
911
|
+
const userUuid = req.user?.uuid;
|
|
912
|
+
if (!userUuid) {
|
|
913
|
+
return res.status(401).json({ error: 'User authentication required' });
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const userPaths = getUserPaths(userUuid);
|
|
917
|
+
const results = [];
|
|
918
|
+
|
|
919
|
+
try {
|
|
920
|
+
await fs.mkdir(userPaths.skillsRepoDir, { recursive: true });
|
|
921
|
+
const owners = await fs.readdir(userPaths.skillsRepoDir);
|
|
922
|
+
|
|
923
|
+
for (const owner of owners) {
|
|
924
|
+
if (owner.startsWith('.')) continue;
|
|
925
|
+
|
|
926
|
+
const ownerPath = path.join(userPaths.skillsRepoDir, owner);
|
|
927
|
+
const stat = await fs.stat(ownerPath);
|
|
928
|
+
if (!stat.isDirectory()) continue;
|
|
929
|
+
|
|
930
|
+
const repoNames = await fs.readdir(ownerPath);
|
|
931
|
+
|
|
932
|
+
for (const repo of repoNames) {
|
|
933
|
+
if (repo.startsWith('.')) continue;
|
|
934
|
+
|
|
935
|
+
const repoPath = path.join(ownerPath, repo);
|
|
936
|
+
let realPath = repoPath;
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
const repoStat = await fs.lstat(repoPath);
|
|
940
|
+
if (repoStat.isSymbolicLink()) {
|
|
941
|
+
realPath = await fs.realpath(repoPath);
|
|
942
|
+
}
|
|
943
|
+
} catch {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
await updateRepository(realPath);
|
|
949
|
+
results.push({ owner, repo, status: 'updated' });
|
|
950
|
+
} catch (err) {
|
|
951
|
+
console.log(`Failed to update ${owner}/${repo}:`, err.message);
|
|
952
|
+
results.push({ owner, repo, status: 'failed', error: err.message });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error('Error refreshing repos:', err);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
res.json({ success: true, results });
|
|
961
|
+
} catch (error) {
|
|
962
|
+
console.error('Error refreshing repos:', error);
|
|
963
|
+
res.status(500).json({ error: error.message });
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
915
967
|
/**
|
|
916
968
|
* DELETE /api/skills/repos/:owner/:repo
|
|
917
969
|
* Remove a skill repository (user's symlink only)
|