From 2ee9b22cec14fc77dd04de70244561bc0f48d307 Mon Sep 17 00:00:00 2001 From: Horus AI Date: Tue, 24 Mar 2026 18:43:42 +0100 Subject: [PATCH] feat: Add analyze and Make My Version buttons - Analyze shows skill description and preview - Make My Version creates custom skill without installing from ClawHub --- app/api/clawhub/route.ts | 82 +++++++++++++++++++++++++++- app/mission-control/clawhub/page.tsx | 75 ++++++++++++++++++++++--- 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/app/api/clawhub/route.ts b/app/api/clawhub/route.ts index 73907bb..61cba84 100644 --- a/app/api/clawhub/route.ts +++ b/app/api/clawhub/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { exec } from "child_process"; import { promisify } from "util"; -import { existsSync } from "fs"; +import { existsSync, readFileSync } from "fs"; +import { readFile, writeFile } from "fs/promises"; import path from "path"; const execAsync = promisify(exec); @@ -82,6 +83,85 @@ export async function GET(req: NextRequest) { 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`); + + return NextResponse.json({ + description: desc, + triggers: triggers, + raw: content.substring(0, 2000) + }); + } 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`); diff --git a/app/mission-control/clawhub/page.tsx b/app/mission-control/clawhub/page.tsx index ee9ac9d..d5aba94 100644 --- a/app/mission-control/clawhub/page.tsx +++ b/app/mission-control/clawhub/page.tsx @@ -8,6 +8,8 @@ interface Skill { name: string; score: number; category?: string; + description?: string; + triggers?: string; } const AGENTS = ["horus", "amun", "cleo"] as const; @@ -43,6 +45,7 @@ export default function ClawHubMarketplace() { const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); const [selectedSkill, setSelectedSkill] = useState(null); + const [skillDetails, setSkillDetails] = useState<{description?: string; triggers?: string} | null>(null); const [selectedAgents, setSelectedAgents] = useState(["horus"]); const [view, setView] = useState<"popular" | "categories" | "search">("popular"); const [action, setAction] = useState<"install" | "delete" | "sync" | "push" | null>(null); @@ -200,6 +203,46 @@ export default function ClawHubMarketplace() { setSyncing(false); }; + const handleAnalyze = async () => { + if (!selectedSkill) return; + setAction("analyze"); + setOutput("Analyzing skill...\n"); + try { + const res = await fetch(`/api/clawhub?action=analyze&slug=${encodeURIComponent(selectedSkill.slug)}`); + const data = await res.json(); + setSkillDetails({ + description: data.description, + triggers: data.triggers + }); + setOutput(`📋 ${selectedSkill.name}\n\n` + + `Description: ${data.description || "N/A"}\n\n` + + `Triggers: ${data.triggers || "N/A"}\n\n` + + (data.raw ? `📄 SKILL.md Preview:\n${data.raw.substring(0, 500)}...` : "")); + } catch (e) { + setOutput("Error: " + String(e)); + } + setAction(null); + }; + + const handleClone = async () => { + if (!selectedSkill) return; + setAction("clone"); + setOutput("Creating custom version...\n"); + try { + const res = await fetch("/api/clawhub", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "clone", slug: selectedSkill.slug }), + }); + const data = await res.json(); + setOutput(data.output || data.error || "Clone complete!"); + fetchInstalled(); + } catch (e) { + setOutput("Error: " + String(e)); + } + setAction(null); + }; + const handlePush = async () => { setAction("push"); setOutput("Pushing to GitHub...\n"); @@ -454,19 +497,35 @@ export default function ClawHubMarketplace() { {/* Action Buttons */}
+
+ + +
{/* Output */}