feat: prebundle and auto-enable document, self-improving, search skills (#413)
This commit is contained in:
@@ -67,6 +67,55 @@ export class ClawHubService {
|
||||
return line.replace(this.ansiRegex, '').trim();
|
||||
}
|
||||
|
||||
private extractFrontmatterName(skillManifestPath: string): string | null {
|
||||
try {
|
||||
const raw = fs.readFileSync(skillManifestPath, 'utf8');
|
||||
// Match the first frontmatter block and read `name: ...`
|
||||
const frontmatterMatch = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||
if (!frontmatterMatch) return null;
|
||||
const body = frontmatterMatch[1];
|
||||
const nameMatch = body.match(/^\s*name\s*:\s*["']?([^"'\n]+)["']?\s*$/m);
|
||||
if (!nameMatch) return null;
|
||||
const name = nameMatch[1].trim();
|
||||
return name || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveSkillDirByManifestName(candidates: string[]): string | null {
|
||||
const skillsRoot = path.join(this.workDir, 'skills');
|
||||
if (!fs.existsSync(skillsRoot)) return null;
|
||||
|
||||
const wanted = new Set(
|
||||
candidates
|
||||
.map((v) => v.trim().toLowerCase())
|
||||
.filter((v) => v.length > 0),
|
||||
);
|
||||
if (wanted.size === 0) return null;
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const skillDir = path.join(skillsRoot, entry.name);
|
||||
const skillManifestPath = path.join(skillDir, 'SKILL.md');
|
||||
if (!fs.existsSync(skillManifestPath)) continue;
|
||||
|
||||
const frontmatterName = this.extractFrontmatterName(skillManifestPath);
|
||||
if (!frontmatterName) continue;
|
||||
if (wanted.has(frontmatterName.toLowerCase())) {
|
||||
return skillDir;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a ClawHub CLI command
|
||||
*/
|
||||
@@ -318,24 +367,33 @@ export class ClawHubService {
|
||||
/**
|
||||
* Open skill README/manual in default editor
|
||||
*/
|
||||
async openSkillReadme(slug: string): Promise<boolean> {
|
||||
const skillDir = path.join(this.workDir, 'skills', slug);
|
||||
async openSkillReadme(skillKeyOrSlug: string, fallbackSlug?: string): Promise<boolean> {
|
||||
const candidates = [skillKeyOrSlug, fallbackSlug]
|
||||
.filter((v): v is string => typeof v === 'string' && v.trim().length > 0)
|
||||
.map(v => v.trim());
|
||||
const uniqueCandidates = [...new Set(candidates)];
|
||||
const directSkillDir = uniqueCandidates
|
||||
.map((id) => path.join(this.workDir, 'skills', id))
|
||||
.find((dir) => fs.existsSync(dir));
|
||||
const skillDir = directSkillDir || this.resolveSkillDirByManifestName(uniqueCandidates);
|
||||
|
||||
// Try to find documentation file
|
||||
const possibleFiles = ['SKILL.md', 'README.md', 'skill.md', 'readme.md'];
|
||||
let targetFile = '';
|
||||
|
||||
for (const file of possibleFiles) {
|
||||
const filePath = path.join(skillDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
targetFile = filePath;
|
||||
break;
|
||||
if (skillDir) {
|
||||
for (const file of possibleFiles) {
|
||||
const filePath = path.join(skillDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
targetFile = filePath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetFile) {
|
||||
// If no md file, just open the directory
|
||||
if (fs.existsSync(skillDir)) {
|
||||
if (skillDir) {
|
||||
targetFile = skillDir;
|
||||
} else {
|
||||
throw new Error('Skill directory not found');
|
||||
|
||||
Reference in New Issue
Block a user