import { NextRequest, NextResponse } from "next/server"; import { exec } from "child_process"; import { promisify } from "util"; import { existsSync, readFileSync } from "fs"; import { readFile, writeFile } 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 (e) { // Ignore sync errors } 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 }); } if (action === "analyze") { try { // Install to temp for analysis const tempPath = `/tmp/clawhub-analyze-${slug}`; await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); // Read SKILL.md const skillMdPath = path.join(tempPath, slug, "SKILL.md"); let content = ""; if (existsSync(skillMdPath)) { content = await readFile(skillMdPath, "utf-8"); } else { // Try without slug subfolder const altPath = path.join(tempPath, "SKILL.md"); if (existsSync(altPath)) { content = await readFile(altPath, "utf-8"); } } // Extract description 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"; // Clean up await execAsync(`rm -rf ${tempPath} 2>/dev/null`); // Now analyze project fit const projectsDir = "/root/.openclaw/workspace/projects"; const projectMatches: string[] = []; try { const { readdirSync } = await import("fs"); const { readFile: readFileAsync } = await import("fs/promises"); const entries = readdirSync(projectsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".json"))) { const projectPath = path.join(projectsDir, entry.name); const projectContent = await readFileAsync(projectPath, "utf-8"); const projectName = entry.name.replace(/\.(md|json)$/, ""); // Simple keyword matching const skillKeywords = (desc + " " + triggers).toLowerCase(); const projectKeywords = projectContent.toLowerCase(); // Check for keyword overlaps const overlaps: string[] = []; const skillWords = skillKeywords.split(/\s+/).filter(w => w.length > 4); for (const word of skillWords) { if (projectKeywords.includes(word)) { overlaps.push(word); } } if (overlaps.length >= 2) { projectMatches.push(`${projectName}: ${overlaps.slice(0, 3).join(", ")}`); } } } } catch {} return NextResponse.json({ description: desc, triggers: triggers, raw: content.substring(0, 2000), projectMatches }); } catch (e: any) { return NextResponse.json({ error: e.message, description: "Could not analyze skill" }); } } if (action === "clone") { try { // Install to temp const tempPath = `/tmp/clawhub-clone-${slug}`; await execAsync(`rm -rf ${tempPath} 2>/dev/null; ${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); // Find the skill folder const entries = await readdir(tempPath); const skillFolder = entries.find(e => e.includes(slug.replace(/-/g, ''))); const sourcePath = path.join(tempPath, skillFolder || slug); // Create custom version const customSlug = `${slug}-custom`; const destPath = path.join(SKILLS_BASE, "horus", customSlug); // Copy files await execAsync(`cp -r ${sourcePath} ${destPath} 2>&1`); // Modify SKILL.md 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); } // Clean up 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 }); } } 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); // Ensure agent dir exists try { await execAsync(`mkdir -p ${agentDir}`); } catch {} // Install to temp first const tempPath = `/tmp/clawhub-install-${slug}-${Date.now()}`; try { await execAsync(`${CLAWHUB_BIN} install ${slug} --dir ${tempPath} 2>&1`); // Move to agent folder 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") }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (e: any) { return NextResponse.json({ error: e.message || "Failed", }, { status: 500 }); } }