feat: Add analyze and Make My Version buttons

- Analyze shows skill description and preview
- Make My Version creates custom skill without installing from ClawHub
This commit is contained in:
2026-03-24 18:43:42 +01:00
parent 5ece3001a3
commit 2ee9b22cec
2 changed files with 148 additions and 9 deletions
+81 -1
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { exec } from "child_process"; import { exec } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
import { existsSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { readFile, writeFile } from "fs/promises";
import path from "path"; import path from "path";
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -82,6 +83,85 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ info: stdout }); 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") { if (action === "sync") {
try { try {
const { stdout, stderr } = await execAsync(`cd ${SKILLS_BASE} && git pull origin main 2>&1`); const { stdout, stderr } = await execAsync(`cd ${SKILLS_BASE} && git pull origin main 2>&1`);
+67 -8
View File
@@ -8,6 +8,8 @@ interface Skill {
name: string; name: string;
score: number; score: number;
category?: string; category?: string;
description?: string;
triggers?: string;
} }
const AGENTS = ["horus", "amun", "cleo"] as const; const AGENTS = ["horus", "amun", "cleo"] as const;
@@ -43,6 +45,7 @@ export default function ClawHubMarketplace() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null); const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [skillDetails, setSkillDetails] = useState<{description?: string; triggers?: string} | null>(null);
const [selectedAgents, setSelectedAgents] = useState<string[]>(["horus"]); const [selectedAgents, setSelectedAgents] = useState<string[]>(["horus"]);
const [view, setView] = useState<"popular" | "categories" | "search">("popular"); const [view, setView] = useState<"popular" | "categories" | "search">("popular");
const [action, setAction] = useState<"install" | "delete" | "sync" | "push" | null>(null); const [action, setAction] = useState<"install" | "delete" | "sync" | "push" | null>(null);
@@ -200,6 +203,46 @@ export default function ClawHubMarketplace() {
setSyncing(false); 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 () => { const handlePush = async () => {
setAction("push"); setAction("push");
setOutput("Pushing to GitHub...\n"); setOutput("Pushing to GitHub...\n");
@@ -454,19 +497,35 @@ export default function ClawHubMarketplace() {
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex flex-col gap-2 mb-4"> <div className="flex flex-col gap-2 mb-4">
<button <button
onClick={handleInstall} onClick={handleAnalyze}
disabled={action !== null || selectedAgents.length === 0} disabled={action !== null}
className="bg-green-600 hover:bg-green-700 disabled:bg-green-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2" className="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
> >
{action === "install" ? "⏳ Installing..." : `✅ Install on ${selectedAgents.length} agent(s)`} {action === "analyze" ? "⏳ Analyzing..." : "🔍 Analyze Skill"}
</button> </button>
<button <button
onClick={handleDelete} onClick={handleClone}
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)} disabled={action !== null}
className="bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2" className="bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
> >
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`} {action === "clone" ? "⏳ Creating..." : "🧬 Make My Version"}
</button> </button>
<div className="border-t border-slate-600 pt-2 mt-2">
<button
onClick={handleInstall}
disabled={action !== null || selectedAgents.length === 0}
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-green-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{action === "install" ? "⏳ Installing..." : `✅ Install on ${selectedAgents.length} agent(s)`}
</button>
<button
onClick={handleDelete}
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)}
className="w-full bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2 mt-2"
>
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`}
</button>
</div>
</div> </div>
{/* Output */} {/* Output */}