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:
@@ -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`);
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -453,21 +496,37 @@ 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
|
||||||
|
onClick={handleAnalyze}
|
||||||
|
disabled={action !== null}
|
||||||
|
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 === "analyze" ? "⏳ Analyzing..." : "🔍 Analyze Skill"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClone}
|
||||||
|
disabled={action !== null}
|
||||||
|
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 === "clone" ? "⏳ Creating..." : "🧬 Make My Version"}
|
||||||
|
</button>
|
||||||
|
<div className="border-t border-slate-600 pt-2 mt-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleInstall}
|
onClick={handleInstall}
|
||||||
disabled={action !== null || selectedAgents.length === 0}
|
disabled={action !== null || selectedAgents.length === 0}
|
||||||
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="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)`}
|
{action === "install" ? "⏳ Installing..." : `✅ Install on ${selectedAgents.length} agent(s)`}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)}
|
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)}
|
||||||
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="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)`}
|
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Output */}
|
{/* Output */}
|
||||||
{output && (
|
{output && (
|
||||||
|
|||||||
Reference in New Issue
Block a user