import { NextRequest, NextResponse } from "next/server"; import { exec } from "child_process"; import { promisify } from "util"; import { existsSync } from "fs"; import { readFile, writeFile, readdir } from "fs/promises"; import path from "path"; const execAsync = promisify(exec); const CLAWHUB_BIN = "/usr/bin/clawhub"; const SKILLS_BASE = "/root/.openclaw/workspace/agents-skills"; export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const action = searchParams.get("action"); const q = searchParams.get("q") || ""; const slug = searchParams.get("slug") || ""; try { if (action === "search") { const { stdout } = await execAsync(`${CLAWHUB_BIN} search "${q}" 2>&1`); const lines = stdout.trim().split("\n"); const skills = lines .filter((line) => line.includes("(")) .map((line) => { const match = line.trim().match(/^(.+?)\s+(.+?)\s+\(([0-9.]+)\)$/); if (match) { return { slug: match[1], name: match[2], score: parseFloat(match[3]) }; } return null; }) .filter(Boolean); return NextResponse.json({ skills }); } if (action === "popular") { const { stdout } = await execAsync(`${CLAWHUB_BIN} search "productivity automation" 2>&1`); const lines = stdout.trim().split("\n"); const skills = lines .filter((line) => line.includes("(")) .map((line) => { const match = line.trim().match(/^(.+?)\s+(.+?)\s+\(([0-9.]+)\)$/); if (match) { return { slug: match[1], name: match[2], score: parseFloat(match[3]) }; } return null; }) .filter(Boolean) .sort((a: any, b: any) => b.score - a.score) .slice(0, 30); return NextResponse.json({ skills }); } if (action === "list") { // Sync Horus repo first try { await execAsync(`cd ${SKILLS_BASE} && git pull origin main 2>&1`); } catch {} const agents = ["horus", "amun", "cleo"]; const installed: Record = {}; for (const agent of agents) { const agentPath = path.join(SKILLS_BASE, agent); if (existsSync(agentPath)) { try { const { stdout } = await execAsync(`ls -1 ${agentPath} 2>/dev/null`); installed[agent] = stdout.trim().split("\n").filter(Boolean); } catch { installed[agent] = []; } } else { installed[agent] = []; } } return NextResponse.json({ installed }); } if (action === "info") { const { stdout } = await execAsync(`${CLAWHUB_BIN} inspect ${slug} 2>&1`); return NextResponse.json({ info: stdout }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (e: any) { return NextResponse.json({ error: e.message || "Failed" }, { status: 500 }); } } export async function POST(req: NextRequest) { try { const body = await req.json(); const { action, slug, agents = ["horus"], name } = body; if (action === "install") { const results: string[] = []; for (const agent of agents) { const agentPath = path.join(SKILLS_BASE, agent, slug); const agentDir = path.join(SKILLS_BASE, agent); try { await execAsync(`mkdir -p ${agentDir}`); } catch {} const tempPath = `/tmp/clawhub-install-${slug}-${Date.now()}`; try { await execAsync(`${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); await execAsync(`mv ${tempPath} ${agentPath} 2>/dev/null || mv ${tempPath}/${slug} ${agentPath} 2>/dev/null`); results.push(`✓ ${agent}: installed ${slug}`); } catch (e: any) { results.push(`✗ ${agent}: failed - ${e.message}`); } } return NextResponse.json({ output: results.join("\n") }); } if (action === "delete") { const results: string[] = []; for (const agent of agents) { const agentPath = path.join(SKILLS_BASE, agent, slug); if (existsSync(agentPath)) { await execAsync(`rm -rf ${agentPath}`); results.push(`✓ ${agent}: removed ${slug}`); } else { results.push(`- ${agent}: ${slug} not found`); } } return NextResponse.json({ output: results.join("\n") }); } if (action === "analyze") { const tempPath = `/tmp/clawhub-analyze-${slug}-${Date.now()}`; try { await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); // Find SKILL.md const skillMdPath = path.join(tempPath, slug, "SKILL.md"); let content = ""; if (existsSync(skillMdPath)) { content = await readFile(skillMdPath, "utf-8"); } else { const altPath = path.join(tempPath, "SKILL.md"); if (existsSync(altPath)) content = await readFile(altPath, "utf-8"); } // Extract description from frontmatter const descMatch = content.match(/description:\s*([^\n]+)/i); const desc = descMatch ? descMatch[1].trim() : "No description available"; // Extract triggers const triggerMatch = content.match(/triggers?:\s*([^\n]+)/i); const triggers = triggerMatch ? triggerMatch[1].trim() : "General use"; // Extract overview section (everything after first ---) const overviewMatch = content.match(/---\n([\s\S]+?)(?=\n##|\n#|$)/i); let overview = ""; if (overviewMatch) { overview = overviewMatch[1].trim(); // Clean up frontmatter keys overview = overview.replace(/^[a-z]+:/gim, "").trim(); } // Extract all ## sections const sections: Record = {}; const sectionMatches = content.matchAll(/##\s+(.+?)\n([\s\S]*?)(?=##\s+|$)/gi); for (const match of sectionMatches) { const title = match[1].trim(); const body = match[2].trim(); if (title.toLowerCase() !== "overview" && body) { sections[title] = body.substring(0, 500); } } // Get scripts info const scriptsDir = path.join(tempPath, slug, "scripts"); let scripts: string[] = []; if (existsSync(scriptsDir)) { try { const files = await readdir(scriptsDir); scripts = files.filter(f => !f.startsWith(".")); } catch {} } await execAsync(`rm -rf ${tempPath} 2>/dev/null`); // Project matching - more thorough analysis const projectsDir = "/root/.openclaw/workspace/projects"; const projectMatches: Array<{name: string; relevance: string; why: string}> = []; try { const entries = await readdir(projectsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith(".md")) { const projectPath = path.join(projectsDir, entry.name); const projectContent = await readFile(projectPath, "utf-8"); const projectName = entry.name.replace(/\.md$/, ""); // Check purpose/goals const purposeMatch = projectContent.match(/purpose:?\s*([^\n]+)/i); const purpose = purposeMatch ? purposeMatch[1].trim() : ""; // Find keyword matches with skill content const skillContent = (desc + " " + triggers + " " + overview).toLowerCase(); const projectKeywords = projectContent.toLowerCase(); // Look for specific relevance const relevantKeywords: string[] = []; const keywordsToCheck = ["automation", "workflow", "ai", "agent", "service", "client", "sales", "marketing", "content", "data", "email", "task", "report", "integration", "api", "web", "voice", "chat"]; for (const kw of keywordsToCheck) { if (skillContent.includes(kw) && projectKeywords.includes(kw)) { relevantKeywords.push(kw); } } if (relevantKeywords.length >= 2 || (purpose && relevantKeywords.length >= 1)) { const why = relevantKeywords.slice(0, 4).join(", "); projectMatches.push({ name: projectName, relevance: `${relevantKeywords.length} keyword matches`, why: `Useful for: ${why}` }); } } } } catch {} return NextResponse.json({ description: desc, triggers: triggers, overview: overview.substring(0, 1000), sections, scripts, projectMatches }); } catch (e: any) { return NextResponse.json({ error: e.message, description: "Could not analyze skill" }); } } if (action === "clone") { const tempPath = `/tmp/clawhub-clone-${slug}-${Date.now()}`; try { await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); const entries = await readdir(tempPath); const skillFolder = entries.find((e: string) => e.includes(slug.replace(/-/g, ""))); const sourcePath = path.join(tempPath, skillFolder || slug); const customSlug = `${slug}-custom`; const destPath = path.join(SKILLS_BASE, "horus", customSlug); await execAsync(`cp -r ${sourcePath} ${destPath} 2>&1`); const skillMdPath = path.join(destPath, "SKILL.md"); if (existsSync(skillMdPath)) { let content = await readFile(skillMdPath, "utf-8"); content = content .replace(/name:\s*([^\n]+)/i, `name: $1 (Custom)`) .replace(/description:\s*([^\n]+)/i, `description: Custom version for Haitham's workflow`); await writeFile(skillMdPath, content); } await execAsync(`rm -rf ${tempPath} 2>/dev/null`); return NextResponse.json({ output: `Created custom skill: ${customSlug}\nPath: ${destPath}\n\nEdit SKILL.md to customize further!` }); } catch (e: any) { return NextResponse.json({ error: e.message }); } } if (action === "sync") { try { const { stdout, stderr } = await execAsync(`cd ${SKILLS_BASE} && git pull origin main 2>&1`); return NextResponse.json({ output: stdout || "Already up to date", error: stderr }); } catch (e: any) { return NextResponse.json({ output: "Sync failed", error: e.message }); } } if (action === "push") { try { const { stdout, stderr } = await execAsync(`cd ${SKILLS_BASE} && git add -A && git commit -m "Update skills" && git push origin main 2>&1`); return NextResponse.json({ output: stdout || "Pushed", error: stderr }); } catch (e: any) { return NextResponse.json({ output: "Push failed", error: e.message }); } } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (e: any) { return NextResponse.json({ error: e.message || "Failed" }, { status: 500 }); } }